title: FinSight
emoji: π
colorFrom: blue
colorTo: green
sdk: docker
app_port: 7860
pinned: false
FinSight β Grounded Financial Filing Analyzer
Upload an annual report or 10-K. Ask questions. Get answers where every number is traceable to the document β with a confidence label, a sector-aware red-flag scan, and an auditable BUY / HOLD / AVOID / SKIP call. Built for the case where the filing is confidential and cloud AI is not an option.
Design thesis: numbers never come from the LLM. Deterministic extraction owns every figure; the LLM only narrates. That is what makes answers grounded β and it is why a small local model on a CPU is enough.
Architecture
flowchart LR
A[PDF / HTML filing] --> B[Parser<br/>PyMuPDF + chunking]
B --> C[Sector detector<br/>BANK / IT / PHARMA / ENERGY / MFG]
C --> D[Metric extractor<br/>multi-candidate, confidence-scored,<br/>currency-aware, subsidiary-boundary checks]
B --> E[Entity resolver<br/>alias table + fuzzy matching]
E --> F[Knowledge graph β NetworkX<br/>companies, filings, chunks, typed relations]
D --> F
B --> G[ChromaDB<br/>local MiniLM embeddings]
F --> H[Red-flag engine<br/>sector thresholds, zero LLM]
H --> I[Recommendation engine<br/>BUY / HOLD / AVOID / SKIP, zero LLM]
F --> J[Query router]
G --> J
J -->|metric question| K[Direct graph lookup<br/>ZERO LLM tokens]
J -->|open question| L[LLM narration<br/>local Ollama default / Groq opt-in]
What makes it different
- Confidence is a first-class value. Every extracted metric is
{value, currency, confidence, alternatives, needs_clarification}β scanned as all plausible candidates, not first-match-wins. Low-confidence numbers are surfaced for clarification, never silently trusted. - Wrong-entity protection. Indian annual reports often bundle a subsidiary's full financial statements inside the parent's PDF. FinSight detects section ownership headings and demotes numbers that belong to a different entity β a wrong-entity bug is worse than a wrong-number bug.
- Indian-filing realities handled. βΉ crore/lakh units, comma-grouped Indian digit formats, and PDF font encodings that render βΉ as
`,J,C, orH(each confirmed in real filings) β recovered via a unit-confirmation heuristic instead of glyph whack-a-mole. - Token efficiency by architecture, not compression. Metric questions are answered by direct graph lookup β zero LLM tokens. Only open-ended questions reach the LLM, and then with top-3 chunks plus a compact metrics header, not the whole document.
- Post-generation numeric verification. A prompt saying "use only the context" is a request, not a guarantee β so every number in a generated answer is checked against the source filing (unit-normalized across βΉ crore/lakh/$B/M formats). Unverified numbers are flagged in the response, never silently shipped. Real run: a full performance summary generated by a local 1.5B model had all 23 of its numbers traced back to the filing.
- Page-level citations. Chunks carry the PDF page they came from; answers cite them (
HDFC Bank 2025 filing, p.210, p.392, p.410), and every extracted metric records the page it was read from. - Analysis without any LLM ("Mode C"). The red-flag engine (sector-specific thresholds: NPA%, CASA, attrition, D/E, YoY declines, severity-weighted risk score) and the recommendation engine (explicit, auditable decision tree β
SKIPis an honest answer when data is thin) are pure rules over extracted metrics. They work with no model running at all. - Typed relationship graph. SUPPLIER_TO / COMPETITOR_OF / SUBSIDIARY_OF / ACQUIRED edges extracted by sentence-level patterns first, with a local-LLM fallback only for ambiguous co-occurring pairs.
Run it
Backend
python -m venv venv
venv\Scripts\activate # Windows (source venv/bin/activate on Unix)
pip install -r requirements.txt
python -m spacy download en_core_web_sm
copy .env.example .env # then edit as needed
uvicorn backend.main:app --reload --port 8000
Frontend (React + TypeScript + Vite)
cd frontend
npm install
npm run dev # http://localhost:5173, proxies /api -> :8000
Local LLM (optional but recommended)
Install Ollama, then:
ollama pull mistral:7b-instruct-q4_K_M # or any instruct model; set OLLAMA_MODEL
No Ollama and no Groq key? Metric Q&A, red flags, and recommendations still work β only free-text narration is disabled.
API
| Endpoint | What it does |
|---|---|
POST /upload |
Parse a filing (PDF/HTML), extract confidence-scored metrics, index into graph + vector store |
POST /query |
Ask a question β direct zero-token lookup when possible, RAG otherwise |
GET /metrics/{company} |
All extracted metrics by year, with confidence and alternatives |
GET /red_flags/{company}/{year} |
Rule-based red-flag scan with severity-weighted risk score |
GET /recommendation/{company}/{year} |
BUY / HOLD / AVOID / SKIP with explicit reasons |
GET /report/{company}/{year} |
Structured narrative report (LLM, grounded on extracted metrics) |
POST /compare |
Compare a metric across companies |
GET /companies |
List indexed companies |
Deploy (Hugging Face Spaces, free)
The repo doubles as a Docker Space β the YAML header above is its config. One container builds the React frontend, installs the backend, and parses the committed demo filing at image-build time, so the Space boots with HDFC Bank's annual report already indexed.
# once: create a Docker-SDK Space at huggingface.co/new-space, then
git remote add space https://huggingface.co/spaces/<user>/FinSight
git push space main
In the Space settings add secrets: GROQ_API_KEY (narration) β LLM_BACKEND=groq is already set in the image. The hosted demo uses cloud narration by design; the private, fully-local mode is what you get by cloning the repo and running Ollama. Red flags, recommendations, and direct metric lookup work on the Space even with no key set.
Measured results
From python -m eval.run_eval against hand-labeled ground truth (every value read from the actual filing, page-referenced β see eval/ground_truth.json):
| Metric | Result |
|---|---|
| Extraction recall (true value found) | 6/6 (100%) |
| Top-candidate accuracy | 4/6 (67%) β misses carry needs_clarification: true and the correct value in alternatives |
| Prompt-token reduction vs naive top-20 RAG | 80.4% (~2,077 vs ~10,616 tokens per question, len/4 approximation) |
| Metric questions answered with zero LLM tokens | 5/5 |
Current eval set is one real 590-page Indian bank annual report; the harness is built for more β add a PDF + labeled entry and re-run.
Honest limitations
- Extraction is heuristic (regex + NER + context windows), not a table parser β a statement-table tier is planned. Treat outputs as assistance, not audited figures; the numbers above quantify exactly how far to trust it.
- Ratio metrics (NPA%, attrition, β¦) don't carry confidence scores yet β only money metrics do. Ratios use majority voting across the filing's repeated statements instead.
- The verifier confirms numbers exist in the source; it does not (yet) confirm they're attached to the right claim in the sentence.
- English filings only; scanned/image PDFs need OCR, which isn't wired in.
- Local narration on CPU is slow (a 1.5B model takes ~2β3 minutes for a long answer). That's the privacy trade; Groq mode is instant.