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)
LayerToolWhy
PDF parsingpdfplumberHandles financial table extraction reasonably well
Embeddingsnomic-embed-textLocal, free, strong on financial terminology
Vector storeSupabase + pgvectorSQL + vector in one; production-representative
LLMllama3 via OllamaFully local — no API costs, reproducible
OrchestrationPlain PythonDebuggable; 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:

QuestionGround TruthSystem ResponseVerdict
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:

DimensionSplit
Issuer22 × Apple, 4 × Microsoft
Document type13 × 10k_filing, 9 × balance_sheet, 4 × earnings_call
Source periodApple 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 typePass threshold
balance_sheet0.95
10k_filing0.85
annual_report0.80
earnings_call0.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:

#TypeStatusContribution
#2743PRMergedContextualPrecisionMetric: source-grouping of retrieval contexts + weighted cumulative precision formula fix
#2594IssueClosedContextual Precision over-penalizes overlapping chunks in financial-document RAG (the original bug report)
#2775IssueOpenFeature: eval metrics for heterogeneous financial document chunks / per-document-type thresholds
#2788IssueOpenBug: ContextualRecallMetric over-penalises overlapping chunks (parallel to #2594)
#2790PROpenDocs example: heterogeneous financial document RAG evaluation with threshold_overrides
#2789PROpenRegression fixtures for ContextualRecallMetric overlapping chunks (closes #2788)
#2787PROpenRegression fixtures for ContextualPrecisionMetric overlapping chunks (rebased on #2743)
#2819PROpenAgentLoopDetectionMetric — 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 proposedImplementation in this repo
Overlapping-chunk precision (source grouping)deepeval #2594merged #2743section_aware_precision + chunk_dedup.py
Overlapping-chunk recall (union coverage)deepeval #2788/#2789section_aware_recall — recall is monotonic under redundancy
Per-document-type thresholdsdeepeval #2775/#2790THRESHOLD_OVERRIDES + document_type dataset tagging
Metadata-aware section routingllama_index #22032/#21862retrieval/section_router.py (opt-in metadata_routing)
Multi-hop per-hop qualitymastra #18258, phoenix #13407, openinference #3256, weave #6946/#7280eval/hop_scorer.py + multihop_dataset.json — surfaces the weakest hop
Structured table-extraction qualityfirecrawl #3587/#3817eval/table_eval.py + table_ground_truth.json (real 10-K cells)
Eval → action feedback looplangsmith #2929eval/feedback_loop.py — maps weak metrics to config knobs (recommend-only)
Automated numeric-hallucination detectionaddresses the project's core "confident wrong number" thesiseval/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

Local RAG eval on real SEC 10-Ks that catches confident financial hallucinations — and surfaced a metric bug now merged upstream into DeepEval.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

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)
LayerToolWhy
PDF parsingpdfplumberHandles financial table extraction reasonably well
Embeddingsnomic-embed-textLocal, free, strong on financial terminology
Vector storeSupabase + pgvectorSQL + vector in one; production-representative
LLMllama3 via OllamaFully local — no API costs, reproducible
OrchestrationPlain PythonDebuggable; 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:

QuestionGround TruthSystem ResponseVerdict
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:

DimensionSplit
Issuer22 × Apple, 4 × Microsoft
Document type13 × 10k_filing, 9 × balance_sheet, 4 × earnings_call
Source periodApple 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 typePass threshold
balance_sheet0.95
10k_filing0.85
annual_report0.80
earnings_call0.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:

