122 lines
5.0 KiB
Python
122 lines
5.0 KiB
Python
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}"
|
|
|