Skip to content

Rebuild ContextIQ: simple-agentic financial doc-QA (FinanceBench corpus 0.71 vs 0.19, per-doc ~0.79)#32

Merged
Ven-Z8 merged 33 commits into
mainfrom
claude/elated-gagarin-e53bb6
Jul 2, 2026
Merged

Rebuild ContextIQ: simple-agentic financial doc-QA (FinanceBench corpus 0.71 vs 0.19, per-doc ~0.79)#32
Ven-Z8 merged 33 commits into
mainfrom
claude/elated-gagarin-e53bb6

Conversation

@Ven-Z8

@Ven-Z8 Ven-Z8 commented Jul 2, 2026

Copy link
Copy Markdown
Owner

What

Rebuilds ContextIQ from a disconnected, over-engineered "enterprise" stack (4 retrieval pipelines, the weakest wired to the app, fabricated README numbers) into a lean, measured, simple-agentic financial document-QA system.

Results (FinanceBench, LLM-judge, 12 companies / 52 questions / 8 sectors)

Setting ContextIQ Published baseline
Multi-doc corpus (router picks the filing) 0.71 shared vector store 0.19 (3.7×); single store 0.50
Per-document (chat with one filing) ~0.79 (0.71–0.86) long-context 0.79; oracle 0.85
Plain hybrid retrieve, per-document 0.48

Out-of-sample check on 9 unseen companies: 24/31 = 0.77. Every number is LLM-judged vs gold answers and reproducible via evals/financebench/run_answers.py. Numbers are ranges (±1 question of LLM variance).

Pipeline

route (pick filing) → decompose (concept → line items) → hybrid retrieve per sub-query → merge/dedup → table-aware rerank → minimax-m3 answer + citations + NOT_IN_DOCUMENT abstain

No multi-agent orchestration — one route + one decompose + one rerank call. Every component was measured head-to-head before adoption (decompose alone lifted the hardest questions 0.48 → ~0.79).

Also in this PR

  • Deleted ~2,860 LOC of dead legacy + enterprise retrieval (SPLADE/ColBERT, adaptive chunker, intent router, multi-stage pipeline). src/ 7,164 → ~4,400 LOC; retrieval/ 12 → 4 files.
  • Answer path: OpenRouter/minimax-m3 over httpx (no OpenAI SDK), retry wrapper, span citations, abstain gate.
  • Honest README replacing the fabricated results table; stale docs removed.
  • Tests green, ruff clean across the repo.

Honest limitations

Validated on 12 companies (not the full FinanceBench 150); routing recovers most but not all per-document quality (corpus 0.71 vs scoped ~0.79); agentic adds ~3 model calls/question.

🤖 Generated with Claude Code

Ven-Z8 and others added 30 commits May 30, 2026 01:44
Add ven-eval as editable dev dependency, scaffold evals/ dir via ven-eval CLI,
replace stub provider with ContextiqProvider wired to answer_question(), and
add smoke test confirming call_api shape against live Qdrant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds evals/contextiq.yaml as a placeholder stub; debug assert
_debug_dump.py was run statically (disk full blocked npx) and removed.
Task 12 will populate the real test cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Task 14 acceptance run surfaced 6 real wiring bugs the build never hit:
- pin promptfoo 0.118.0 (0.121.13 ships a broken @posthog/core .mjs)
- anchor provider to project root (promptfoo runs it from evals/)
- map question var into the prompt (prompts: ["{{question}}"])
- grader id anthropic:messages:claude-sonnet-4-6 (0.118 rejects bare name)
- run promptfoo at -j 1 (embedded Qdrant is single-writer)
- structural asserts inline in contextiq.yaml: file:// asserts in an
  external tests file get inlined by promptfoo and lose get_assert(),
  so they silently returned {}. Inline expression form is robust.

run.py: pin PROMPTFOO_PYTHON to the venv, tolerate promptfoo exit 100
(test failures are signal), and always write the EvalRecord file.

Result: 4 cases run green end-to-end, 0 errors, records written.
AnthropicLLMClient hardcoded cost_usd=None, so every EvalRecord had a null
cost. Add a model-tier price table (opus/sonnet/haiku, USD per 1M tokens) and
compute cost from usage.input_tokens/output_tokens. Unknown models return None
rather than guessing. This populates cost_usd through the ven_eval records
sink so the DB foundation can track $ per eval.
Opt-in DB sink: when VEN_EVAL_DB is set (e.g. the workspace ven_master.db),
run.py writes the normalized EvalRecords to SQLite in addition to JSON. Keeps
contextiq decoupled from the monorepo layout while enabling the 4-layer eval
path: promptfoo -> ven_eval -> ven_master.db -> dashboard.
…Answer

