first commit
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .models import CompareIssue, GradeReport, LessonStep, SkillStep, StepCompareResult
|
||||
|
||||
|
||||
CORE_ARGUMENTS = {
|
||||
"extrude_boss_mm": ["depth_mm", "end_condition_code", "through_all"],
|
||||
"extrude_cut_mm": ["depth_mm", "end_condition_code", "through_all"],
|
||||
"shell_remove_face_at_point_mm": ["thickness_mm"],
|
||||
"draw_circle_diameter_mm": ["diameter_mm", "center_model_mm", "cx_mm", "cy_mm"],
|
||||
"fillet_edges_radius_mm": ["radius_mm"],
|
||||
"chamfer_edges_angle_distance_mm": ["distance_mm", "angle_deg"],
|
||||
"linear_pattern_feature_mm": ["count1", "spacing1_mm"],
|
||||
"circular_pattern_feature": ["count", "angle_deg"],
|
||||
}
|
||||
|
||||
|
||||
class FeatureGrader:
|
||||
def compare_step(self, expected: LessonStep, observed: SkillStep | None) -> StepCompareResult:
|
||||
if observed is None:
|
||||
return StepCompareResult(
|
||||
ok=False,
|
||||
score=0,
|
||||
expected_step=expected,
|
||||
feedback_text=f"没有检测到本步骤的操作。请执行:{expected.title}。",
|
||||
issues=[
|
||||
CompareIssue(
|
||||
code="missing_operation",
|
||||
message=f"Expected {expected.expected_skill}, but no user feature was observed.",
|
||||
expected={"skill": expected.expected_skill},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
issues: list[CompareIssue] = []
|
||||
if expected.expected_skill != observed.skill:
|
||||
issues.append(
|
||||
CompareIssue(
|
||||
code="wrong_feature_method",
|
||||
message=f"方法错误:标准答案要求 {expected.expected_skill},学生实际使用 {observed.skill}。",
|
||||
expected={"skill": expected.expected_skill, "title": expected.title},
|
||||
actual={"skill": observed.skill, "name": observed.name, "source_feature": observed.source_feature},
|
||||
)
|
||||
)
|
||||
|
||||
issues.extend(compare_core_arguments(expected.expected_skill, expected.expected_arguments, observed.arguments))
|
||||
ok = not issues
|
||||
return StepCompareResult(
|
||||
ok=ok,
|
||||
score=100.0 if ok else max(0.0, 60.0 - 20.0 * len(issues)),
|
||||
expected_step=expected,
|
||||
observed_step=observed,
|
||||
issues=issues,
|
||||
feedback_text=success_feedback(expected) if ok else failure_feedback(expected, issues),
|
||||
)
|
||||
|
||||
def grade_sequence(self, expected: list[LessonStep], observed: list[SkillStep]) -> GradeReport:
|
||||
issues: list[CompareIssue] = []
|
||||
passed = 0
|
||||
total = len(expected)
|
||||
for index, step in enumerate(expected):
|
||||
obs = observed[index] if index < len(observed) else None
|
||||
result = self.compare_step(step, obs)
|
||||
if result.ok:
|
||||
passed += 1
|
||||
issues.extend(result.issues)
|
||||
|
||||
score = 100.0 if total == 0 else round((passed / total) * 100.0, 2)
|
||||
return GradeReport(
|
||||
ok=score >= 90.0 and not any(issue.severity == "error" for issue in issues),
|
||||
score=score,
|
||||
passed_steps=passed,
|
||||
total_steps=total,
|
||||
issues=issues,
|
||||
summary=f"Passed {passed}/{total} feature-method steps.",
|
||||
)
|
||||
|
||||
|
||||
def compare_core_arguments(skill: str, expected: dict[str, object], actual: dict[str, object]) -> list[CompareIssue]:
|
||||
result: list[CompareIssue] = []
|
||||
for key in CORE_ARGUMENTS.get(skill, []):
|
||||
if key not in expected:
|
||||
continue
|
||||
if key not in actual:
|
||||
result.append(
|
||||
CompareIssue(
|
||||
code="missing_parameter",
|
||||
message=f"参数缺失:{key}。",
|
||||
expected={key: expected.get(key)},
|
||||
actual={},
|
||||
)
|
||||
)
|
||||
continue
|
||||
if not values_close(expected.get(key), actual.get(key)):
|
||||
result.append(
|
||||
CompareIssue(
|
||||
code="wrong_parameter",
|
||||
message=f"参数错误:{key} 标准值为 {expected.get(key)},实际值为 {actual.get(key)}。",
|
||||
expected={key: expected.get(key)},
|
||||
actual={key: actual.get(key)},
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def values_close(expected: object, actual: object, tolerance: float = 0.05) -> bool:
|
||||
if isinstance(expected, (int, float)) and isinstance(actual, (int, float)):
|
||||
return abs(float(expected) - float(actual)) <= tolerance
|
||||
if isinstance(expected, list) and isinstance(actual, list) and len(expected) == len(actual):
|
||||
return all(values_close(e, a, tolerance) for e, a in zip(expected, actual))
|
||||
return str(expected) == str(actual)
|
||||
|
||||
|
||||
def success_feedback(expected: LessonStep) -> str:
|
||||
return f"正确。你使用了本步骤要求的 {expected.title},可以进入下一步。"
|
||||
|
||||
|
||||
def failure_feedback(expected: LessonStep, issues: list[CompareIssue]) -> str:
|
||||
first = issues[0].message if issues else "操作不符合标准答案。"
|
||||
return f"{first} 本步骤要求:{expected.voice_text}"
|
||||
|
||||
Reference in New Issue
Block a user