|
| 1 | +""" |
| 2 | +run_and_judge.py — Run LoCoMo eval then immediately judge the output. |
| 3 | +
|
| 4 | +Usage (smoke test, 2 questions): |
| 5 | + python scripts/run_and_judge.py --max-questions 2 |
| 6 | +
|
| 7 | +Usage (full run, PPR on — default): |
| 8 | + python scripts/run_and_judge.py |
| 9 | +
|
| 10 | +Usage (full run, PPR off — for AWS ablation): |
| 11 | + python scripts/run_and_judge.py --no-ppr --run-name locomo_ppr_off |
| 12 | +
|
| 13 | +Required env vars: |
| 14 | + GOOGLE_API_KEY — Google AI Studio key (for Gemini eval + judge) |
| 15 | + CLOUDFLARE_API_TOKEN — Cloudflare Workers AI token (for embeddings) |
| 16 | + CLOUDFLARE_ACCOUNT_ID — Cloudflare account ID |
| 17 | +""" |
| 18 | + |
| 19 | +from __future__ import annotations |
| 20 | + |
| 21 | +import argparse |
| 22 | +import asyncio |
| 23 | +import os |
| 24 | +import sys |
| 25 | +from datetime import datetime |
| 26 | +from pathlib import Path |
| 27 | + |
| 28 | +# Ensure repo root is on sys.path regardless of where the script is invoked from |
| 29 | +_REPO_ROOT = Path(__file__).resolve().parent.parent |
| 30 | +if str(_REPO_ROOT) not in sys.path: |
| 31 | + sys.path.insert(0, str(_REPO_ROOT)) |
| 32 | + |
| 33 | +# ── Env var check ───────────────────────────────────────────────────────────── |
| 34 | + |
| 35 | +REQUIRED_ENV = { |
| 36 | + "GOOGLE_API_KEY": "Google AI Studio — aistudio.google.com → Get API key", |
| 37 | + "CLOUDFLARE_API_TOKEN": "Cloudflare dashboard → Workers AI → API tokens", |
| 38 | + "CLOUDFLARE_ACCOUNT_ID": "Cloudflare dashboard → top-right account ID", |
| 39 | +} |
| 40 | + |
| 41 | + |
| 42 | +def _check_env() -> None: |
| 43 | + missing = [k for k in REQUIRED_ENV if not os.environ.get(k)] |
| 44 | + if missing: |
| 45 | + print("ERROR: Missing required environment variables:\n") |
| 46 | + for k in missing: |
| 47 | + print(f" {k}\n → {REQUIRED_ENV[k]}") |
| 48 | + print("\nSet them and re-run.") |
| 49 | + sys.exit(1) |
| 50 | + |
| 51 | + |
| 52 | +# ── Args ────────────────────────────────────────────────────────────────────── |
| 53 | + |
| 54 | +def _parse_args() -> argparse.Namespace: |
| 55 | + p = argparse.ArgumentParser( |
| 56 | + description="Run LoCoMo eval + LLM-as-judge in one shot" |
| 57 | + ) |
| 58 | + # Eval args (passed through to run_locomo) |
| 59 | + p.add_argument("--max-questions", type=int, default=0, |
| 60 | + help="Limit questions (0 = full dataset, 2 = smoke test)") |
| 61 | + p.add_argument("--depth", default="l1", choices=["l0", "l1", "l2"]) |
| 62 | + p.add_argument("--no-ppr", action="store_true", |
| 63 | + help="Disable PPR (ablation run)") |
| 64 | + p.add_argument("--run-name", default=None, |
| 65 | + help="Custom run name (auto-timestamped if omitted)") |
| 66 | + p.add_argument("--output-dir", default="benchmark_results") |
| 67 | + p.add_argument("--data-dir", default="data") |
| 68 | + p.add_argument("--eval-model", default="gemini:gemini-2.5-flash-lite") |
| 69 | + p.add_argument("--extraction-model", default="gemini:gemini-2.5-flash-lite") |
| 70 | + p.add_argument("--no-cache", action="store_true") |
| 71 | + p.add_argument("--cache-namespace", default=None) |
| 72 | + # Judge args |
| 73 | + p.add_argument("--judge-model", default="gemini:gemini-2.5-flash-lite") |
| 74 | + p.add_argument("--judge-n", type=int, default=0, |
| 75 | + help="How many answers to judge (0 = all, default)") |
| 76 | + return p.parse_args() |
| 77 | + |
| 78 | + |
| 79 | +# ── Step 1: Eval ────────────────────────────────────────────────────────────── |
| 80 | + |
| 81 | +async def _run_eval(args: argparse.Namespace, run_name: str, run_output_dir: str) -> Path: |
| 82 | + """Run the LoCoMo benchmark. Returns path to full_results.json.""" |
| 83 | + from benchmarks.locomo.locomo_runner import LoCoMoConfig, LoCoMoBenchmark |
| 84 | + |
| 85 | + max_q = args.max_questions if args.max_questions and args.max_questions > 0 else None |
| 86 | + |
| 87 | + config = LoCoMoConfig( |
| 88 | + dataset_name="locomo10_cooked", |
| 89 | + embedding_model="cloudflare:@cf/baai/bge-m3", |
| 90 | + extraction_model=args.extraction_model, |
| 91 | + eval_model=args.eval_model, |
| 92 | + retrieval_depth=args.depth, |
| 93 | + output_dir=run_output_dir, |
| 94 | + data_dir=args.data_dir, |
| 95 | + run_name=run_name, |
| 96 | + max_questions=max_q, |
| 97 | + use_cache=not args.no_cache, |
| 98 | + cache_namespace=args.cache_namespace, |
| 99 | + use_ppr=not args.no_ppr, |
| 100 | + ) |
| 101 | + |
| 102 | + print(f"\n{'='*60}") |
| 103 | + print(f"STEP 1 — EVAL") |
| 104 | + print(f" run_name : {run_name}") |
| 105 | + print(f" output : {run_output_dir}") |
| 106 | + print(f" PPR : {'OFF' if args.no_ppr else 'ON'}") |
| 107 | + print(f" questions: {'ALL' if not max_q else max_q}") |
| 108 | + print(f"{'='*60}\n") |
| 109 | + |
| 110 | + await LoCoMoBenchmark(config).run() |
| 111 | + |
| 112 | + full_results = Path(run_output_dir) / f"{run_name}_full_results.json" |
| 113 | + if not full_results.exists(): |
| 114 | + print(f"ERROR: Expected full results at {full_results} but file not found.") |
| 115 | + sys.exit(1) |
| 116 | + |
| 117 | + return full_results |
| 118 | + |
| 119 | + |
| 120 | +# ── Step 2: Judge ───────────────────────────────────────────────────────────── |
| 121 | + |
| 122 | +async def _run_judge( |
| 123 | + full_results: Path, |
| 124 | + run_output_dir: str, |
| 125 | + judge_model: str, |
| 126 | + judge_n: int, |
| 127 | +) -> dict: |
| 128 | + from benchmarks.locomo.locomo_judge import JudgeConfig, run as judge_run |
| 129 | + |
| 130 | + print(f"\n{'='*60}") |
| 131 | + print(f"STEP 2 — JUDGE") |
| 132 | + print(f" input : {full_results}") |
| 133 | + print(f" model : {judge_model}") |
| 134 | + print(f" sample : {'ALL' if judge_n <= 0 else judge_n}") |
| 135 | + print(f"{'='*60}\n") |
| 136 | + |
| 137 | + config = JudgeConfig( |
| 138 | + full_results=str(full_results), |
| 139 | + judge_model=judge_model, |
| 140 | + n=judge_n if judge_n > 0 else 0, |
| 141 | + output_dir=run_output_dir, |
| 142 | + ) |
| 143 | + |
| 144 | + return await judge_run(config) |
| 145 | + |
| 146 | + |
| 147 | +# ── Step 3: Summary ─────────────────────────────────────────────────────────── |
| 148 | + |
| 149 | +def _print_scorecard(judge_output: dict, run_name: str, ppr_on: bool) -> None: |
| 150 | + s = judge_output.get("summary", {}) |
| 151 | + total = s.get("total", 0) |
| 152 | + correct = s.get("correct", 0) |
| 153 | + partial = s.get("partially_correct", 0) |
| 154 | + wrong = s.get("wrong", 0) |
| 155 | + abstained = s.get("abstained", 0) |
| 156 | + combined = correct + partial |
| 157 | + |
| 158 | + correct_pct = 100 * s.get("correct_rate", 0) |
| 159 | + combined_pct = 100 * s.get("combined_rate", 0) |
| 160 | + ctx_pct = 100 * s.get("context_has_answer_rate", 0) |
| 161 | + qa_fail = s.get("qa_failure", 0) |
| 162 | + ret_fail = s.get("retrieval_failure", 0) |
| 163 | + |
| 164 | + print(f"\n{'='*60}") |
| 165 | + print(f"FINAL SCORECARD — {run_name}") |
| 166 | + print(f" PPR: {'ON' if ppr_on else 'OFF'}") |
| 167 | + print(f"{'='*60}") |
| 168 | + print(f" Total judged : {total}") |
| 169 | + print(f" Correct : {correct:>4} ({correct_pct:.1f}%)") |
| 170 | + print(f" Partial : {partial:>4} ({100*partial/total:.1f}%)" if total else "") |
| 171 | + print(f" Combined C+PC : {combined:>4} ({combined_pct:.1f}%)") |
| 172 | + print(f" Wrong : {wrong:>4}") |
| 173 | + print(f" Abstained : {abstained:>4}") |
| 174 | + print(f" Context OK : {s.get('context_has_answer'):>4} ({ctx_pct:.1f}%)") |
| 175 | + print(f" QA failures : {qa_fail}") |
| 176 | + print(f" Retrieval fail: {ret_fail}") |
| 177 | + |
| 178 | + by_type = s.get("by_type", {}) |
| 179 | + if by_type: |
| 180 | + print(f"\n By question type:") |
| 181 | + for qtype, counts in sorted(by_type.items()): |
| 182 | + t = counts["total"] |
| 183 | + c = counts["correct"] |
| 184 | + pc = counts["partial"] |
| 185 | + pct = 100 * (c + pc) / t if t else 0 |
| 186 | + print(f" {qtype:<20} {c+pc:>3}/{t:<3} ({pct:.0f}% C+PC)") |
| 187 | + print(f"{'='*60}\n") |
| 188 | + |
| 189 | + |
| 190 | +# ── Main ────────────────────────────────────────────────────────────────────── |
| 191 | + |
| 192 | +async def main() -> None: |
| 193 | + _check_env() |
| 194 | + args = _parse_args() |
| 195 | + |
| 196 | + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
| 197 | + ppr_tag = "ppr_off" if args.no_ppr else "ppr_on" |
| 198 | + run_name = args.run_name or f"locomo_{ppr_tag}_{timestamp}" |
| 199 | + run_output_dir = str(Path(args.output_dir) / run_name) |
| 200 | + |
| 201 | + full_results = await _run_eval(args, run_name, run_output_dir) |
| 202 | + judge_output = await _run_judge( |
| 203 | + full_results, |
| 204 | + run_output_dir, |
| 205 | + args.judge_model, |
| 206 | + args.judge_n, |
| 207 | + ) |
| 208 | + _print_scorecard(judge_output, run_name, ppr_on=not args.no_ppr) |
| 209 | + |
| 210 | + |
| 211 | +if __name__ == "__main__": |
| 212 | + asyncio.run(main()) |
0 commit comments