Skip to content

Commit 21d2801

Browse files
committed
test(eval_optimize_loop): add integration tests for trace mode
- Add 3 integration tests: ACCEPT, REJECT, and overfitting scenario - Fix pipeline._run_eval to catch _EvaluationCasesFailed (expected when eval cases fail; result is populated before the exception is raised, matching the pattern used by run_evaluator in the SDK) - All 43 tests pass (40 existing + 3 new)
1 parent eb89054 commit 21d2801

2 files changed

Lines changed: 213 additions & 4 deletions

File tree

examples/optimization/eval_optimize_loop/pipeline.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
EvaluateResult,
1616
TargetPrompt,
1717
)
18+
from trpc_agent_sdk.evaluation._agent_evaluator import _EvaluationCasesFailed
1819

1920
from .delta import compute_delta
2021
from .failure_attribution import attribute_failures
@@ -137,8 +138,14 @@ async def _run_eval(self, evalset_path: str) -> EvaluateResult:
137138
print_detailed_results=False,
138139
print_summary_report=False,
139140
)
140-
await executer.evaluate()
141-
return executer.get_result()
141+
try:
142+
await executer.evaluate()
143+
except _EvaluationCasesFailed:
144+
pass
145+
result = executer.get_result()
146+
if result is None:
147+
result = EvaluateResult()
148+
return result
142149
finally:
143150
os.unlink(eval_config_path)
144151

@@ -177,8 +184,14 @@ async def _run_eval_with_agent(self, evalset_path: str) -> EvaluateResult:
177184
print_detailed_results=False,
178185
print_summary_report=False,
179186
)
180-
await executer.evaluate()
181-
return executer.get_result()
187+
try:
188+
await executer.evaluate()
189+
except _EvaluationCasesFailed:
190+
pass
191+
result = executer.get_result()
192+
if result is None:
193+
result = EvaluateResult()
194+
return result
182195
finally:
183196
os.unlink(eval_config_path)
184197

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import os
5+
import tempfile
6+
from pathlib import Path
7+
8+
import pytest
9+
10+
_HERE = Path(__file__).resolve().parent
11+
_EXAMPLE_ROOT = _HERE.parent
12+
13+
from ..pipeline import EvalOptimizePipeline # noqa: E402
14+
15+
16+
@pytest.mark.asyncio
17+
async def test_trace_mode_accept():
18+
pipeline_json = {
19+
"mode": "trace",
20+
"baseline_prompt_path": str(_EXAMPLE_ROOT / "prompts" / "baseline_system.md"),
21+
"candidate_prompt_path": str(_EXAMPLE_ROOT / "prompts" / "optimized_system.md"),
22+
"train_baseline_evalset": str(_EXAMPLE_ROOT / "evalsets" / "train_baseline.evalset.json"),
23+
"train_candidate_evalset": str(_EXAMPLE_ROOT / "evalsets" / "train_candidate.evalset.json"),
24+
"val_baseline_evalset": str(_EXAMPLE_ROOT / "evalsets" / "val_baseline.evalset.json"),
25+
"val_candidate_evalset": str(_EXAMPLE_ROOT / "evalsets" / "val_candidate.evalset.json"),
26+
"output_dir": str(tempfile.mkdtemp()),
27+
"evaluate": {
28+
"metrics": [
29+
{
30+
"metric_name": "final_response_avg_score",
31+
"threshold": 1.0,
32+
"criterion": {"final_response": {"text": {"match": "contains", "case_insensitive": True}}},
33+
}
34+
],
35+
"num_runs": 1,
36+
},
37+
"gate": {
38+
"min_improvement": 0.0,
39+
"allow_new_fails": True,
40+
},
41+
"seed": 42,
42+
}
43+
44+
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
45+
json.dump(pipeline_json, f)
46+
config_path = f.name
47+
48+
try:
49+
pipeline = EvalOptimizePipeline.from_config(config_path)
50+
result = await pipeline.run()
51+
52+
assert result.mode == "trace"
53+
assert result.gate_decision == "ACCEPT"
54+
55+
output_dir = pipeline_json["output_dir"]
56+
assert os.path.isfile(os.path.join(output_dir, "optimization_report.json"))
57+
assert os.path.isfile(os.path.join(output_dir, "optimization_report.md"))
58+
59+
baseline_train = result.baseline["train"]
60+
candidate_train = result.candidate["train"]
61+
assert baseline_train.pass_rate == pytest.approx(0.333, abs=0.01)
62+
assert candidate_train.pass_rate == pytest.approx(0.333, abs=0.01)
63+
64+
baseline_val = result.baseline["val"]
65+
candidate_val = result.candidate["val"]
66+
assert baseline_val.pass_rate == pytest.approx(0.333, abs=0.01)
67+
assert candidate_val.pass_rate == pytest.approx(0.333, abs=0.01)
68+
69+
assert "case_train_optimizable" in result.delta.train.newly_passing
70+
assert "case_train_regression" in result.delta.train.newly_failing
71+
assert "case_val_improves" in result.delta.val.newly_passing
72+
assert "case_val_regression" in result.delta.val.newly_failing
73+
74+
assert result.failure_attribution.failed_cases >= 1
75+
assert len(result.failure_attribution.categories) >= 1
76+
77+
finally:
78+
os.unlink(config_path)
79+
80+
81+
@pytest.mark.asyncio
82+
async def test_trace_mode_reject():
83+
pipeline_json = {
84+
"mode": "trace",
85+
"baseline_prompt_path": str(_EXAMPLE_ROOT / "prompts" / "baseline_system.md"),
86+
"candidate_prompt_path": str(_EXAMPLE_ROOT / "prompts" / "optimized_system.md"),
87+
"train_baseline_evalset": str(_EXAMPLE_ROOT / "evalsets" / "train_baseline.evalset.json"),
88+
"train_candidate_evalset": str(_EXAMPLE_ROOT / "evalsets" / "train_candidate.evalset.json"),
89+
"val_baseline_evalset": str(_EXAMPLE_ROOT / "evalsets" / "val_baseline.evalset.json"),
90+
"val_candidate_evalset": str(_EXAMPLE_ROOT / "evalsets" / "val_candidate.evalset.json"),
91+
"output_dir": str(tempfile.mkdtemp()),
92+
"evaluate": {
93+
"metrics": [
94+
{
95+
"metric_name": "final_response_avg_score",
96+
"threshold": 1.0,
97+
"criterion": {"final_response": {"text": {"match": "contains", "case_insensitive": True}}},
98+
}
99+
],
100+
"num_runs": 1,
101+
},
102+
"gate": {
103+
"min_improvement": 0.0,
104+
"allow_new_fails": False,
105+
},
106+
"seed": 42,
107+
}
108+
109+
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
110+
json.dump(pipeline_json, f)
111+
config_path = f.name
112+
113+
try:
114+
pipeline = EvalOptimizePipeline.from_config(config_path)
115+
result = await pipeline.run()
116+
117+
assert result.mode == "trace"
118+
assert result.gate_decision == "REJECT"
119+
assert any("newly failing" in r.lower() for r in result.gate_reasons)
120+
121+
output_dir = pipeline_json["output_dir"]
122+
assert os.path.isfile(os.path.join(output_dir, "optimization_report.json"))
123+
assert os.path.isfile(os.path.join(output_dir, "optimization_report.md"))
124+
125+
finally:
126+
os.unlink(config_path)
127+
128+
129+
@pytest.mark.asyncio
130+
async def test_trace_mode_overfitting():
131+
with tempfile.TemporaryDirectory() as tmpdir:
132+
train_base_path = os.path.join(tmpdir, "train_base.json")
133+
train_cand_path = os.path.join(tmpdir, "train_cand.json")
134+
val_base_path = os.path.join(tmpdir, "val_base.json")
135+
val_cand_path = os.path.join(tmpdir, "val_cand.json")
136+
137+
base_evalset = lambda eid, actual: {
138+
"eval_set_id": eid,
139+
"eval_cases": [
140+
{
141+
"eval_id": "case_1",
142+
"eval_mode": "trace",
143+
"conversation": [{
144+
"invocation_id": "t1",
145+
"user_content": {"parts": [{"text": "q"}], "role": "user"},
146+
"final_response": {"parts": [{"text": "答案:42"}], "role": "model"},
147+
}],
148+
"actual_conversation": [{
149+
"invocation_id": "t1",
150+
"user_content": {"parts": [{"text": "q"}], "role": "user"},
151+
"final_response": {"parts": [{"text": actual}], "role": "model"},
152+
}],
153+
"session_input": {"app_name": "test", "user_id": "u", "state": {}},
154+
}
155+
]
156+
}
157+
158+
with open(train_base_path, "w") as f:
159+
json.dump(base_evalset("train_base", "答案:99"), f)
160+
with open(train_cand_path, "w") as f:
161+
json.dump(base_evalset("train_cand", "答案:42"), f)
162+
with open(val_base_path, "w") as f:
163+
json.dump(base_evalset("val_base", "答案:42"), f)
164+
with open(val_cand_path, "w") as f:
165+
json.dump(base_evalset("val_cand", "答案:99"), f)
166+
167+
pipeline_json = {
168+
"mode": "trace",
169+
"train_baseline_evalset": train_base_path,
170+
"train_candidate_evalset": train_cand_path,
171+
"val_baseline_evalset": val_base_path,
172+
"val_candidate_evalset": val_cand_path,
173+
"output_dir": tmpdir,
174+
"evaluate": {
175+
"metrics": [{
176+
"metric_name": "final_response_avg_score",
177+
"threshold": 1.0,
178+
"criterion": {"final_response": {"text": {"match": "contains", "case_insensitive": True}}},
179+
}],
180+
"num_runs": 1,
181+
},
182+
"gate": {"min_improvement": -1.0, "allow_new_fails": True},
183+
"seed": 42,
184+
}
185+
186+
config_path = os.path.join(tmpdir, "config.json")
187+
with open(config_path, "w") as f:
188+
json.dump(pipeline_json, f)
189+
190+
pipeline = EvalOptimizePipeline.from_config(config_path)
191+
result = await pipeline.run()
192+
193+
assert result.overfitting_warning is True
194+
assert result.delta.train_pass_rate_delta > 0
195+
assert result.delta.val_pass_rate_delta < 0
196+
assert result.gate_decision == "ACCEPT"

0 commit comments

Comments
 (0)