Skip to content

Commit eaed59f

Browse files
committed
test(examples): prove issue 91 acceptance thresholds
1 parent 41d9e89 commit eaed59f

4 files changed

Lines changed: 479 additions & 0 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{"error_code": "json_parse_failure", "evidence": "JSON parser failed", "expected": "format_violation"},
3+
{"error_code": "required_key_missing", "evidence": "missing key status", "expected": "final_response_mismatch"},
4+
{"error_code": "exact_answer_mismatch", "evidence": "expected YES", "expected": "final_response_mismatch"},
5+
{"error_code": "tool_call_error", "evidence": "expected lookup", "expected": "tool_call_error"},
6+
{"error_code": "parameter_error", "evidence": "id mismatch", "expected": "parameter_error"},
7+
{"error_code": "missing_rubric_terms", "evidence": "missing latency", "expected": "llm_rubric_not_met"},
8+
{"error_code": "knowledge_recall_insufficient", "evidence": "missing doc-a", "expected": "knowledge_recall_insufficient"},
9+
{"error_code": "forbidden_pattern", "evidence": "forbidden JSON", "expected": "format_violation"}
10+
]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
{"id": "safe_gain", "expected": true, "train": [0, 1, 1], "candidate_train": [1, 1, 1], "validation": [0, 1, 1], "candidate_validation": [1, 1, 1], "protected": [], "max_drop": 0.0, "cost": 0.02, "budget": 1.0},
3+
{"id": "no_gain", "expected": false, "train": [1, 1, 1], "candidate_train": [1, 1, 1], "validation": [1, 1, 1], "candidate_validation": [1, 1, 1], "protected": [], "max_drop": 0.0, "cost": 0.02, "budget": 1.0},
4+
{"id": "overfit", "expected": false, "train": [0, 0, 1], "candidate_train": [1, 1, 1], "validation": [0, 1, 1], "candidate_validation": [1, 0, 0], "protected": [], "max_drop": 1.0, "cost": 0.02, "budget": 1.0},
5+
{"id": "validation_regression", "expected": false, "train": [1, 1, 1], "candidate_train": [1, 1, 1], "validation": [1, 1, 1], "candidate_validation": [1, 1, 0], "protected": [], "max_drop": 1.0, "cost": 0.02, "budget": 1.0},
6+
{"id": "protected_regression", "expected": false, "train": [0, 1, 1], "candidate_train": [1, 1, 1], "validation": [1, 0, 0], "candidate_validation": [0, 1, 1], "protected": ["v0"], "max_drop": 1.0, "cost": 0.02, "budget": 1.0},
7+
{"id": "new_hard_fail", "expected": false, "train": [0, 1, 1], "candidate_train": [1, 1, 1], "validation": [1, 0, 0], "candidate_validation": [0, 1, 1], "protected": [], "max_drop": 1.0, "cost": 0.02, "budget": 1.0},
8+
{"id": "soft_to_hard", "expected": false, "train": [0, 1, 1], "candidate_train": [1, 1, 1], "validation": [0.5, 0, 0], "candidate_validation": [0, 1, 1], "protected": [], "max_drop": 1.0, "cost": 0.02, "budget": 1.0},
9+
{"id": "single_case_drop", "expected": false, "train": [0, 1, 1], "candidate_train": [1, 1, 1], "validation": [0.7, 0, 0], "candidate_validation": [0.5, 1, 1], "protected": [], "max_drop": 0.1, "cost": 0.02, "budget": 1.0},
10+
{"id": "over_budget", "expected": false, "train": [0, 1, 1], "candidate_train": [1, 1, 1], "validation": [0, 1, 1], "candidate_validation": [1, 1, 1], "protected": [], "max_drop": 0.0, "cost": 1.1, "budget": 1.0},
11+
{"id": "safe_gain_with_soft_failure", "expected": true, "train": [0, 1, 1], "candidate_train": [1, 1, 1], "validation": [0.5, 0, 1], "candidate_validation": [0.5, 1, 1], "protected": ["v0"], "max_drop": 0.0, "cost": 0.02, "budget": 1.0}
12+
]
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import shutil
5+
import time
6+
from pathlib import Path
7+
from typing import Any
8+
9+
from examples.optimization.eval_optimize_loop.eval_loop.attribution import attribute_failure
10+
from examples.optimization.eval_optimize_loop.eval_loop.gate import AcceptanceGate
11+
from examples.optimization.eval_optimize_loop.eval_loop.report import compute_case_deltas
12+
from examples.optimization.eval_optimize_loop.eval_loop.schemas import CaseResult
13+
from examples.optimization.eval_optimize_loop.eval_loop.schemas import CostSummary
14+
from examples.optimization.eval_optimize_loop.eval_loop.schemas import EvalResult
15+
from examples.optimization.eval_optimize_loop.eval_loop.schemas import GateDecision
16+
from examples.optimization.eval_optimize_loop.run_pipeline import DEFAULT_PROMPT
17+
from examples.optimization.eval_optimize_loop.run_pipeline import DEFAULT_TRAIN
18+
from examples.optimization.eval_optimize_loop.run_pipeline import DEFAULT_VAL
19+
from examples.optimization.eval_optimize_loop.run_pipeline import run_pipeline
20+
21+
FIXTURES = Path(__file__).parent / "fixtures"
22+
23+
24+
def test_holdout_gate_fixture_has_ten_unique_scenarios() -> None:
25+
scenarios = _load_fixture("holdout_gate_cases.json")
26+
27+
assert len(scenarios) >= 10
28+
scenario_ids = [str(scenario["id"]) for scenario in scenarios]
29+
assert len(scenario_ids) == len(set(scenario_ids))
30+
expected_labels = {bool(scenario["expected"]) for scenario in scenarios}
31+
assert expected_labels == {False, True}
32+
logical_inputs = [
33+
json.dumps(
34+
{
35+
key: value
36+
for key, value in scenario.items() if key not in {"id", "expected"}
37+
},
38+
ensure_ascii=False,
39+
sort_keys=True,
40+
separators=(",", ":"),
41+
) for scenario in scenarios
42+
]
43+
assert len(logical_inputs) == len(set(logical_inputs))
44+
45+
46+
def test_gate_thresholds_reject_always_false_predictions() -> None:
47+
scenarios = _load_fixture("holdout_gate_cases.json")
48+
expected = [bool(scenario["expected"]) for scenario in scenarios]
49+
50+
assert not _gate_acceptance_thresholds_met(
51+
expected,
52+
predictions=[False] * len(expected),
53+
)
54+
55+
56+
def test_attribution_fixture_has_eight_unique_evidence_inputs() -> None:
57+
scenarios = _load_fixture("attribution_cases.json")
58+
59+
assert len(scenarios) >= 8
60+
evidence_inputs = [(str(scenario["error_code"]), str(scenario["evidence"])) for scenario in scenarios]
61+
assert len(evidence_inputs) == len(set(evidence_inputs))
62+
error_codes = [str(scenario["error_code"]) for scenario in scenarios]
63+
assert len(error_codes) == len(set(error_codes))
64+
65+
66+
def test_holdout_gate_decision_accuracy_is_at_least_eighty_percent() -> None:
67+
scenarios = _load_fixture("holdout_gate_cases.json")
68+
expected_labels: list[bool] = []
69+
predictions: list[bool] = []
70+
for scenario in scenarios:
71+
expected_labels.append(bool(scenario["expected"]))
72+
inputs = {key: value for key, value in scenario.items() if key != "expected"}
73+
decision = _decision_for_scenario(inputs)
74+
predictions.append(decision.accepted)
75+
76+
overall_accuracy, positive_recall, negative_specificity = (_gate_classification_rates(expected_labels, predictions))
77+
assert overall_accuracy >= 0.80
78+
assert positive_recall >= 0.80
79+
assert negative_specificity >= 0.80
80+
81+
82+
def test_independent_attribution_accuracy_is_at_least_seventy_five_percent() -> None:
83+
scenarios = _load_fixture("attribution_cases.json")
84+
inputs = [{key: value for key, value in scenario.items() if key != "expected"} for scenario in scenarios]
85+
predictions = [_attribute_scenario(item) for item in inputs]
86+
87+
correct = sum(prediction[0] == scenario["expected"]
88+
for prediction, scenario in zip(predictions, scenarios, strict=True))
89+
assert correct / len(scenarios) >= 0.75
90+
assert all(prediction[1] and prediction[2] for prediction in predictions)
91+
92+
93+
def test_fake_trace_pipeline_finishes_under_three_minutes(tmp_path: Path) -> None:
94+
inputs_dir = tmp_path / "inputs"
95+
inputs_dir.mkdir()
96+
train_path = inputs_dir / "train.evalset.json"
97+
val_path = inputs_dir / "val.evalset.json"
98+
prompt_path = inputs_dir / "baseline_system_prompt.txt"
99+
optimizer_config_path = inputs_dir / "optimizer.json"
100+
shutil.copyfile(DEFAULT_TRAIN, train_path)
101+
shutil.copyfile(DEFAULT_VAL, val_path)
102+
shutil.copyfile(DEFAULT_PROMPT, prompt_path)
103+
optimizer_config_path.write_text(
104+
json.dumps(
105+
{
106+
"seed": 91,
107+
"optimizer": {},
108+
"metrics": {},
109+
"gate": {
110+
"min_val_score_improvement": 0.01,
111+
"allow_new_hard_fail": False,
112+
"protected_case_ids": [],
113+
"max_score_drop_per_case": 0.0,
114+
"max_total_cost": 1.0,
115+
},
116+
},
117+
indent=2,
118+
) + "\n",
119+
encoding="utf-8",
120+
)
121+
122+
started = time.perf_counter()
123+
report = run_pipeline(
124+
train_path=train_path,
125+
val_path=val_path,
126+
optimizer_config_path=optimizer_config_path,
127+
prompt_path=prompt_path,
128+
output_dir=tmp_path / "out",
129+
mode="fake",
130+
trace=True,
131+
run_id="performance",
132+
)
133+
elapsed = time.perf_counter() - started
134+
135+
assert elapsed < 180
136+
assert 0 < report.audit["duration_seconds"] <= elapsed
137+
138+
139+
def _load_fixture(name: str) -> list[dict[str, Any]]:
140+
return json.loads((FIXTURES / name).read_text(encoding="utf-8"))
141+
142+
143+
def _gate_acceptance_thresholds_met(
144+
expected: list[bool],
145+
predictions: list[bool],
146+
) -> bool:
147+
rates = _gate_classification_rates(expected, predictions)
148+
return all(rate >= 0.80 for rate in rates)
149+
150+
151+
def _gate_classification_rates(
152+
expected: list[bool],
153+
predictions: list[bool],
154+
) -> tuple[float, float, float]:
155+
paired = list(zip(predictions, expected, strict=True))
156+
positive_predictions = [prediction for prediction, label in paired if label]
157+
negative_predictions = [prediction for prediction, label in paired if not label]
158+
assert paired
159+
assert positive_predictions
160+
assert negative_predictions
161+
overall_accuracy = sum(prediction == label for prediction, label in paired) / len(paired)
162+
positive_recall = sum(positive_predictions) / len(positive_predictions)
163+
negative_specificity = (sum(not prediction for prediction in negative_predictions) / len(negative_predictions))
164+
return overall_accuracy, positive_recall, negative_specificity
165+
166+
167+
def _eval(
168+
prompt_id: str,
169+
split: str,
170+
scores: list[float],
171+
*,
172+
cost: float = 0.0,
173+
) -> EvalResult:
174+
cases = [
175+
CaseResult(
176+
case_id=f"{split[0]}{index}",
177+
split=split,
178+
score=float(score),
179+
passed=score >= 1.0,
180+
output=str(score),
181+
metrics={"holdout": float(score)},
182+
hard_failed=score == 0.0,
183+
) for index, score in enumerate(scores)
184+
]
185+
return EvalResult(
186+
prompt_id=prompt_id,
187+
split=split,
188+
score=sum(scores) / len(scores),
189+
passed=all(case.passed for case in cases),
190+
cost=cost,
191+
cases=cases,
192+
)
193+
194+
195+
def _decision_for_scenario(scenario: dict[str, Any]) -> GateDecision:
196+
assert "expected" not in scenario
197+
baseline_train = _eval("baseline", "train", scenario["train"])
198+
baseline_validation = _eval("baseline", "validation", scenario["validation"])
199+
candidate_train = _eval("candidate", "train", scenario["candidate_train"])
200+
candidate_validation = _eval(
201+
"candidate",
202+
"validation",
203+
scenario["candidate_validation"],
204+
)
205+
deltas = compute_case_deltas(
206+
candidate_id="candidate",
207+
baseline_train=baseline_train,
208+
baseline_validation=baseline_validation,
209+
candidate_train=candidate_train,
210+
candidate_validation=candidate_validation,
211+
)
212+
gate = AcceptanceGate({
213+
"min_val_score_improvement": 0.01,
214+
"allow_new_hard_fail": False,
215+
"protected_case_ids": scenario["protected"],
216+
"max_score_drop_per_case": scenario["max_drop"],
217+
"max_total_cost": scenario["budget"],
218+
})
219+
incurred_cost = float(scenario["cost"])
220+
return gate.decide(
221+
candidate_id="candidate",
222+
baseline_train=baseline_train,
223+
baseline_validation=baseline_validation,
224+
candidate_train=candidate_train,
225+
candidate_validation=candidate_validation,
226+
deltas=deltas,
227+
cumulative_cost=incurred_cost,
228+
cost_summary=CostSummary(
229+
optimizer=incurred_cost,
230+
total=incurred_cost,
231+
complete=True,
232+
),
233+
)
234+
235+
236+
def _attribute_scenario(scenario: dict[str, Any]) -> tuple[str, str, str]:
237+
assert "expected" not in scenario
238+
return attribute_failure(scenario["error_code"], scenario["evidence"])

0 commit comments

Comments
 (0)