Skip to content

Commit 45fcfb0

Browse files
committed
feat: add run_and_judge.py convenience script; fix session_cache compat
- scripts/run_and_judge.py: single command to run eval + judge + print scorecard; supports --no-ppr, --max-questions, --run-name flags - session_cache.py: normalise string facts from old cache entries to {"text": "..."} dicts so replay_facts() doesn't crash on legacy cache
1 parent 5a1e47d commit 45fcfb0

2 files changed

Lines changed: 216 additions & 2 deletions

File tree

benchmarks/longmemeval/session_cache.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,11 @@ async def get(self, session_id: str) -> dict[str, Any] | None:
6666
if row is None:
6767
return None
6868
data = json.loads(row[0])
69-
# Backwards compat: old entries stored a plain list of facts
69+
# Backwards compat: old entries stored a plain list of facts (some as strings, some as dicts)
7070
if isinstance(data, list):
71-
return {"facts": data, "episodes": []}
71+
# Normalise string facts to {"text": "..."} dicts for compatibility with replay_facts()
72+
facts = [f if isinstance(f, dict) else {"text": f} for f in data]
73+
return {"facts": facts, "episodes": []}
7274
return data
7375

7476
async def put(

scripts/run_and_judge.py

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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

Comments
 (0)