Download eval_harness.py from DHDRL/AgentSquares: direct link, hf CLI and curl.
- Browser
- Download file 8.28 kB
-
https://huggingface.co/DHDRL/AgentSquares/resolve/main/eval_harness.py
- Command line
-
hf download hf://DHDRL/AgentSquares/eval_harness.py
-
curl -L -o eval_harness.py https://huggingface.co/DHDRL/AgentSquares/resolve/main/eval_harness.py
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() | |