"""EvalRecipe: what a benchmark's evaluation IS, declared not coded. Mirrors the data-layer plugin shape: a recipe binds extract -- extractor spec (primitive name / cascade / custom fn) scorers -- {metric: scorer spec}; spec = name | fn | {'name': ..., **params} aggregators -- {metric: aggregator name | (name, params)} (default 'mean') judge -- optional LLM-judge config (model tag; wiring comes later) Registry: @register_eval('gsm8k') -> get_eval('gsm8k') -> EvalRecipe """ from dataclasses import dataclass, field from typing import Any, Callable, Dict, List, Optional, Tuple, Union from .aggregator import get_aggregator from .extractor import ExtractorSpec, make_extractor from .scorer import ScorerSpec, make_scorer from .registry import EvalRegistry EVAL_REGISTRY = EvalRegistry('eval recipe') def register_eval(name: str): def decorator(factory: Callable[[], 'EvalRecipe']): EVAL_REGISTRY.register(name, factory) return factory return decorator def get_eval(name: str) -> 'EvalRecipe': return EVAL_REGISTRY.get(name)() def list_evals() -> List[str]: return EVAL_REGISTRY.names() @dataclass class JudgeConfig: """LLM-judge wiring; the actual callable is injected by the runner.""" model: str = '' # model tag / url, resolved by ModelAdapter later temperature: float = 0.0 max_retries: int = 2 @dataclass class EvalRecipe: name: str = '' extract: ExtractorSpec = None scorers: Dict[str, ScorerSpec] = field(default_factory=dict) # metric -> aggregator name | (name, params); missing -> 'mean' aggregators: Dict[str, Union[str, Tuple[str, Dict[str, Any]]]] = field(default_factory=dict) judge: Optional[JudgeConfig] = None description: str = '' def resolve_extract(self): return make_extractor(self.extract) def resolve_scorers(self) -> Dict[str, Callable]: if not self.scorers: raise ValueError(f'recipe {self.name!r} has no scorers') return {m: make_scorer(m, s) for m, s in self.scorers.items()} def resolve_aggregators(self) -> Dict[str, Callable]: out = {} for metric, spec in self.aggregators.items(): if isinstance(spec, tuple): name, params = spec base = get_aggregator(name) def with_params(results, m, _base=base, _params=params): return _base(results, m, **_params) if _params else _base(results, m) out[metric] = with_params else: out[metric] = get_aggregator(spec or 'mean') return out def primary_metric(self) -> str: return next(iter(self.scorers), 'acc')