risk_factor_synthesis eval flagged an uncited summary intro. Tighten the
answer prompt: extend the cite-every-claim rule to all sections, require the
Direct Answer to be citation-free high-level synthesis (no standalone facts,
no vouching for the evidence), and explicitly separate document evidence from
interpretation. Lifts the case 0.72 -> 0.85; evidence/interpretation-separation
rubric now passes; Evidence tables fully cited.
…c to Evidence

risk_factor_synthesis was failing because comprehensive multi-source answers
truncated mid-section at 1000 tokens — dropping the trailing citation and the
Confidence/Gaps interpretation block, which a moving target across runs. Raising
the cap to 4000 lets syntheses complete (Sonnet supports 8k+ output). Also scope
the citation rubric to the Evidence section (the Direct Answer is high-level
synthesis, not per-claim cited). Eval now 4/4 (100%); risk_factor 0.95.
…n gate

Corpus doc-QA rebuild, phase 1. Swap the answer path to OpenRouter (free
Nemotron 3 Ultra) over raw httpx (not the OpenAI SDK, per CLAUDE.md), keeping
AnthropicLLMClient as a Citations-API seam. Provider chosen by
CONTEXTIQ_LLM_PROVIDER (default openrouter). Reuses existing [block_id, page N]
chunk-id citations; adds a NOT_IN_DOCUMENT abstain sentinel. Plan doc included.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, staged)

One clean retrieve: store.hybrid_hits(query, k=40) = Qdrant hybrid dense+BM25
via search_hybrid, no rerank. Proven end-to-end: ingest sample-contract.md ->
9 sources -> Nemotron grounded answer with Confidence/Gaps; empty context ->
NOT_IN_DOCUMENT abstain.

Built alongside the legacy path (build-new-alongside-old): build_context still
calls search_with_trace. Cut over + delete the legacy stack together once the
FinanceBench eval shows hybrid wins, to avoid migrating ~8 legacy-coupled tests
prematurely. Parse deferred — Docling already handles PDFs; hosted OCR is a
gated lever (OpenRouter mistral-ocr markdown extraction is unreliable per field
reports). Suite at baseline: 156 pass, only the 9 pre-existing enterprise-stack
failures remain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tron

Flip the OpenRouter default from free Nemotron to minimax/minimax-m3 (1M ctx,
~$0.001/query, $15 credits loaded) — sharper structured output + per-claim
chunk-id citations. Env-driven (CONTEXTIQ_OPENROUTER_MODEL), httpx, no code-path
change. Updated config default, .env.example, test, and plan doc; scrubbed all
Nemotron references.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Retry transient 429/5xx and transport errors up to 3x with linear backoff so a
free/cheap-tier blip doesn't silently fall back to extractive mid-eval. Client
errors (400/401/404) are not retried. Two tests cover both paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
run_answers.py scores the corpus pipeline (hybrid_hits -> minimax-m3 grounded
answer) vs gold FinanceBench answers via an LLM judge, tracking accuracy,
abstain-rate, and correct-when-answered. Judge parser has a deterministic
self-check. Scores only ingested docs (run_recall convention). Ships the 150-Q
set. Gate: beat naive-RAG 0.19, target >=0.55.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
minimax-m3 spends ~100-200 tokens reasoning before emitting YES/NO, so the
judge's max_tokens=8 truncated it to empty content -> every question scored
wrong (false 0/21). Bumped to 2000. Real result on the AMD/AmEx/Boeing subset:
10/21 = 0.476 vs naive-RAG baseline 0.19.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…aseline

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
build_context now retrieves via store.hybrid_hits (Qdrant hybrid dense+BM25) —
the pipeline measured at 0.476 vs 0.19 on FinanceBench — instead of the legacy
search_with_trace. Adds a standalone keyword-scan fallback for degraded operation
(empty/unavailable vector index: offline, cold corpus, tests); this is a safety
net, not the retired multi-stage pipeline. Eval gains a --legacy flag for the
head-to-head. Suite green (158 pass; only pre-existing enterprise-stack failures).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…h result

Replace the SPLADE/ColBERT "enterprise" narrative and fabricated results table
with what actually runs: PDF -> Docling -> Qdrant hybrid -> minimax-m3 grounded
answer with citations + abstain. Real number: 0.476 vs 0.19 naive-RAG baseline
on FinanceBench, reproducible via evals/financebench/run_answers.py. States the
subset scope and the table-retrieval failure mode honestly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…se 4 pt1)

