Skip to content

Ruthwik-Data/finrag-eval

Repository files navigation

finrag-eval

CI

A local RAG evaluation pipeline on real SEC filings (Apple FY 2024/2025 10-Ks, Microsoft FY 2025 10-K, Apple Q4 FY2025 earnings) — built to understand where financial document retrieval breaks down, not just to make a demo that works. The work here surfaced a real metric bug that is now merged into DeepEval.

Problem & Why

Financial analysts and AI teams building RAG over SEC filings face a dangerous failure mode: the system answers confidently even when it shouldn't. Standard RAG evals don't catch the difference between "I don't know" (good) and "Revenue was $387.2B" when the actual number is $383.3B (dangerous). This project measures that gap on real data.

What This Is

An end-to-end local RAG pipeline — SEC EDGAR ingestion through answer generation — with a 26-question evaluation suite grounded against real figures from Apple (FY 2024 + FY 2025 10-Ks and the Q4 FY2025 earnings release) and Microsoft (FY 2025 10-K). Questions span two issuers and three document types (10-K narrative, balance-sheet statements, earnings-call/guidance), each scored against a document-type-specific pass threshold (see Heterogeneous thresholds).

Architecture

SEC EDGAR (10-K PDF)
    → pdfplumber (section-aware PDF parsing)
    → Chunker (configurable overlap, section boundaries preserved)
    → Ollama nomic-embed-text (local embeddings)
    → Supabase pgvector / Docker (local vector store)
    → Top-k retrieval
    → Ollama llama3 (local LLM, $0 API cost)
    → Metadata-aware section routing (optional re-rank by query intent)
    → Evaluation layer (DeepEval metrics + custom section-aware precision/recall,
      multi-hop per-hop scoring, table-extraction quality, eval→action feedback)
Layer Tool Why
PDF parsing pdfplumber Handles financial table extraction reasonably well
Embeddings nomic-embed-text Local, free, strong on financial terminology
Vector store Supabase + pgvector SQL + vector in one; production-representative
LLM llama3 via Ollama Fully local — no API costs, reproducible
Orchestration Plain Python Debuggable; no hidden abstractions

Evaluation & Results

Initial manual finding (FY 2024)

The first 3-question probe against Apple's FY 2024 10-K (ground truth pulled directly from the filing) is what exposed the core failure mode:

Question Ground Truth System Response Verdict
Total net revenue vs FY 2023? $391.0B (+2% YoY) Refused — "not found in context" ✅ Honest refusal
Gross margin %? 46.2% Refused — "not found in context" ✅ Honest refusal
R&D spend + % of revenue? $31.4B / 8.0% Answered confidently with wrong figures ❌ Confident hallucination

Key finding: 2/3 honest refusals. 1/3 confident hallucination with precise but incorrect numbers.

The dangerous failure mode is not "I don't know" — it's "The answer is X" where X is wrong and sounds credible.

Expanded suite (two issuers, three document types)

The dataset (src/eval/eval_dataset.json) now holds 26 questions grounded against real filings from two issuers:

Dimension Split
Issuer 22 × Apple, 4 × Microsoft
Document type 13 × 10k_filing, 9 × balance_sheet, 4 × earnings_call
Source period Apple FY2024 + FY2025 10-Ks, Apple Q4 FY2025 earnings release, Microsoft FY2025 10-K

Every figure is sourced from filings and official press releases (e.g., Apple FY2025 revenue $416.2B, Apple Q4 FY2025 revenue $102.5B, Microsoft FY2025 revenue $281.7B / net income $101.8B). No figures are fabricated.

Heterogeneous document thresholds

