Skip to content

Replace six per-source parsers with LLM extraction - #214

Merged
alexwolson merged 8 commits into
mainfrom
205-llm-extraction
Aug 7, 2026
Merged

Replace six per-source parsers with LLM extraction#214
alexwolson merged 8 commits into
mainfrom
205-llm-extraction

Conversation

@alexwolson

Copy link
Copy Markdown
Collaborator

Summary

  • Adds an extraction cache (extraction_cache table) and orchestrator that sends board-report text to GPT-5.6-Luna via OpenRouter, validates against declared bid counts, and stores structured JSON keyed on (sha256, extractor_version)
  • Backfills all store tables (agency_award, agency_bid, agency_solicitation, composite_award, bid) from the cached extractions — Phase A
  • Deletes six incumbent parsers (TRCA regex, EP pdfplumber+regex, Zoo regex, award_summary pdfplumber, committee regex, composite regex line-walker) and their dead support modules (pdf_tables.py, agency_report.py) — Phase B
  • Rewires each store_* function to call extract_and_backfill(), which skips extraction when all docs are cached and no API key is set

What this changes

One extraction path instead of six. A City PDF reformat that would have silently broken one parser now degrades gracefully through the model's general reading ability. Net -3,470 lines of parser code removed across the branch.

Full corpus: 1,722 documents extracted, validated against ground truth (declared bid counts in the documents themselves), total cost $6.08.

Download/scrape/cache/discovery infrastructure is untouched — only the parse-and-store seam changed.

Commits

SHA What
de500d0 Extraction cache seam and LLM client (#207, #208)
54c0b90 Extraction orchestrator with classification gate and validation (#209)
9682243 Declared-count invariant — runtime self-check on extracted bid counts (#210)
98c531c Split large documents and dedup contracts by reference (#211)
3c152f1 Full-corpus validation — 1,722 docs extracted, $1.26 (#212)
64a552f Backfill store tables from extraction cache (#213 phase A)
e936004 Delete incumbent parsers, rewire store functions (#213 phase B)

Also includes 2af4f89 (fix: refuse to overwrite an Ariba bundle with fewer files, #199).

Test plan

  • Full test suite passes (734 tests, offline, no network)
  • All six parser deletions verified: no import of deleted functions remains
  • Export tests pass — model-derived rows reach the export unchanged
  • Extraction is offline by default — no API key needed when all docs are cached

Closes #213, closes #209, closes #210, closes #211, closes #212.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RiYypnPbDwFtp9zAp7tz6W

Alex Waolson and others added 8 commits August 6, 2026 12:47
Add the two frontier tickets for #205 (LLM-based extraction):

- extraction_cache table keyed on (sha256, extractor_version) so re-runs
  skip documents whose content and prompt haven't changed
- ExtractionClient: OpenRouter API client with Nemotron 3 Ultra (free)
  default, GPT-5.6-Luna fallback, retry with backoff, placeholder
  credential guard, flex tier for OpenAI models
- tb extract --dry-run <sha256> CLI command for one-off testing
- 26 new tests (5 cache + 21 extraction), 826 total passing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiYypnPbDwFtp9zAp7tz6W
capture_files now compares the new capture's file count against the
existing bundle's top-level entry count before calling build_bundle.
A re-capture with strictly fewer files is refused — the existing
bundle stays, partials are kept for a future retry, and a log message
names both counts. First captures and equal-or-larger re-captures
are unaffected; _MIN_CAPTURE_RATIO is unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiYypnPbDwFtp9zAp7tz6W
…#209)

Adds extract_corpus() to iterate corpus documents through the LLM client,
gated by machine-classification labels and cached in extraction_cache with
result_json. Validation compares cached results against ground-truth labels.
CLI wires --corpus, --limit, --labels, --validate flags on tb extract.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiYypnPbDwFtp9zAp7tz6W
…counts (#210)

check_declared_counts compares each contract's declared_submissions against
the actual bid count, flagging shortfalls while keeping overshoots. Flags are
stored in extraction_cache.result_json under _flags and surfaced in the
validate CLI output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiYypnPbDwFtp9zAp7tz6W
split_document breaks docs exceeding max_chars on double-newline boundaries;
each chunk is extracted independently and the contracts merged. dedup_contracts
collapses contracts sharing a reference, keeping the one with more bids.
extract_corpus tracks split/chunks stats.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiYypnPbDwFtp9zAp7tz6W
All six corpora extracted through GPT-5.6-Luna. Per-corpus recall
against incumbent parsers: EP 98.6%, Zoo 96.3%, award_summary 88.9%,
committee 100%, TRCA 74.3% (partial join coverage). Composite has
no per-document join — corpus-level only.

Adds --model flag for forcing a specific OpenRouter model, --report
flag for generating a validation report comparing LLM vs incumbent
parser output, and corpus_validation_report() in extraction.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiYypnPbDwFtp9zAp7tz6W
backfill_from_extraction() maps cached LLM extractions to the store's
agency_bid/agency_award, bid, and composite_award tables using the
rebuild pattern (delete old source rows, insert new). Wired as
`tb extract --corpus <name> --backfill`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiYypnPbDwFtp9zAp7tz6W
…ion (#213 phase B)

Six corpus files (TRCA, EP, Zoo, award_summary, committee, composite)
had their parse functions deleted and store functions rewired to call
extract_and_backfill(), which reads from the extraction_cache populated
in #212. Infrastructure code (download, scrape, cache, discovery) is
kept intact.

Deleted modules: pdf_tables.py, agency_report.py (dead once parsers gone).
Deleted test files: test_composite_awards.py, test_composite_reports.py,
test_pdf_tables.py, test_agency_report.py.
Remaining test files keep only infrastructure tests.

-3,028 lines removed, 734 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiYypnPbDwFtp9zAp7tz6W
@alexwolson
alexwolson merged commit d5c4c7e into main Aug 7, 2026
1 check passed
@alexwolson
alexwolson deleted the 205-llm-extraction branch August 7, 2026 13:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment