1111from agents .chclient import ROClickHouseClient
1212from agents .llm import OpenRouterClient , cost_usd
1313from agents .loop import run_agent
14+ from agents .policy import load_policy , with_policy
1415from eval .golden import GoldenQuestion , load_golden , fewshot_examples
1516from eval .langfuse_adapter import LangfuseTracer , emit_agent_trace
1617from eval .serialize import result_payload , golden_payload
1718
1819
19- def main () -> None :
20+ DEFAULT_REQUIRED_SCORE_NAMES = (
21+ "correctness" ,
22+ "agent-arena-llm-judge" ,
23+ )
24+
25+
26+ def parse_args (argv = None ):
2027 ap = argparse .ArgumentParser ()
2128 ap .add_argument ("--run-id" , default = None )
2229 ap .add_argument ("--eval-timeout" , type = int , default = 180 ,
@@ -28,10 +35,41 @@ def main() -> None:
2835 ap .add_argument ("--models-file" , default = "" ,
2936 help = "JSON file of model specs [{id,name,family,price_per_1m_in,price_per_1m_out}] "
3037 "to run instead of config.yaml models (used by the web UI)" )
31- args = ap .parse_args ()
38+ ap .add_argument ("--policy-version" , default = "policy-v2" )
39+ ap .add_argument ("--wait-for-score" , action = "append" , default = [])
40+ return ap .parse_args (argv )
41+
42+
43+ def effective_run_id (run_id : str , policy_version : str ) -> str :
44+ return f"{ run_id } --{ policy_version } "
45+
46+
47+ def _dataset_item (q : GoldenQuestion , * , rows , cols , default_float_dp : int ) -> dict :
48+ float_dp = q .float_dp if q .float_dp is not None else default_float_dp
49+ return {
50+ "id" : q .id ,
51+ "question" : q .question ,
52+ "expected_output" : golden_payload (
53+ golden_sql = q .golden_sql ,
54+ rows = rows ,
55+ cols = cols ,
56+ ordered = q .ordered ,
57+ ),
58+ "metadata" : {
59+ "tier" : q .tier ,
60+ "ordered" : q .ordered ,
61+ "float_dp" : float_dp ,
62+ },
63+ }
64+
65+
66+ def main (argv = None ) -> None :
67+ args = parse_args (argv )
3268
3369 cfg = load_config ()
34- run_id = args .run_id or f"run-{ uuid .uuid4 ().hex [:8 ]} "
70+ release = args .run_id or f"run-{ uuid .uuid4 ().hex [:8 ]} "
71+ policy = load_policy (args .policy_version )
72+ run_id = effective_run_id (release , policy .version )
3573
3674 # The web UI can hand the harness an arbitrary set of models (from the live
3775 # OpenRouter catalog) via a JSON file, overriding config.yaml's model list.
@@ -56,7 +94,7 @@ def main() -> None:
5694 llm = OpenRouterClient (cfg .openrouter .base_url , cfg .openrouter .api_key )
5795
5896 with open ("schema/schema_context.md" ) as f :
59- schema_ctx = f .read ()
97+ schema_ctx = with_policy ( f .read (), policy )
6098
6199 all_questions = load_golden ()
62100 # Hold out the few-shot example questions so P2 is not graded on its own
@@ -71,15 +109,15 @@ def main() -> None:
71109 golden_cache [q .id ] = (gr .rows , gr .cols )
72110
73111 # Upload the golden set as a Langfuse Dataset (each config becomes a Run).
74- tracer .ensure_dataset ([{
75- "id" : q . id , "question" : q . question ,
76- "expected_output" : golden_payload ( golden_sql = q . golden_sql ,
77- rows = golden_cache [q .id ][0 ],
78- cols = golden_cache [q .id ][1 ],
79- ordered = q . ordered ) ,
80- "metadata" : { "tier" : q . tier , "ordered" : q . ordered ,
81- "float_dp" : cfg . eval . float_dp },
82- } for q in questions ])
112+ tracer .ensure_dataset ([
113+ _dataset_item (
114+ q ,
115+ rows = golden_cache [q .id ][0 ],
116+ cols = golden_cache [q .id ][1 ],
117+ default_float_dp = cfg . eval . float_dp ,
118+ )
119+ for q in questions
120+ ])
83121
84122 model_names , prompt_names = cfg .resolved_grid ()
85123 if args .models :
@@ -126,9 +164,11 @@ def task(*, item, _m=mname, _p=pname, _mc=mcfg, _pc=pcfg,
126164 payload , c , latency_ms , ar = run_one (_m , _p , _mc , _pc , _ex , _cid , q )
127165 trace_id = emit_agent_trace (
128166 trace_name = "agent_run" , session_id = _sid ,
129- tags = [_cid , f"run:{ run_id } " , _m , _p ],
167+ tags = [_cid , f"run:{ run_id } " , policy . version , _m , _p ],
130168 metadata = {"config_id" : _cid , "question_id" : q .id ,
131- "model" : _m , "prompt" : _p , "run_id" : run_id },
169+ "model" : _m , "prompt" : _p , "run_id" : run_id ,
170+ "policy_version" : policy .version ,
171+ "release" : release },
132172 question = q .question , model = _m ,
133173 transcript = ar .transcript , sql = ar .sql ,
134174 output_payload = payload , usage = ar .usage )
@@ -148,7 +188,13 @@ def task(*, item, _m=mname, _p=pname, _mc=mcfg, _pc=pcfg,
148188 tracer .flush ()
149189
150190 if pending_trace_ids :
151- _wait_for_scores (tracer , pending_trace_ids , args .eval_timeout , args .eval_poll )
191+ _wait_for_scores (
192+ tracer ,
193+ pending_trace_ids ,
194+ args .eval_timeout ,
195+ args .eval_poll ,
196+ args .wait_for_score ,
197+ )
152198 print (f"done. run_id={ run_id } " , flush = True )
153199
154200
@@ -193,25 +239,39 @@ def _question_for_item(item, qmap) -> GoldenQuestion:
193239 golden_sql = str (golden_sql ))
194240
195241
196- def _wait_for_scores (tracer , trace_ids , timeout , poll ) -> None :
197- """Wait until both required asynchronous Langfuse evaluators have finished."""
242+ def _wait_for_scores (tracer , trace_ids , timeout , poll , required_names = None ) -> None :
243+ """Compatibility wrapper for the two default asynchronous evaluators."""
244+ _wait_for_required_scores (
245+ tracer , trace_ids , timeout , poll , required_names = required_names
246+ )
247+
248+
249+ def _wait_for_required_scores (
250+ tracer , trace_ids , timeout , poll , required_names = None
251+ ) -> None :
252+ """Wait for the default scores and every additional exact score name."""
253+ required = list (dict .fromkeys ([
254+ * DEFAULT_REQUIRED_SCORE_NAMES ,
255+ * (required_names or []),
256+ ]))
198257 remaining = set (trace_ids )
199258 print (f"grading via LangFuse evaluators — waiting on { len (remaining )} traces "
200259 f"(timeout { timeout } s)…" , flush = True )
201260 deadline = time .time () + timeout
202- last_missing = {tid : { "correctness" , "llm_judge" } for tid in remaining }
261+ last_missing = {tid : set ( required ) for tid in remaining }
203262 while remaining and time .time () < deadline :
204263 tids = list (remaining )
205264 with ThreadPoolExecutor (max_workers = min (8 , len (tids ))) as pool :
206265 score_lists = list (pool .map (tracer .fetch_trace_scores , tids ))
207266 done = []
208267 for tid , scores in zip (tids , score_lists ):
209- corr , judge , _outcome = _classify_scores (scores )
210- missing = set ()
211- if corr is None :
212- missing .add ("correctness" )
213- if judge is None :
214- missing .add ("llm_judge" )
268+ available = {
269+ score .get ("name" )
270+ for score in scores or []
271+ if score .get ("name" )
272+ and (score .get ("value" ) is not None or score .get ("string" ) is not None )
273+ }
274+ missing = set (required ) - available
215275 last_missing [tid ] = missing
216276 if not missing :
217277 done .append (tid )
@@ -224,7 +284,7 @@ def _wait_for_scores(tracer, trace_ids, timeout, poll) -> None:
224284 missing_names = sorted ({name for tid in remaining for name in last_missing [tid ]})
225285 raise RuntimeError (
226286 f"{ len (remaining )} traces are missing { ', ' .join (missing_names )} after { timeout } s; "
227- "configure both Langfuse evaluators for Experiments on arena-golden. "
287+ "configure the required Langfuse evaluators for Experiments on arena-golden. "
228288 "See eval/langfuse_evaluators/README.md."
229289 )
230290 print (f"Langfuse scored all { len (trace_ids )} traces; leaderboard ready" , flush = True )
0 commit comments