A single pass threshold across all financial document types is wrong: structured statements (a balance sheet is either right or it isn't) should tolerate zero hallucination, while hedged / forward-looking narrative can be scored more leniently. The runner picks the threshold per question from its document_type:

Document type Pass threshold
balance_sheet 0.95
10k_filing 0.85
annual_report 0.80
earnings_call 0.70

This is applied client-side today (each metric is constructed with the resolved threshold) because DeepEval's native threshold_overrides argument is still landing upstream — see Issue #2775 and the demo in PR #2790. The custom section-aware precision metric (src/eval/metrics.py) runs alongside DeepEval's built-ins and groups overlapping chunks by doc_id:section_id so redundant retrieval windows count as a single relevant unit.

What This Led To

Running DeepEval's ContextualPrecisionMetric on this pipeline exposed a metric-level bug: overlapping chunks (10-20% overlap, standard for preserving table/section boundaries) were being penalized as independent retrieval failures — making eval scores worse as chunk quality improved.

That finding became GitHub Issue #2594, and the fix I authored — grouping retrieval contexts by source and correcting the weighted cumulative precision formula — was merged into DeepEval as PR #2743.

The eval found a bug in the eval framework, and the fix shipped. That's the point.

Upstream Impact: Contributions to DeepEval

This project is the real-world workload that surfaced several issues in DeepEval (Confident AI's open-source LLM eval framework). The custom metrics in src/eval/metrics.py are the local prototypes of ideas being upstreamed:

# Type Status Contribution
#2743 PR Merged ContextualPrecisionMetric: source-grouping of retrieval contexts + weighted cumulative precision formula fix
#2594 Issue Closed Contextual Precision over-penalizes overlapping chunks in financial-document RAG (the original bug report)
#2775 Issue Open Feature: eval metrics for heterogeneous financial document chunks / per-document-type thresholds
#2788 Issue Open Bug: ContextualRecallMetric over-penalises overlapping chunks (parallel to #2594)
#2790 PR Open Docs example: heterogeneous financial document RAG evaluation with threshold_overrides
#2789 PR Open Regression fixtures for ContextualRecallMetric overlapping chunks (closes #2788)
#2787 PR Open Regression fixtures for ContextualPrecisionMetric overlapping chunks (rebased on #2743)
#2819 PR Open AgentLoopDetectionMetric — detect infinite loops / cyclical tool-call patterns in agent traces

The overlapping-chunk penalty documented in Lessons Learned is exactly the behavior fixed in #2743 and being hardened by the regression PRs above.

Ideas prototyped in this repo

The eval ideas I've proposed across the RAG/eval ecosystem aren't just issues — each is implemented and verified here, on real Apple filings. This repo is the reference implementation behind the proposals:

Idea (upstream proposal) Where proposed Implementation in this repo
Overlapping-chunk precision (source grouping) deepeval #2594merged #2743 section_aware_precision + chunk_dedup.py
Overlapping-chunk recall (union coverage) deepeval #2788/#2789 section_aware_recall — recall is monotonic under redundancy
Per-document-type thresholds deepeval #2775/#2790 THRESHOLD_OVERRIDES + document_type dataset tagging
Metadata-aware section routing llama_index #22032/#21862 retrieval/section_router.py (opt-in metadata_routing)
Multi-hop per-hop quality mastra #18258, phoenix #13407, openinference #3256, weave #6946/#7280 eval/hop_scorer.py + multihop_dataset.json — surfaces the weakest hop
Structured table-extraction quality firecrawl #3587/#3817 eval/table_eval.py + table_ground_truth.json (real 10-K cells)
Eval → action feedback loop langsmith #2929 eval/feedback_loop.py — maps weak metrics to config knobs (recommend-only)
Automated numeric-hallucination detection addresses the project's core "confident wrong number" thesis eval/hallucination.py — extracts asserted figures, flags any unsupported by context

How to Use

git clone https://github.com/Ruthwik-Data/finrag-eval
cd finrag-eval
docker-compose up -d
python scripts/init_db.py

# Download the two most recent 10-Ks (FY 2024 + FY 2025) and ingest each
python src/ingestion/edgar_download.py --ticker AAPL --count 2
python src/ingestion/ingest.py --input data/raw/<downloaded-filing>.htm --ticker AAPL

# Query and evaluate
python src/retrieval/query.py "What was Apple's total net revenue for fiscal year 2025?"
python src/eval/run_eval.py                 # full 26-question suite
python src/eval/run_eval.py --compare       # raw vs. deduped comparison

Each eval question prints its doc_type and the pass threshold applied to it. Manual verdicts from the initial probe are in notes.

Development & Testing

The custom metrics are decoupled from DeepEval (the framework is an optional import), so the full metric suite is unit-tested with no external stack — no DB, no Ollama, no LLM calls:

make dev        # install dev/test deps
make test       # run the suite (also runs in CI on every push/PR)

Covered by tests: section-aware precision/recall (incl. redundancy-monotonicity), metadata routing, multi-hop hop-scoring, table-extraction quality, the feedback loop, numeric-hallucination detection, threshold resolution, and dataset integrity. CI runs on Python 3.11/3.12 via GitHub Actions (.github/workflows/ci.yml).

Ablation (the core thesis)

The project's thesis — overlap helps retrieval but hurts naive eval — is isolated with three runs:

make ablation   # runs raw / --deduplicate / --metadata-routing and stores each

Lessons Learned

  1. Confident hallucination is worse than refusal. A system that says "I don't know" is safer than one that gives a precise wrong number. Calibrating refusal behavior is a product decision, not just a technical one.
  2. Overlap helps retrieval, hurts naive evals. Increasing chunk overlap improved answer grounding but lowered DeepEval precision scores — because the metric penalized redundant chunks as misses. Evaluation metrics can lie about retrieval quality.
  3. Local-first forced honesty. Running fully locally ($0 API cost) meant I couldn't rely on GPT-4 to paper over weak retrieval. The results are less polished but more honest.

Known Limitations

  • 26-question eval is illustrative, not statistically significant
  • Ground truth extracted from Apple/Microsoft filings and official press releases — possible human error on edge cases
  • Retrieval scores require the relevant filings (Apple FY2024/FY2025 10-Ks, Microsoft FY2025 10-K, Apple Q4 FY2025 release) to be ingested; dataset labels are provided regardless of what is currently in the vector store
  • llama3 locally is weaker than GPT-4 class models
  • The custom section-aware / hallucination metrics use keyword/figure heuristics (an optional embedding relevance mode exists); they are designed to demonstrate the failure modes, not to replace an LLM judge
  • Section detection in pdfplumber is heuristic and may miss boundaries in complex filings

About

RAG eval pipeline on Apple's FY 2024 10-K — found confident hallucinations, filed a metric-level bug in DeepEval, and built section-aware chunking.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors