"""MathVerse evaluation helpers.""" from __future__ import annotations import re import string from skillopt.model import chat_with_deployment from skillopt.prompts import load_prompt _EVAL_MODE = "mathverse_choice_or_judge_v1" def normalize_text(text: str) -> str: text = str(text or "").strip().lower() text = text.replace("\\,", " ") text = text.replace("\\ ", " ") text = "".join(ch for ch in text if ch not in string.punctuation) return " ".join(text.split()) def normalize_math_text(text: str) -> str: text = str(text or "").strip() text = text.replace("$", "") text = text.replace("\\mathrm", "") text = text.replace("{", "") text = text.replace("}", "") text = text.replace("~", " ") text = text.replace("\\,", " ") text = text.replace("\\ ", " ") return " ".join(text.split()).lower() def extract_answer(text: str | None) -> str: raw = str(text or "").strip() if not raw: return "" tags = re.findall(r"\s*(.*?)\s*", raw, re.IGNORECASE | re.DOTALL) if tags: return tags[-1].strip() boxed = re.findall(r"\\boxed\{(.*?)\}", raw, re.IGNORECASE | re.DOTALL) if boxed: return boxed[-1].strip() lines = [ln.strip() for ln in raw.splitlines() if ln.strip()] if lines: return lines[-1] return raw def _judge_answer( *, item: dict, extracted_answer: str, judge_model: str, max_completion_tokens: int, retries: int, ) -> dict: question = str(item.get("question_for_eval") or item.get("question") or "").strip() ground_truth = str(item.get("answer") or "").strip() raw, _ = chat_with_deployment( deployment=judge_model, system="You are a careful and strict mathematical answer evaluator.", user=load_prompt("judge", env="mathverse").format( question=question, groundtruth=ground_truth, modeloutput=extracted_answer, ), max_completion_tokens=max_completion_tokens, retries=retries, stage="mathverse_judge", ) response = str(raw).strip().lower() if "true" in response: correct = True elif "false" in response: correct = False else: correct = False return { "raw": raw, "correct": correct, "reason": response, "matched_gold": ground_truth if correct else "", } def evaluate_item( *, item: dict, prediction_text: str, judge_model: str, max_completion_tokens: int = 256, retries: int = 5, ) -> dict: extracted = extract_answer(prediction_text) if item.get("is_choice"): predicted_label = str(extracted).strip().upper().rstrip(".):") correct_label = str(item["correct_choice"].get("label") or "").strip().upper() predicted_text = "" for choice in item.get("choices") or []: if str(choice.get("label") or "").strip().upper() == predicted_label: predicted_text = str(choice.get("text") or "").strip() break hard = 1.0 if predicted_label == correct_label else 0.0 return { "evaluation_mode": _EVAL_MODE, "predicted_answer": extracted, "predicted_label": predicted_label, "predicted_text": predicted_text, "correct_label": correct_label, "correct_text": str(item["correct_choice"].get("text") or "").strip(), "em": hard, "f1": hard, "sub_em": hard, "judge_raw": "", "judge_reason": "exact_label_match" if hard else "label_mismatch", "matched_gold": correct_label if hard else "", } gold_answer = str(item.get("answer") or "").strip() pred_norm = normalize_math_text(extracted) gold_norm = normalize_math_text(gold_answer) if pred_norm and gold_norm and pred_norm == gold_norm: return { "evaluation_mode": _EVAL_MODE, "predicted_answer": extracted, "em": 1.0, "f1": 1.0, "sub_em": 1.0, "judge_raw": "", "judge_reason": "normalized_exact_match", "matched_gold": gold_answer, "string_f1": 1.0, } judge = _judge_answer( item=item, extracted_answer=extracted, judge_model=judge_model, max_completion_tokens=max_completion_tokens, retries=retries, ) hard = 1.0 if judge["correct"] else 0.0 pred_tokens = normalize_text(extracted).split() gold_tokens = normalize_text(gold_answer).split() overlap = 0 gold_counts: dict[str, int] = {} for tok in gold_tokens: gold_counts[tok] = gold_counts.get(tok, 0) + 1 for tok in pred_tokens: count = gold_counts.get(tok, 0) if count > 0: overlap += 1 gold_counts[tok] = count - 1 if pred_tokens and gold_tokens and overlap: precision = overlap / len(pred_tokens) recall = overlap / len(gold_tokens) string_f1 = 2 * precision * recall / (precision + recall) else: string_f1 = 0.0 return { "evaluation_mode": _EVAL_MODE, "predicted_answer": extracted, "em": hard, "f1": hard, "sub_em": hard, "judge_raw": judge["raw"], "judge_reason": judge["reason"], "matched_gold": judge["matched_gold"], "string_f1": string_f1, } def evaluation_mode() -> str: return _EVAL_MODE