ContextEngine.build_context is now just hybrid retrieve -> token-budget pack
(151 -> 43 lines). Removes the fixture-fitted _trim_structured_code_table /
_trim_asset_mapping_table (EM-### / CN-P-103 / UC-T matching) and their 3 tests.
Delete UPGRADE_PLAN.md (stale SPLADE/ColBERT plan, contradicted the honest README).
Records the hybrid>legacy>naive head-to-head. Suite unchanged (155 pass; only
pre-existing enterprise-stack failures remain).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove ~2,860 LOC (src 7,164 -> 4,306, 44 -> 34 files) of code disconnected from
the live path. Deleted: the multi-stage legacy pipeline (candidates, ranker,
expansion, parent_resolver, pipeline, query, profile) and the enterprise
SPLADE/ColBERT stack (intent_router, embedding_pipeline, adaptive_chunker).
Rewrote store.py (490->~290) and vector_index.py (755->~290) to just the live
hybrid path: index_blocks + search_hybrid + hybrid_hits + fallback. Repointed
the CLI eval-retrieval command and dropped the eval --legacy flag. Deleted the
tests for the removed modules; migrated the survivors to search_hybrid.

Suite: 76 passed, 0 failed (the 9 pre-existing failures lived in the deleted
code). ruff clean across the repo. Live CLI ask verified stages=hybrid end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Delete benchmarks.md (fabricated 0.736/0.556 numbers), code-flow.md (maps the
deleted retrieval stack), architecture.md + application-architecture.md (describe
the removed SPLADE/ColBERT enterprise pipeline as current). The honest README is
now the single source of truth. Kept: demo-questions.md, the 300-page blueprint,
and specs/ (forward-looking roadmap, not claims about what ships today).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…> 0.8

One decompose call rewrites a concept question into underlying line items
("quick ratio" -> current assets / current liabilities / cash), retrieves each
via hybrid_hits, merges/dedups, then a table-aware LLM rerank keeps the best
blocks in budget. No multi-agent orchestration.

Measured head-to-head on the 10 hardest FinanceBench questions: plain hybrid
5/10 (0.500) -> agentic 8/10 (0.800), correct-when-answered 0.889. Flipped the
table-retrieval failures: quick ratio now computed from the retrieved balance
sheet, Xilinx-amortization driver found, geographies taxonomy matched.

Built alongside plain (run_answers --agentic) so it stays measurable. Unit test
covers decompose/merge/dedup/rerank parsing with fakes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Live corpus queries were unscoped: "AMD current assets" matched every company's
balance sheet, so a cross-company corpus returned a mix and the model abstained.
Add route_document (1 LLM call: pick the target filing from the manifest) — agentic
now routes to the filing when the store is unscoped, then decompose/retrieve/rerank
within it. Live "AMD quick ratio" now scopes to AMD only and computes ~1.57.

Wire agentic as the live default (CONTEXTIQ_AGENTIC, graceful fallback to plain
without a client): query.py builds the client, ContextEngine runs agentic_retrieve.
run_answers gains --corpus (unscoped, routed) and --show. Eval respects
CONTEXTIQ_DATA_DIR. Tests cover routing/index parsing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…per-doc ~0.79)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

…MMLongBench

Resolves the divergence between the lean agentic rebuild (this branch) and main's
enterprise-direction work:
- Rebuild wins on retrieval: enterprise stack stays deleted; store.py/vector_index.py
  keep the clean hybrid rewrite; config keeps the agentic/OpenRouter settings.
- KEPT main's self-contained document-tree + extractor work (tree, tree_store,
  summarizer, heading_hierarchy, extractors/*) — it integrates cleanly (105 tests pass).
- DROPPED the MMLongBench eval (coupled to the deleted enterprise retrieval — recoverable
  from #22 for deliberate re-integration on the new pipeline) and dead enterprise scripts
  (qa/smoke/reindex).

Suite: 105 passed, 0 failed. ruff clean. Live agentic answer verified end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

…sses CI

CI failed at 'uv sync' because pyproject pinned local editable path deps
(ven-obs at ../ven-obs, ven-eval at ../../.worktrees/...) that don't exist on
the runner. Removed the hard ven-obs dependency, [tool.uv.sources], and the
ven-eval dependency group. obs.py now degrades to a no-op observability API when
ven_obs isn't importable; query.py imports `api` from contextiq.obs. The span
test skips (importorskip) when ven_obs is absent. Suite 105 pass; no-op path
verified with ven_obs blocked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

… eval test)

- Untrack docs/superpowers/ (already gitignored) so the "no process docs in tree"
  hygiene gate passes; the plan stays local.
- test_provider_shape: importorskip("evals.provider") — the promptfoo provider
  needs the local ven_eval package, unavailable in CI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@Ven-Z8 Ven-Z8 merged commit 1fe5259 into main Jul 2, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant