Files
mech-ai/backend/agent4/grader.py
T
2026-07-17 17:45:56 +08:00

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