Spaces:
Sleeping
Sleeping
| """Deterministic retrieval Recall@10 harness (the tuning testbed). | |
| Mean Recall@10 is our headline metric (CLAUDE.md §5). To tune retrieval fast and | |
| reproducibly we measure RETRIEVAL recall directly, without the LLM in the loop: | |
| - query per trace = the concatenation of that trace's user messages (the full | |
| context the agent has by commit time), OR the router's synthesized query when | |
| ``--router`` is passed; | |
| - expected shortlist items are mapped to catalog ids by URL (fallback: name); | |
| - recall@10 = |expected ∩ top-10 retrieved ids| / |expected|, averaged. | |
| This isolates retrieval changes (RRF/N, embedded text, test_type bias) from the | |
| router's nondeterminism. Run: | |
| python eval/recall_eval.py # deterministic, user-text query | |
| python eval/recall_eval.py --router # uses the live router query (LLM) | |
| python eval/recall_eval.py --write # also (re)write eval/REPORT.md | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import sys | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parents[1] | |
| sys.path.insert(0, str(ROOT)) | |
| from app.retrieval import get_retriever # noqa: E402 | |
| from eval.metrics import mean_recall_at_k, recall_at_k # noqa: E402 | |
| from eval.traces_loader import load_traces # noqa: E402 | |
| CATALOG_PATH = ROOT / "data" / "catalog.json" | |
| REPORT_PATH = ROOT / "eval" / "REPORT.md" | |
| def _norm_url(url: str) -> str: | |
| return url.rstrip("/").lower() | |
| def _norm_name(name: str) -> str: | |
| return " ".join(name.split()).lower() | |
| def _catalog_maps() -> tuple[dict[str, str], dict[str, str]]: | |
| recs = json.loads(CATALOG_PATH.read_text(encoding="utf-8")) | |
| url2id = {_norm_url(r["url"]): r["id"] for r in recs} | |
| name2id = {_norm_name(r["name"]): r["id"] for r in recs} | |
| return url2id, name2id | |
| def expected_ids_for(trace, url2id, name2id) -> tuple[set[str], list[str]]: | |
| """Map a trace's expected shortlist to catalog ids. Returns (ids, unmapped).""" | |
| ids: set[str] = set() | |
| unmapped: list[str] = [] | |
| for rec in trace.expected_shortlist: | |
| cid = url2id.get(_norm_url(rec.url)) or name2id.get(_norm_name(rec.name)) | |
| if cid: | |
| ids.add(cid) | |
| else: | |
| unmapped.append(rec.name) | |
| return ids, unmapped | |
| def trace_query(trace) -> str: | |
| """Deterministic query = all user messages joined (full revealed context).""" | |
| return " ".join(trace.user_messages) | |
| def router_query(trace) -> str: | |
| """Live router-synthesized query (one LLM call). Falls back to user text.""" | |
| from app.router import route | |
| messages = [{"role": "user", "content": m} for m in trace.user_messages] | |
| result = route(messages) | |
| return result.search_query or trace_query(trace) | |
| def evaluate(k: int = 10, use_router: bool = False, assemble: bool = False) -> dict: | |
| traces = load_traces() | |
| url2id, name2id = _catalog_maps() | |
| retriever = get_retriever() | |
| rows = [] | |
| pairs: list[tuple[list[str], set[str]]] = [] | |
| for tr in traces: | |
| expected, unmapped = expected_ids_for(tr, url2id, name2id) | |
| query = router_query(tr) if use_router else trace_query(tr) | |
| if assemble: | |
| from app.assembly import assemble_ids, default_flags | |
| add_p, add_c = default_flags(None, " ".join(tr.user_messages)) | |
| retrieved = assemble_ids( | |
| retriever.retrieve_ids(query, k=k * 2), | |
| k=k, | |
| add_personality=add_p, | |
| add_cognitive=add_c, | |
| ) | |
| else: | |
| retrieved = retriever.retrieve_ids(query, k=k) | |
| r = recall_at_k(retrieved, expected, k) | |
| pairs.append((retrieved, expected)) | |
| rows.append( | |
| { | |
| "id": tr.id, | |
| "expected": len(expected), | |
| "unmapped": unmapped, | |
| "hits": len(expected & set(retrieved[:k])), | |
| "recall": r, | |
| "missed": sorted(expected - set(retrieved[:k])), | |
| } | |
| ) | |
| mean = mean_recall_at_k(pairs, k) | |
| return {"k": k, "use_router": use_router, "mean_recall": mean, "rows": rows} | |
| def print_report(result: dict) -> None: | |
| line = "=" * 74 | |
| print(line) | |
| print(f"RETRIEVAL RECALL@{result['k']} (query=" | |
| f"{'router' if result['use_router'] else 'user-text'})") | |
| print(line) | |
| for row in result["rows"]: | |
| flag = " (unmapped: %s)" % row["unmapped"] if row["unmapped"] else "" | |
| print(f" {row['id']:<4} recall={row['recall']:.2f} " | |
| f"{row['hits']}/{row['expected']}{flag}") | |
| if row["missed"]: | |
| print(f" missed: {row['missed']}") | |
| print(line) | |
| print(f" MEAN RECALL@{result['k']} = {result['mean_recall']:.4f}") | |
| print(line) | |
| def write_report(result: dict) -> None: | |
| lines = [ | |
| "# Evaluation Report — Retrieval Recall@10", | |
| "", | |
| f"Query mode: **{'router-synthesized' if result['use_router'] else 'user-text (deterministic)'}** ", | |
| f"**Mean Recall@{result['k']} = {result['mean_recall']:.4f}**", | |
| "", | |
| "| Trace | Recall@10 | Hits/Expected | Missed ids |", | |
| "|-------|----------:|--------------:|------------|", | |
| ] | |
| for row in result["rows"]: | |
| missed = ", ".join(row["missed"]) if row["missed"] else "—" | |
| lines.append( | |
| f"| {row['id']} | {row['recall']:.2f} | " | |
| f"{row['hits']}/{row['expected']} | {missed} |" | |
| ) | |
| lines += [ | |
| "", | |
| "Recall is measured on retrieval (top-10 catalog ids vs the labeled " | |
| "shortlist mapped to ids). Items the agent adds as defaults (e.g. OPQ32r, " | |
| "Verify G+) that the user never mentions are the main miss source — see " | |
| "`eval/TUNING_LOG.md`.", | |
| "", | |
| ] | |
| REPORT_PATH.write_text("\n".join(lines), encoding="utf-8") | |
| print(f"Wrote {REPORT_PATH}") | |
| def main() -> None: | |
| ap = argparse.ArgumentParser(description=__doc__) | |
| ap.add_argument("--router", action="store_true", help="Use live router query.") | |
| ap.add_argument("--assemble", action="store_true", | |
| help="Apply test_type-aware assembly (default injection).") | |
| ap.add_argument("--write", action="store_true", help="Write eval/REPORT.md.") | |
| ap.add_argument("-k", type=int, default=10) | |
| args = ap.parse_args() | |
| result = evaluate(k=args.k, use_router=args.router, assemble=args.assemble) | |
| print_report(result) | |
| if args.write: | |
| write_report(result) | |
| if __name__ == "__main__": | |
| main() | |