Mines Financial
A production-style multi-agent LangGraph RAG chatbot for querying ASX mining company annual reports.
Covers BHP, Rio Tinto (RIO), Fortescue (FMG), Mineral Resources (MIN), and Northern Star (NST)
for fiscal years FY2023 – FY2025.
Tech Stack
LangGraph
LangChain
ChromaDB
BM25
DeepSeek
Jina Embeddings v3
Tavily Search
PostgreSQL
FastAPI
Next.js
Chart.js
PyMuPDF
Gemini Vision
System Architecture
The system is built as two nested LangGraph state graphs: a supervisor graph
that handles conversation memory, routing, tool selection, aggregation, guardrails, and final
answer synthesis; and a retrieval subgraph that runs an adaptive RAG loop over
annual-report chunks.
The retrieval layer fans out by company, combines BM25 keyword search with
ChromaDB vector search using Jina embeddings, and fuses rankings through
EnsembleRetriever. The supervisor can also call Tavily for recent-news questions and a
ReAct calculator for arithmetic on retrieved financial figures.
Learning Paths
Static, offline-friendly study pages for understanding how this project works. These pages use
built-in examples only; no backend, server, or API calls are required.
nodes
Graph Walkthrough
Click each supervisor or retrieval node to inspect purpose, state reads/writes, routes, and source links.
trace
Follow a Query
Step through a realistic question and watch the static state diff change at each graph stage.
compare
Supervisor vs Retrieval
Learn where orchestration ends and the evidence pipeline begins.
rag
Retrieval Debug Panel
Compare BM25 hits, Chroma hits, merged docs, graded docs, and answer-grade output.
terms
Glossary
Search project-specific definitions for LangGraph, RAG, cache, routing, and citation concepts.
why
Design Rationale
Short answers to why the system uses subgraphs, graders, cache tiers, calculators, and guardrails.
tests
Tests and Evals Map
Connect the unit tests and eval suites to the behavior they protect.
Supervisor Graph Flow
START
└─► compress_context — if messages > 10, summarize older turns and keep latest 4
└─► memory — semantic cache lookup + company/FY enrichment from history
├─► answer ─► END (L1 cache hit, score >= 0.95)
└─► retrieve_decision
├─► answer ─► END (out-of-scope refusal)
├─► guardrails (direct answer, no retrieval/news needed)
├─► clarify ───────────────────────────────► dynamic_tool_selector
└─► dynamic_tool_selector — select retrieval, news, and calc flag
├─► retrieval_agent ──┐
└─► news_agent ───────┴─► aggregate
├─► calculator_agent ─► guardrails
└─────────────────────► guardrails
├─► answer ─► END
└─► fallback ─► END
Retrieval Subgraph Flow
START
└─► query_rewrite — rewrite keywords, detect tickers, build company_queries
└─► retrieve_companies × N — Send API fan-out; one hybrid search branch per company
└─► merge — dedupe by source/page/source_type/chunk_index
└─► grade_docs — LLM keeps chunks with relevant financial facts
├─► query_rewrite (no relevant docs and retry_count < 2)
└─► synthesize — draft answer from graded annual-report excerpts
└─► grade_answer
├─► query_rewrite (draft failed, or some companies still missing; max 2 retries)
└─► END (pass, all targeted companies covered, or retry limit reached)
Agents
calculator_agent.py
ReAct agent that extracts numbers from context and calls arithmetic tools. Never guesses figures.
news_agent.py
Calls Tavily Search for current news about ASX mining companies — covers events beyond annual reports.
retrieval_agent.py
Compiles the retrieval subgraph. Uses Send API fan-out and retries only missing companies when coverage is incomplete.
Nodes (Supervisor Graph)
compress_context.py
Summarises conversation history exceeding 10 messages to keep the context window manageable.
memory.py
Semantic memory lookup: L1 direct cache hit at 0.95+, L2 context seeding at 0.80+. Also enriches follow-up queries with company/FY from recent turns.
retrieve_decision.py
LLM router that classifies out-of-scope, retrieval, news, clarification, and direct-answer paths.
dynamic_tool_selector.py
Selects which agents to run in parallel: retrieval, news, and whether to call the calculator.
clarify.py
Uses LangGraph interrupt() to pause the graph and request clarification from the user.
aggregate.py
Merges parallel agent outputs (retrieval draft + news snippets) into a single context block.
guardrails.py
Rule-based check: rejects empty or too-short aggregated context before it reaches the answer node.
answer.py
Final synthesis: handles out-of-scope and cache-hit returns, writes cited answers, extracts chart data, builds source previews, and saves high-quality conclusions.
fallback.py
Returns a helpful guidance message when guardrails fail, explaining what data is available.
Retrieval Nodes
query_rewrite.py
Rewrites user text into retrieval keywords, extracts explicit tickers, builds per-company queries, and on retry targets only companies without graded docs.
retrieve_parallel.py
Hybrid BM25 + ChromaDB retrieval for one company. Uses company-scoped filters, RRF-style fusion weights 0.45 / 0.55, and BM25 fallback if vector search fails.
merge.py
Deduplicates documents gathered from parallel company branches using source, page, source_type, and chunk_index as the stable key.
grade_docs.py
LLM grader that filters retrieved chunks to relevant financial facts and updates company_status based on graded docs, not raw retrieval hits.
synthesize.py
Drafts an answer strictly from graded document excerpts with inline (Company FY, p.X) citations.
grade_answer.py
LLM grader that checks answer usefulness and groundedness, records unsupported claims, and triggers retry when the draft fails or company coverage is incomplete.
Ingest Pipeline
step_1_ingest_text.py
Extracts PDF page text, splits it into overlapping chunks, embeds with Jina, and stores text chunks in ChromaDB.
step_2_PyMuPDF_filter.py
Scans annual reports for pages with tables, images, charts, or high numeric density before vision processing.
step_3_ingest_vision.py
Renders filtered pages, calls the vision model, repairs JSON output, and checkpoints extracted tables.
step_3_prompts.py
Defines the strict table-extraction prompt plus four few-shot examples for nested tables and charts.
step_4_embed_vision.py
Serialises extracted table structures into searchable text and embeds new vision chunks into ChromaDB.
step_5_bm25.py
Reads all ChromaDB chunks and rebuilds per-company BM25 keyword indices for hybrid retrieval.
Supporting Modules
state.py
TypedDict definitions: MainState, RetrievalState, CompanyDocsState, RetrievalOutput.
graph.py
Assembles the full supervisor LangGraph with PostgreSQL checkpointing.
utils/llm.py
Singleton DeepSeek LLM client (via OpenAI-compatible API, temperature=0).
utils/chart.py
Extracts Chart.js-compatible bar chart specs from graded docs and synthesised prose using regex.
utils/citation.py
Builds [N] citation lists, filters uncited sources, renumbers markers, converts to HTML superscripts.
memory/semantic.py
Thread-safe ChromaDB conclusions store. Scores 0.95+ skip RAG; scores 0.80+ seed the full pipeline as supplementary context.
tools/calculator.py
Four LangChain tools: calculate, calculate_growth_rate, calculate_ratio, calculate_average.
db/checkpointer.py
One-time script to initialise PostgreSQL checkpointer tables via PostgresSaver.setup().