math-solver / eval /runner.py
Cuong2004
Deploy API from GitHub Actions
0772b5a
Raw
History Blame Contribute Delete
5.6 kB
"""
Pipeline Evaluation Runner.
Runs evaluation suites over benchmark datasets and computes structured performance metrics.
"""
from __future__ import annotations
import asyncio
import logging
import time
from typing import Any, Dict, List, Optional
from eval.benchmark import BenchmarkDataset, BenchmarkSample
from eval.metrics import (
OCRMetrics,
ParserMetrics,
PipelineEvalSummary,
SolverMetrics,
compute_cer,
compute_wer,
)
from solver.dsl_parser import DSLParser
from solver.engine import GeometryEngine
from solver.validator import GeometryStatus, GeometryValidator
logger = logging.getLogger(__name__)
class EvalRunner:
"""Runs pipeline evaluation over benchmark datasets."""
def __init__(
self,
dsl_parser: Optional[DSLParser] = None,
geometry_engine: Optional[GeometryEngine] = None,
geometry_validator: Optional[GeometryValidator] = None,
):
self.dsl_parser = dsl_parser or DSLParser()
self.geometry_engine = geometry_engine or GeometryEngine()
self.geometry_validator = geometry_validator or GeometryValidator()
def evaluate_dsl_deterministic(self, dataset: BenchmarkDataset) -> ParserMetrics:
"""
Evaluates DSL parsing, solving, and geometric invariant validation
deterministically without LLM calls.
"""
total = len(dataset)
if total == 0:
return ParserMetrics()
valid_dsl_count = 0
solvable_count = 0
validated_count = 0
degraded_count = 0
for sample in dataset:
dsl = sample.expected_dsl or ""
if not dsl:
continue
try:
points, constraints, is_3d = self.dsl_parser.parse(dsl)
valid_dsl_count += 1
engine_res = self.geometry_engine.solve(points, constraints, is_3d)
if engine_res and engine_res.get("coordinates"):
solvable_count += 1
val_res = self.geometry_validator.validate(engine_res, constraints, is_3d)
if val_res.is_valid:
validated_count += 1
else:
degraded_count += 1
except Exception as e:
logger.debug(f"[EvalRunner] Sample {sample.id} evaluation error: {e}")
return ParserMetrics(
total_samples=total,
json_valid_rate=1.0,
dsl_valid_rate=valid_dsl_count / total,
solvability_rate=solvable_count / total,
validation_pass_rate=validated_count / total,
degradation_rate=degraded_count / total,
)
async def evaluate_full_pipeline(
self,
dataset: BenchmarkDataset,
orchestrator: Any = None,
) -> PipelineEvalSummary:
"""
Executes end-to-end evaluation using Orchestrator across benchmark samples.
"""
from agents.orchestrator import Orchestrator
orch = orchestrator or Orchestrator()
total = len(dataset)
if total == 0:
return PipelineEvalSummary()
e2e_successes = 0
total_latency_ms = 0.0
parser_metrics = ParserMetrics(total_samples=total)
solver_metrics = SolverMetrics(total_samples=total)
ocr_metrics = OCRMetrics(total_samples=total)
valid_dsl_count = 0
solvable_count = 0
validated_count = 0
degraded_count = 0
correct_answer_count = 0
for sample in dataset:
t0 = time.time()
try:
result = await orch.run(
text=sample.problem_text,
image_url=sample.image_url,
generate_video=False,
)
latency = (time.time() - t0) * 1000
total_latency_ms += latency
if result.get("status") == "success":
e2e_successes += 1
# Check geometry status
geo_status = result.get("geometry_status")
if result.get("geometry_dsl"):
valid_dsl_count += 1
if result.get("coordinates"):
solvable_count += 1
if geo_status == GeometryStatus.VALID.value:
validated_count += 1
elif geo_status == GeometryStatus.DEGRADED.value:
degraded_count += 1
# Check answer if expected_answer is present
if sample.expected_answer:
actual_ans = str((result.get("solution") or {}).get("answer", ""))
if sample.expected_answer.strip() in actual_ans or actual_ans.strip() in sample.expected_answer:
correct_answer_count += 1
except Exception as e:
logger.error(f"[EvalRunner] Full pipeline run failed on sample {sample.id}: {e}")
parser_metrics.dsl_valid_rate = valid_dsl_count / total
parser_metrics.solvability_rate = solvable_count / total
parser_metrics.validation_pass_rate = validated_count / total
parser_metrics.degradation_rate = degraded_count / total
solver_metrics.answer_exact_match_rate = correct_answer_count / max(total, 1)
return PipelineEvalSummary(
ocr=ocr_metrics,
parser=parser_metrics,
solver=solver_metrics,
e2e_success_rate=e2e_successes / total,
avg_latency_ms=total_latency_ms / max(total, 1),
total_samples=total,
)