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}"