A production-style, multi-agent RAG chatbot that answers questions about the annual reports of five major ASX-listed mining companies: BHP, Rio Tinto (RIO), Fortescue (FMG), Mineral Resources (MIN), and Northern Star (NST) across fiscal years FY2023–FY2025.
Built on LangGraph with a Next.js frontend, FastAPI backend, PostgreSQL conversation persistence, and a hybrid (dense + BM25) retrieval engine.
- Multi-agent supervisor graph — a LangGraph state machine orchestrates independent retrieval, calculation, and news agents in parallel.
- Hybrid retrieval — combines Jina dense vector search (ChromaDB) with BM25 keyword search for high-precision document recall.
- Vision-aware ingest pipeline — PyMuPDF page-filtering + Gemini vision model extracts structured financial tables from PDF pages that text extraction would miss.
- Two-tier semantic cache — past answers are embedded and re-used (L1: direct cache hit at ≥0.95 similarity; L2: context seeding at ≥0.80).
- Adaptive clarification — the graph interrupts and asks the user for clarification when the query is ambiguous, then resumes.
- Dynamic tool selection — an LLM router decides at runtime whether to call the retrieval agent, the news agent (Tavily web search), or both, and whether arithmetic calculation is required.
- Calculator agent — a ReAct agent equipped with financial math tools (growth rate, ratio, average) that operates strictly on retrieved numbers.
- Guardrails node — every answer path passes through a validation node before reaching the user; failures route to a graceful fallback.
- Streaming API — Server-Sent Events stream graph node progress to the frontend in real time.
- Persistent conversation threads — PostgreSQL checkpointer stores full conversation state per thread; threads are resumable across server restarts.
- Eval framework — four evaluation suites (routing, retrieval, calculation, boundary) with mock and real modes.
User Query
│
▼
compress_context ──► memory ──► [cache hit] ──────────────────► answer ──► END
│
└── [miss] ──► retrieve_decision
│
┌───────────────┼───────────────┐
clarify dynamic_tool_selector guardrails
│ │
└──► ┌─────────┴──────────┐
│ │
retrieval_agent news_agent
(sub-graph) (Tavily)
│ │
└─────────┬───────────┘
aggregate
│
[needs_calculation?]
yes │ no │
calculator_agent │
│ │
guardrails
pass │ fail │
answer fallback ──► END
│
END
Retrieval sub-graph (runs per query, fans out per company):
query_rewrite ──► [fan-out: retrieve per company in parallel] ──► merge
│
grade_docs
pass │ fail (retry < 2)
synthesize ──► query_rewrite
│
grade_answer
pass │ fail (retry < 2)
END ──► query_rewrite
| Layer | Technology |
|---|---|
| Orchestration | LangGraph 0.6+, LangChain 0.3+ |
| LLM | OpenAI API (GPT-4o class), DeepSeek |
| Embeddings | Jina AI (jina-embeddings-v3) |
| Vector store | ChromaDB |
| Keyword search | BM25 (rank-bm25) |
| Web search | Tavily |
| PDF processing | PyMuPDF |
| Vision extraction | Google Gemini (ingest only) |
| Backend API | FastAPI + Uvicorn |
| Frontend | Next.js 15, React 19, TypeScript, Tailwind CSS, Chart.js |
| Database | PostgreSQL 16 (conversation checkpointing) |
| Tracing | LangSmith |
| Package manager | uv |
| Python | >= 3.12 |
mines_financial/
├── app.py # FastAPI application and all REST/SSE endpoints
├── graph.py # Main supervisor LangGraph definition
├── graph_for_studio.py # Variant compatible with LangGraph Studio
├── state.py # All TypedDict state definitions
├── langgraph.json # LangGraph deployment config
├── docker-compose.yml # PostgreSQL service
├── pyproject.toml # Python dependencies (uv)
│
├── nodes/ # Supervisor graph nodes
│ ├── compress_context.py # Trim conversation history when context grows long
│ ├── memory.py # Entity enrichment (L1) then semantic cache lookup (L2 hit detection)
│ ├── retrieve_decision.py # Classify query: needs retrieval? needs clarification?
│ ├── dynamic_tool_selector.py# LLM router: retrieval / news / calculation flags
│ ├── clarify.py # Interrupt and ask the user for clarification
│ ├── aggregate.py # Merge retrieval + news results into one context block
│ ├── guardrails.py # Validate answer length and quality before delivery
│ ├── fallback.py # Graceful response when guardrails fail
│ └── answer.py # Format and emit final answer + chart data + sources
│
├── agents/
│ ├── retrieval_agent.py # Retrieval sub-graph (query rewrite → retrieve → grade → synthesize)
│ ├── calculator_agent.py # ReAct financial calculator agent
│ └── news_agent.py # Tavily web search agent
│
├── retrieval_nodes/ # Nodes used only inside the retrieval sub-graph
│ ├── query_rewrite.py # Rewrite and decompose query per company
│ ├── retrieve_parallel.py # Fan-out: parallel BM25 + vector search per company
│ ├── merge.py # Deduplicate and rank retrieved chunks
│ ├── grade_docs.py # LLM grades relevance of merged docs
│ ├── synthesize.py # Draft answer from graded docs
│ └── grade_answer.py # LLM grades answer quality; triggers retry if needed
│
├── memory/
│ └── semantic.py # ChromaDB-backed semantic cache (conclusions store)
│
├── tools/
│ └── calculator.py # Math tools: growth rate, ratio, average, generic calculate
│
├── utils/
│ ├── llm.py # Shared LLM client factory
│ ├── chart.py # Chart data extraction helpers
│ └── citation.py # Source citation formatting
│
├── db/
│ └── checkpointer.py # PostgreSQL checkpointer setup helpers
│
├── ingest/ # One-time PDF to ChromaDB pipeline (see below)
│ ├── step_1_ingest_text.py
│ ├── step_2_PyMuPDF_filter.py
│ ├── step_3_ingest_vision.py
│ ├── step_3_prompts.py
│ ├── step_4_embed_vision.py
│ └── step_5_bm25.py
│
├── evals/ # Evaluation framework
│ ├── run_eval.py
│ └── evaluators/
│ ├── common.py
│ ├── routing.py
│ ├── retrieval.py
│ ├── calculation.py
│ └── boundary.py
│
├── tests/ # pytest unit tests
└── frontend/ # Next.js 15 chat UI
- Python >= 3.12
uvpackage manager (pip install uv)- Node.js >= 18 and npm
- Docker (for PostgreSQL)
- API keys: OpenAI, Jina AI, Tavily; optionally DeepSeek and LangSmith
git clone <repo-url>
cd mines_financial
# Python dependencies
uv sync
# Frontend dependencies
cd frontend && npm install && cd ..cp .env.example .envEdit .env:
OPENAI_API_KEY=sk-...
JINA_API_KEY=jina_...
TAVILY_API_KEY=tvly-...
DEEPSEEK_API_KEY=... # optional, if using DeepSeek as the LLM
LANGSMITH_API_KEY=... # optional, enables LangSmith tracing
LANGSMITH_TRACING=true
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/mines_financialdocker compose up -dBefore the chatbot can answer questions, run the ingest pipeline to populate ChromaDB. See the Ingest Pipeline section below.
uv run uvicorn app:app --reload --port 8000cd frontend
npm run devOpen http://localhost:3000 in your browser.
The ingest pipeline converts raw PDF annual reports into a searchable ChromaDB vector store and a BM25 keyword index. Run it once before first use, and re-run steps 1–5 whenever you add new PDFs.
PDF filenames must follow the pattern {COMPANY}_{FY}.pdf — for example BHP_FY2024.pdf, RIO_FY2023.pdf. The scripts parse the company ticker and fiscal year from the filename.
Splits PDF pages into overlapping text chunks (1,000 chars / 200 char overlap), embeds them with Jina, and stores them in ChromaDB. Idempotent — already-processed chunks are skipped based on their deterministic chunk ID.
uv run python ingest/step_1_ingest_text.py
# process a single PDF:
uv run python ingest/step_1_ingest_text.py --pdf BHP_FY2024.pdfScans every page with PyMuPDF and identifies pages that likely contain tables or data-dense charts. Saves results to ingest/filtered_pages.json. Parallelised across 15 worker threads and resumable.
uv run python ingest/step_2_PyMuPDF_filter.py
# single PDF or a page range:
uv run python ingest/step_2_PyMuPDF_filter.py --pdf RIO_FY2024.pdf --pages 101-200Renders filtered pages to PNG and calls Gemini vision to extract structured tables (title, headers, rows). Saves to ingest/output_vison.json. Resumable — a checkpoint file tracks completed pages.
uv run python ingest/step_3_ingest_vision.pyRequires GOOGLE_API_KEY in .env.
Embeds the extracted vision tables into ChromaDB using distinct chunk IDs (__vision suffix). Idempotent.
uv run python ingest/step_4_embed_vision.pyReads all chunks from ChromaDB (text + vision) and builds a BM25 keyword index saved as bm25_index.pkl in the project root. Must be re-run after any ingest that adds or removes chunks.
uv run python ingest/step_5_bm25.pyAll endpoints are served by app.py on port 8000 by default.
| Method | Path | Description |
|---|---|---|
POST |
/api/threads |
Create a new conversation thread; returns { thread_id } |
GET |
/api/threads |
List the 30 most recently updated threads |
GET |
/api/threads/{thread_id}/history |
Return the full message history for a thread |
DELETE |
/api/threads/{thread_id} |
Delete all checkpoint data for a thread |
POST |
/api/chat |
Single-shot chat; returns the complete answer synchronously |
POST |
/api/chat/stream |
SSE streaming endpoint |
GET |
/api/graph/mermaid |
Return the supervisor graph as a Mermaid diagram string |
{
"thread_id": "uuid-string",
"message": "What was BHP's revenue in FY2024?",
"resume": false
}Set resume: true when the user is responding to a clarification question from the chatbot.
| Event | Key fields | Description |
|---|---|---|
node_start |
node |
A graph node has started executing |
node_done |
node, state |
A graph node finished; includes a serialised state snapshot |
done |
answer, chart_data, sources, confidence |
Final answer is ready |
interrupt |
question |
The graph needs clarification from the user |
error |
detail |
An unhandled exception occurred |
The eval runner supports four suites and two execution modes.
Suites: routing, retrieval, calculation, boundary
Modes:
mock— fast and free; validates evaluator logic with synthetic outputs, no LLM or API callsreal— invokes the actual LLM and/or retrieval nodes; consumes API credits
# Run all mock evals (recommended before every commit)
uv run python evals/run_eval.py --mode mock
# Run one suite in real mode with a cost cap
uv run python evals/run_eval.py --mode real --suite routing --limit 3
# Debug a single case by ID
uv run python evals/run_eval.py --mode real --suite routing --id routing_003_single_report_fact
# Run multiple specific cases
uv run python evals/run_eval.py --mode mock --suite routing \
--id routing_003_single_report_fact --id routing_005_calculation_required
# Show all CLI options
uv run python evals/run_eval.py --helpuv run pytest tests/A graph_for_studio.py entry point and langgraph.json config are included for use with LangGraph Studio.
# Ensure the CLI dev dependency is installed
uv sync --group dev
# Launch Studio (reads langgraph.json automatically)
langgraph devThis project is released for educational and portfolio purposes. No license for commercial use is granted.