Skip to content

Commit e7eb97e

Browse files
committed
wip: add eval optimization loop example
1 parent 73655ab commit e7eb97e

10 files changed

Lines changed: 1681 additions & 0 deletions

File tree

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Evaluation + Optimization Loop
2+
3+
This example demonstrates a reproducible pipeline that connects evaluation,
4+
failure attribution, prompt optimization, validation regression checks, gate
5+
decisions, and audit reports.
6+
7+
It is intentionally different from `ci_integration/`: CI integration focuses on
8+
PR gates and nightly write-back, while this example focuses on the complete
9+
review loop before deciding whether a candidate prompt is worth accepting.
10+
11+
## Run
12+
13+
The default mode is deterministic and does not require an API key:
14+
15+
```bash
16+
cd examples/optimization/eval_optimize_loop
17+
PYTHONPATH=../../.. python run_pipeline.py --mode fake
18+
```
19+
20+
If your environment only exposes `python3`, use:
21+
22+
```bash
23+
PYTHONPATH=../../.. python3 run_pipeline.py --mode fake
24+
```
25+
26+
To refresh the committed sample reports:
27+
28+
```bash
29+
PYTHONPATH=../../.. python run_pipeline.py --mode fake --update-sample-outputs
30+
```
31+
32+
Trace replay mode also runs without model credentials. It first records fake
33+
outputs into generated `eval_mode: "trace"` evalsets, then evaluates those
34+
`actual_conversation` records without invoking `call_agent`:
35+
36+
```bash
37+
PYTHONPATH=../../.. python run_pipeline.py --mode trace
38+
```
39+
40+
The optional real optimizer path delegates candidate search to
41+
`AgentOptimizer.optimize` and requires the normal optimization dependencies and
42+
model environment variables:
43+
44+
```bash
45+
export TRPC_AGENT_API_KEY="<your-key>"
46+
export TRPC_AGENT_BASE_URL="<your-endpoint>"
47+
export TRPC_AGENT_MODEL_NAME="<your-model>"
48+
PYTHONPATH=../../.. python run_pipeline.py --mode optimizer
49+
```
50+
51+
## Inputs
52+
53+
| File | Role |
54+
| --- | --- |
55+
| `train.evalset.json` | Training split used for baseline scoring and optimization feedback. |
56+
| `val.evalset.json` | Validation split used for regression and gate decisions. |
57+
| `optimizer.json` | Shared metric, optimizer, fake candidate, and gate configuration. |
58+
| `agent/prompts/system.md` | Baseline prompt source registered as `TargetPrompt("system_prompt")`. |
59+
60+
The fake mode still uses `AgentEvaluator` for all train and validation scoring.
61+
Only candidate generation is deterministic, so the example can run in CI or on a
62+
laptop without model credentials.
63+
64+
## Outputs
65+
66+
Each run writes to `runs/<mode_timestamp>/`:
67+
68+
| File or directory | Contents |
69+
| --- | --- |
70+
| `optimization_report.json` | Machine-readable baseline, candidate, delta, attribution, gate, and audit payload. |
71+
| `optimization_report.md` | Human-readable report for review. |
72+
| `candidate_prompts/` | Candidate prompt text per target field. |
73+
| `eval_*` | Raw `AgentEvaluator` result files for every phase. |
74+
| `config.snapshot.json` | Reproducible copy of the input config. |
75+
| `eval_metrics.snapshot.json` | Evaluator-compatible metric config extracted from `optimizer.json.evaluate`. |
76+
77+
Committed examples live in `sample_outputs/`.
78+
79+
## Case Coverage
80+
81+
The six sample cases cover the required outcomes:
82+
83+
| Case | Split | Expected behavior |
84+
| --- | --- | --- |
85+
| `train_refund_double_charge` | train | Candidate fixes a baseline failure. |
86+
| `train_password_reset` | train | Already passing; candidate is unchanged. |
87+
| `train_legacy_sync` | train | Candidate does not help. |
88+
| `val_vip_refund` | validation | Candidate creates a new pass. |
89+
| `val_plan_question` | validation | Candidate has no effect. |
90+
| `val_checkout_outage` | validation | Candidate regresses a critical case. |
91+
92+
The default fake candidate improves train score but does not improve validation
93+
overall because it also introduces a new hard fail. The gate therefore rejects
94+
the candidate and records the overfitting reason.
95+
96+
## Design Notes
97+
98+
The pipeline treats evaluation results as the source of truth and converts
99+
`AgentEvaluator.get_executer(...).get_result()` into a stable per-case report.
100+
For each split, it records score, pass/fail status, normalized actual and
101+
expected final responses, metric details, and a short trace summary. Failure
102+
attribution is deterministic: invalid JSON or missing keys are classified as
103+
format violations; category mismatches indicate knowledge-recall gaps; priority
104+
mismatches are parameter errors; action mismatches are final-response errors.
105+
This keeps fake mode explainable and also gives real optimizer runs a fallback
106+
when no LLM judge reason is available.
107+
108+
Candidate prompts are validated on the held-out validation set before any source
109+
prompt is updated. The configurable gate checks minimum validation improvement,
110+
new hard failures, critical-case regressions, and cost budget. It also includes
111+
an explicit overfitting guard: when training improves but validation does not,
112+
the candidate is rejected even if some individual validation case improves. All
113+
decisions include concrete reasons, so a reviewer can see whether rejection came
114+
from score, hard-fail, critical-case, cost, or overfitting policy.
115+
116+
Auditability is handled by snapshotting config, writing candidate prompt files,
117+
preserving raw evaluator outputs, and emitting both JSON and Markdown reports.
118+
The JSON report is suitable for CI parsing, while the Markdown report summarizes
119+
score deltas, per-case outcomes, failure clusters, and the final accept/reject
120+
decision for humans. Fake mode makes this loop reproducible without API keys;
121+
real mode can swap in `AgentOptimizer` while keeping the same validation, gate,
122+
and report machinery. The source prompt is never overwritten by this example;
123+
reviewers can inspect `candidate_prompts/` and decide whether to copy or submit
124+
the change through a normal PR. Hidden samples can reuse the same gate because
125+
all decisions are based on held-out validation deltas rather than train-set
126+
scores alone. Trace-style replay can also be added by replacing `call_agent`
127+
with eval cases that provide `actual_conversation`, while the attribution, delta,
128+
gate, and report stages remain unchanged.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"""Agent entry points for the eval-optimize-loop example."""
2+
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""Deterministic fake agent for the eval-optimize-loop example.
7+
8+
The example's default mode must run without an API key, so this module exposes
9+
an async ``call_agent`` that derives behavior from the current prompt text on
10+
disk. The pipeline still uses AgentEvaluator for scoring; this module only
11+
stands in for the model.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import json
17+
import re
18+
from pathlib import Path
19+
20+
21+
SYSTEM_PROMPT_PATH = Path(__file__).parent / "prompts" / "system.md"
22+
23+
_SPACE_RE = re.compile(r"\s+")
24+
25+
26+
def _compact_json(payload: dict[str, str]) -> str:
27+
return json.dumps(payload, sort_keys=True, separators=(",", ":"))
28+
29+
30+
def normalize_json_text(raw: str) -> str:
31+
"""Normalize JSON-like model output for stable exact matching."""
32+
try:
33+
parsed = json.loads(raw)
34+
except json.JSONDecodeError:
35+
return _SPACE_RE.sub(" ", raw.strip())
36+
return _compact_json(parsed)
37+
38+
39+
def _prompt_flags() -> dict[str, bool]:
40+
prompt = SYSTEM_PROMPT_PATH.read_text(encoding="utf-8").lower()
41+
return {
42+
"refund_rule": "treat double charge" in prompt or "vip refund requests" in prompt,
43+
"keep_outage_p1": "p1 for urgent production outages" in prompt,
44+
"keep_plan_p3": "p3 for low-risk informational requests" in prompt,
45+
"overfit_payment_outage": "overfit_payment_outage" in prompt,
46+
}
47+
48+
49+
def answer_for_query(query: str) -> str:
50+
"""Return the fake model answer for one support ticket."""
51+
text = query.lower()
52+
flags = _prompt_flags()
53+
54+
if "production checkout outage" in text:
55+
if flags["overfit_payment_outage"]:
56+
return _compact_json({
57+
"category": "billing",
58+
"priority": "p2",
59+
"action": "refund_review",
60+
})
61+
priority = "p1" if flags["keep_outage_p1"] else "p2"
62+
return _compact_json({
63+
"category": "technical",
64+
"priority": priority,
65+
"action": "escalate",
66+
})
67+
68+
if "vip customer" in text and "double charged" in text:
69+
if flags["refund_rule"]:
70+
return _compact_json({
71+
"category": "billing",
72+
"priority": "p1",
73+
"action": "refund_review",
74+
})
75+
return _compact_json({
76+
"category": "account",
77+
"priority": "p2",
78+
"action": "answer",
79+
})
80+
81+
if "double charged" in text or "refund" in text:
82+
if flags["refund_rule"]:
83+
return _compact_json({
84+
"category": "billing",
85+
"priority": "p2",
86+
"action": "refund_review",
87+
})
88+
return _compact_json({
89+
"category": "account",
90+
"priority": "p2",
91+
"action": "answer",
92+
})
93+
94+
if "password reset" in text or "email address" in text:
95+
return _compact_json({
96+
"category": "account",
97+
"priority": "p3",
98+
"action": "answer",
99+
})
100+
101+
if "plan comparison" in text or "pricing tiers" in text:
102+
priority = "p3" if flags["keep_plan_p3"] else "p2"
103+
return _compact_json({
104+
"category": "billing",
105+
"priority": priority,
106+
"action": "answer",
107+
})
108+
109+
if "mobile app crashes" in text:
110+
return _compact_json({
111+
"category": "technical",
112+
"priority": "p2",
113+
"action": "troubleshooting",
114+
})
115+
116+
if "legacy desktop sync" in text:
117+
return _compact_json({
118+
"category": "technical",
119+
"priority": "p2",
120+
"action": "troubleshooting",
121+
})
122+
123+
return _compact_json({
124+
"category": "technical",
125+
"priority": "p2",
126+
"action": "troubleshooting",
127+
})
128+
129+
130+
async def call_agent(query: str) -> str:
131+
"""AgentEvaluator / AgentOptimizer compatible black-box entry point."""
132+
return answer_for_query(query)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
You are a support triage assistant.
2+
3+
Return one compact JSON object with exactly these keys:
4+
- category
5+
- priority
6+
- action
7+
8+
Known categories:
9+
- account
10+
- billing
11+
- technical
12+
13+
Known priorities:
14+
- p1 for urgent production outages
15+
- p2 for normal user-impacting issues
16+
- p3 for low-risk informational requests
17+
18+
Known actions:
19+
- escalate
20+
- refund_review
21+
- troubleshooting
22+
- answer
23+
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"evaluate": {
3+
"metrics": [
4+
{
5+
"metric_name": "final_response_avg_score",
6+
"threshold": 1.0,
7+
"criterion": {
8+
"final_response": {
9+
"text": {
10+
"match": "exact",
11+
"case_insensitive": false
12+
}
13+
}
14+
}
15+
}
16+
],
17+
"num_runs": 1
18+
},
19+
"gate": {
20+
"min_validation_score_delta": 0.1,
21+
"allow_new_hard_fail": false,
22+
"critical_case_ids": ["val_vip_refund", "val_checkout_outage"],
23+
"max_cost_usd": 0.01
24+
},
25+
"fake_optimizer": {
26+
"candidate_id": "candidate_refund_rule_overfit",
27+
"seed": 42,
28+
"notes": "Adds refund handling learned from train failures but intentionally omits the outage guard to demonstrate overfitting rejection."
29+
},
30+
"optimize": {
31+
"eval_case_parallelism": 1,
32+
"stop": {
33+
"required_metrics": "all"
34+
},
35+
"algorithm": {
36+
"name": "gepa_reflective",
37+
"seed": 42,
38+
"reflection_lm": {
39+
"model_name": "${TRPC_AGENT_MODEL_NAME}",
40+
"base_url": "${TRPC_AGENT_BASE_URL}",
41+
"api_key": "${TRPC_AGENT_API_KEY}",
42+
"generation_config": {
43+
"max_tokens": 2048,
44+
"temperature": 0.4
45+
}
46+
},
47+
"candidate_selection_strategy": "pareto",
48+
"module_selector": "round_robin",
49+
"frontier_type": "instance",
50+
"reflection_minibatch_size": 3,
51+
"reflection_history_top_k": 2,
52+
"skip_perfect_score": false,
53+
"use_merge": false,
54+
"max_metric_calls": 18,
55+
"score_threshold": 1.0,
56+
"max_iterations_without_improvement": 2
57+
}
58+
}
59+
}
60+

0 commit comments

Comments
 (0)