""" 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, )