Commit Β·
24fa45d
0
Parent(s):
Add FinRAG PRD specification
Browse filesCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
finrag.md
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FinRAG β Fintech Research Agent with Persistent Memory
|
| 2 |
+
## Product Requirements Document (PRD) for Claude Code
|
| 3 |
+
|
| 4 |
+
**Version:** 1.0
|
| 5 |
+
**Author:** Ben Roshan D
|
| 6 |
+
**Date:** April 2026
|
| 7 |
+
**Status:** Ready for scaffolding
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## 1. Project Overview
|
| 12 |
+
|
| 13 |
+
### 1.1 Problem Statement
|
| 14 |
+
Fintech professionals and analysts spend hours manually reading RBI circulars, earnings call transcripts, and NPCI reports to answer domain questions. Existing RAG systems retrieve context but provide no signal on whether retrieval is actually working β they fail silently.
|
| 15 |
+
|
| 16 |
+
### 1.2 Solution
|
| 17 |
+
FinRAG is a production-grade conversational research agent that:
|
| 18 |
+
- Ingests fintech documents (PDFs, text) into a persistent vector store
|
| 19 |
+
- Answers multi-turn questions with conversation memory
|
| 20 |
+
- Evaluates its own retrieval quality (Precision@K, Faithfulness score) per query
|
| 21 |
+
- Surfaces eval metrics in a dedicated dashboard tab
|
| 22 |
+
|
| 23 |
+
### 1.3 What Makes It Different
|
| 24 |
+
Most RAG portfolios ship without retrieval evaluation. FinRAG adds a self-scoring eval layer β Precision@K and LLM-as-Judge faithfulness scoring β so retrieval degradation is visible before users notice it.
|
| 25 |
+
|
| 26 |
+
### 1.4 Target Audience (for README framing)
|
| 27 |
+
- Fintech analysts querying RBI policy, UPI stats, earnings data
|
| 28 |
+
- DS/AI interviewers evaluating production RAG architecture understanding
|
| 29 |
+
|
| 30 |
+
---
|
| 31 |
+
|
| 32 |
+
## 2. Tech Stack
|
| 33 |
+
|
| 34 |
+
| Layer | Technology |
|
| 35 |
+
|---|---|
|
| 36 |
+
| Vector store | ChromaDB (persistent, local) |
|
| 37 |
+
| Embeddings | `sentence-transformers/all-MiniLM-L6-v2` (free, no API cost) |
|
| 38 |
+
| LLM | Euron API (`gpt-5.3-instant`) via `openai` SDK (base_url: `https://api.euron.one/api/v1/euri`) |
|
| 39 |
+
| Memory | LangChain `ConversationBufferMemory` |
|
| 40 |
+
| Orchestration | LangChain `RetrievalQA` + custom chain |
|
| 41 |
+
| Document loading | `pypdf`, `langchain.document_loaders` |
|
| 42 |
+
| Chunking | `RecursiveCharacterTextSplitter` |
|
| 43 |
+
| Eval | Custom Python module β no external eval library |
|
| 44 |
+
| UI | Streamlit (multi-tab: Chat + Eval Dashboard) |
|
| 45 |
+
| Deployment | Render (Dockerfile included, GitHub repo: https://github.com/BenRoshan100/fin-rag.git) |
|
| 46 |
+
| Config | `.env` for API keys, `config.yaml` for chunking/retrieval params |
|
| 47 |
+
|
| 48 |
+
---
|
| 49 |
+
|
| 50 |
+
## 3. Directory Structure
|
| 51 |
+
|
| 52 |
+
```
|
| 53 |
+
finrag/
|
| 54 |
+
βββ README.md
|
| 55 |
+
βββ requirements.txt
|
| 56 |
+
βββ .env.example
|
| 57 |
+
βββ config.yaml
|
| 58 |
+
βββ Dockerfile
|
| 59 |
+
βββ .gitignore
|
| 60 |
+
β
|
| 61 |
+
βββ data/
|
| 62 |
+
β βββ raw/ # Drop PDFs here
|
| 63 |
+
β β βββ rbi_annual_report_2024.pdf
|
| 64 |
+
β β βββ npci_upi_report_2024.pdf
|
| 65 |
+
β β βββ bajaj_finance_q3_2024_transcript.txt
|
| 66 |
+
β βββ ground_truth/
|
| 67 |
+
β βββ eval_pairs.json # 20 query/relevant-chunk pairs for Precision@K
|
| 68 |
+
β
|
| 69 |
+
βββ src/
|
| 70 |
+
β βββ __init__.py
|
| 71 |
+
β βββ ingest.py # Document loading, chunking, embedding, ChromaDB storage
|
| 72 |
+
β βββ retriever.py # Query ChromaDB, return top-K chunks with metadata
|
| 73 |
+
β βββ memory.py # ConversationBufferMemory setup and management
|
| 74 |
+
β βββ chain.py # LangChain QA chain combining retriever + memory + Claude
|
| 75 |
+
β βββ eval/
|
| 76 |
+
β β βββ __init__.py
|
| 77 |
+
β β βββ precision.py # Precision@K computation against ground truth
|
| 78 |
+
β β βββ faithfulness.py # LLM-as-Judge faithfulness scorer
|
| 79 |
+
β βββ utils.py # Logging, config loader, token counter
|
| 80 |
+
β
|
| 81 |
+
βββ app/
|
| 82 |
+
β βββ streamlit_app.py # Main Streamlit entrypoint
|
| 83 |
+
β βββ pages/
|
| 84 |
+
β β βββ chat.py # Chat tab UI
|
| 85 |
+
β β βββ eval_dashboard.py # Eval metrics tab UI
|
| 86 |
+
β βββ components/
|
| 87 |
+
β βββ message_bubble.py # Chat message component
|
| 88 |
+
β βββ source_expander.py # Source chunk expander component
|
| 89 |
+
β
|
| 90 |
+
βββ scripts/
|
| 91 |
+
β βββ run_ingest.py # CLI: python scripts/run_ingest.py --data-dir data/raw
|
| 92 |
+
β βββ run_eval.py # CLI: python scripts/run_eval.py --queries data/ground_truth/eval_pairs.json
|
| 93 |
+
β
|
| 94 |
+
βββ tests/
|
| 95 |
+
β βββ test_ingest.py
|
| 96 |
+
β βββ test_retriever.py
|
| 97 |
+
β βββ test_chain.py
|
| 98 |
+
β βββ test_eval.py
|
| 99 |
+
β
|
| 100 |
+
βββ chroma_db/ # Auto-created by ChromaDB, gitignored
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
---
|
| 104 |
+
|
| 105 |
+
## 4. Module Specifications
|
| 106 |
+
|
| 107 |
+
### 4.1 `src/ingest.py`
|
| 108 |
+
|
| 109 |
+
**Purpose:** Load documents from `data/raw/`, chunk them, embed them, store in ChromaDB.
|
| 110 |
+
|
| 111 |
+
**Functions to implement:**
|
| 112 |
+
|
| 113 |
+
```python
|
| 114 |
+
def load_documents(data_dir: str) -> list[Document]:
|
| 115 |
+
"""
|
| 116 |
+
Load all PDFs and .txt files from data_dir.
|
| 117 |
+
Use PyPDFLoader for PDFs, TextLoader for .txt.
|
| 118 |
+
Return list of LangChain Document objects with metadata:
|
| 119 |
+
- source: filename
|
| 120 |
+
- page: page number (PDFs only)
|
| 121 |
+
"""
|
| 122 |
+
|
| 123 |
+
def chunk_documents(documents: list[Document], chunk_size: int = 500, chunk_overlap: int = 50) -> list[Document]:
|
| 124 |
+
"""
|
| 125 |
+
Split documents using RecursiveCharacterTextSplitter.
|
| 126 |
+
chunk_size and chunk_overlap from config.yaml.
|
| 127 |
+
Preserve metadata from parent document.
|
| 128 |
+
Add chunk_index to metadata.
|
| 129 |
+
"""
|
| 130 |
+
|
| 131 |
+
def embed_and_store(chunks: list[Document], collection_name: str = "finrag") -> Chroma:
|
| 132 |
+
"""
|
| 133 |
+
Embed chunks using HuggingFaceEmbeddings (all-MiniLM-L6-v2).
|
| 134 |
+
Store in ChromaDB at ./chroma_db.
|
| 135 |
+
If collection already exists, skip re-embedding (idempotent).
|
| 136 |
+
Return Chroma retriever object.
|
| 137 |
+
"""
|
| 138 |
+
|
| 139 |
+
def run_ingestion_pipeline(data_dir: str) -> Chroma:
|
| 140 |
+
"""
|
| 141 |
+
Orchestrates: load β chunk β embed β store.
|
| 142 |
+
Print progress: N docs loaded, N chunks created, stored in ChromaDB.
|
| 143 |
+
"""
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
**Important:** Ingestion must be idempotent. Running twice should not duplicate chunks. Use document hash as ChromaDB document ID.
|
| 147 |
+
|
| 148 |
+
---
|
| 149 |
+
|
| 150 |
+
### 4.2 `src/retriever.py`
|
| 151 |
+
|
| 152 |
+
**Purpose:** Query ChromaDB and return top-K chunks with similarity scores and metadata.
|
| 153 |
+
|
| 154 |
+
```python
|
| 155 |
+
def get_retriever(collection_name: str = "finrag", k: int = 5) -> VectorStoreRetriever:
|
| 156 |
+
"""
|
| 157 |
+
Load existing ChromaDB collection.
|
| 158 |
+
Return LangChain retriever with k=5 (from config.yaml).
|
| 159 |
+
"""
|
| 160 |
+
|
| 161 |
+
def retrieve_with_scores(query: str, k: int = 5) -> list[dict]:
|
| 162 |
+
"""
|
| 163 |
+
Return list of dicts:
|
| 164 |
+
[
|
| 165 |
+
{
|
| 166 |
+
"content": "chunk text...",
|
| 167 |
+
"source": "rbi_annual_report_2024.pdf",
|
| 168 |
+
"page": 12,
|
| 169 |
+
"chunk_index": 34,
|
| 170 |
+
"similarity_score": 0.87
|
| 171 |
+
},
|
| 172 |
+
...
|
| 173 |
+
]
|
| 174 |
+
"""
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
---
|
| 178 |
+
|
| 179 |
+
### 4.3 `src/memory.py`
|
| 180 |
+
|
| 181 |
+
**Purpose:** Manage conversation memory across turns.
|
| 182 |
+
|
| 183 |
+
```python
|
| 184 |
+
def create_memory(memory_key: str = "chat_history", max_token_limit: int = 2000) -> ConversationBufferMemory:
|
| 185 |
+
"""
|
| 186 |
+
Create LangChain ConversationBufferMemory.
|
| 187 |
+
memory_key = "chat_history"
|
| 188 |
+
return_messages = True
|
| 189 |
+
max_token_limit = 2000 (truncate oldest messages when exceeded)
|
| 190 |
+
"""
|
| 191 |
+
|
| 192 |
+
def get_memory_as_string(memory: ConversationBufferMemory) -> str:
|
| 193 |
+
"""
|
| 194 |
+
Return conversation history as formatted string for display in UI.
|
| 195 |
+
"""
|
| 196 |
+
|
| 197 |
+
def clear_memory(memory: ConversationBufferMemory) -> None:
|
| 198 |
+
"""
|
| 199 |
+
Clear all messages. Called on "New Conversation" button.
|
| 200 |
+
"""
|
| 201 |
+
```
|
| 202 |
+
|
| 203 |
+
---
|
| 204 |
+
|
| 205 |
+
### 4.4 `src/chain.py`
|
| 206 |
+
|
| 207 |
+
**Purpose:** Assemble the full RAG + memory chain. Core of the application.
|
| 208 |
+
|
| 209 |
+
```python
|
| 210 |
+
def build_qa_chain(retriever, memory) -> ConversationalRetrievalChain:
|
| 211 |
+
"""
|
| 212 |
+
Build LangChain ConversationalRetrievalChain:
|
| 213 |
+
- LLM: Euron API (gpt-5.3-instant) via ChatOpenAI with base_url="https://api.euron.one/api/v1/euri"
|
| 214 |
+
- Retriever: from retriever.py
|
| 215 |
+
- Memory: from memory.py
|
| 216 |
+
- return_source_documents: True
|
| 217 |
+
- verbose: False
|
| 218 |
+
|
| 219 |
+
System prompt to inject:
|
| 220 |
+
"You are FinRAG, a fintech research assistant. Answer questions using
|
| 221 |
+
only the provided context. If the answer is not in the context, say
|
| 222 |
+
'I could not find this in the loaded documents.' Do not hallucinate.
|
| 223 |
+
Be concise and cite your source document."
|
| 224 |
+
"""
|
| 225 |
+
|
| 226 |
+
def run_query(chain, question: str) -> dict:
|
| 227 |
+
"""
|
| 228 |
+
Run chain on question.
|
| 229 |
+
Return:
|
| 230 |
+
{
|
| 231 |
+
"answer": "...",
|
| 232 |
+
"source_documents": [...],
|
| 233 |
+
"question": "..."
|
| 234 |
+
}
|
| 235 |
+
"""
|
| 236 |
+
```
|
| 237 |
+
|
| 238 |
+
---
|
| 239 |
+
|
| 240 |
+
### 4.5 `src/eval/precision.py`
|
| 241 |
+
|
| 242 |
+
**Purpose:** Compute Precision@K against a ground truth set.
|
| 243 |
+
|
| 244 |
+
**Ground truth format (`data/ground_truth/eval_pairs.json`):**
|
| 245 |
+
```json
|
| 246 |
+
[
|
| 247 |
+
{
|
| 248 |
+
"query": "What was India's UPI transaction volume in FY24?",
|
| 249 |
+
"relevant_sources": ["npci_upi_report_2024.pdf"],
|
| 250 |
+
"relevant_chunk_keywords": ["billion transactions", "FY2024", "NPCI"]
|
| 251 |
+
},
|
| 252 |
+
...
|
| 253 |
+
]
|
| 254 |
+
```
|
| 255 |
+
|
| 256 |
+
```python
|
| 257 |
+
def compute_precision_at_k(query: str, retrieved_chunks: list[dict], ground_truth: dict, k: int = 5) -> float:
|
| 258 |
+
"""
|
| 259 |
+
Precision@K = (relevant chunks in top-K) / K
|
| 260 |
+
|
| 261 |
+
A chunk is "relevant" if:
|
| 262 |
+
- Its source matches ground_truth["relevant_sources"], OR
|
| 263 |
+
- Its content contains any keyword from ground_truth["relevant_chunk_keywords"]
|
| 264 |
+
|
| 265 |
+
Return float between 0 and 1.
|
| 266 |
+
"""
|
| 267 |
+
|
| 268 |
+
def run_batch_precision_eval(eval_pairs_path: str, k: int = 5) -> dict:
|
| 269 |
+
"""
|
| 270 |
+
Run precision@K for all queries in eval_pairs.json.
|
| 271 |
+
Return:
|
| 272 |
+
{
|
| 273 |
+
"mean_precision_at_k": 0.74,
|
| 274 |
+
"per_query_results": [
|
| 275 |
+
{"query": "...", "precision_at_k": 0.8, "retrieved_sources": [...]},
|
| 276 |
+
...
|
| 277 |
+
]
|
| 278 |
+
}
|
| 279 |
+
"""
|
| 280 |
+
```
|
| 281 |
+
|
| 282 |
+
---
|
| 283 |
+
|
| 284 |
+
### 4.6 `src/eval/faithfulness.py`
|
| 285 |
+
|
| 286 |
+
**Purpose:** Score whether the generated answer is faithful to the retrieved context using LLM-as-Judge.
|
| 287 |
+
|
| 288 |
+
```python
|
| 289 |
+
FAITHFULNESS_PROMPT = """
|
| 290 |
+
You are an evaluation judge. Given a context and an answer, score how faithful
|
| 291 |
+
the answer is to the context on a scale of 1-5.
|
| 292 |
+
|
| 293 |
+
1 = Answer contradicts or ignores the context entirely
|
| 294 |
+
2 = Answer uses context minimally, adds significant unsupported claims
|
| 295 |
+
3 = Answer mostly uses context with minor unsupported additions
|
| 296 |
+
4 = Answer is well-grounded in context with trivial additions only
|
| 297 |
+
5 = Answer is entirely and accurately derived from the context
|
| 298 |
+
|
| 299 |
+
Context:
|
| 300 |
+
{context}
|
| 301 |
+
|
| 302 |
+
Answer:
|
| 303 |
+
{answer}
|
| 304 |
+
|
| 305 |
+
Respond ONLY with valid JSON: {{"score": <int>, "reason": "<one sentence>"}}
|
| 306 |
+
"""
|
| 307 |
+
|
| 308 |
+
def score_faithfulness(answer: str, source_chunks: list[dict]) -> dict:
|
| 309 |
+
"""
|
| 310 |
+
Call Euron API (gpt-5.3-instant) with FAITHFULNESS_PROMPT.
|
| 311 |
+
Parse JSON response.
|
| 312 |
+
Return:
|
| 313 |
+
{
|
| 314 |
+
"score": 4,
|
| 315 |
+
"reason": "Answer accurately summarizes the retrieved UPI statistics.",
|
| 316 |
+
"raw_response": "..."
|
| 317 |
+
}
|
| 318 |
+
Handle JSON parse errors gracefully β return score: -1 on failure.
|
| 319 |
+
"""
|
| 320 |
+
```
|
| 321 |
+
|
| 322 |
+
---
|
| 323 |
+
|
| 324 |
+
### 4.7 `src/utils.py`
|
| 325 |
+
|
| 326 |
+
```python
|
| 327 |
+
def load_config(config_path: str = "config.yaml") -> dict:
|
| 328 |
+
"""Load config.yaml and return as dict."""
|
| 329 |
+
|
| 330 |
+
def count_tokens(text: str) -> int:
|
| 331 |
+
"""Approximate token count: len(text.split()) * 1.3"""
|
| 332 |
+
|
| 333 |
+
def setup_logger(name: str) -> logging.Logger:
|
| 334 |
+
"""Return configured logger with timestamp format."""
|
| 335 |
+
```
|
| 336 |
+
|
| 337 |
+
---
|
| 338 |
+
|
| 339 |
+
### 4.8 `config.yaml`
|
| 340 |
+
|
| 341 |
+
```yaml
|
| 342 |
+
chunking:
|
| 343 |
+
chunk_size: 500
|
| 344 |
+
chunk_overlap: 50
|
| 345 |
+
|
| 346 |
+
retrieval:
|
| 347 |
+
k: 5
|
| 348 |
+
collection_name: "finrag"
|
| 349 |
+
|
| 350 |
+
memory:
|
| 351 |
+
max_token_limit: 2000
|
| 352 |
+
|
| 353 |
+
llm:
|
| 354 |
+
model: "gpt-5.3-instant"
|
| 355 |
+
base_url: "https://api.euron.one/api/v1/euri"
|
| 356 |
+
max_tokens: 1000
|
| 357 |
+
temperature: 0.1
|
| 358 |
+
|
| 359 |
+
eval:
|
| 360 |
+
ground_truth_path: "data/ground_truth/eval_pairs.json"
|
| 361 |
+
precision_k: 5
|
| 362 |
+
```
|
| 363 |
+
|
| 364 |
+
---
|
| 365 |
+
|
| 366 |
+
## 5. Streamlit Application
|
| 367 |
+
|
| 368 |
+
### 5.1 `app/streamlit_app.py`
|
| 369 |
+
|
| 370 |
+
**Entry point.** Sets up page config, loads chain + memory, renders tab navigation.
|
| 371 |
+
|
| 372 |
+
```python
|
| 373 |
+
# Page config
|
| 374 |
+
st.set_page_config(page_title="FinRAG", layout="wide", page_icon="π")
|
| 375 |
+
|
| 376 |
+
# Tabs
|
| 377 |
+
tab1, tab2 = st.tabs(["π¬ Chat", "π Eval Dashboard"])
|
| 378 |
+
|
| 379 |
+
with tab1:
|
| 380 |
+
render_chat_tab()
|
| 381 |
+
|
| 382 |
+
with tab2:
|
| 383 |
+
render_eval_dashboard()
|
| 384 |
+
```
|
| 385 |
+
|
| 386 |
+
**Session state to initialise:**
|
| 387 |
+
```python
|
| 388 |
+
if "chain" not in st.session_state:
|
| 389 |
+
st.session_state.chain = build_qa_chain(retriever, memory)
|
| 390 |
+
if "memory" not in st.session_state:
|
| 391 |
+
st.session_state.memory = create_memory()
|
| 392 |
+
if "messages" not in st.session_state:
|
| 393 |
+
st.session_state.messages = []
|
| 394 |
+
if "eval_log" not in st.session_state:
|
| 395 |
+
st.session_state.eval_log = [] # List of {query, answer, precision, faithfulness}
|
| 396 |
+
```
|
| 397 |
+
|
| 398 |
+
---
|
| 399 |
+
|
| 400 |
+
### 5.2 Chat Tab (`app/pages/chat.py`)
|
| 401 |
+
|
| 402 |
+
**Layout:**
|
| 403 |
+
```
|
| 404 |
+
[Sidebar] [Main panel]
|
| 405 |
+
- Loaded documents list - Chat message history
|
| 406 |
+
- Chunk count - Input box (bottom)
|
| 407 |
+
- "New Conversation" btn - Source expander below each answer
|
| 408 |
+
- Eval summary (last 5)
|
| 409 |
+
```
|
| 410 |
+
|
| 411 |
+
**Behaviour:**
|
| 412 |
+
- User types question β `run_query(chain, question)` β display answer
|
| 413 |
+
- Below each answer: collapsible `st.expander("π Sources (K chunks)")` showing source filename, page, similarity score, chunk preview (first 200 chars)
|
| 414 |
+
- After each answer: run `score_faithfulness()` β display `π’ Faithful (4/5)` or `π‘ Moderate (3/5)` or `π΄ Low (1-2/5)` badge inline
|
| 415 |
+
- "New Conversation" button clears memory and resets `st.session_state.messages`
|
| 416 |
+
|
| 417 |
+
---
|
| 418 |
+
|
| 419 |
+
### 5.3 Eval Dashboard Tab (`app/pages/eval_dashboard.py`)
|
| 420 |
+
|
| 421 |
+
**Three sections:**
|
| 422 |
+
|
| 423 |
+
**Section 1 β Session Eval Log**
|
| 424 |
+
Table of all queries in current session:
|
| 425 |
+
| Query | Faithfulness Score | Reason |
|
| 426 |
+
|---|---|---|
|
| 427 |
+
| What was UPI volume in FY24? | 4/5 | Accurate summary of NPCI data |
|
| 428 |
+
|
| 429 |
+
**Section 2 β Batch Precision@K Runner**
|
| 430 |
+
- Button: "Run Precision@K Eval"
|
| 431 |
+
- On click: runs `run_batch_precision_eval()` against `eval_pairs.json`
|
| 432 |
+
- Shows: mean Precision@K score + per-query breakdown table
|
| 433 |
+
- Bar chart: precision score per query (use `st.bar_chart`)
|
| 434 |
+
|
| 435 |
+
**Section 3 β Retrieval Health**
|
| 436 |
+
- Mean faithfulness score (current session)
|
| 437 |
+
- Mean Precision@K (last batch run)
|
| 438 |
+
- Simple traffic light: π’ if both > 0.7, π‘ if either 0.5β0.7, π΄ if either < 0.5
|
| 439 |
+
|
| 440 |
+
---
|
| 441 |
+
|
| 442 |
+
## 6. Ground Truth Setup (`data/ground_truth/eval_pairs.json`)
|
| 443 |
+
|
| 444 |
+
Create 20 eval pairs covering the 3 loaded documents. Sample structure β Claude Code should generate all 20:
|
| 445 |
+
|
| 446 |
+
```json
|
| 447 |
+
[
|
| 448 |
+
{
|
| 449 |
+
"query": "What was the total UPI transaction volume in FY2024?",
|
| 450 |
+
"relevant_sources": ["npci_upi_report_2024.pdf"],
|
| 451 |
+
"relevant_chunk_keywords": ["billion", "FY2024", "transaction volume", "NPCI"]
|
| 452 |
+
},
|
| 453 |
+
{
|
| 454 |
+
"query": "What is RBI's stance on digital lending regulations?",
|
| 455 |
+
"relevant_sources": ["rbi_annual_report_2024.pdf"],
|
| 456 |
+
"relevant_chunk_keywords": ["digital lending", "NBFC", "regulation", "guidelines"]
|
| 457 |
+
},
|
| 458 |
+
{
|
| 459 |
+
"query": "What were Bajaj Finance's AUM figures in Q3 FY24?",
|
| 460 |
+
"relevant_sources": ["bajaj_finance_q3_2024_transcript.txt"],
|
| 461 |
+
"relevant_chunk_keywords": ["AUM", "assets under management", "Q3", "crore"]
|
| 462 |
+
}
|
| 463 |
+
]
|
| 464 |
+
```
|
| 465 |
+
|
| 466 |
+
**Note for Claude Code:** Generate 20 realistic fintech eval pairs in this format. Do not make up specific numbers β use keyword-based matching only.
|
| 467 |
+
|
| 468 |
+
---
|
| 469 |
+
|
| 470 |
+
## 7. CLI Scripts
|
| 471 |
+
|
| 472 |
+
### `scripts/run_ingest.py`
|
| 473 |
+
```
|
| 474 |
+
Usage: python scripts/run_ingest.py --data-dir data/raw [--reset]
|
| 475 |
+
--reset: wipe ChromaDB and re-ingest from scratch
|
| 476 |
+
Output:
|
| 477 |
+
Loading documents from data/raw...
|
| 478 |
+
Loaded 3 documents (127 pages total)
|
| 479 |
+
Chunking... 847 chunks created
|
| 480 |
+
Embedding and storing in ChromaDB... done
|
| 481 |
+
Collection 'finrag': 847 chunks ready
|
| 482 |
+
```
|
| 483 |
+
|
| 484 |
+
### `scripts/run_eval.py`
|
| 485 |
+
```
|
| 486 |
+
Usage: python scripts/run_eval.py --queries data/ground_truth/eval_pairs.json [--k 5]
|
| 487 |
+
Output:
|
| 488 |
+
Running Precision@5 eval on 20 queries...
|
| 489 |
+
Mean Precision@5: 0.74
|
| 490 |
+
Results saved to: eval_results_<timestamp>.json
|
| 491 |
+
```
|
| 492 |
+
|
| 493 |
+
---
|
| 494 |
+
|
| 495 |
+
## 8. Tests
|
| 496 |
+
|
| 497 |
+
### `tests/test_ingest.py`
|
| 498 |
+
- Test `chunk_documents` returns chunks with correct metadata
|
| 499 |
+
- Test `embed_and_store` is idempotent (run twice, chunk count stays same)
|
| 500 |
+
|
| 501 |
+
### `tests/test_retriever.py`
|
| 502 |
+
- Test `retrieve_with_scores` returns exactly K results
|
| 503 |
+
- Test each result has required keys: content, source, similarity_score
|
| 504 |
+
|
| 505 |
+
### `tests/test_chain.py`
|
| 506 |
+
- Test `run_query` returns dict with keys: answer, source_documents, question
|
| 507 |
+
- Test answer is non-empty string
|
| 508 |
+
|
| 509 |
+
### `tests/test_eval.py`
|
| 510 |
+
- Test `compute_precision_at_k` returns float between 0 and 1
|
| 511 |
+
- Test `score_faithfulness` returns dict with score key
|
| 512 |
+
- Test faithfulness handles JSON parse error (returns score: -1)
|
| 513 |
+
|
| 514 |
+
---
|
| 515 |
+
|
| 516 |
+
## 9. `requirements.txt`
|
| 517 |
+
|
| 518 |
+
```
|
| 519 |
+
openai>=1.0.0
|
| 520 |
+
langchain>=0.1.0
|
| 521 |
+
langchain-openai>=0.1.0
|
| 522 |
+
langchain-community>=0.0.20
|
| 523 |
+
langchain-chroma>=0.1.0
|
| 524 |
+
chromadb>=0.4.0
|
| 525 |
+
sentence-transformers>=2.2.0
|
| 526 |
+
pypdf>=3.0.0
|
| 527 |
+
streamlit>=1.32.0
|
| 528 |
+
pyyaml>=6.0
|
| 529 |
+
python-dotenv>=1.0.0
|
| 530 |
+
pytest>=7.0.0
|
| 531 |
+
```
|
| 532 |
+
|
| 533 |
+
---
|
| 534 |
+
|
| 535 |
+
## 10. `.env.example`
|
| 536 |
+
|
| 537 |
+
```
|
| 538 |
+
EURON_API_KEY=your_key_here
|
| 539 |
+
```
|
| 540 |
+
|
| 541 |
+
---
|
| 542 |
+
|
| 543 |
+
## 11. `Dockerfile`
|
| 544 |
+
|
| 545 |
+
```dockerfile
|
| 546 |
+
FROM python:3.11-slim
|
| 547 |
+
|
| 548 |
+
WORKDIR /app
|
| 549 |
+
|
| 550 |
+
COPY requirements.txt .
|
| 551 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 552 |
+
|
| 553 |
+
COPY . .
|
| 554 |
+
|
| 555 |
+
# Pre-run ingestion at build time (optional β comment out if data not bundled)
|
| 556 |
+
# RUN python scripts/run_ingest.py --data-dir data/raw
|
| 557 |
+
|
| 558 |
+
EXPOSE 8501
|
| 559 |
+
|
| 560 |
+
CMD ["streamlit", "run", "app/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
| 561 |
+
```
|
| 562 |
+
|
| 563 |
+
---
|
| 564 |
+
|
| 565 |
+
## 12. README Structure (write after ship)
|
| 566 |
+
|
| 567 |
+
```markdown
|
| 568 |
+
# FinRAG β Fintech Research Agent with Persistent Memory
|
| 569 |
+
|
| 570 |
+
## Problem
|
| 571 |
+
[2 paragraphs β fintech analysts manually reading PDFs]
|
| 572 |
+
|
| 573 |
+
## What makes it different
|
| 574 |
+
[The eval layer β most RAG ships without retrieval quality signals]
|
| 575 |
+
|
| 576 |
+
## Architecture diagram
|
| 577 |
+
[ASCII or image]
|
| 578 |
+
|
| 579 |
+
## Demo GIF
|
| 580 |
+
[Screen recording of chat + eval dashboard]
|
| 581 |
+
|
| 582 |
+
## Eval results
|
| 583 |
+
| Metric | Score |
|
| 584 |
+
|---|---|
|
| 585 |
+
| Mean Precision@5 | 0.XX |
|
| 586 |
+
| Mean Faithfulness | X.X/5 |
|
| 587 |
+
|
| 588 |
+
## How to run locally
|
| 589 |
+
[5 steps]
|
| 590 |
+
|
| 591 |
+
## Tech stack
|
| 592 |
+
[Table]
|
| 593 |
+
```
|
| 594 |
+
|
| 595 |
+
---
|
| 596 |
+
|
| 597 |
+
## 13. Build Order β Phased Implementation
|
| 598 |
+
|
| 599 |
+
### Phase 1: Project Setup & Configuration
|
| 600 |
+
> **Goal:** Scaffold the project, set up config, and install dependencies.
|
| 601 |
+
|
| 602 |
+
- [ ] 1.1 Scaffold full directory structure with empty files
|
| 603 |
+
- [ ] 1.2 Write `requirements.txt`
|
| 604 |
+
- [ ] 1.3 Implement `config.yaml` and `.env.example`
|
| 605 |
+
- [ ] 1.4 Implement `src/utils.py` (config loader, logger, token counter)
|
| 606 |
+
- [ ] 1.5 Set up `.gitignore` (chroma_db/, .env, __pycache__, etc.)
|
| 607 |
+
|
| 608 |
+
**Milestone:** `pip install -r requirements.txt` succeeds, config loads without error.
|
| 609 |
+
|
| 610 |
+
---
|
| 611 |
+
|
| 612 |
+
### Phase 2: Ingestion & Retrieval Pipeline
|
| 613 |
+
> **Goal:** Build the core data pipeline β load documents, chunk, embed, store, and retrieve.
|
| 614 |
+
|
| 615 |
+
- [ ] 2.1 Implement `src/ingest.py` β all 4 functions (load, chunk, embed, orchestrate)
|
| 616 |
+
- [ ] 2.2 Implement `src/retriever.py` β both functions (get_retriever, retrieve_with_scores)
|
| 617 |
+
- [ ] 2.3 Implement `scripts/run_ingest.py` (CLI for ingestion)
|
| 618 |
+
- [ ] 2.4 Add sample documents to `data/raw/`
|
| 619 |
+
- [ ] 2.5 Verify: `python scripts/run_ingest.py --data-dir data/raw` processes documents and reports chunk count
|
| 620 |
+
|
| 621 |
+
**Milestone:** Documents are ingested into ChromaDB, retrieval returns top-K chunks with scores.
|
| 622 |
+
|
| 623 |
+
---
|
| 624 |
+
|
| 625 |
+
### Phase 3: Memory & RAG Chain
|
| 626 |
+
> **Goal:** Wire up conversation memory and the full RAG chain with Claude.
|
| 627 |
+
|
| 628 |
+
- [ ] 3.1 Implement `src/memory.py` β all 3 functions (create, get_as_string, clear)
|
| 629 |
+
- [ ] 3.2 Implement `src/chain.py` β both functions (build_qa_chain, run_query)
|
| 630 |
+
- [ ] 3.3 Verify: chain answers a fintech question from CLI and returns source documents
|
| 631 |
+
|
| 632 |
+
**Milestone:** End-to-end RAG pipeline works β query β retrieve β generate answer with sources.
|
| 633 |
+
|
| 634 |
+
---
|
| 635 |
+
|
| 636 |
+
### Phase 4: Evaluation Layer
|
| 637 |
+
> **Goal:** Add self-scoring retrieval eval β Precision@K and faithfulness.
|
| 638 |
+
|
| 639 |
+
- [ ] 4.1 Generate `data/ground_truth/eval_pairs.json` β 20 query/relevant-chunk pairs
|
| 640 |
+
- [ ] 4.2 Implement `src/eval/precision.py` β both functions (compute_precision_at_k, run_batch)
|
| 641 |
+
- [ ] 4.3 Implement `src/eval/faithfulness.py` β LLM-as-Judge scorer
|
| 642 |
+
- [ ] 4.4 Implement `scripts/run_eval.py` (CLI for batch eval)
|
| 643 |
+
- [ ] 4.5 Verify: `python scripts/run_eval.py` outputs mean Precision@5 and per-query scores
|
| 644 |
+
|
| 645 |
+
**Milestone:** Eval pipeline produces Precision@K and faithfulness scores for all ground truth queries.
|
| 646 |
+
|
| 647 |
+
---
|
| 648 |
+
|
| 649 |
+
### Phase 5: Streamlit UI
|
| 650 |
+
> **Goal:** Build the multi-tab Streamlit app β Chat + Eval Dashboard.
|
| 651 |
+
|
| 652 |
+
- [ ] 5.1 Implement `app/streamlit_app.py` (entry point, page config, session state, tabs)
|
| 653 |
+
- [ ] 5.2 Implement `app/components/message_bubble.py` and `app/components/source_expander.py`
|
| 654 |
+
- [ ] 5.3 Implement `app/pages/chat.py` (chat UI, source expanders, faithfulness badges)
|
| 655 |
+
- [ ] 5.4 Implement `app/pages/eval_dashboard.py` (session log, batch Precision@K, retrieval health)
|
| 656 |
+
- [ ] 5.5 Verify: `streamlit run app/streamlit_app.py` launches both tabs and chat works end-to-end
|
| 657 |
+
|
| 658 |
+
**Milestone:** Full UI is functional β chat with sources, inline faithfulness badges, eval dashboard with bar chart.
|
| 659 |
+
|
| 660 |
+
---
|
| 661 |
+
|
| 662 |
+
### Phase 6: Testing
|
| 663 |
+
> **Goal:** Write and pass all unit tests.
|
| 664 |
+
|
| 665 |
+
- [ ] 6.1 Implement `tests/test_ingest.py` (chunking metadata, idempotency)
|
| 666 |
+
- [ ] 6.2 Implement `tests/test_retriever.py` (K results, required keys)
|
| 667 |
+
- [ ] 6.3 Implement `tests/test_chain.py` (response structure, non-empty answer)
|
| 668 |
+
- [ ] 6.4 Implement `tests/test_eval.py` (precision range, faithfulness structure, error handling)
|
| 669 |
+
- [ ] 6.5 Verify: `pytest` passes all tests
|
| 670 |
+
|
| 671 |
+
**Milestone:** All 4 test files pass with `pytest`.
|
| 672 |
+
|
| 673 |
+
---
|
| 674 |
+
|
| 675 |
+
### Phase 7: Deployment & Polish
|
| 676 |
+
> **Goal:** Containerize, deploy, and finalize the project.
|
| 677 |
+
|
| 678 |
+
- [ ] 7.1 Write `Dockerfile`
|
| 679 |
+
- [ ] 7.2 Test Docker build and run locally
|
| 680 |
+
- [ ] 7.3 Deploy to Render (connect GitHub repo: https://github.com/BenRoshan100/fin-rag.git)
|
| 681 |
+
- [ ] 7.4 Verify live URL is accessible and functional
|
| 682 |
+
- [ ] 7.5 Write `README.md` (problem, architecture, demo GIF, eval results, setup steps)
|
| 683 |
+
|
| 684 |
+
**Milestone:** App is live on Railway, README is complete, project is portfolio-ready.
|
| 685 |
+
|
| 686 |
+
---
|
| 687 |
+
|
| 688 |
+
## 14. Acceptance Criteria
|
| 689 |
+
|
| 690 |
+
- [ ] `run_ingest.py` processes 3 documents and confirms chunk count in terminal
|
| 691 |
+
- [ ] Chat tab answers a fintech question and shows source chunks in expander
|
| 692 |
+
- [ ] Follow-up question uses prior context (memory working)
|
| 693 |
+
- [ ] Each answer shows faithfulness badge (π’/π‘/π΄)
|
| 694 |
+
- [ ] Eval dashboard batch run shows Precision@5 score and bar chart
|
| 695 |
+
- [ ] Ingestion is idempotent β running twice does not duplicate chunks
|
| 696 |
+
- [ ] All 4 test files pass with `pytest`
|
| 697 |
+
- [ ] App deploys to Render via Dockerfile (GitHub repo connected)
|
| 698 |
+
- [ ] Live URL accessible and functional
|
| 699 |
+
|
| 700 |
+
---
|
| 701 |
+
|
| 702 |
+
*PRD v1.0 β FinRAG. Feed this entire file to Claude Code as the project specification.*
|