#TypeStatusContribution
#2743PRMergedContextualPrecisionMetric: source-grouping of retrieval contexts + weighted cumulative precision formula fix
#2594IssueClosedContextual Precision over-penalizes overlapping chunks in financial-document RAG (the original bug report)
#2775IssueOpenFeature: eval metrics for heterogeneous financial document chunks / per-document-type thresholds
#2788IssueOpenBug: ContextualRecallMetric over-penalises overlapping chunks (parallel to #2594)
#2790PROpenDocs example: heterogeneous financial document RAG evaluation with threshold_overrides
#2789PROpenRegression fixtures for ContextualRecallMetric overlapping chunks (closes #2788)
#2787PROpenRegression fixtures for ContextualPrecisionMetric overlapping chunks (rebased on #2743)
#2819PROpenAgentLoopDetectionMetric — 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 proposedImplementation in this repo
Overlapping-chunk precision (source grouping)deepeval #2594merged #2743section_aware_precision + chunk_dedup.py
Overlapping-chunk recall (union coverage)deepeval #2788/#2789section_aware_recall — recall is monotonic under redundancy
Per-document-type thresholdsdeepeval #2775/#2790THRESHOLD_OVERRIDES + document_type dataset tagging
Metadata-aware section routingllama_index #22032/#21862retrieval/section_router.py (opt-in metadata_routing)
Multi-hop per-hop qualitymastra #18258, phoenix #13407, openinference #3256, weave #6946/#7280eval/hop_scorer.py + multihop_dataset.json — surfaces the weakest hop
Structured table-extraction qualityfirecrawl #3587/#3817eval/table_eval.py + table_ground_truth.json (real 10-K cells)
Eval → action feedback looplangsmith #2929eval/feedback_loop.py — maps weak metrics to config knobs (recommend-only)
Automated numeric-hallucination detectionaddresses the project's core "confident wrong number" thesiseval/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

Local RAG eval on real SEC 10-Ks that catches confident financial hallucinations — and surfaced a metric bug now merged upstream into DeepEval.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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)
LayerToolWhy
PDF parsingpdfplumberHandles financial table extraction reasonably well
Embeddingsnomic-embed-textLocal, free, strong on financial terminology
Vector storeSupabase + pgvectorSQL + vector in one; production-representative
LLMllama3 via OllamaFully local — no API costs, reproducible
OrchestrationPlain PythonDebuggable; 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:

QuestionGround TruthSystem ResponseVerdict
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:

DimensionSplit
Issuer22 × Apple, 4 × Microsoft
Document type13 × 10k_filing, 9 × balance_sheet, 4 × earnings_call
Source periodApple 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 typePass threshold
balance_sheet0.95
10k_filing0.85
annual_report0.80
earnings_call0.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:

#TypeStatusContribution
#2743PRMergedContextualPrecisionMetric: source-grouping of retrieval contexts + weighted cumulative precision formula fix
#2594IssueClosedContextual Precision over-penalizes overlapping chunks in financial-document RAG (the original bug report)
#2775IssueOpenFeature: eval metrics for heterogeneous financial document chunks / per-document-type thresholds
#2788IssueOpenBug: ContextualRecallMetric over-penalises overlapping chunks (parallel to #2594)
#2790PROpenDocs example: heterogeneous financial document RAG evaluation with threshold_overrides
#2789PROpenRegression fixtures for ContextualRecallMetric overlapping chunks (closes #2788)
#2787PROpenRegression fixtures for ContextualPrecisionMetric overlapping chunks (rebased on #2743)
#2819PROpenAgentLoopDetectionMetric — 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 proposedImplementation in this repo
Overlapping-chunk precision (source grouping)deepeval #2594merged #2743section_aware_precision + chunk_dedup.py
Overlapping-chunk recall (union coverage)deepeval #2788/#2789section_aware_recall — recall is monotonic under redundancy
Per-document-type thresholdsdeepeval #2775/#2790THRESHOLD_OVERRIDES + document_type dataset tagging
Metadata-aware section routingllama_index #22032/#21862retrieval/section_router.py (opt-in metadata_routing)
Multi-hop per-hop qualitymastra #18258, phoenix #13407, openinference #3256, weave #6946/#7280eval/hop_scorer.py + multihop_dataset.json — surfaces the weakest hop
Structured table-extraction qualityfirecrawl #3587/#3817eval/table_eval.py + table_ground_truth.json (real 10-K cells)
Eval → action feedback looplangsmith #2929eval/feedback_loop.py — maps weak metrics to config knobs (recommend-only)
Automated numeric-hallucination detectionaddresses the project's core "confident wrong number" thesiseval/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

Local RAG eval on real SEC 10-Ks that catches confident financial hallucinations — and surfaced a metric bug now merged upstream into DeepEval.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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)
LayerToolWhy
PDF parsingpdfplumberHandles financial table extraction reasonably well
Embeddingsnomic-embed-textLocal, free, strong on financial terminology
Vector storeSupabase + pgvectorSQL + vector in one; production-representative
LLMllama3 via OllamaFully local — no API costs, reproducible
OrchestrationPlain PythonDebuggable; 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:

QuestionGround TruthSystem ResponseVerdict
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:

DimensionSplit
Issuer22 × Apple, 4 × Microsoft
Document type13 × 10k_filing, 9 × balance_sheet, 4 × earnings_call
Source periodApple 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 typePass threshold
balance_sheet0.95
10k_filing0.85
annual_report0.80
earnings_call0.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:

#TypeStatusContribution
#2743PRMergedContextualPrecisionMetric: source-grouping of retrieval contexts + weighted cumulative precision formula fix
#2594IssueClosedContextual Precision over-penalizes overlapping chunks in financial-document RAG (the original bug report)
#2775IssueOpenFeature: eval metrics for heterogeneous financial document chunks / per-document-type thresholds
#2788IssueOpenBug: ContextualRecallMetric over-penalises overlapping chunks (parallel to #2594)
#2790PROpenDocs example: heterogeneous financial document RAG evaluation with threshold_overrides
#2789PROpenRegression fixtures for ContextualRecallMetric overlapping chunks (closes #2788)
#2787PROpenRegression fixtures for ContextualPrecisionMetric overlapping chunks (rebased on #2743)
#2819PROpenAgentLoopDetectionMetric — 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 proposedImplementation in this repo
Overlapping-chunk precision (source grouping)deepeval #2594merged #2743section_aware_precision + chunk_dedup.py
Overlapping-chunk recall (union coverage)deepeval #2788/#2789section_aware_recall — recall is monotonic under redundancy
Per-document-type thresholdsdeepeval #2775/#2790THRESHOLD_OVERRIDES + document_type dataset tagging
Metadata-aware section routingllama_index #22032/#21862retrieval/section_router.py (opt-in metadata_routing)
Multi-hop per-hop qualitymastra #18258, phoenix #13407, openinference #3256, weave #6946/#7280eval/hop_scorer.py + multihop_dataset.json — surfaces the weakest hop
Structured table-extraction qualityfirecrawl #3587/#3817eval/table_eval.py + table_ground_truth.json (real 10-K cells)
Eval → action feedback looplangsmith #2929eval/feedback_loop.py — maps weak metrics to config knobs (recommend-only)
Automated numeric-hallucination detectionaddresses the project's core "confident wrong number" thesiseval/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

Local RAG eval on real SEC 10-Ks that catches confident financial hallucinations — and surfaced a metric bug now merged upstream into DeepEval.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

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)
LayerToolWhy
PDF parsingpdfplumberHandles financial table extraction reasonably well
Embeddingsnomic-embed-textLocal, free, strong on financial terminology
Vector storeSupabase + pgvectorSQL + vector in one; production-representative
LLMllama3 via OllamaFully local — no API costs, reproducible
OrchestrationPlain PythonDebuggable; 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:

QuestionGround TruthSystem ResponseVerdict
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:

DimensionSplit
Issuer22 × Apple, 4 × Microsoft
Document type13 × 10k_filing, 9 × balance_sheet, 4 × earnings_call
Source periodApple 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 typePass threshold
balance_sheet0.95
10k_filing0.85
annual_report0.80
earnings_call0.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:

#TypeStatusContribution
#2743PRMergedContextualPrecisionMetric: source-grouping of retrieval contexts + weighted cumulative precision formula fix
#2594IssueClosedContextual Precision over-penalizes overlapping chunks in financial-document RAG (the original bug report)
#2775IssueOpenFeature: eval metrics for heterogeneous financial document chunks / per-document-type thresholds
#2788IssueOpenBug: ContextualRecallMetric over-penalises overlapping chunks (parallel to #2594)
#2790PROpenDocs example: heterogeneous financial document RAG evaluation with threshold_overrides
#2789PROpenRegression fixtures for ContextualRecallMetric overlapping chunks (closes #2788)
#2787PROpenRegression fixtures for ContextualPrecisionMetric overlapping chunks (rebased on #2743)
#2819PROpenAgentLoopDetectionMetric — 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 proposedImplementation in this repo
Overlapping-chunk precision (source grouping)deepeval #2594merged #2743section_aware_precision + chunk_dedup.py
Overlapping-chunk recall (union coverage)deepeval #2788/#2789section_aware_recall — recall is monotonic under redundancy
Per-document-type thresholdsdeepeval #2775/#2790THRESHOLD_OVERRIDES + document_type dataset tagging
Metadata-aware section routingllama_index #22032/#21862retrieval/section_router.py (opt-in metadata_routing)
Multi-hop per-hop qualitymastra #18258, phoenix #13407, openinference #3256, weave #6946/#7280eval/hop_scorer.py + multihop_dataset.json — surfaces the weakest hop
Structured table-extraction qualityfirecrawl #3587/#3817eval/table_eval.py + table_ground_truth.json (real 10-K cells)
Eval → action feedback looplangsmith #2929eval/feedback_loop.py — maps weak metrics to config knobs (recommend-only)
Automated numeric-hallucination detectionaddresses the project's core "confident wrong number" thesiseval/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

Local RAG eval on real SEC 10-Ks that catches confident financial hallucinations — and surfaced a metric bug now merged upstream into DeepEval.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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)
LayerToolWhy
PDF parsingpdfplumberHandles financial table extraction reasonably well
Embeddingsnomic-embed-textLocal, free, strong on financial terminology
Vector storeSupabase + pgvectorSQL + vector in one; production-representative
LLMllama3 via OllamaFully local — no API costs, reproducible
OrchestrationPlain PythonDebuggable; 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:

QuestionGround TruthSystem ResponseVerdict
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:

DimensionSplit
Issuer22 × Apple, 4 × Microsoft
Document type13 × 10k_filing, 9 × balance_sheet, 4 × earnings_call
Source periodApple 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 typePass threshold
balance_sheet0.95
10k_filing0.85
annual_report0.80
earnings_call0.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:

#TypeStatusContribution
#2743PRMergedContextualPrecisionMetric: source-grouping of retrieval contexts + weighted cumulative precision formula fix
#2594IssueClosedContextual Precision over-penalizes overlapping chunks in financial-document RAG (the original bug report)
#2775IssueOpenFeature: eval metrics for heterogeneous financial document chunks / per-document-type thresholds
#2788IssueOpenBug: ContextualRecallMetric over-penalises overlapping chunks (parallel to #2594)
#2790PROpenDocs example: heterogeneous financial document RAG evaluation with threshold_overrides
#2789PROpenRegression fixtures for ContextualRecallMetric overlapping chunks (closes #2788)
#2787PROpenRegression fixtures for ContextualPrecisionMetric overlapping chunks (rebased on #2743)
#2819PROpenAgentLoopDetectionMetric — 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 proposedImplementation in this repo
Overlapping-chunk precision (source grouping)deepeval #2594merged #2743section_aware_precision + chunk_dedup.py
Overlapping-chunk recall (union coverage)deepeval #2788/#2789section_aware_recall — recall is monotonic under redundancy
Per-document-type thresholdsdeepeval #2775/#2790THRESHOLD_OVERRIDES + document_type dataset tagging
Metadata-aware section routingllama_index #22032/#21862retrieval/section_router.py (opt-in metadata_routing)
Multi-hop per-hop qualitymastra #18258, phoenix #13407, openinference #3256, weave #6946/#7280eval/hop_scorer.py + multihop_dataset.json — surfaces the weakest hop
Structured table-extraction qualityfirecrawl #3587/#3817eval/table_eval.py + table_ground_truth.json (real 10-K cells)
Eval → action feedback looplangsmith #2929eval/feedback_loop.py — maps weak metrics to config knobs (recommend-only)
Automated numeric-hallucination detectionaddresses the project's core "confident wrong number" thesiseval/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

Local RAG eval on real SEC 10-Ks that catches confident financial hallucinations — and surfaced a metric bug now merged upstream into DeepEval.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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)
LayerToolWhy
PDF parsingpdfplumberHandles financial table extraction reasonably well
Embeddingsnomic-embed-textLocal, free, strong on financial terminology
Vector storeSupabase + pgvectorSQL + vector in one; production-representative
LLMllama3 via OllamaFully local — no API costs, reproducible
OrchestrationPlain PythonDebuggable; 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:

QuestionGround TruthSystem ResponseVerdict
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:

DimensionSplit
Issuer22 × Apple, 4 × Microsoft
Document type13 × 10k_filing, 9 × balance_sheet, 4 × earnings_call
Source periodApple 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 typePass threshold
balance_sheet0.95
10k_filing0.85
annual_report0.80
earnings_call0.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:

