SurfSense/surfsense_evals/scripts/inspect_first30.py

60 lines
1.9 KiB
Python
Raw Normal View History

feat(evals): publish multimodal_doc parser_compare benchmark + n=171 report Adds the full parser_compare experiment for the multimodal_doc suite: six arms compared on 30 PDFs / 171 questions from MMLongBench-Doc with anthropic/claude-sonnet-4.5 across the board. Source code: - core/parsers/{azure_di,llamacloud,pdf_pages}.py: direct parser SDK callers (Azure Document Intelligence prebuilt-read/layout, LlamaParse parse_page_with_llm/parse_page_with_agent) used by the LC arms, bypassing the SurfSense backend so each (basic/premium) extraction is a clean A/B independent of backend ETL routing. - suites/multimodal_doc/parser_compare/{ingest,runner,prompt}.py: six-arm benchmark (native_pdf, azure_basic_lc, azure_premium_lc, llamacloud_basic_lc, llamacloud_premium_lc, surfsense_agentic) with byte-identical prompts per question, deterministic grader, Wilson CIs, and the per-page preprocessing tariff cost overlay. Reproducibility: - pyproject.toml + uv.lock pin pypdf, azure-ai-documentintelligence, llama-cloud-services as new deps. - .env.example documents the AZURE_DI_* and LLAMA_CLOUD_API_KEY env vars now required for parser_compare. - 12 analysis scripts under scripts/: retry pass with exponential backoff, post-retry accuracy merge, McNemar / latency / per-PDF stats, context-overflow hypothesis test, etc. Each produces one number cited by the blog report. Citation surface: - reports/blog/multimodal_doc_parser_compare_n171_report.md: 1219-line technical writeup (16 sections) covering headline accuracy, per-format accuracy, McNemar pairwise significance, latency / token / per-PDF distributions, error analysis, retry experiment, post-retry final accuracy, cost amortization model with closed-form derivation, threats to validity, and reproducibility appendix. - data/multimodal_doc/runs/2026-05-14T00-53-19Z/parser_compare/{raw, raw_retries,raw_post_retry}.jsonl + run_artifact.json + retry summary whitelisted via data/.gitignore as the verifiable numbers source. Gitignore: - ignore logs_*.txt + retry_run.log; structured artifacts cover the citation surface, debug logs are noise. - data/.gitignore default-ignores everything, whitelists the n=171 run artifacts only (parser manifest left ignored to avoid leaking local Windows usernames in absolute paths; manifest is fully regenerable via 'ingest multimodal_doc parser_compare'). - reports/.gitignore now whitelists hand-curated reports/blog/. Also retires the abandoned CRAG Task 3 implementation (download script, streaming Task 3 ingest, CragTask3Benchmark + tests) and trims the runner / ingest module APIs to match. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 19:54:41 -07:00
"""Inspect what the first 30 MMLongBench-Doc PDFs would look like for scoping.
Run from surfsense_evals/ root via:
python scripts/inspect_first30.py
Prints which docs are already ingested (existing 5), which are new (25 to
upload), how many questions cover those 30 PDFs, and the answerable /
unanswerable + format mix.
"""
from __future__ import annotations
import json
from collections import Counter
from pathlib import Path
def main() -> None:
qpath = Path("data/multimodal_doc/mmlongbench/questions.jsonl")
lines = qpath.read_text(encoding="utf-8").splitlines()
rows = [json.loads(line) for line in lines if line.strip()]
docs_by_id = sorted({r["doc_id"] for r in rows})
first30 = docs_by_id[:30]
existing5 = {
"05-03-18-political-release.pdf",
"0b85477387a9d0cc33fca0f4becaa0e5.pdf",
"0e94b4197b10096b1f4c699701570fbf.pdf",
"11-21-16-Updated-Post-Election-Release.pdf",
"12-15-15-ISIS-and-terrorism-release-final.pdf",
}
new25 = [d for d in first30 if d not in existing5]
print(
f"first 30 docs (alphabetical) — {len(new25)} new, "
f"{len(first30) - len(new25)} already in SurfSense"
)
qs_in_30 = [r for r in rows if r["doc_id"] in set(first30)]
fmts = Counter((r.get("answer_format") or "").lower() for r in qs_in_30)
answerable = sum(v for k, v in fmts.items() if k != "none")
unanswerable = fmts.get("none", 0)
print(
f"questions covering first 30 docs: total={len(qs_in_30)} "
f"answerable={answerable} unanswerable={unanswerable}"
)
print(
f"avg Qs/PDF: {len(qs_in_30) / 30:.1f} "
f"answerable/PDF: {answerable / 30:.1f}"
)
print(f"format mix in scope: {dict(fmts)}")
print()
print("25 new PDFs to ingest:")
for d in new25:
print(f" - {d}")
if __name__ == "__main__":
main()