Skip to content

Commit b91bf05

Browse files
committed
fix(evaluation): make sdk backend integration real
1 parent c6b5172 commit b91bf05

5 files changed

Lines changed: 699 additions & 33 deletions

File tree

examples/optimization/eval_optimize_loop/eval_loop/backends.py

Lines changed: 204 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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

553596
def _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

560621
def _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+
580665
def _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+
689790
def _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+
707822
def _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+
9691154
def _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

Comments
 (0)