#TypeStatusContribution
#2743PRMergedContextualPrecisionMetric: source-grouping of retrieval contexts + weighted cumulative precision formula fix
#2594IssueClosedContextual Precision over-penalizes overlapping chunks in financial-document RAG (the original bug report)
#2775IssueOpenFeature: eval metrics for heterogeneous financial document chunks / per-document-type thresholds
#2788IssueOpenBug: ContextualRecallMetric over-penalises overlapping chunks (parallel to #2594)
#2790PROpenDocs example: heterogeneous financial document RAG evaluation with threshold_overrides
#2789PROpenRegression fixtures for ContextualRecallMetric overlapping chunks (closes #2788)
#2787PROpenRegression fixtures for ContextualPrecisionMetric overlapping chunks (rebased on #2743)
#2819PROpenAgentLoopDetectionMetric — 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 proposedImplementation in this repo
Overlapping-chunk precision (source grouping)deepeval #2594merged #2743section_aware_precision + chunk_dedup.py
Overlapping-chunk recall (union coverage)deepeval #2788/#2789section_aware_recall — recall is monotonic under redundancy
Per-document-type thresholdsdeepeval #2775/#2790THRESHOLD_OVERRIDES + document_type dataset tagging
Metadata-aware section routingllama_index #22032/#21862retrieval/section_router.py (opt-in metadata_routing)
Multi-hop per-hop qualitymastra #18258, phoenix #13407, openinference #3256, weave #6946/#7280eval/hop_scorer.py + multihop_dataset.json — surfaces the weakest hop
Structured table-extraction qualityfirecrawl #3587/#3817eval/table_eval.py + table_ground_truth.json (real 10-K cells)
Eval → action feedback looplangsmith #2929eval/feedback_loop.py — maps weak metrics to config knobs (recommend-only)
Automated numeric-hallucination detectionaddresses the project's core "confident wrong number" thesiseval/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

Local RAG eval on real SEC 10-Ks that catches confident financial hallucinations — and surfaced a metric bug now merged upstream into DeepEval.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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)
LayerToolWhy
PDF parsingpdfplumberHandles financial table extraction reasonably well
Embeddingsnomic-embed-textLocal, free, strong on financial terminology
Vector storeSupabase + pgvectorSQL + vector in one; production-representative
LLMllama3 via OllamaFully local — no API costs, reproducible
OrchestrationPlain PythonDebuggable; 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:

QuestionGround TruthSystem ResponseVerdict
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:

DimensionSplit
Issuer22 × Apple, 4 × Microsoft
Document type13 × 10k_filing, 9 × balance_sheet, 4 × earnings_call
Source periodApple 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 typePass threshold
balance_sheet0.95
10k_filing0.85
annual_report0.80
earnings_call0.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:

#TypeStatusContribution
#2743PRMergedContextualPrecisionMetric: source-grouping of retrieval contexts + weighted cumulative precision formula fix
#2594IssueClosedContextual Precision over-penalizes overlapping chunks in financial-document RAG (the original bug report)
#2775IssueOpenFeature: eval metrics for heterogeneous financial document chunks / per-document-type thresholds
#2788IssueOpenBug: ContextualRecallMetric over-penalises overlapping chunks (parallel to #2594)
#2790PROpenDocs example: heterogeneous financial document RAG evaluation with threshold_overrides
#2789PROpenRegression fixtures for ContextualRecallMetric overlapping chunks (closes #2788)
#2787PROpenRegression fixtures for ContextualPrecisionMetric overlapping chunks (rebased on #2743)
#2819PROpenAgentLoopDetectionMetric — 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 proposedImplementation in this repo
Overlapping-chunk precision (source grouping)deepeval #2594merged #2743section_aware_precision + chunk_dedup.py
Overlapping-chunk recall (union coverage)deepeval #2788/#2789section_aware_recall — recall is monotonic under redundancy
Per-document-type thresholdsdeepeval #2775/#2790THRESHOLD_OVERRIDES + document_type dataset tagging
Metadata-aware section routingllama_index #22032/#21862retrieval/section_router.py (opt-in metadata_routing)
Multi-hop per-hop qualitymastra #18258, phoenix #13407, openinference #3256, weave #6946/#7280eval/hop_scorer.py + multihop_dataset.json — surfaces the weakest hop
Structured table-extraction qualityfirecrawl #3587/#3817eval/table_eval.py + table_ground_truth.json (real 10-K cells)
Eval → action feedback looplangsmith #2929eval/feedback_loop.py — maps weak metrics to config knobs (recommend-only)
Automated numeric-hallucination detectionaddresses the project's core "confident wrong number" thesiseval/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

Local RAG eval on real SEC 10-Ks that catches confident financial hallucinations — and surfaced a metric bug now merged upstream into DeepEval.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages