Spaces:
Running
Running
| """Agentic benchmark: does the AGENT LOOP assemble complete answers? | |
| Usage: python -m eval.run_agentic (needs NEON_URL + an LLM key) | |
| Catalog / compare / recipe questions (eval/agentic_v1.jsonl) can't be scored | |
| by single-shot retrieval β their answer is spread across several pages, which | |
| is exactly what the agent loop (search β plan β search again β answer) is for. | |
| So we score the FINISHED answer's CITATIONS against expected_any: each group | |
| is a source a complete answer must ground on; coverage = matched groups / | |
| groups. | |
| The headline metric is a DELTA, not an absolute: the same questions are also | |
| run through the single-shot grounded path (answer_grounded, one retrieval | |
| pass). agentic_coverage β single_shot_coverage is the quantified value of the | |
| loop β if the loop assembles more of the catalog than one search, the number | |
| shows it; if not, that's an honest negative result. | |
| No LLM judge: coverage is an objective URL/substring match. Results β | |
| eval/results/agentic_v1.jsonl. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| import sys | |
| from pathlib import Path | |
| from dotenv import load_dotenv | |
| EVAL = Path(__file__).parent | |
| QUESTIONS = EVAL / "agentic_v1.jsonl" | |
| # The full loop runs many LLM calls per question (plan β search β search β | |
| # answer) for BOTH the agentic and single-shot paths; on free-tier models the | |
| # 20-question set blows past a CI timeout on rate-limit backoff alone. LIMIT | |
| # runs the first N questions so the loop-vs-single-shot delta is measurable in a | |
| # bounded run β raise it (or set 0 for all) once a faster provider is wired. | |
| LIMIT = int(os.environ.get("TORCHDOCS_AGENTIC_LIMIT", "0")) | |
| # a partial run must not overwrite (masquerade as) the full-set results file | |
| RESULTS = EVAL / "results" / (f"agentic_v1_first{LIMIT}.jsonl" if LIMIT else "agentic_v1.jsonl") | |
| def citation_haystacks(answer) -> list[str]: | |
| """One lowercased url+anchor+title string per citation the answer grounded on.""" | |
| return [ | |
| f"{c.url} {c.anchor} {c.title}".lower() for c in getattr(answer, "citations", []) | |
| ] | |
| def answer_coverage(expected_any: list[list[str]], answer) -> float: | |
| """Share of expected source-groups that appear in the answer's citations.""" | |
| if not expected_any: | |
| return 0.0 | |
| hay = citation_haystacks(answer) | |
| matched = 0 | |
| for group in expected_any: | |
| if any(sub.lower() in h for sub in group for h in hay): | |
| matched += 1 | |
| return matched / len(expected_any) | |
| def main() -> int: | |
| load_dotenv() | |
| from agent.grounded import answer_grounded | |
| from agent.loop import answer_agentic | |
| questions = [json.loads(line) for line in QUESTIONS.open()] | |
| if LIMIT: | |
| questions = questions[:LIMIT] | |
| print(f"(limited to first {LIMIT} of the agentic questions)") | |
| RESULTS.parent.mkdir(exist_ok=True) | |
| records, agentic_cov, single_cov = [], [], [] | |
| print(f"{'id':<6}{'kind':<10}{'agentic':<9}{'1-shot':<8}delta") | |
| for q in questions: | |
| exp = q["expected_any"] | |
| try: | |
| a_ans = answer_agentic(q["question"]) | |
| s_ans = answer_grounded(q["question"]) | |
| except Exception as exc: # noqa: BLE001 β record and continue, never lose the run | |
| print(f"{q['id']:<6}{q['kind']:<10}failed: {type(exc).__name__}: {exc}") | |
| records.append({"id": q["id"], "error": str(exc)}) | |
| continue | |
| a_c, s_c = answer_coverage(exp, a_ans), answer_coverage(exp, s_ans) | |
| agentic_cov.append(a_c) | |
| single_cov.append(s_c) | |
| delta = a_c - s_c | |
| flag = " β " if delta > 0 else (" β οΈ" if delta < 0 else "") | |
| print(f"{q['id']:<6}{q['kind']:<10}{a_c:<9.2f}{s_c:<8.2f}{delta:+.2f}{flag}") | |
| records.append( | |
| { | |
| "id": q["id"], "kind": q["kind"], "agentic_coverage": a_c, | |
| "single_shot_coverage": s_c, | |
| "citations": [c.url for c in a_ans.citations], | |
| } | |
| ) | |
| with RESULTS.open("w") as out: | |
| for r in records: | |
| out.write(json.dumps(r, ensure_ascii=False) + "\n") | |
| if agentic_cov: | |
| n = len(agentic_cov) | |
| ma, ms = sum(agentic_cov) / n, sum(single_cov) / n | |
| print(f"\naggregate over {n}: agentic={ma:.3f} single-shot={ms:.3f} " | |
| f"delta={ma - ms:+.3f}") | |
| print("(delta > 0 means the loop assembled more of the answer than one search)") | |
| print(f"results β {RESULTS}") | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |