A focused evaluation toolkit for one specific, dangerous RAG failure mode: a financial RAG system confidently asserting a dollar or percent figure that the evidence it actually retrieved does not support. It answers one question, deterministically and offline:
Did the model assert a financial number that the retrieved evidence does not support?
It ships as three layers that work independently:
- An importable Python evaluator (
finrag_eval/src/eval/) — no Docker, Ollama, database, or LLM required. This is the product's core. - A reference RAG pipeline (
src/ingestion/,src/retrieval/) over real SEC EDGAR filings, used to exercise and validate the evaluator against real financial documents, and to produce the CLI workflows (make ask,make eval) documented indocs/USE_CASES.md. - A local dashboard (
frontend/) for inspecting evaluation runs — reads the JSON the evaluator produces, never re-implements scoring logic itself.
See docs/REAL_WORLD_USE_CASES.md for the full write-up. In short:
- A financial RAG engineer doing pre-release QA on their own system's answers.
- A team changing chunking/retrieval config who wants to know whether a change that improves generic retrieval metrics quietly makes numeric hallucination worse.
- An engineer debugging one specific bad answer, trying to tell whether the failure came from retrieval, table extraction, unit normalization, or generation.
- A RAG-eval framework maintainer or researcher interested in the overlapping-chunk precision/recall fix this project contributed upstream to DeepEval (PR #2743).
Standard RAG eval metrics (contextual precision/recall, answer relevancy)
don't distinguish "the model refused" from "the model gave a precise,
confident, wrong number." In financial documents specifically, a wrong
number is far more dangerous than an honest refusal — and metrics built
for general RAG don't catch it, because a confidently-wrong figure can
still score well on relevance and fluency. FinRAG Eval separates two
questions that are easy to accidentally conflate (and were, in fact,
conflated in an earlier version of this project's own code — see
PLAN.md/RESULTS.md for that history):
- Context grounding — is the asserted figure actually present in the retrieved evidence?
- Ground-truth correctness — does it match the expected answer?
A wrong number that happens to match a gold answer must never read as "grounded" just because it's correct; a correct number that the system never actually retrieved must never read as "grounded" just because it's right. Both are checked independently.
from finrag_eval import score_answer
result = score_answer(
question="What was Apple's R&D spend?",
answer="Apple's R&D expense was $14.2 billion in fiscal 2024.",
contexts=["Research and development expense was approximately $31.4 billion."],
)
result["verdict"] # "unsupported"No Docker, Ollama, or network. Optional ground_truth and
document_type arguments — see README.md's "Python API" section.
contexts must be a list[str] — one string per retrieved chunk, not one
big string. Passing a bare string raises a clear TypeError rather than
silently corrupting the context (found by adversarial review: a bare
string used to get joined character-by-character downstream, which could
flip the verdict with no error at all). result["metrics"] always has
numeric_grounding; it additionally gains section_aware_precision/
section_aware_recall only when both ground_truth and contexts are
non-empty — the key set is additive, never a fixed shape, so don't assume
every call returns the same set of metric keys.
python -m src.eval.score_answer --question "..." --answer "..." --context "..." [--ground-truth "..."] [--document-type ...]Prints stable JSON. Exit code 2 on unsupported, 0 otherwise —
scriptable as a CI gate (echo $?).
make evalRuns the full dataset against the live RAG pipeline, writes
eval_runs/<run_id>.jsonl (one row per question) and
eval_runs/<run_id>.summary.json (aggregate rates, including the
headline unsupported_confident_rate) — see docs/USE_CASES.md.
make uiReads eval_runs/ artifacts (or bundled fixture data when none exist
yet) — see frontend/README.md.
- Not a general-purpose RAG evaluation framework — it does one thing (numeric grounding for financial figures) well, and leaves contextual precision/recall/faithfulness to DeepEval, which it uses alongside its own metrics rather than replacing.
- Not an LLM judge —
score_answer()is deterministic regex/unit- normalization heuristics, documented as such (seesrc/eval/hallucination.py's module docstring), not a model call. This is why it needs no API key and produces identical output on repeat runs. - Not a production RAG pipeline to adopt wholesale —
src/ingestion/andsrc/retrieval/exist to exercise and validate the evaluator against real filings, not as a system meant to be deployed as-is. A team with its own RAG pipeline should only need the Python API. - Not a hosted service — everything runs locally; there is no server component, telemetry, or account system.
The smallest integration is a function call: pipe your own system's
(question, answer, retrieved_contexts) through score_answer() and act
on result["verdict"]. Nothing about the RAG pipeline, database, or
dashboard in this repo is required for that. See pyproject.toml for the
importable package; requirements.txt's live-stack dependencies
(ollama, psycopg2-binary, supabase, deepeval) are not needed for
this integration path.
- Context-grounding vs. ground-truth-correctness independence: enforced
by code structure and covered by regression tests (see
tests/test_hallucination.py,tests/test_score_answer.py). - Unit/scale normalization (
$31.4B==$31.4 billion==31,400 million), negative-sign and parenthetical-accounting-negative handling, bare-calendar-year filtering, refusal/no-figure classification — all unit-tested, offline, deterministic (seeRESULTS.md's "Verified now" table for the exact test names). - The reference RAG pipeline genuinely runs end-to-end against real SEC
EDGAR filings on local infrastructure (Postgres/pgvector via Docker,
Ollama for embeddings and generation) — see
RESULTS.mdfor whether a captured live run exists as of the current state of this repo. - The DeepEval upstream contribution (issue #2594, merged fix PR #2743)
is real — independently verified against the live DeepEval GitHub repo
during a prior review pass (see
PLAN.md).
- Statistical significance: the evaluation dataset is 30 questions across two issuers — illustrative of the failure mode, not a large-scale benchmark.
- Broad filing-format coverage: HTML section-splitting is regex-based
(
Item N.markers) and has only been exercised against Apple and Microsoft 10-Ks; non-standard filing formatting could break it. - Scale: this has not been run against a large question set, multiple
issuers beyond AAPL/MSFT, or under production load — see
RESULTS.md's Known Gaps for the current, honest state.