Spaces:
Runtime error
Runtime error
| from __future__ import annotations | |
| import re | |
| import unicodedata | |
| from app.schemas.cross_validation import ( | |
| AnswerKind, | |
| StudentMathAnswerCandidate, | |
| ValidationConfidence, | |
| ) | |
| _NUMBER_PATTERN = r"[-+]?\d+(?:[.,]\d+)?(?:/\d+)?" | |
| def normalize_math_text(value: str) -> str: | |
| """Normalise une réponse mathématique courte sans décider si elle est correcte.""" | |
| text = value.strip().lower() | |
| text = unicodedata.normalize("NFKD", text) | |
| text = "".join(char for char in text if not unicodedata.combining(char)) | |
| replacements = { | |
| "ℝ": "r", | |
| "−": "-", | |
| "–": "-", | |
| "—": "-", | |
| "\\mathbb{r}": "r", | |
| "\\mathbb{R}": "r", | |
| "\\infty": "infinity", | |
| "∞": "infinity", | |
| "donc": " ", | |
| "alors": " ", | |
| "d'ou": " ", | |
| "d’où": " ", | |
| " ": "", | |
| } | |
| for old, new in replacements.items(): | |
| text = text.replace(old, new) | |
| text = re.sub(r"\s+", "", text) | |
| return text | |
| def _extract_x_equals_value(normalized_answer: str) -> str | None: | |
| match = re.search(rf"x=({_NUMBER_PATTERN}|[a-z])", normalized_answer) | |
| if not match: | |
| return None | |
| return match.group(1).replace(",", ".") | |
| def _extract_solution_set_value(normalized_answer: str) -> str | None: | |
| match = re.search(rf"s=\{{?({_NUMBER_PATTERN}|[a-z])\}}?", normalized_answer) | |
| if not match: | |
| return None | |
| return match.group(1).replace(",", ".") | |
| def _extract_last_equality_value(normalized_answer: str) -> str | None: | |
| if "=" not in normalized_answer: | |
| return None | |
| right_side = normalized_answer.rsplit("=", 1)[-1] | |
| match = re.fullmatch(rf"({_NUMBER_PATTERN}|[a-z])", right_side) | |
| if not match: | |
| return None | |
| return match.group(1).replace(",", ".") | |
| def _extract_domain(normalized_answer: str) -> str | None: | |
| if "df=" in normalized_answer: | |
| return normalized_answer.split("df=", 1)[-1] | |
| if "d_f=" in normalized_answer: | |
| return normalized_answer.split("d_f=", 1)[-1] | |
| if normalized_answer.startswith("r\\") or normalized_answer.startswith("r-"): | |
| return normalized_answer | |
| return None | |
| def _extract_boolean_property(normalized_answer: str) -> str | None: | |
| if "impaire" in normalized_answer: | |
| return "impaire" | |
| if "paire" in normalized_answer: | |
| return "paire" | |
| return None | |
| def _infer_answer_kind( | |
| normalized_answer: str, | |
| expected_answer_kind: AnswerKind | None, | |
| ) -> AnswerKind: | |
| if expected_answer_kind is not None and expected_answer_kind != AnswerKind.UNKNOWN: | |
| return expected_answer_kind | |
| if _extract_domain(normalized_answer): | |
| return AnswerKind.DOMAIN | |
| if _extract_solution_set_value(normalized_answer) or _extract_x_equals_value(normalized_answer): | |
| return AnswerKind.SOLUTION_SET | |
| if _extract_boolean_property(normalized_answer): | |
| return AnswerKind.BOOLEAN_PROPERTY | |
| if _extract_last_equality_value(normalized_answer): | |
| return AnswerKind.VALUE | |
| if normalized_answer: | |
| return AnswerKind.EXPRESSION | |
| return AnswerKind.UNKNOWN | |
| def _extract_value_for_kind(normalized_answer: str, answer_kind: AnswerKind) -> str | None: | |
| if answer_kind == AnswerKind.DOMAIN: | |
| return _extract_domain(normalized_answer) | |
| if answer_kind == AnswerKind.SOLUTION_SET: | |
| return ( | |
| _extract_solution_set_value(normalized_answer) | |
| or _extract_x_equals_value(normalized_answer) | |
| or _extract_last_equality_value(normalized_answer) | |
| ) | |
| if answer_kind == AnswerKind.VALUE: | |
| return _extract_last_equality_value(normalized_answer) or _extract_x_equals_value( | |
| normalized_answer | |
| ) | |
| if answer_kind == AnswerKind.BOOLEAN_PROPERTY: | |
| return _extract_boolean_property(normalized_answer) | |
| if answer_kind == AnswerKind.EXPRESSION: | |
| return normalized_answer or None | |
| return None | |
| def _extract_student_math_answer_legacy( | |
| answer_text: str, | |
| expected_answer_kind: AnswerKind | None = None, | |
| ) -> StudentMathAnswerCandidate: | |
| """Extrait une réponse élève exploitable pour comparaison future. | |
| Cette fonction ne note pas. | |
| Cette fonction ne valide pas la correction. | |
| Elle prépare seulement une représentation normalisée. | |
| """ | |
| raw_answer = answer_text.strip() | |
| normalized_answer = normalize_math_text(raw_answer) | |
| answer_kind = _infer_answer_kind(normalized_answer, expected_answer_kind) | |
| extracted_value = _extract_value_for_kind(normalized_answer, answer_kind) | |
| if extracted_value: | |
| confidence = ValidationConfidence.CERTAIN | |
| needs_human_review = False | |
| reason = "student_math_answer_extracted" | |
| elif normalized_answer: | |
| confidence = ValidationConfidence.PROBABLE | |
| needs_human_review = False | |
| reason = "student_math_answer_normalized_without_specific_value" | |
| else: | |
| confidence = ValidationConfidence.UNCERTAIN | |
| needs_human_review = True | |
| reason = "student_math_answer_empty_after_normalization" | |
| return StudentMathAnswerCandidate( | |
| raw_answer=raw_answer, | |
| normalized_answer=normalized_answer, | |
| answer_kind=answer_kind, | |
| extracted_value=extracted_value, | |
| confidence=confidence, | |
| needs_human_review=needs_human_review, | |
| reason=reason, | |
| ) | |
| from app.schemas.cross_validation import AnswerKind, StudentMathAnswerCandidate, ValidationConfidence | |
| from app.services.math_structure_scanner import MathStructureKind, scan_math_structures | |
| def _scanner_solution_value(m): | |
| e=m.children.get("elements", []) | |
| if m.kind == MathStructureKind.ASSIGN_SET: return e[0] if len(e)==1 else m.children.get("value") | |
| if m.kind == MathStructureKind.ASSIGN_NUM: return m.children.get("value") | |
| if m.kind == MathStructureKind.SET_LITERAL: return e[0] if len(e)==1 else "{"+",".join(e)+"}" | |
| return m.normalized_text | |
| def _extract_solution_set_with_scanner(answer_text: str): | |
| ms=scan_math_structures(answer_text) | |
| for m in ms: | |
| if m.kind in {MathStructureKind.ASSIGN_SET, MathStructureKind.ASSIGN_NUM, MathStructureKind.SET_LITERAL}: return StudentMathAnswerCandidate(raw_answer=answer_text, normalized_answer=normalize_math_text(answer_text), answer_kind=AnswerKind.SOLUTION_SET, extracted_value=_scanner_solution_value(m), confidence=ValidationConfidence.CERTAIN, needs_human_review=False, reason="deterministic_structure_scanner") | |
| for m in ms: | |
| if m.kind == MathStructureKind.BARE_NUMBER: return StudentMathAnswerCandidate(raw_answer=answer_text, normalized_answer=normalize_math_text(answer_text), answer_kind=AnswerKind.SOLUTION_SET, extracted_value=m.normalized_text, confidence=ValidationConfidence.PROBABLE, needs_human_review=True, reason="bare_number_requires_semantic_confirmation") | |
| return None | |
| def extract_student_math_answer(answer_text: str, expected_answer_kind: AnswerKind | None = None) -> StudentMathAnswerCandidate: | |
| if expected_answer_kind == AnswerKind.SOLUTION_SET: | |
| r=_extract_solution_set_with_scanner(answer_text) | |
| if r is not None: return r | |
| return _extract_student_math_answer_legacy(answer_text, expected_answer_kind) | |