Samuel GarcíaAI / Software Engineer
GitHub (opens in a new tab)Repository Contact

ENGINEERING CASE STUDY / 2026

FinRAG

Evidence-first financial document question answering, built as a local research system rather than a hosted chatbot.

FinRAG keeps the evidence visible: it ingests PDFs, retrieves with TF-IDF, MiniLM and Multi-Query, checks reporting periods, generates answers locally with Qwen, and returns the passages and page references behind each response. The repository records retrieval coverage, tests and the limits of those measurements.

Published on main. Local research application. Not a production financial-advice tool.

FinRAG answering an AMD FY2022 revenue question with source evidence from PDF page 48.
Authentic repository screenshot: local Qwen inference with inspectable evidence. The financial figure was manually checked against the report; answers are not automatically fact-verified.

OVERVIEW

What was built, how it was validated, and what remains limited

FinRAG is a local financial-document question-answering system. Python owns extraction, retrieval, evidence handling, period checks and generation; React displays the result and keeps evidence attached to the original response. Recorded holdout retrieval coverage is 20.6% for MiniLM and 26.5% for Multi-Query. Those are retrieval metrics, not answer-accuracy measurements.

  • Built: PDF ingestion, TF-IDF/MiniLM/Multi-Query retrieval, evidence fusion, local Qwen generation, FastAPI service and React workspace.
  • Validated: 146 Python tests, Ruff, 9 web tests, 17 browser tests, production frontend build and FinanceBench retrieval experiments.
  • Limited: citations are inspectable rather than fact-verified; Qwen may abstain or reason incorrectly; retrieval coverage on holdout remains low.
46.4%Pilot MiniLM Hit@528 questions · 6 reports
20.6%Holdout MiniLM Hit@534 questions · 10 reports
26.5%Holdout Multi-Query Hit@5best recorded holdout coverage
146Python testsplus 9 web and 17 browser tests

KEY ENGINEERING DECISION

Local inference over API dependence

FinRAG uses a local Ollama model instead of a paid API. This supports privacy, reproducibility and offline inspection, but generation quality, latency and capability are constrained by the local model. The implementation keeps retrieval evidence available even when generation fails.

PROBLEM AND DECISIONS

Retrieval is not the same as a correct answer.

A financial answer may require facts from several pages, the correct reporting period and arithmetic. Finding passages that look relevant is not evidence that the generated answer is right.

Problem

The system must answer questions about financial reports while keeping the evidence visible. It cannot treat a plausible model response as proof. It also has to distinguish missing evidence, abstention, malformed output and backend failure instead of collapsing them into one error message.

Constraints

  • No paid LLM API or API key for inference.
  • Local, single-user research application; not an internet-facing service.
  • PDF extraction depends on selectable text; no validated OCR pipeline.
  • FinanceBench data is CC BY-NC 4.0; reports and annotations are not redistributed.
  • Models are pretrained and are not fine-tuned by this project.

ARCHITECTURE

Python owns the evidence; React displays it.

The backend performs extraction, retrieval, evidence handling, period checks and generation. The frontend does not recompute retrieval or financial figures.

INGESTION

PDF and text reports
pypdf page extraction
Word chunks
25/5 direct · 80/15 page

RETRIEVAL AND GROUNDING

TF-IDF and MiniLM
API retrievers
Multi-Query fusion
plan, rank pages, RRF, neighbours
Period checks
reporting context only

DELIVERY

Extractive answer or Qwen
FastAPI responses
status, evidence, timings
React workspace
evidence, PDF pages, states

Implementation components

  • Ingestion: pypdf page extraction; word-based chunks.
  • Retrieval: TfidfVectorizer cosine similarity and Sentence Transformers MiniLM.
  • Evidence: chunk formatting, distinct-chunk selection and page-neighbour selection.
  • Generation: local Ollama qwen3:4b-instruct at temperature 0.
  • API: FastAPI, Pydantic strict request models, upload validation, sandboxed PDF responses.
  • Stack: Python 3.13, React, Vite, pytest, Ruff, Playwright.

RETRIEVAL AND EVIDENCE

Multi-Query is page fusion, not a prompt trick.

Direct retrieval returns top chunks. Multi-Query expands a financial question into evidence-oriented searches, ranks pages from each search, fuses the page rankings and then selects neighbouring chunks in reading order.

01

Plan queries

The original question is retained. Recognised financial concepts add statement-heading searches.

02

Search all chunks

Each query searches the fitted retriever over all chunks in the selected document.

03

Rank pages

Chunks are grouped by physical page. Each page keeps its highest chunk score.

04

Fuse rankings

With more than one query, page rankings are combined with Reciprocal Rank Fusion using constant 60.

05

Select neighbours

For each top page, the code selects the nearest chunk IDs around the best-scoring chunk.

06

Return evidence

The result is a page-grounded evidence set with chunk text, scores and page indexes.

Chunking settings

ModeChunk sizeOverlapUnit
Direct255words per physical page
Multi-Query8015words per physical page

These are API ingestion settings. They do not change the historical evaluation scripts or algorithms.

Query planning example

For a quick-ratio question, the code adds searches for balance-sheet cash, short-term investments, accounts receivable and total current liabilities.

FINANCIAL_CONCEPTS = {
  "quick ratio": [
    "consolidated balance sheet cash short-term investments accounts receivable",
    "consolidated balance sheet total current liabilities",
  ],
}

Response states

producedPassed structural checks and citation range checks.
model_abstentionModel emitted an abstention marker.
invalid_outputMissing, malformed or out-of-range citations.
no_evidenceNo evidence supplied.
insufficient_contextRequested period not verified in recognized reporting context.
backend_errorOllama or network failure; evidence is still returned.

Period grounding

The period checker searches complete evidence-document pages for explicit fiscal periods or supported English statement dates. It can add recognized reporting-context headers to model evidence.

  • Requested years found in recognized context: available.
  • Requested years missing from recognized context: insufficient_context.
  • Unknown date format: unverified, query is not blocked.
  • It does not verify amount-to-column alignment, units or arithmetic.

EVALUATION AND RELIABILITY

Recorded retrieval coverage, not answer accuracy.

The metrics below are retrieval evaluations. They measure whether annotated supporting pages are recovered, not whether the generator uses them correctly.

FinanceBench all-evidence Hit@5

SplitQuestionsReportsMiniLMMulti-Query
Pilot28646.4%53.6%
Holdout341020.6%26.5%

All-evidence Hit@5 requires every annotated supporting page to appear among the five highest-ranked distinct pages for the relevant report. It is stricter than finding at least one supporting page and does not assess answer correctness.

Synthetic regression corpus

RetrieverAccepted chunk @1Accepted chunk @3
TF-IDF83.3%100.0%
MiniLM75.0%100.0%

4 documents, 12 chunks and 12 answerable questions, plus 6 unanswerable questions. This is an any-evidence hit rate used to verify retrieval code, not a FinanceBench result.

Failure analysis

In 39 real Ollama inferences across several evidence and prompt conditions, retrieval supplied the required figures for a financial calculation, while generation could abstain, omit components or describe arithmetic incorrectly. This motivates the strict separation between retrieving evidence and verifying numeric claims.

Recorded checks and boundaries

PYTHONPATH=src python -m pytest -q146 passed, one third-party deprecation warning
ruff check .Passed
cd web && npm test9 passed
cd web && npx playwright test17 passed, mostly mocked HTTP
cd web && npm run buildPassed

Playwright tests are not a browser-to-real-Ollama accuracy benchmark. Backend generation errors return HTTP 200 with evidence and a backend-error state; malformed requests return 422 and unavailable services return 503.

LIMITATIONS AND SOURCE

The boundaries are published with the results.

These limits are taken from the repository documentation, not added as an afterthought.

CONTACT

Have a hard problem?
Let's make it measurable.

Open to AI Engineer, Applied ML and Software Engineer roles where evaluation and engineering judgment matter as much as model choice. Based in Madrid.

samcarjes@gmail.com

Open to AI Engineer, Applied ML and Software Engineer roles