benroshan Claude Opus 4.6 (1M context) commited on
Commit
24fa45d
Β·
0 Parent(s):

Add FinRAG PRD specification

Browse files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. finrag.md +702 -0
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.*