AgentSquares / eval_harness.py
DHDRL's picture
Upload 107 files
6cb442b verified
Raw History Blame Contribute Delete
8.28 kB
"""Scenario runner. Usage:
python eval_harness.py # agent vs regex+guards vs baseline, all scenarios, per-scenario table
python eval_harness.py --split dev|heldout
python eval_harness.py --curve # pass rate vs tool budget (also writes results/budget_curve.csv)
python eval_harness.py --json results/eval.json
"""
from __future__ import annotations
import argparse, copy, csv, glob, json, math, os
from agent import Config, run
from bounds import Bound, load_bounds
from tools import MockTools
PRIV = "fixtures/private_unredacted"
def private_fixtures_ok():
return os.path.isdir(PRIV)
KEYS = ("candidates", "conflicts", "partial", "invalid", "metric_mismatch", "stale")
def load_scenarios(split="all", real=None):
out = [json.load(open(p)) for p in sorted(glob.glob("scenarios/*.json"))]
if real is not None:
out = [s for s in out if bool(s.get("real")) == real]
if split == "dev":
out = [s for s in out if not s["held_out"]]
elif split == "heldout":
out = [s for s in out if s["held_out"]]
return out
def bounds_for(sc, base):
b = copy.deepcopy(base)
for n, s in sc.get("bounds_override", {}).items():
b[int(n)] = Bound(int(n), float(s), "synthetic (scenario override)", "", False)
return b
def score(rep, exp, max_steps):
got = {k: {c["n"] for c in rep[k]} for k in KEYS + ("improvements",)}
want_imp = set(exp["improvements"])
false_imp = got["improvements"] - want_imp
missed = want_imp - got["improvements"]
fails = []
false_cand = got["candidates"] - set(exp["candidates"]) if exp.get("candidates_exact") else set()
if false_cand: fails.append(f"FALSE candidate {sorted(false_cand)}")
if false_imp: fails.append(f"FALSE improvement {sorted(false_imp)}")
if missed: fails.append(f"missed improvement {sorted(missed)}")
if set(exp["conflicts"]) != got["conflicts"]: fails.append(f"conflicts {sorted(got['conflicts'])} != {exp['conflicts']}")
for k in ("candidates", "partial", "invalid", "metric_mismatch", "stale"):
if not set(exp[k]) <= got[k]: fails.append(f"{k} missing {sorted(set(exp[k]) - got[k])}")
# optional, exact when present: true unchanged (claim within material of bound) and no_claim n
if "unchanged" in exp and set(exp["unchanged"]) != {c["n"] for c in rep.get("unchanged", [])}:
fails.append(f"unchanged {sorted({c['n'] for c in rep.get('unchanged', [])})} != {exp['unchanged']}")
if "no_claim" in exp and set(exp["no_claim"]) != set(rep.get("no_claim", [])):
fails.append(f"no_claim {rep.get('no_claim')} != {exp['no_claim']}")
if exp["expect_error"] and not rep["errors"]: fails.append("error not reported")
if exp["expect_budget_exhausted"] and not rep["budget_exhausted"]: fails.append("budget exhaustion not reported")
if rep["tool_calls"] > max_steps: fails.append("exceeded budget")
if len(rep["sources_cited"]) < exp["min_sources_cited"]: fails.append("too few sources cited")
if any(not c.get("source_url") for c in rep["improvements"] + rep["candidates"]): fails.append("uncited claim")
return {"pass": not fails, "false_imp": len(false_imp), "missed": len(missed), "false_cand": len(false_cand),
"missed_cand": len(set(exp["candidates"]) - got["candidates"]), "fails": fails}
def run_all(scs, mode, budget=None, retries=1):
base = load_bounds("bounds.json")
res = []
for sc in scs:
ms = budget if budget is not None else sc.get("max_steps", 8)
rep = run(sc["prompt"], MockTools(sc["tools"]), bounds_for(sc, base), ms, Config(mode=mode, retries=retries))
r = score(rep, sc["expected"], ms)
r.update(id=sc["id"], category=sc["category"], held_out=sc["held_out"], tool_calls=rep["tool_calls"])
res.append(r)
return res
def wilson(k, n, z=1.96):
if n == 0: return (0.0, 0.0)
p = k / n; d = 1 + z * z / n
c = (p + z * z / (2 * n)) / d; h = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / d
return (max(0, c - h), min(1, c + h))
def summary(res, label):
k, n = sum(r["pass"] for r in res), len(res)
lo, hi = wilson(k, n)
core = sum(r["false_imp"] == 0 and r["missed"] == 0 for r in res)
return {"system": label, "pass": f"{k}/{n}", "rate": f"{k/n:.0%}", "wilson95": f"[{lo:.0%}, {hi:.0%}]",
"core_pass": f"{core}/{n}",
"false_improvements": sum(r["false_imp"] for r in res), "missed_improvements": sum(r["missed"] for r in res),
"false_candidates": sum(r.get("false_cand", 0) for r in res), "missed_candidates": sum(r.get("missed_cand", 0) for r in res)}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--split", default="all"); ap.add_argument("--curve", action="store_true")
ap.add_argument("--json"); ap.add_argument("--set", default="synthetic", choices=["synthetic", "real", "all"])
a = ap.parse_args()
if a.set in ("real", "all") and not private_fixtures_ok():
print("SKIP real-page eval: fixtures/private_unredacted/ is missing. "
"Real-page tables cannot be reproduced from the publishable tree alone. "
"Running synthetic-only instead." if a.set == "all" else
"SKIP real-page eval: fixtures/private_unredacted/ is missing. "
"Real-page tables cannot be reproduced from the publishable tree alone.")
if a.set == "real":
return
a.set = "synthetic"
scs = load_scenarios(a.split, {"synthetic": False, "real": True, "all": None}[a.set])
A, B = run_all(scs, "agent"), run_all(scs, "baseline")
G = run_all(scs, "regex_guards") # middle arm: naive regex + every agent guard, no LLM, no rule patterns
A0 = run_all(scs, "agent", retries=0) # ablation: no retry
print(f"{'scenario':32} {'ho':3} {'agent':6} {'rx+g':6} {'base':6} agent failure reasons")
P = lambda r: "PASS" if r["pass"] else "FAIL"
for ra, rg, rb in zip(A, G, B):
print(f"{ra['id']:32} {'*' if ra['held_out'] else '':3} {P(ra):6} {P(rg):6} {P(rb):6} {'; '.join(ra['fails'])}")
print()
rows = []
for split, f in (("all", lambda r: True), ("dev", lambda r: not r["held_out"]), ("held-out", lambda r: r["held_out"])):
for lab, R in (("agent", A), ("agent_no_retry", A0), ("regex_guards", G), ("baseline_regex", B)):
sub = [r for r in R if f(r)]
if sub: rows.append({"split": split, **summary(sub, lab)})
print(f"{'split':9} {'system':15} {'pass':6} {'rate':5} {'wilson95':12} {'core_pass':9} {'false_imp':9} {'missed_imp':10} {'false_cand':10} {'missed_cand':10}")
for r in rows:
print(f"{r['split']:9} {r['system']:15} {r['pass']:6} {r['rate']:5} {r['wilson95']:12} {r['core_pass']:9} "
f"{r['false_improvements']:<9} {r['missed_improvements']:<10} {r['false_candidates']:<10} {r['missed_candidates']:<10}")
print("core_pass = no false and no missed improvements (the fair comparison for the baseline,"
" which structurally cannot emit partial/stale/invalid buckets).")
print(f"\nNote: N={len(scs)}; one scenario flip moves the pass rate by {100/len(scs):.1f} points.")
if a.curve:
os.makedirs("results", exist_ok=True)
print("\nBudget curve (max_steps override for every scenario):")
print(f"{'budget':7} {'agent':7} {'baseline':8} {'agent_false_imp':15}")
with open("results/budget_curve.csv", "w", newline="") as fh:
w = csv.writer(fh); w.writerow(["budget", "agent_pass_rate", "baseline_pass_rate", "agent_false_imp", "agent_missed_imp"])
for bud in (1, 2, 3, 4, 6, 8):
ra, rb = run_all(scs, "agent", bud), run_all(scs, "baseline", bud)
pa, pb = sum(r["pass"] for r in ra) / len(ra), sum(r["pass"] for r in rb) / len(rb)
fi, mi = sum(r["false_imp"] for r in ra), sum(r["missed"] for r in ra)
w.writerow([bud, f"{pa:.3f}", f"{pb:.3f}", fi, mi])
print(f"{bud:<7} {pa:<7.0%} {pb:<8.0%} {fi:<15}")
if a.json:
os.makedirs(os.path.dirname(a.json) or ".", exist_ok=True)
json.dump({"summary": rows, "agent": A, "regex_guards": G, "baseline": B}, open(a.json, "w"), indent=1)
if __name__ == "__main__":
main()