@@ -218,10 +218,12 @@ async def optimize_async(
218218 """Compatibility async wrapper returning the historical candidate list."""
219219
220220 target_paths = self ._target_prompt_paths ()
221- if set (target_paths ) == {"system_prompt" }:
222- baseline_prompts = {"system_prompt" : baseline_prompt }
223- else :
221+ try :
224222 baseline_prompts = _read_prompt_bundle (target_paths )
223+ except FileNotFoundError :
224+ # Let optimize_candidates report dependency/import failures before
225+ # it reaches its authoritative source snapshot.
226+ baseline_prompts = {name : baseline_prompt for name in target_paths }
225227 result = await self .optimize_candidates (
226228 baseline_prompts = baseline_prompts ,
227229 baseline_train = EvalResult (
@@ -260,12 +262,13 @@ async def optimize_candidates(
260262 raise ValueError (f"sdk mode could not import AgentOptimizer/TargetPrompt: { exc } " ) from exc
261263
262264 target_paths = self ._target_prompt_paths ()
265+ snapshot = snapshot_prompt_files (target_paths )
266+ source_bundle = _prompt_bundle_from_snapshot (snapshot )
263267 baseline_bundle = _validated_prompt_bundle (
264268 baseline_prompts ,
265269 target_paths ,
266270 context = "baseline prompt bundle" ,
267271 )
268- source_bundle = _read_prompt_bundle (target_paths )
269272 mismatched = sorted (
270273 name for name in target_paths if baseline_bundle [name ] != source_bundle [name ]
271274 )
@@ -279,7 +282,6 @@ async def optimize_candidates(
279282 for name , path in target_paths .items ():
280283 target_prompt .add_path (name , str (path ))
281284
282- snapshot = snapshot_prompt_files (target_paths )
283285 try :
284286 sdk_result = await AgentOptimizer .optimize (
285287 config_path = str (config_path ),
@@ -299,10 +301,27 @@ async def optimize_candidates(
299301 + ", " .join (changed_sources )
300302 )
301303
302- total_llm_cost = _finite_result_field (
304+ _require_successful_optimize_result (sdk_result )
305+ total_llm_cost = _nonnegative_result_field (
303306 "total_llm_cost" ,
304307 getattr (sdk_result , "total_llm_cost" , 0.0 ),
305308 )
309+ _pass_rate_result_field (
310+ "baseline_pass_rate" ,
311+ getattr (sdk_result , "baseline_pass_rate" , 0.0 ),
312+ )
313+ _pass_rate_result_field (
314+ "best_pass_rate" ,
315+ getattr (sdk_result , "best_pass_rate" , 0.0 ),
316+ )
317+ _finite_result_field (
318+ "pass_rate_improvement" ,
319+ getattr (sdk_result , "pass_rate_improvement" , 0.0 ),
320+ )
321+ _nonnegative_result_field (
322+ "duration_seconds" ,
323+ getattr (sdk_result , "duration_seconds" , 0.0 ),
324+ )
306325 best_raw = getattr (sdk_result , "best_prompts" , None )
307326 if not best_raw :
308327 raise ValueError ("sdk mode completed but OptimizeResult.best_prompts was empty" )
@@ -315,8 +334,12 @@ async def optimize_candidates(
315334 candidates : list [CandidatePrompt ] = []
316335 rounds : list [OptimizationRound ] = []
317336 seen_bundles : set [tuple [tuple [str , str ], ...]] = set ()
337+ seen_round_ids : set [int ] = set ()
318338 for index , sdk_round in enumerate (getattr (sdk_result , "rounds" , []) or [], start = 1 ):
319339 round_id = _round_id (sdk_round , fallback = index )
340+ if round_id in seen_round_ids :
341+ raise ValueError (f"duplicate SDK round id: { round_id } " )
342+ seen_round_ids .add (round_id )
320343 candidate_id = f"sdk_round_{ round_id :03d} "
321344 round_raw_prompts = getattr (sdk_round , "candidate_prompts" , {}) or {}
322345 round_prompts = (
@@ -333,11 +356,19 @@ async def optimize_candidates(
333356 getattr (sdk_round , "metric_breakdown" , {}) or {},
334357 context = f"SDK round { round_id } metric_breakdown" ,
335358 )
336- round_cost_value = _finite_number (
359+ _pass_rate_number (
360+ getattr (sdk_round , "train_pass_rate" , 0.0 ),
361+ context = f"SDK round { round_id } train_pass_rate" ,
362+ )
363+ _pass_rate_number (
364+ getattr (sdk_round , "validation_pass_rate" , 0.0 ),
365+ context = f"SDK round { round_id } validation_pass_rate" ,
366+ )
367+ round_cost_value = _nonnegative_number (
337368 getattr (sdk_round , "round_llm_cost" , 0.0 ),
338369 context = f"SDK round { round_id } round_llm_cost" ,
339370 )
340- duration_seconds = _finite_number (
371+ duration_seconds = _nonnegative_number (
341372 getattr (sdk_round , "duration_seconds" , 0.0 ),
342373 context = f"SDK round { round_id } duration_seconds" ,
343374 )
@@ -413,6 +444,8 @@ async def evaluate(
413444 call_agent = self ._load_required_call_agent (for_evaluation = True )
414445 try :
415446 from trpc_agent_sdk .evaluation import AgentEvaluator
447+ from trpc_agent_sdk .evaluation import EvalConfig
448+ from trpc_agent_sdk .evaluation import EvaluationCasesFailed
416449 except Exception as exc : # pragma: no cover - depends on optional SDK import health
417450 raise ValueError (f"sdk mode could not import AgentEvaluator: { exc } " ) from exc
418451
@@ -423,6 +456,15 @@ async def evaluate(
423456 context = f"cannot evaluate { prompt_id } " ,
424457 )
425458 expected_cases = _load_sdk_expected_cases (dataset_path , split = split )
459+ artifact_path = Path (artifact_dir )
460+ artifact_path .mkdir (parents = True , exist_ok = True )
461+ eval_config_path = artifact_path / "eval_config.json"
462+ eval_config_path .write_text (
463+ EvalConfig (
464+ criteria = {"final_response_avg_score" : 1.0 }
465+ ).model_dump_json (indent = 2 ),
466+ encoding = "utf-8" ,
467+ )
426468 snapshot = snapshot_prompt_files (target_paths )
427469 result : Any | None = None
428470 with temporary_prompt_bundle (snapshot , candidate_prompts ):
@@ -432,10 +474,11 @@ async def evaluate(
432474 print_detailed_results = False ,
433475 print_summary_report = False ,
434476 eval_result_output_dir = str (artifact_dir ),
477+ eval_metrics_file_path_or_dir = str (eval_config_path ),
435478 )
436479 try :
437480 await executer .evaluate ()
438- except Exception :
481+ except EvaluationCasesFailed :
439482 result = executer .get_result ()
440483 if result is None :
441484 raise
@@ -551,10 +594,28 @@ def _validated_prompt_bundle(
551594
552595
553596def _read_prompt_bundle (paths : dict [str , str | Path ]) -> dict [str , str ]:
554- return {
555- name : Path (path ).read_text (encoding = "utf-8" )
556- for name , path in paths .items ()
557- }
597+ prompts : dict [str , str ] = {}
598+ for name , path in paths .items ():
599+ prompt_path = Path (path )
600+ try :
601+ prompts [name ] = prompt_path .read_bytes ().decode ("utf-8" )
602+ except UnicodeDecodeError as exc :
603+ raise ValueError (
604+ f"source prompt field { name !r} is not valid UTF-8: { prompt_path } "
605+ ) from exc
606+ return prompts
607+
608+
609+ def _prompt_bundle_from_snapshot (snapshot : Any ) -> dict [str , str ]:
610+ prompts : dict [str , str ] = {}
611+ for name , prompt_file in snapshot .files .items ():
612+ try :
613+ prompts [name ] = prompt_file .content .decode ("utf-8" )
614+ except UnicodeDecodeError as exc :
615+ raise ValueError (
616+ f"source prompt field { name !r} is not valid UTF-8: { prompt_file .path } "
617+ ) from exc
618+ return prompts
558619
559620
560621def _changed_snapshot_files (snapshot : Any ) -> list [str ]:
@@ -577,6 +638,30 @@ def _round_id(round_record: Any, *, fallback: int) -> int:
577638 return value
578639
579640
641+ def _require_successful_optimize_result (result : Any ) -> None :
642+ status = _sdk_result_text (getattr (result , "status" , None )).upper ()
643+ if status == "SUCCEEDED" :
644+ return
645+ raise ValueError (
646+ "SDK optimization did not succeed: "
647+ f"status={ status } ; "
648+ f"error_message={ _sdk_result_text (getattr (result , 'error_message' , None ))} ; "
649+ f"finish_reason={ _sdk_result_text (getattr (result , 'finish_reason' , None ))} ; "
650+ f"stop_reason={ _sdk_result_text (getattr (result , 'stop_reason' , None ))} "
651+ )
652+
653+
654+ def _sdk_result_text (value : Any ) -> str :
655+ enum_value = getattr (value , "value" , None )
656+ if enum_value is not None :
657+ value = enum_value
658+ else :
659+ enum_name = getattr (value , "name" , None )
660+ if enum_name is not None :
661+ value = enum_name
662+ return str (value ).split ("." )[- 1 ]
663+
664+
580665def _prompt_bundle_key (prompts : dict [str , str ]) -> tuple [tuple [str , str ], ...]:
581666 return tuple (sorted (prompts .items ()))
582667
@@ -609,11 +694,11 @@ def _summarize_sdk_result(result: Any) -> dict[str, Any]:
609694 "finish_reason" : _safe_jsonable (getattr (result , "finish_reason" , None )),
610695 "stop_reason" : _safe_jsonable (getattr (result , "stop_reason" , None )),
611696 "error_message" : _safe_jsonable (getattr (result , "error_message" , None )),
612- "baseline_pass_rate" : _finite_result_field (
697+ "baseline_pass_rate" : _pass_rate_result_field (
613698 "baseline_pass_rate" ,
614699 getattr (result , "baseline_pass_rate" , 0.0 ),
615700 ),
616- "best_pass_rate" : _finite_result_field (
701+ "best_pass_rate" : _pass_rate_result_field (
617702 "best_pass_rate" ,
618703 getattr (result , "best_pass_rate" , 0.0 ),
619704 ),
@@ -636,12 +721,12 @@ def _summarize_sdk_result(result: Any) -> dict[str, Any]:
636721 "per_metric_best_candidates" : _safe_jsonable (
637722 getattr (result , "per_metric_best_candidates" , {})
638723 ),
639- "total_llm_cost" : _finite_result_field (
724+ "total_llm_cost" : _nonnegative_result_field (
640725 "total_llm_cost" ,
641726 getattr (result , "total_llm_cost" , 0.0 ),
642727 ),
643728 "total_token_usage" : _safe_jsonable (getattr (result , "total_token_usage" , {})),
644- "duration_seconds" : _finite_result_field (
729+ "duration_seconds" : _nonnegative_result_field (
645730 "duration_seconds" ,
646731 getattr (result , "duration_seconds" , 0.0 ),
647732 ),
@@ -686,6 +771,22 @@ def _finite_result_field(field_name: str, value: Any) -> float:
686771 raise ValueError (f"SDK OptimizeResult field { field_name } must be a finite number" ) from exc
687772
688773
774+ def _nonnegative_result_field (field_name : str , value : Any ) -> float :
775+ number = _finite_result_field (field_name , value )
776+ if number < 0.0 :
777+ raise ValueError (f"SDK OptimizeResult field { field_name } must be non-negative" )
778+ return number
779+
780+
781+ def _pass_rate_result_field (field_name : str , value : Any ) -> float :
782+ number = _finite_result_field (field_name , value )
783+ if not 0.0 <= number <= 1.0 :
784+ raise ValueError (
785+ f"SDK OptimizeResult field { field_name } must be between 0 and 1"
786+ )
787+ return number
788+
789+
689790def _finite_metric_map (value : Any , * , context : str ) -> dict [str , float ]:
690791 if not isinstance (value , dict ):
691792 raise ValueError (f"{ context } must be a metric mapping" )
@@ -704,6 +805,20 @@ def _finite_number(value: Any, *, context: str) -> float:
704805 return number
705806
706807
808+ def _nonnegative_number (value : Any , * , context : str ) -> float :
809+ number = _finite_number (value , context = context )
810+ if number < 0.0 :
811+ raise ValueError (f"{ context } must be non-negative" )
812+ return number
813+
814+
815+ def _pass_rate_number (value : Any , * , context : str ) -> float :
816+ number = _finite_number (value , context = context )
817+ if not 0.0 <= number <= 1.0 :
818+ raise ValueError (f"{ context } must be between 0 and 1" )
819+ return number
820+
821+
707822def _safe_jsonable (value : Any ) -> Any :
708823 if hasattr (value , "model_dump" ):
709824 return _safe_jsonable (value .model_dump (mode = "json" ))
@@ -744,6 +859,8 @@ def _load_sdk_expected_cases(
744859 raw_cases = payload ["eval_cases" ]
745860 if not isinstance (raw_cases , list ):
746861 raise ValueError (f"SDK evalset { dataset_path } eval_cases must be a list" )
862+ if not raw_cases :
863+ raise ValueError (f"SDK evalset { dataset_path } eval_cases must not be empty" )
747864
748865 expected_cases : dict [str , EvalCase ] = {}
749866 for index , raw_case in enumerate (raw_cases ):
@@ -872,13 +989,29 @@ def _eval_result_from_sdk_result(
872989
873990 sdk_runs_by_id : dict [str , list [Any ]] = {}
874991 results_by_eval_set_id = getattr (result , "results_by_eval_set_id" , {}) or {}
875- for set_result in results_by_eval_set_id .values ():
992+ if not results_by_eval_set_id :
993+ raise ValueError ("SDK EvaluateResult contains no eval set results" )
994+ for raw_eval_set_id , set_result in results_by_eval_set_id .items ():
995+ eval_set_id = str (raw_eval_set_id )
996+ num_runs = _optional_num_runs (set_result , eval_set_id = eval_set_id )
876997 eval_results_by_eval_id = getattr (set_result , "eval_results_by_eval_id" , {}) or {}
877998 for raw_eval_id , runs in eval_results_by_eval_id .items ():
878999 eval_id = str (raw_eval_id )
8791000 if eval_id in sdk_runs_by_id :
8801001 raise ValueError (f"SDK evaluation result contains duplicate case id: { eval_id } " )
881- sdk_runs_by_id [eval_id ] = list (runs or [])
1002+ run_list = list (runs or [])
1003+ if num_runs is not None and len (run_list ) != num_runs :
1004+ raise ValueError (
1005+ f"SDK eval set { eval_set_id !r} declares num_runs={ num_runs } , "
1006+ f"but case { eval_id !r} contains { len (run_list )} runs"
1007+ )
1008+ _validate_sdk_run_ids (
1009+ run_list ,
1010+ eval_set_id = eval_set_id ,
1011+ eval_id = eval_id ,
1012+ num_runs = num_runs ,
1013+ )
1014+ sdk_runs_by_id [eval_id ] = run_list
8821015
8831016 expected_ids = set (expected_by_id )
8841017 sdk_ids = set (sdk_runs_by_id )
@@ -966,6 +1099,58 @@ def _eval_result_from_sdk_result(
9661099 )
9671100
9681101
1102+ def _optional_num_runs (set_result : Any , * , eval_set_id : str ) -> int | None :
1103+ if not hasattr (set_result , "num_runs" ):
1104+ return None
1105+ num_runs = getattr (set_result , "num_runs" )
1106+ if isinstance (num_runs , bool ) or not isinstance (num_runs , int ) or num_runs <= 0 :
1107+ raise ValueError (
1108+ f"SDK eval set { eval_set_id !r} num_runs must be a positive integer"
1109+ )
1110+ return num_runs
1111+
1112+
1113+ def _validate_sdk_run_ids (
1114+ runs : list [Any ],
1115+ * ,
1116+ eval_set_id : str ,
1117+ eval_id : str ,
1118+ num_runs : int | None ,
1119+ ) -> None :
1120+ seen_run_ids : set [int ] = set ()
1121+ for run in runs :
1122+ internal_eval_id = getattr (run , "eval_id" , None )
1123+ if internal_eval_id not in (None , "" ) and str (internal_eval_id ) != eval_id :
1124+ raise ValueError (
1125+ f"SDK run internal eval_id { internal_eval_id !r} does not match "
1126+ f"container case id { eval_id !r} "
1127+ )
1128+ internal_eval_set_id = getattr (run , "eval_set_id" , None )
1129+ if (
1130+ internal_eval_set_id not in (None , "" )
1131+ and str (internal_eval_set_id ) != eval_set_id
1132+ ):
1133+ raise ValueError (
1134+ f"SDK run internal eval_set_id { internal_eval_set_id !r} does not match "
1135+ f"container eval set id { eval_set_id !r} "
1136+ )
1137+
1138+ run_id = getattr (run , "run_id" , None )
1139+ if run_id is None :
1140+ continue
1141+ if isinstance (run_id , bool ) or not isinstance (run_id , int ) or run_id <= 0 :
1142+ raise ValueError (
1143+ f"SDK case { eval_id !r} run_id must be a positive integer or None"
1144+ )
1145+ if num_runs is not None and run_id > num_runs :
1146+ raise ValueError (
1147+ f"SDK case { eval_id !r} run_id { run_id } exceeds num_runs={ num_runs } "
1148+ )
1149+ if run_id in seen_run_ids :
1150+ raise ValueError (f"SDK evaluation result contains duplicate run_id { run_id } for case { eval_id } " )
1151+ seen_run_ids .add (run_id )
1152+
1153+
9691154def _aggregate_case_metrics (runs : list [Any ], * , case_id : str ) -> dict [str , float ]:
9701155 scores_by_metric : dict [str , list [float ]] = {}
9711156 for run in runs :
0 commit comments