22"""Eval-Optimize Loop CLI entry point.
33
44Usage:
5- python run_pipeline.py # fake mode
5+ python run_pipeline.py # fake mode (fast smoke test)
66 python run_pipeline.py --mode real # real mode (needs PlateAgent)
77 python run_pipeline.py --max-iter 3 # max optimization iterations
88"""
99
10- import argparse , asyncio , json , sys , time
10+ import argparse , asyncio , json , os as _os , sys , time
1111from pathlib import Path
1212from datetime import datetime , timezone
1313
@@ -28,6 +28,16 @@ def load_config():
2828 return json .load (f )
2929
3030
31+ def _read_critical_case_ids (val_path : Path ) -> list [str ]:
32+ """Dynamically read critical case ids from evalset (fixed: was hardcoded)."""
33+ try :
34+ with open (val_path , "r" , encoding = "utf-8" ) as f :
35+ data = json .load (f )
36+ return [c ["case_id" ] for c in data .get ("cases" , []) if c .get ("critical" , False )]
37+ except Exception :
38+ return ["val_001" ] # fallback
39+
40+
3141async def main ():
3242 parser = argparse .ArgumentParser (description = "Eval-Optimize Loop Pipeline" )
3343 parser .add_argument ("--mode" , default = "fake" , choices = ["fake" , "real" , "real-agent" , "trace" ])
@@ -44,105 +54,135 @@ async def main():
4454 val_path = Path (args .val ) if args .val else BASE_DIR / "config" / "val.evalset.json"
4555 output_dir = Path (args .output ) if args .output else BASE_DIR / "output"
4656 started_at = datetime .now (timezone .utc ).strftime ("%Y-%m-%dT%H:%M:%SZ" )
57+ run_mode = args .mode
4758
48-
49- # ---- 并发锁:防止多个 pipeline 实例同时写入 ----
50- import os as _os
59+ # ---- concurrent lock (mkdir atomic) ----
5160 LOCK_DIR = _os .path .join (str (BASE_DIR ), "output" , ".pipeline.lock" )
5261 try :
5362 _os .makedirs (LOCK_DIR , exist_ok = False )
5463 except FileExistsError :
55- print ("另一个 pipeline 实例正在运行,停止本次任务 " , file = sys .stderr )
64+ print ("another pipeline instance is running, aborting " , file = sys .stderr )
5665 sys .exit (75 )
57- # ---------------------------------------------------
58-
59- if not args .quiet :
60- print (f"Eval-Optimize Loop | mode={ args .mode } seed={ args .seed } " )
61- print ()
62-
63- # Phase 1: Baseline
64- if not args .quiet : print ("[1/6] Baseline..." )
65- br = BaselineRunner (mode = "fake" )
66- baseline = await br .run (train_path , val_path )
67- train_bl , val_bl = baseline ["train" ], baseline ["val" ]
68- if not args .quiet :
69- print (f" train: { train_bl .summary .pass_rate :.1%} val: { val_bl .summary .pass_rate :.1%} " )
70-
71- # Phase 2: Attribution
72- if not args .quiet : print ("[2/6] Attribution..." )
73- ar = AttributionRunner ()
74- attr = ar .run (train_bl , val_bl )
75- if not args .quiet :
76- p = attr .primary_failure_category
77- print (f" failures: { attr .total_failures } primary: { p .category if p else 'none' } " )
78-
79- # Phase 3: Optimization
80- if not args .quiet : print ("[3/6] Optimization..." )
81- opt_runner = OptimizationRunner (mode = "fake" , config = config .get ("pipeline" , {}))
82- opt_result = opt_runner .run (attr )
83- if not args .quiet : print (f" candidates: { opt_result .total_iterations } " )
84-
85- # Phase 4: Validation
86- if not args .quiet : print ("[4/6] Validation..." )
87- vr = ValidationRunner (mode = "fake" )
88- val_result = vr .run (val_bl , opt_result )
89- if not args .quiet : print (f" delta: { val_result .summary .avg_score_delta :+.3f} " )
90-
91- # Phase 5: Gate
92- if not args .quiet : print ("[5/6] Gate..." )
93- gate = AcceptanceGate (config .get ("gate" , {}))
94- decision = gate .decide (
95- baseline_scores = val_bl .score_map ,
96- candidate_scores = val_result .score_map ,
97- baseline_train_scores = train_bl .score_map ,
98- candidate_train_scores = train_bl .score_map ,
99- baseline_cost = val_bl .summary .avg_cost * val_bl .summary .total ,
100- candidate_cost = val_result .summary .total_cost_candidate ,
101- critical_case_ids = ["val_001" ],
102- )
103- gate_dict = {
104- "accepted" : decision .accepted ,
105- "reason" : decision .reason ,
106- "checks" : [{"name" : c .name , "passed" : c .passed , "detail" : c .detail } for c in decision .checks ],
107- }
108- if not args .quiet : print (f" decision: { 'ACCEPTED' if decision .accepted else 'REJECTED' } " )
109-
110- # Phase 6: Audit
111- if not args .quiet : print ("[6/6] Audit..." )
112- auditor = Auditor (output_dir = output_dir )
113- trail = auditor .build_trail (
114- pipeline_name = "PlateAgent Eval-Optimize Loop" ,
115- mode = args .mode , random_seed = args .seed ,
116- optimization = opt_result , baseline_val = val_bl ,
117- validation = val_result , gate_decision = gate_dict ,
118- started_at = started_at ,
119- )
120- audit_path = auditor .save (
121- audit_trail = trail , baseline = baseline , attribution = attr ,
122- optimization = opt_result , validation = val_result , gate_decision = gate_dict ,
123- )
124-
125- # Standalone reports
126- report_dir = output_dir / "reports"
127- report_dir .mkdir (parents = True , exist_ok = True )
128- generate_json_report (train_bl , val_bl , attr , opt_result , val_result , gate_dict ,
129- report_dir / "optimization_report.json" )
130- generate_markdown_report (train_bl , val_bl , attr , opt_result , val_result , gate_dict ,
131- report_dir / "optimization_report.md" )
132-
133- if not args .quiet :
134- print (f" audit: { audit_path } " )
135- print (f" reports: { report_dir } " )
136- print ("Done. 6 phases completed." )
137-
138-
139-
140-
141- # 释放并发锁
66+
67+ # ---- try/finally: ensure lock release even on exception (fix: Critical) ----
14268 try :
143- _os .rmdir (LOCK_DIR )
144- except Exception :
145- pass
69+ if not args .quiet :
70+ print (f"Eval-Optimize Loop | mode={ run_mode } seed={ args .seed } " )
71+ print ()
72+
73+ # Phase 1: Baseline
74+ if not args .quiet : print ("[1/6] Baseline..." )
75+ br = BaselineRunner (mode = run_mode if run_mode in ("real" ,) else "fake" ,
76+ plate_agent_root = str (BASE_DIR .parent .parent .parent / "plate-agent" ))
77+ baseline = await br .run (train_path , val_path )
78+ train_bl , val_bl = baseline ["train" ], baseline ["val" ]
79+ if not args .quiet :
80+ print (f" train: { train_bl .summary .pass_rate :.1%} val: { val_bl .summary .pass_rate :.1%} " )
81+
82+ # Phase 2: Attribution
83+ if not args .quiet : print ("[2/6] Attribution..." )
84+ ar = AttributionRunner ()
85+ attr = ar .run (train_bl , val_bl )
86+ if not args .quiet :
87+ p = attr .primary_failure_category
88+ print (f" failures: { attr .total_failures } primary: { p .category if p else 'none' } " )
89+
90+ # Phase 3: Optimization
91+ if not args .quiet : print ("[3/6] Optimization..." )
92+ use_real_agent = run_mode == "real-agent"
93+ if use_real_agent :
94+ sdk_train = BASE_DIR / "config" / "train.sdk.evalset.json"
95+ sdk_val = BASE_DIR / "config" / "val.sdk.evalset.json"
96+ sdk_opt = BASE_DIR / "config" / "optimizer.sdk.json"
97+ prompt_dir = BASE_DIR / "config" / "prompts"
98+ from src .call_agent import echo_call_agent
99+ opt_runner = OptimizationRunner (
100+ mode = "real" ,
101+ config = config .get ("pipeline" , {}),
102+ call_agent = echo_call_agent ,
103+ train_dataset = str (sdk_train ),
104+ validation_dataset = str (sdk_val ),
105+ sdk_config_path = str (sdk_opt ),
106+ prompt_dir = str (prompt_dir ),
107+ output_dir = str (output_dir / "optimizer" ),
108+ )
109+ else :
110+ opt_runner = OptimizationRunner (mode = "fake" , config = config .get ("pipeline" , {}))
111+ opt_result = opt_runner .run (attr )
112+ if not args .quiet : print (f" candidates: { opt_result .total_iterations } " )
113+
114+ # Phase 4: Validation
115+ if not args .quiet : print ("[4/6] Validation..." )
116+ vr = ValidationRunner (mode = run_mode if run_mode in ("real" ,) else "fake" )
117+ val_result = vr .run (val_bl , opt_result )
118+ if not args .quiet : print (f" delta: { val_result .summary .avg_score_delta :+.3f} " )
119+
120+ # Phase 5: Gate
121+ if not args .quiet : print ("[5/6] Gate..." )
122+ gate = AcceptanceGate (config .get ("gate" , {}))
123+
124+ # fix: overfit detection — run candidate on train set for real delta
125+ if run_mode == "real" :
126+ candidate_train = await br .run_split (train_path , "train_candidate" )
127+ candidate_train_scores = candidate_train .score_map
128+ else :
129+ # fake mode: derive candidate train scores from baseline + simulated delta
130+ candidate_train_scores = {
131+ cid : min (1.0 , score + 0.05 ) for cid , score in train_bl .score_map .items ()
132+ }
133+
134+ critical_case_ids = _read_critical_case_ids (val_path )
135+
136+ decision = gate .decide (
137+ baseline_scores = val_bl .score_map ,
138+ candidate_scores = val_result .score_map ,
139+ baseline_train_scores = train_bl .score_map ,
140+ candidate_train_scores = candidate_train_scores ,
141+ baseline_cost = val_bl .summary .avg_cost * val_bl .summary .total ,
142+ candidate_cost = val_result .summary .total_cost_candidate ,
143+ critical_case_ids = critical_case_ids ,
144+ )
145+ gate_dict = {
146+ "accepted" : decision .accepted ,
147+ "reason" : decision .reason ,
148+ "checks" : [{"name" : c .name , "passed" : c .passed , "detail" : c .detail } for c in decision .checks ],
149+ }
150+ if not args .quiet : print (f" decision: { 'ACCEPTED' if decision .accepted else 'REJECTED' } " )
151+
152+ # Phase 6: Audit
153+ if not args .quiet : print ("[6/6] Audit..." )
154+ auditor = Auditor (output_dir = output_dir )
155+ trail = auditor .build_trail (
156+ pipeline_name = "PlateAgent Eval-Optimize Loop" ,
157+ mode = run_mode , random_seed = args .seed ,
158+ optimization = opt_result , baseline_val = val_bl ,
159+ validation = val_result , gate_decision = gate_dict ,
160+ started_at = started_at ,
161+ )
162+ audit_path = auditor .save (
163+ audit_trail = trail , baseline = baseline , attribution = attr ,
164+ optimization = opt_result , validation = val_result , gate_decision = gate_dict ,
165+ )
166+
167+ report_dir = output_dir / "reports"
168+ report_dir .mkdir (parents = True , exist_ok = True )
169+ generate_json_report (train_bl , val_bl , attr , opt_result , val_result , gate_dict ,
170+ report_dir / "optimization_report.json" )
171+ generate_markdown_report (train_bl , val_bl , attr , opt_result , val_result , gate_dict ,
172+ report_dir / "optimization_report.md" )
173+
174+ if not args .quiet :
175+ print (f" audit: { audit_path } " )
176+ print (f" reports: { report_dir } " )
177+ print ("Done. 6 phases completed." )
178+
179+ finally :
180+ # release lock — always runs, even on exception
181+ try :
182+ _os .rmdir (LOCK_DIR )
183+ except Exception :
184+ pass
185+
146186
147187if __name__ == "__main__" :
148- asyncio .run (main ())
188+ asyncio .run (main ())
0 commit comments