Skip to content

Commit d6c22e9

Browse files
committed
Decouple SDK wrapper gate config
1 parent 680369c commit d6c22e9

5 files changed

Lines changed: 360 additions & 26 deletions

File tree

examples/optimization/eval_optimize_loop/README.md

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,44 @@ python examples/optimization/eval_optimize_loop/run_pipeline.py \
6666
--output-dir /tmp/eval-optimize-loop-sdk
6767
```
6868

69+
Optional wrapper gate and multi-prompt form:
70+
71+
```bash
72+
python examples/optimization/eval_optimize_loop/run_pipeline.py \
73+
--mode sdk \
74+
--train path/to/sdk_train.evalset.json \
75+
--val path/to/sdk_val.evalset.json \
76+
--optimizer-config path/to/sdk_optimizer.json \
77+
--gate-config path/to/wrapper_gate.json \
78+
--target-prompt system_prompt=prompts/system.md \
79+
--target-prompt router_prompt=prompts/router.md \
80+
--sdk-call-agent your_package.your_module:call_agent \
81+
--output-dir /tmp/eval-optimize-loop-sdk
82+
```
83+
6984
`--sdk-call-agent` must point to an async callable compatible with
7085
`AgentOptimizer.optimize(call_agent=...)`. Configure any real model credentials
71-
needed by that callable. SDK mode never silently falls back to fake mode. When
72-
the SDK optimizer returns a best prompt, this wrapper maps `OptimizeResult`
86+
needed by that callable. SDK mode never silently falls back to fake mode.
87+
88+
SDK optimizer config and wrapper gate config are intentionally separate.
89+
`--optimizer-config` is passed unchanged to `AgentOptimizer.optimize`, so it must
90+
follow the SDK `OptimizeConfigFile` schema. Put wrapper-only gate settings in
91+
`--gate-config` (for example `{"gate": {"min_val_score_improvement": 0.05,
92+
"max_total_cost": 1.0}}`). If `--gate-config` is omitted, the wrapper uses the
93+
same default aggregate gate values as the fake example.
94+
95+
Fake mode is the complete per-case closed loop. SDK mode is the real
96+
`AgentOptimizer` / `TargetPrompt` path with an aggregate wrapper gate. When the
97+
SDK optimizer returns a best prompt, this wrapper maps `OptimizeResult`
7398
aggregate fields into the JSON/Markdown report: baseline/best pass rate,
74-
pass-rate improvement, metric breakdowns, token usage, duration, LLM cost, and
75-
round summaries. SDK mode applies an aggregate gate with
99+
pass-rate improvement, metric breakdowns, token usage, duration, LLM cost,
100+
all `best_prompts`, and round summaries. SDK mode applies
76101
`gate_status: partial_applied`: it checks `status == SUCCEEDED`, validation
77102
improvement against `gate.min_val_score_improvement`, and total LLM cost against
78-
`gate.max_total_cost`. Per-case checks that require full validation case scores
79-
are listed in `not_applied_checks` instead of being silently treated as passed.
103+
`gate.max_total_cost`. Protected-case regression, new-hard-failure, per-case
104+
delta, and per-case score-drop checks are not claimed in SDK mode unless the SDK
105+
exposes full per-case validation scores; they are listed in
106+
`not_applied_checks`.
80107

81108
## Source Prompt Writes
82109

examples/optimization/eval_optimize_loop/eval_loop/backends.py

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class SDKBackend:
5454
prompt_path: str | Path
5555
call_agent_path: str | None = None
5656
update_source: bool = False
57+
target_prompt_paths: dict[str, str | Path] | None = None
5758
last_result: Any | None = None
5859
last_result_summary: dict[str, Any] | None = None
5960
last_artifact_dir: str | None = None
@@ -104,7 +105,10 @@ async def optimize_async(
104105
except Exception as exc: # pragma: no cover - depends on optional SDK import health
105106
raise ValueError(f"sdk mode could not import AgentOptimizer/TargetPrompt: {exc}") from exc
106107

107-
target_prompt = TargetPrompt().add_path("system_prompt", str(self.prompt_path))
108+
target_prompt_paths = self._target_prompt_paths()
109+
target_prompt = TargetPrompt()
110+
for name, path in target_prompt_paths.items():
111+
target_prompt.add_path(name, str(path))
108112
result = await AgentOptimizer.optimize(
109113
config_path=str(optimizer_config_path),
110114
call_agent=call_agent,
@@ -121,20 +125,22 @@ async def optimize_async(
121125
self.last_result = result
122126
self.last_result_summary = _summarize_sdk_result(result)
123127
self.last_artifact_dir = str(output_dir)
128+
best_prompts = dict(getattr(result, "best_prompts", {}) or {})
129+
baseline_prompts = _read_prompt_bundle(target_prompt_paths)
124130
return [
125131
CandidatePrompt(
126132
candidate_id="sdk_best",
127-
prompt=best_prompt,
133+
prompt=_render_prompt_bundle(best_prompts),
128134
rationale="Best prompt returned by AgentOptimizer.optimize.",
129-
prompt_diff=make_unified_diff(
130-
baseline_prompt,
131-
best_prompt,
132-
before_name="baseline_system_prompt.txt",
133-
after_name="sdk_best/system_prompt.txt",
134-
),
135+
prompt_diff=_render_prompt_bundle_diff(baseline_prompts, best_prompts),
135136
)
136137
]
137138

139+
def _target_prompt_paths(self) -> dict[str, str | Path]:
140+
if self.target_prompt_paths:
141+
return dict(self.target_prompt_paths)
142+
return {"system_prompt": self.prompt_path}
143+
138144

139145
def _load_call_agent(path: str):
140146
if ":" not in path:
@@ -171,6 +177,8 @@ def _summarize_sdk_result(result: Any) -> dict[str, Any]:
171177
"total_token_usage": _safe_jsonable(getattr(result, "total_token_usage", {})),
172178
"duration_seconds": _safe_jsonable(getattr(result, "duration_seconds", 0.0)),
173179
"total_rounds": _safe_jsonable(getattr(result, "total_rounds", 0)),
180+
"baseline_prompts": _safe_jsonable(getattr(result, "baseline_prompts", {})),
181+
"best_prompts": _safe_jsonable(getattr(result, "best_prompts", {})),
174182
"rounds": [
175183
{
176184
"validation_pass_rate": _safe_jsonable(getattr(round_record, "validation_pass_rate", None)),
@@ -197,3 +205,40 @@ def _safe_jsonable(value: Any) -> Any:
197205
if isinstance(value, (str, int, float, bool)) or value is None:
198206
return value
199207
return repr(value)
208+
209+
210+
def _read_prompt_bundle(paths: dict[str, str | Path]) -> dict[str, str]:
211+
return {
212+
name: Path(path).read_text(encoding="utf-8")
213+
for name, path in paths.items()
214+
}
215+
216+
217+
def _render_prompt_bundle(prompts: dict[str, str]) -> str:
218+
if set(prompts) == {"system_prompt"}:
219+
return prompts["system_prompt"]
220+
sections = []
221+
for name in sorted(prompts):
222+
sections.append(f"## {name}\n\n{prompts[name]}")
223+
return "\n\n".join(sections)
224+
225+
226+
def _render_prompt_bundle_diff(baseline_prompts: dict[str, str], best_prompts: dict[str, str]) -> str:
227+
if set(baseline_prompts) == {"system_prompt"} and set(best_prompts) == {"system_prompt"}:
228+
return make_unified_diff(
229+
baseline_prompts.get("system_prompt", ""),
230+
best_prompts.get("system_prompt", ""),
231+
before_name="baseline_system_prompt.txt",
232+
after_name="sdk_best/system_prompt.txt",
233+
)
234+
diffs = []
235+
for name in sorted(set(baseline_prompts) | set(best_prompts)):
236+
diffs.append(
237+
make_unified_diff(
238+
baseline_prompts.get(name, ""),
239+
best_prompts.get(name, ""),
240+
before_name=f"baseline/{name}.txt",
241+
after_name=f"sdk_best/{name}.txt",
242+
)
243+
)
244+
return "\n\n".join(diffs)

examples/optimization/eval_optimize_loop/eval_loop/report.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,20 @@ def render_markdown(report: OptimizationReport) -> str:
127127
"Update source prompt: "
128128
+ ("yes" if report.run.get("update_source") else "no (default)"),
129129
"",
130+
])
131+
if report.run.get("mode") == "sdk":
132+
availability = report.audit.get("sdk_result_availability", {})
133+
lines.extend([
134+
"SDK mode uses OptimizeResult aggregate validation metrics. "
135+
"Full train scores and full per-case validation deltas are not exposed by the SDK result.",
136+
"",
137+
"SDK availability: "
138+
f"aggregate_validation_result={availability.get('aggregate_validation_result')}, "
139+
f"full_train_eval_result={availability.get('full_train_eval_result')}, "
140+
f"full_per_case_validation_delta={availability.get('full_per_case_validation_delta')}.",
141+
"",
142+
])
143+
lines.extend([
130144
"",
131145
"## Gate Reasons",
132146
"",

0 commit comments

Comments
 (0)