benroshan commited on
Commit
f5628ad
·
1 Parent(s): 24fa45d

Add Dockerfile and Render config

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +1 -0
  2. .gitignore +36 -0
  3. Dockerfile +25 -0
  4. config.yaml +23 -0
  5. data/ground_truth/eval_pairs.json +102 -0
  6. data/raw/bajaj_finance_q3_2024_transcript.txt +66 -0
  7. data/raw/npci_upi_report_2024.txt +85 -0
  8. data/raw/rbi_annual_report_2024.txt +66 -0
  9. finrag.md +152 -102
  10. frontend/.gitignore +24 -0
  11. frontend/README.md +16 -0
  12. frontend/eslint.config.js +29 -0
  13. frontend/index.html +14 -0
  14. frontend/package-lock.json +0 -0
  15. frontend/package.json +30 -0
  16. frontend/public/favicon.svg +1 -0
  17. frontend/public/icons.svg +24 -0
  18. frontend/src/App.jsx +52 -0
  19. frontend/src/api.js +42 -0
  20. frontend/src/assets/hero.png +0 -0
  21. frontend/src/assets/vite.svg +1 -0
  22. frontend/src/components/ChatArea.jsx +133 -0
  23. frontend/src/components/FileUpload.jsx +102 -0
  24. frontend/src/components/MessageBubble.jsx +72 -0
  25. frontend/src/components/Sidebar.jsx +137 -0
  26. frontend/src/components/SourceExpander.jsx +69 -0
  27. frontend/src/index.css +12 -0
  28. frontend/src/main.jsx +10 -0
  29. frontend/vite.config.js +12 -0
  30. render.yaml +8 -0
  31. requirements.txt +14 -0
  32. sample_data/bajaj_finance_q3_2024_transcript.txt +66 -0
  33. sample_data/npci_upi_report_2024.txt +85 -0
  34. sample_data/rbi_annual_report_2024.txt +66 -0
  35. scripts/benchmark_chunks.py +169 -0
  36. scripts/run_eval.py +39 -0
  37. scripts/run_ingest.py +28 -0
  38. server/__init__.py +0 -0
  39. server/chain.py +89 -0
  40. server/eval/__init__.py +0 -0
  41. server/eval/faithfulness.py +74 -0
  42. server/eval/precision.py +66 -0
  43. server/ingest.py +176 -0
  44. server/main.py +55 -0
  45. server/memory.py +44 -0
  46. server/retriever.py +83 -0
  47. server/routes/__init__.py +0 -0
  48. server/routes/chat.py +53 -0
  49. server/routes/eval.py +29 -0
  50. server/routes/upload.py +58 -0
.env.example ADDED
@@ -0,0 +1 @@
 
 
1
+ EURON_API_KEY=your_key_here
.gitignore ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ChromaDB
2
+ chroma_db/
3
+
4
+ # Environment
5
+ .env
6
+
7
+ # Python
8
+ __pycache__/
9
+ *.py[cod]
10
+ *$py.class
11
+ *.egg-info/
12
+ dist/
13
+ build/
14
+ .eggs/
15
+
16
+ # Virtual environments
17
+ venv/
18
+ .venv/
19
+ env/
20
+
21
+ # IDE
22
+ .vscode/
23
+ .idea/
24
+ *.swp
25
+ *.swo
26
+
27
+ # OS
28
+ .DS_Store
29
+ Thumbs.db
30
+
31
+ # Node / Frontend
32
+ node_modules/
33
+ frontend/dist/
34
+
35
+ # Eval results
36
+ eval_results_*.json
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stage 1: Build React frontend
2
+ FROM node:20-slim AS frontend-build
3
+ WORKDIR /app/frontend
4
+ COPY frontend/package.json frontend/package-lock.json ./
5
+ RUN npm ci
6
+ COPY frontend/ ./
7
+ RUN npm run build
8
+
9
+ # Stage 2: Python backend + serve frontend
10
+ FROM python:3.11-slim
11
+ WORKDIR /app
12
+
13
+ COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ COPY server/ server/
17
+ COPY config.yaml .
18
+ COPY data/ground_truth/ data/ground_truth/
19
+
20
+ # Copy built frontend from stage 1
21
+ COPY --from=frontend-build /app/frontend/dist frontend/dist
22
+
23
+ EXPOSE 8000
24
+
25
+ CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8000"]
config.yaml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ chunking:
2
+ chunk_size: 500
3
+ chunk_overlap: 50
4
+
5
+ retrieval:
6
+ k: 5
7
+ collection_name: "finrag"
8
+
9
+ memory:
10
+ max_token_limit: 2000
11
+
12
+ llm:
13
+ # Euron API supports multiple providers via the same endpoint.
14
+ # Free: gpt-4.1-mini, gpt-4.1-nano, gemini-2.5-flash, llama-4-scout-17b-16e-instruct
15
+ # Premium: claude-sonnet-4, gpt-5, gemini-3-flash, o3, o4-mini
16
+ model: "gpt-4.1-mini"
17
+ base_url: "https://api.euron.one/api/v1/euri"
18
+ max_tokens: 1000
19
+ temperature: 0.1
20
+
21
+ eval:
22
+ ground_truth_path: "data/ground_truth/eval_pairs.json"
23
+ precision_k: 5
data/ground_truth/eval_pairs.json ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "query": "What was the total UPI transaction volume in FY2024?",
4
+ "relevant_sources": ["npci_upi_report_2024.txt"],
5
+ "relevant_chunk_keywords": ["131 billion", "14.04 billion", "transaction volume", "FY2024"]
6
+ },
7
+ {
8
+ "query": "What was India's GDP growth rate in FY2024?",
9
+ "relevant_sources": ["rbi_annual_report_2024.txt"],
10
+ "relevant_chunk_keywords": ["7.6 per cent", "GDP growth", "FY2024", "domestic demand"]
11
+ },
12
+ {
13
+ "query": "What were Bajaj Finance's Assets Under Management in Q3 FY2024?",
14
+ "relevant_sources": ["bajaj_finance_q3_2024_transcript.txt"],
15
+ "relevant_chunk_keywords": ["AUM", "3,10,672 crore", "35%", "assets under management"]
16
+ },
17
+ {
18
+ "query": "What is RBI's stance on digital lending regulations?",
19
+ "relevant_sources": ["rbi_annual_report_2024.txt"],
20
+ "relevant_chunk_keywords": ["digital lending", "NBFC", "Lending Service Providers", "DLA", "disclosure"]
21
+ },
22
+ {
23
+ "query": "How many banks were live on UPI by March 2024?",
24
+ "relevant_sources": ["npci_upi_report_2024.txt"],
25
+ "relevant_chunk_keywords": ["580 banks", "live on UPI", "March 2024"]
26
+ },
27
+ {
28
+ "query": "What was the UPI fraud rate in FY2024?",
29
+ "relevant_sources": ["npci_upi_report_2024.txt"],
30
+ "relevant_chunk_keywords": ["0.0006%", "fraud rate", "3.07 lakh", "fraud cases"]
31
+ },
32
+ {
33
+ "query": "What was the RBI repo rate during FY2024?",
34
+ "relevant_sources": ["rbi_annual_report_2024.txt"],
35
+ "relevant_chunk_keywords": ["6.50 per cent", "repo rate", "monetary policy", "MPC"]
36
+ },
37
+ {
38
+ "query": "What was Bajaj Finance's profit after tax in Q3 FY2024?",
39
+ "relevant_sources": ["bajaj_finance_q3_2024_transcript.txt"],
40
+ "relevant_chunk_keywords": ["3,639 crore", "profit after tax", "22%", "earnings per share"]
41
+ },
42
+ {
43
+ "query": "What is UPI Lite and how many users does it have?",
44
+ "relevant_sources": ["npci_upi_report_2024.txt"],
45
+ "relevant_chunk_keywords": ["UPI Lite", "small-value", "Rs 500", "5.2 crore", "near-zero decline"]
46
+ },
47
+ {
48
+ "query": "What was India's CPI inflation rate in FY2024?",
49
+ "relevant_sources": ["rbi_annual_report_2024.txt"],
50
+ "relevant_chunk_keywords": ["5.4 per cent", "CPI", "inflation", "core inflation", "4.2 per cent"]
51
+ },
52
+ {
53
+ "query": "What is Bajaj Finance's capital adequacy ratio?",
54
+ "relevant_sources": ["bajaj_finance_q3_2024_transcript.txt"],
55
+ "relevant_chunk_keywords": ["CRAR", "23.8%", "capital adequacy", "Tier-I", "22.1%"]
56
+ },
57
+ {
58
+ "query": "Which countries accept UPI payments internationally?",
59
+ "relevant_sources": ["npci_upi_report_2024.txt"],
60
+ "relevant_chunk_keywords": ["Singapore", "UAE", "France", "Sri Lanka", "international", "cross-border"]
61
+ },
62
+ {
63
+ "query": "What was India's foreign exchange reserves in March 2024?",
64
+ "relevant_sources": ["rbi_annual_report_2024.txt"],
65
+ "relevant_chunk_keywords": ["USD 645.6 billion", "foreign exchange reserves", "March 2024"]
66
+ },
67
+ {
68
+ "query": "What is Bajaj Finance's Gross NPA ratio?",
69
+ "relevant_sources": ["bajaj_finance_q3_2024_transcript.txt"],
70
+ "relevant_chunk_keywords": ["GNPA", "0.95%", "net NPA", "0.36%", "provision coverage"]
71
+ },
72
+ {
73
+ "query": "What is the market share of PhonePe on UPI?",
74
+ "relevant_sources": ["npci_upi_report_2024.txt"],
75
+ "relevant_chunk_keywords": ["PhonePe", "47%", "Google Pay", "34%", "market share"]
76
+ },
77
+ {
78
+ "query": "What is the banking sector's GNPA ratio as reported by RBI?",
79
+ "relevant_sources": ["rbi_annual_report_2024.txt"],
80
+ "relevant_chunk_keywords": ["3.2 per cent", "GNPA", "net NPA", "0.8 per cent", "banking sector"]
81
+ },
82
+ {
83
+ "query": "How much did Bajaj Finance spend on technology?",
84
+ "relevant_sources": ["bajaj_finance_q3_2024_transcript.txt"],
85
+ "relevant_chunk_keywords": ["1,180 crore", "technology spend", "12%", "operating expenses"]
86
+ },
87
+ {
88
+ "query": "What are the BBPS transaction statistics for FY2024?",
89
+ "relevant_sources": ["npci_upi_report_2024.txt"],
90
+ "relevant_chunk_keywords": ["BBPS", "1,243 crore", "15.4 lakh crore", "42%"]
91
+ },
92
+ {
93
+ "query": "What is the Digital Rupee pilot status?",
94
+ "relevant_sources": ["rbi_annual_report_2024.txt"],
95
+ "relevant_chunk_keywords": ["Digital Rupee", "CBDC", "e-Rupee", "50 cities", "10 lakh", "retail"]
96
+ },
97
+ {
98
+ "query": "What is Bajaj Housing Finance's AUM and IPO plans?",
99
+ "relevant_sources": ["bajaj_finance_q3_2024_transcript.txt"],
100
+ "relevant_chunk_keywords": ["Bajaj Housing Finance", "72,400 crore", "IPO", "DRHP", "listing"]
101
+ }
102
+ ]
data/raw/bajaj_finance_q3_2024_transcript.txt ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ BAJAJ FINANCE LIMITED — Q3 FY2024 EARNINGS CALL TRANSCRIPT
2
+ Date: January 30, 2024
3
+ Participants: Rajeev Jain (MD & CEO), Sandeep Jain (CFO), Analysts
4
+
5
+ OPENING REMARKS — RAJEEV JAIN, MD & CEO
6
+
7
+ Good evening, everyone. Thank you for joining Bajaj Finance's Q3 FY2024 earnings call. I'm pleased to report another strong quarter for the company.
8
+
9
+ Let me begin with the key highlights for Q3 FY2024:
10
+
11
+ Assets Under Management (AUM) grew 35% year-on-year to Rs 3,10,672 crore as of December 31, 2023. This represents a sequential increase of Rs 15,363 crore from Q2 FY2024. We continue to be on track to achieve our medium-term AUM target of Rs 5,00,000 crore by March 2026.
12
+
13
+ New loans booked during Q3 FY2024 stood at 91.3 lakh accounts, a 24% increase over Q3 FY2023. The customer franchise expanded to 7.86 crore, adding 39.6 lakh new customers during the quarter. Cross-sell franchise reached 5.12 crore, representing 65% of total customers.
14
+
15
+ FINANCIAL PERFORMANCE
16
+
17
+ Net Interest Income (NII) for Q3 FY2024 was Rs 8,845 crore, up 28% year-on-year. Net interest margin (NIM) for the quarter stood at 11.61% compared to 11.52% in Q2 FY2024 and 11.48% in Q3 FY2023.
18
+
19
+ Pre-provision operating profit was Rs 6,244 crore, a growth of 31% year-on-year. Profit after tax for Q3 FY2024 was Rs 3,639 crore, a growth of 22% year-on-year. Earnings per share for the quarter was Rs 58.8.
20
+
21
+ Cost-to-income ratio improved to 33.4% from 34.1% in Q3 FY2023, driven by operating leverage and continued investments in technology.
22
+
23
+ ASSET QUALITY
24
+
25
+ Gross NPA stood at 0.95% as of December 2023, compared to 1.14% in December 2022. Net NPA was at 0.36%, improving from 0.44% a year ago. Provision coverage ratio remained healthy at 62%.
26
+
27
+ Loan loss and provision for Q3 FY2024 was Rs 1,729 crore. We have maintained a management overlay provision of Rs 1,230 crore for macroeconomic uncertainties. Total provision buffer stands at Rs 5,840 crore, which is approximately 1.88% of AUM.
28
+
29
+ The RBI's advisory on unsecured lending has had limited impact on our portfolio. We proactively tightened underwriting standards for personal loans and credit card portfolios in October 2023.
30
+
31
+ SEGMENT PERFORMANCE
32
+
33
+ Consumer B2C Business:
34
+ - AUM: Rs 1,42,500 crore (46% of total AUM)
35
+ - Products: Personal loans, consumer durable loans, lifestyle finance, digital product finance
36
+ - Growth: 38% YoY
37
+
38
+ SME and Commercial Lending:
39
+ - AUM: Rs 58,300 crore (19% of total AUM)
40
+ - New SME accounts: 2.8 lakh in Q3
41
+ - Average ticket size: Rs 18.4 lakh
42
+ - Growth: 29% YoY
43
+
44
+ Rural Lending:
45
+ - AUM: Rs 22,800 crore (7% of total AUM)
46
+ - Presence: 1,140 rural locations
47
+ - Growth: 45% YoY
48
+
49
+ Mortgages (Bajaj Housing Finance):
50
+ - AUM: Rs 72,400 crore (23% of total AUM)
51
+ - New home loans: Rs 8,200 crore disbursed in Q3
52
+ - NPA: 0.28%
53
+
54
+ TECHNOLOGY AND DIGITAL INITIATIVES
55
+
56
+ Technology spend for 9M FY2024 was Rs 1,180 crore, approximately 12% of operating expenses. 72% of new personal loans originated through the app. Average loan disbursement time reduced to 14 seconds for pre-approved customers.
57
+
58
+ CAPITAL AND LIQUIDITY
59
+
60
+ Capital adequacy ratio (CRAR) stood at 23.8%, well above the regulatory requirement of 15%. Tier-I capital was 22.1%. Cost of funds for Q3 FY2024 was 7.82%.
61
+
62
+ GUIDANCE AND OUTLOOK
63
+
64
+ For FY2024 guidance: AUM growth 32-34%, new loans 35-37 million accounts, profit after tax growth 20-22%, GNPA below 1.1%, return on equity 21-23%.
65
+
66
+ For FY2025: AUM to cross Rs 4,00,000 crore, Bajaj Housing Finance IPO targeted for H1 FY2025.
data/raw/npci_upi_report_2024.txt ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ NATIONAL PAYMENTS CORPORATION OF INDIA — UPI ECOSYSTEM REPORT FY2024
2
+
3
+ EXECUTIVE SUMMARY
4
+
5
+ The Unified Payments Interface (UPI) completed another year of record-breaking growth in FY2024. Total UPI transaction volume reached 14.04 billion transactions per month by March 2024, with annual aggregate volume crossing 131 billion transactions valued at approximately Rs 200 lakh crore. UPI has firmly established itself as India's primary digital payment rail, processing more transactions than all other digital payment modes combined.
6
+
7
+ SECTION 1: UPI TRANSACTION STATISTICS
8
+
9
+ Monthly Transaction Trends:
10
+ - April 2023: 8.89 billion transactions, Rs 14.07 lakh crore value
11
+ - July 2023: 9.96 billion transactions, Rs 15.34 lakh crore value
12
+ - October 2023: 11.41 billion transactions, Rs 17.16 lakh crore value
13
+ - January 2024: 12.20 billion transactions, Rs 18.41 lakh crore value
14
+ - March 2024: 14.04 billion transactions, Rs 19.78 lakh crore value
15
+
16
+ Year-on-year growth: 56% in volume, 43% in value compared to FY2023.
17
+
18
+ Average ticket size decreased from Rs 1,730 in FY2023 to Rs 1,524 in FY2024, indicating increased adoption for small-value everyday transactions including transit, vending machines, and micropayments.
19
+
20
+ SECTION 2: UPI PARTICIPANT ECOSYSTEM
21
+
22
+ Active UPI handles crossed 400 million by March 2024, with approximately 300 million unique users transacting at least once per month. The merchant ecosystem expanded significantly:
23
+
24
+ - Registered UPI merchants: 3.2 crore (32 million)
25
+ - QR code deployments: 28 crore across India
26
+ - Third-party app providers (TPAPs): 57 approved apps
27
+ - Market share by volume: PhonePe (47%), Google Pay (34%), Paytm (14%), Others (5%)
28
+
29
+ Banking Infrastructure:
30
+ - 580 banks live on UPI as of March 2024 (up from 437 in March 2023)
31
+ - PSU banks accounted for 35% of UPI originating transactions
32
+ - Private banks accounted for 42% of UPI originating transactions
33
+ - Small Finance Banks and Payment Banks accounted for 23%
34
+
35
+ SECTION 3: UPI PRODUCT INNOVATIONS
36
+
37
+ UPI Lite:
38
+ UPI Lite was launched to enable small-value transactions (up to Rs 500) with near-zero decline rates. As of March 2024, UPI Lite had 5.2 crore enabled users processing 18 crore transactions monthly. UPI Lite X, the offline variant, was piloted in 12 cities.
39
+
40
+ UPI AutoPay:
41
+ Recurring mandates on UPI grew to 12.8 crore active mandates, commonly used for OTT subscriptions (38%), utility bills (28%), insurance premiums (18%), and mutual fund SIPs (16%).
42
+
43
+ Credit Line on UPI:
44
+ RBI permitted banks to offer pre-approved credit lines via UPI. By March 2024, 8 banks had launched credit-on-UPI products, disbursing Rs 4,200 crore in cumulative credit.
45
+
46
+ UPI International:
47
+ UPI acceptance was enabled in 7 countries: Singapore, UAE, France, Sri Lanka, Mauritius, Nepal, and Bhutan. Inbound UPI usage by foreign nationals was piloted during the G20 summit. Total cross-border UPI transactions reached 1.4 crore in FY2024.
48
+
49
+ SECTION 4: FRAUD AND RISK MANAGEMENT
50
+
51
+ UPI fraud rate remained low at 0.0006% of total transactions by volume. NPCI's Central Fraud Registry flagged 2.1 lakh suspicious accounts during FY2024. Key fraud mitigation measures included:
52
+
53
+ - Device binding and SIM verification for new UPI registrations
54
+ - AI-based transaction monitoring detecting anomalous patterns in real-time
55
+ - Cool-off period of 4 hours for first-time transfers exceeding Rs 2,000 to new beneficiaries
56
+ - Collaboration with telecom operators for SIM swap fraud detection
57
+
58
+ Total reported UPI fraud cases: 3.07 lakh in FY2024 (up from 2.22 lakh in FY2023), with a combined value of Rs 1,087 crore. The increase is attributed to higher transaction volumes and improved reporting mechanisms.
59
+
60
+ SECTION 5: BHARAT BILL PAYMENT SYSTEM (BBPS)
61
+
62
+ BBPS processed 1,243 crore transactions valued at Rs 15.4 lakh crore during FY2024, a 42% growth over FY2023. Biller categories expanded to include:
63
+ - Electricity and water utilities (largest category at 41%)
64
+ - Telecom and DTH (22%)
65
+ - Insurance premiums (14%)
66
+ - Loan EMI payments (12%)
67
+ - Municipal taxes and fees (6%)
68
+ - Education fees (5%)
69
+
70
+ SECTION 6: IMPS AND OTHER NPCI PRODUCTS
71
+
72
+ Immediate Payment Service (IMPS) processed 588 crore transactions worth Rs 69.4 lakh crore in FY2024. While IMPS growth has moderated (12% YoY) due to UPI substitution, it remains important for bank-to-bank high-value transfers.
73
+
74
+ RuPay card transactions grew 34% to reach 435 crore transactions in FY2024. RuPay's domestic debit card market share stood at 60%. RuPay credit cards on UPI were used in 8.4 crore transactions monthly by March 2024.
75
+
76
+ NETC FASTag processed 384 crore toll transactions valued at Rs 58,200 crore, covering 99% of national highway toll plazas.
77
+
78
+ SECTION 7: OUTLOOK AND STRATEGY
79
+
80
+ NPCI targets 100 billion monthly UPI transactions by 2028. Strategic priorities include:
81
+ - Expanding UPI to 20 countries for cross-border payments
82
+ - UPI Lite adoption target of 50 crore users
83
+ - Enabling UPI for capital markets (IPO, mutual funds, bonds)
84
+ - IoT-based payments integration for smart devices
85
+ - Carbon footprint tracking for UPI transactions
data/raw/rbi_annual_report_2024.txt ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ RESERVE BANK OF INDIA — ANNUAL REPORT 2023-24
2
+
3
+ CHAPTER 1: ASSESSMENT OF THE ECONOMY
4
+
5
+ India's real GDP growth for FY2024 is estimated at 7.6 per cent, driven by strong domestic demand and resilient services activity. The manufacturing sector showed signs of recovery, while agriculture growth moderated due to uneven monsoon distribution.
6
+
7
+ Inflation, as measured by the Consumer Price Index (CPI), averaged 5.4 per cent during FY2024, remaining within the RBI's target band of 2-6 per cent. Core inflation (excluding food and fuel) moderated to 4.2 per cent by March 2024, reflecting the impact of monetary policy tightening.
8
+
9
+ The current account deficit narrowed to 1.2 per cent of GDP in FY2024, supported by robust services exports and steady remittance inflows. Foreign exchange reserves stood at USD 645.6 billion as of March 2024.
10
+
11
+ CHAPTER 2: MONETARY POLICY OPERATIONS
12
+
13
+ The Monetary Policy Committee (MPC) maintained the policy repo rate at 6.50 per cent through FY2024, after cumulative hikes of 250 basis points since May 2022. The stance remained focused on withdrawal of accommodation to ensure inflation aligns with the 4 per cent target on a durable basis.
14
+
15
+ Liquidity conditions transitioned from surplus to deficit during the year. The RBI conducted variable rate repo (VRR) auctions and variable rate reverse repo (VRRR) auctions to manage liquidity. The weighted average call rate (WACR) remained closely aligned with the policy repo rate.
16
+
17
+ CHAPTER 3: FINANCIAL REGULATION AND SUPERVISION
18
+
19
+ The RBI strengthened its regulatory framework for Non-Banking Financial Companies (NBFCs). Key regulatory actions during FY2024 included:
20
+
21
+ - Scale-Based Regulation (SBR) framework implementation for NBFCs, classifying them into four layers: Base, Middle, Upper, and Top.
22
+ - Enhanced guidelines on digital lending, requiring all digital loans to be disbursed and repaid through borrower bank accounts. Lending Service Providers (LSPs) and Digital Lending Apps (DLAs) must comply with disclosure requirements.
23
+ - Revised guidelines on Fair Practices Code for NBFCs, emphasizing transparency in loan pricing and customer communication.
24
+ - Risk-based supervision adopted for systemically important NBFCs with asset size above Rs 1,000 crore.
25
+
26
+ The RBI also issued guidelines on climate-related financial risk management for regulated entities, requiring banks and NBFCs to integrate climate risk into their governance and risk management frameworks.
27
+
28
+ CHAPTER 4: PAYMENT AND SETTLEMENT SYSTEMS
29
+
30
+ The digital payments ecosystem in India continued its rapid expansion during FY2024. Total digital payment transactions grew by 44 per cent to reach 16,416 crore transactions valued at Rs 2,210 lakh crore.
31
+
32
+ Unified Payments Interface (UPI) remained the dominant payment mode, processing 11,761 crore transactions worth Rs 199.9 lakh crore during FY2024. This represents year-on-year growth of 56 per cent in volume and 43 per cent in value.
33
+
34
+ The RBI introduced the following initiatives to strengthen the payments infrastructure:
35
+ - UPI for secondary market investments, enabling retail investors to block funds in their bank accounts for IPO applications.
36
+ - Conversational payments on UPI, allowing users to initiate transactions through AI-powered conversational interfaces.
37
+ - UPI Lite X for offline transactions up to Rs 500, enabling payments without internet connectivity.
38
+ - Introduction of UPI for inbound travellers from G20 nations using their home country mobile numbers.
39
+
40
+ RTGS and NEFT systems processed 33.2 crore and 3,259 crore transactions respectively during FY2024. The RBI continued to operate RTGS on a 24x7x365 basis.
41
+
42
+ CHAPTER 5: FINANCIAL INCLUSION AND DIGITAL FINANCE
43
+
44
+ The Pradhan Mantri Jan Dhan Yojana (PMJDY) accounts reached 51.5 crore as of March 2024, with total deposits of Rs 2.18 lakh crore. The average deposit per account increased to Rs 4,234.
45
+
46
+ The RBI's financial literacy initiatives reached 8.7 crore participants through Centre for Financial Literacy (CFL) programmes across 812 districts.
47
+
48
+ Digital Rupee (e-Rupee) pilot programmes continued with both retail (e₹-R) and wholesale (e₹-W) variants. The retail CBDC pilot was expanded to cover 50 cities with participation from 13 banks. As of March 2024, approximately 10 lakh retail e₹ wallets were active.
49
+
50
+ CHAPTER 6: BANKING SECTOR DEVELOPMENTS
51
+
52
+ The banking sector demonstrated improved financial health during FY2024:
53
+ - Gross Non-Performing Assets (GNPA) ratio declined to 3.2 per cent from 3.9 per cent in March 2023.
54
+ - Net NPA ratio fell to 0.8 per cent from 1.0 per cent.
55
+ - Capital to Risk-Weighted Assets Ratio (CRAR) of scheduled commercial banks stood at 16.8 per cent, well above the regulatory minimum of 9 per cent.
56
+ - Return on Assets (ROA) improved to 1.3 per cent and Return on Equity (ROE) to 13.8 per cent.
57
+
58
+ Credit growth was broad-based at 16.3 per cent year-on-year as of March 2024. Personal loans grew at 29.8 per cent, services sector at 20.2 per cent, and industry at 8.5 per cent. The RBI issued advisories cautioning banks about rapid growth in unsecured personal loans and credit card outstanding.
59
+
60
+ CHAPTER 7: FOREIGN EXCHANGE MANAGEMENT
61
+
62
+ The Indian rupee exhibited relative stability during FY2024, depreciating marginally by 1.4 per cent against the US dollar. The RBI's intervention in the foreign exchange market was aimed at curbing excessive volatility rather than targeting any specific level.
63
+
64
+ Foreign Direct Investment (FDI) inflows stood at USD 44.4 billion during FY2024. The services sector, computer software, and telecommunications attracted the highest share of FDI.
65
+
66
+ The RBI permitted international trade settlements in Indian rupees (INR) with 22 countries, promoting INR as a settlement currency for cross-border transactions.
finrag.md CHANGED
@@ -41,7 +41,8 @@ Most RAG portfolios ship without retrieval evaluation. FinRAG adds a self-scorin
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
 
@@ -51,13 +52,6 @@ Most RAG portfolios ship without retrieval evaluation. FinRAG adds a self-scorin
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
@@ -66,26 +60,39 @@ finrag/
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
@@ -97,6 +104,12 @@ finrag/
97
  │ ├── test_chain.py
98
  │ └── test_eval.py
99
 
 
 
 
 
 
 
100
  └── chroma_db/ # Auto-created by ChromaDB, gitignored
101
  ```
102
 
@@ -104,7 +117,7 @@ finrag/
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
 
@@ -147,7 +160,7 @@ def run_ingestion_pipeline(data_dir: str) -> Chroma:
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
 
@@ -176,7 +189,7 @@ def retrieve_with_scores(query: str, k: int = 5) -> list[dict]:
176
 
177
  ---
178
 
179
- ### 4.3 `src/memory.py`
180
 
181
  **Purpose:** Manage conversation memory across turns.
182
 
@@ -202,7 +215,7 @@ def clear_memory(memory: ConversationBufferMemory) -> None:
202
 
203
  ---
204
 
205
- ### 4.4 `src/chain.py`
206
 
207
  **Purpose:** Assemble the full RAG + memory chain. Core of the application.
208
 
@@ -237,7 +250,7 @@ def run_query(chain, question: str) -> dict:
237
 
238
  ---
239
 
240
- ### 4.5 `src/eval/precision.py`
241
 
242
  **Purpose:** Compute Precision@K against a ground truth set.
243
 
@@ -281,7 +294,7 @@ def run_batch_precision_eval(eval_pairs_path: str, k: int = 5) -> dict:
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
 
@@ -321,7 +334,7 @@ def score_faithfulness(answer: str, source_chunks: list[dict]) -> dict:
321
 
322
  ---
323
 
324
- ### 4.7 `src/utils.py`
325
 
326
  ```python
327
  def load_config(config_path: str = "config.yaml") -> dict:
@@ -363,79 +376,90 @@ eval:
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.50.7, 🔴 if either < 0.5
439
 
440
  ---
441
 
@@ -513,7 +537,7 @@ Output:
513
 
514
  ---
515
 
516
- ## 9. `requirements.txt`
517
 
518
  ```
519
  openai>=1.0.0
@@ -524,7 +548,8 @@ 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
@@ -543,6 +568,16 @@ EURON_API_KEY=your_key_here
543
  ## 11. `Dockerfile`
544
 
545
  ```dockerfile
 
 
 
 
 
 
 
 
 
 
546
  FROM python:3.11-slim
547
 
548
  WORKDIR /app
@@ -551,13 +586,14 @@ 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
  ---
@@ -597,23 +633,24 @@ CMD ["streamlit", "run", "app/streamlit_app.py", "--server.port=8501", "--server
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
@@ -625,8 +662,8 @@ CMD ["streamlit", "run", "app/streamlit_app.py", "--server.port=8501", "--server
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.
@@ -637,8 +674,8 @@ CMD ["streamlit", "run", "app/streamlit_app.py", "--server.port=8501", "--server
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
 
@@ -646,14 +683,27 @@ CMD ["streamlit", "run", "app/streamlit_app.py", "--server.port=8501", "--server
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
 
@@ -681,7 +731,7 @@ CMD ["streamlit", "run", "app/streamlit_app.py", "--server.port=8501", "--server
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
 
 
41
  | Document loading | `pypdf`, `langchain.document_loaders` |
42
  | Chunking | `RecursiveCharacterTextSplitter` |
43
  | Eval | Custom Python module — no external eval library |
44
+ | Backend API | FastAPI + Uvicorn |
45
+ | Frontend | React (Vite + Tailwind CSS) |
46
  | Deployment | Render (Dockerfile included, GitHub repo: https://github.com/BenRoshan100/fin-rag.git) |
47
  | Config | `.env` for API keys, `config.yaml` for chunking/retrieval params |
48
 
 
52
 
53
  ```
54
  finrag/
 
 
 
 
 
 
 
55
  ├── data/
56
  │ ├── raw/ # Drop PDFs here
57
  │ │ ├── rbi_annual_report_2024.pdf
 
60
  │ └── ground_truth/
61
  │ └── eval_pairs.json # 20 query/relevant-chunk pairs for Precision@K
62
 
63
+ ├── server/
64
  │ ├── __init__.py
65
+ │ ├── main.py # FastAPI app entrypoint
66
+ │ ├── routes/
67
+ │ │ ├── __init__.py
68
+ │ │ ├── chat.py # POST /api/chat, DELETE /api/chat/memory
69
+ │ │ └── eval.py # GET /api/eval/session, POST /api/eval/precision
70
  │ ├── ingest.py # Document loading, chunking, embedding, ChromaDB storage
71
  │ ├── retriever.py # Query ChromaDB, return top-K chunks with metadata
72
  │ ├── memory.py # ConversationBufferMemory setup and management
73
+ │ ├── chain.py # LangChain QA chain combining retriever + memory + LLM
74
  │ ├── eval/
75
  │ │ ├── __init__.py
76
  │ │ ├── precision.py # Precision@K computation against ground truth
77
  │ │ └── faithfulness.py # LLM-as-Judge faithfulness scorer
78
  │ └── utils.py # Logging, config loader, token counter
79
 
80
+ ├── frontend/
81
+ │ ├── package.json
82
+ │ ├── vite.config.js
83
+ │ ├── tailwind.config.js
84
+ ── postcss.config.js
85
+ ── index.html
86
+ ── src/
87
+ ── main.jsx # React entrypoint
88
+ │ ├── App.jsx # Root component with tab navigation
89
+ │ ├── api.js # Axios client for FastAPI backend
90
+ │ ├── components/
91
+ │ │ ├── ChatTab.jsx # Chat interface
92
+ │ │ ├── EvalDashboard.jsx # Eval metrics dashboard
93
+ │ │ ├── MessageBubble.jsx # Chat message component
94
+ │ │ └── SourceExpander.jsx # Source chunk expander component
95
+ │ └── index.css # Tailwind imports
96
 
97
  ├── scripts/
98
  │ ├── run_ingest.py # CLI: python scripts/run_ingest.py --data-dir data/raw
 
104
  │ ├── test_chain.py
105
  │ └── test_eval.py
106
 
107
+ ├── requirements.txt # Python backend dependencies
108
+ ├── .env.example
109
+ ├── config.yaml
110
+ ├── Dockerfile
111
+ ├── .gitignore
112
+
113
  └── chroma_db/ # Auto-created by ChromaDB, gitignored
114
  ```
115
 
 
117
 
118
  ## 4. Module Specifications
119
 
120
+ ### 4.1 `server/ingest.py`
121
 
122
  **Purpose:** Load documents from `data/raw/`, chunk them, embed them, store in ChromaDB.
123
 
 
160
 
161
  ---
162
 
163
+ ### 4.2 `server/retriever.py`
164
 
165
  **Purpose:** Query ChromaDB and return top-K chunks with similarity scores and metadata.
166
 
 
189
 
190
  ---
191
 
192
+ ### 4.3 `server/memory.py`
193
 
194
  **Purpose:** Manage conversation memory across turns.
195
 
 
215
 
216
  ---
217
 
218
+ ### 4.4 `server/chain.py`
219
 
220
  **Purpose:** Assemble the full RAG + memory chain. Core of the application.
221
 
 
250
 
251
  ---
252
 
253
+ ### 4.5 `server/eval/precision.py`
254
 
255
  **Purpose:** Compute Precision@K against a ground truth set.
256
 
 
294
 
295
  ---
296
 
297
+ ### 4.6 `server/eval/faithfulness.py`
298
 
299
  **Purpose:** Score whether the generated answer is faithful to the retrieved context using LLM-as-Judge.
300
 
 
334
 
335
  ---
336
 
337
+ ### 4.7 `server/utils.py`
338
 
339
  ```python
340
  def load_config(config_path: str = "config.yaml") -> dict:
 
376
 
377
  ---
378
 
379
+ ## 5. FastAPI Backend + React Frontend
380
 
381
+ ### 5.1 `server/main.py` — FastAPI Application
382
 
383
+ **Entry point.** Initializes FastAPI app, CORS middleware, and includes route modules.
384
 
385
  ```python
386
+ from fastapi import FastAPI
387
+ from fastapi.middleware.cors import CORSMiddleware
388
+ from server.routes import chat, eval
389
+
390
+ app = FastAPI(title="FinRAG API")
391
+
392
+ app.add_middleware(
393
+ CORSMiddleware,
394
+ allow_origins=["http://localhost:5173"], # Vite dev server
395
+ allow_methods=["*"],
396
+ allow_headers=["*"],
397
+ )
398
+
399
+ app.include_router(chat.router, prefix="/api")
400
+ app.include_router(eval.router, prefix="/api")
401
+
402
+ # On startup: initialize chain, memory, retriever as app state
403
+ @app.on_event("startup")
404
+ def startup():
405
+ app.state.memory = create_memory()
406
+ retriever = get_retriever()
407
+ app.state.chain = build_qa_chain(retriever, app.state.memory)
408
+ app.state.eval_log = []
409
+ ```
410
 
411
+ ---
 
412
 
413
+ ### 5.2 `server/routes/chat.py` — Chat API
 
 
414
 
 
415
  ```python
416
+ # POST /api/chat
417
+ # Request: { "question": "What was UPI volume in FY24?" }
418
+ # Response: {
419
+ # "answer": "...",
420
+ # "sources": [{ "content", "source", "page", "chunk_index", "similarity_score" }],
421
+ # "faithfulness": { "score": 4, "reason": "..." }
422
+ # }
423
+
424
+ # DELETE /api/chat/memory
425
+ # Clears conversation memory. Returns { "status": "cleared" }
426
  ```
427
 
428
  ---
429
 
430
+ ### 5.3 `server/routes/eval.py` — Eval API
431
 
432
+ ```python
433
+ # GET /api/eval/session
434
+ # Returns the session eval log: list of { query, answer, faithfulness_score, reason }
435
+
436
+ # POST /api/eval/precision
437
+ # Runs batch Precision@K eval against ground truth.
438
+ # Response: {
439
+ # "mean_precision_at_k": 0.74,
440
+ # "per_query_results": [{ "query", "precision_at_k", "retrieved_sources" }]
441
+ # }
442
  ```
443
 
 
 
 
 
 
 
444
  ---
445
 
446
+ ### 5.4 React Frontend (`frontend/`)
447
 
448
+ **Built with:** Vite + React + Tailwind CSS
449
 
450
+ **Two-tab layout via tab navigation in `App.jsx`:**
 
 
 
 
451
 
452
+ **Chat Tab (`ChatTab.jsx`):**
453
+ - Sidebar: loaded documents list, chunk count, "New Conversation" button, eval summary (last 5)
454
+ - Main panel: chat message history, input box at bottom
455
+ - Each assistant message: collapsible source expander showing source filename, page, similarity score, chunk preview (first 200 chars)
456
+ - Faithfulness badge inline after each answer: green (4-5/5), yellow (3/5), red (1-2/5)
457
+ - "New Conversation" calls `DELETE /api/chat/memory` and clears local message state
458
 
459
+ **Eval Dashboard (`EvalDashboard.jsx`):**
460
+ - Section 1 Session eval log table (fetched from `GET /api/eval/session`)
461
+ - Section 2 — "Run Precision@K Eval" button → calls `POST /api/eval/precision` → shows mean score + per-query breakdown table + bar chart
462
+ - Section 3 — Retrieval health traffic light: green if both > 0.7, yellow if either 0.5-0.7, red if either < 0.5
463
 
464
  ---
465
 
 
537
 
538
  ---
539
 
540
+ ## 9. `requirements.txt` (Python backend)
541
 
542
  ```
543
  openai>=1.0.0
 
548
  chromadb>=0.4.0
549
  sentence-transformers>=2.2.0
550
  pypdf>=3.0.0
551
+ fastapi>=0.110.0
552
+ uvicorn>=0.27.0
553
  pyyaml>=6.0
554
  python-dotenv>=1.0.0
555
  pytest>=7.0.0
 
568
  ## 11. `Dockerfile`
569
 
570
  ```dockerfile
571
+ # --- Stage 1: Build React frontend ---
572
+ FROM node:20-slim AS frontend-build
573
+
574
+ WORKDIR /app/frontend
575
+ COPY frontend/package.json frontend/package-lock.json ./
576
+ RUN npm ci
577
+ COPY frontend/ .
578
+ RUN npm run build
579
+
580
+ # --- Stage 2: Python backend + serve static ---
581
  FROM python:3.11-slim
582
 
583
  WORKDIR /app
 
586
  RUN pip install --no-cache-dir -r requirements.txt
587
 
588
  COPY . .
589
+ COPY --from=frontend-build /app/frontend/dist /app/frontend/dist
590
 
591
  # Pre-run ingestion at build time (optional — comment out if data not bundled)
592
  # RUN python scripts/run_ingest.py --data-dir data/raw
593
 
594
+ EXPOSE 8000
595
 
596
+ CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8000"]
597
  ```
598
 
599
  ---
 
633
  ## 13. Build Order — Phased Implementation
634
 
635
  ### Phase 1: Project Setup & Configuration
636
+ > **Goal:** Scaffold the project (backend + frontend), set up config, and install dependencies.
637
 
638
+ - [ ] 1.1 Scaffold full directory structure with empty files (server/, frontend/, scripts/, tests/, data/)
639
+ - [ ] 1.2 Write `requirements.txt` (Python backend deps)
640
+ - [ ] 1.3 Initialize React frontend with Vite + Tailwind CSS (`frontend/`)
641
+ - [ ] 1.4 Implement `config.yaml` and `.env.example`
642
+ - [ ] 1.5 Implement `server/utils.py` (config loader, logger, token counter)
643
+ - [ ] 1.6 Set up `.gitignore` (chroma_db/, .env, __pycache__, node_modules/, dist/, etc.)
644
 
645
+ **Milestone:** `pip install -r requirements.txt` succeeds, `cd frontend && npm install` succeeds, config loads without error.
646
 
647
  ---
648
 
649
  ### Phase 2: Ingestion & Retrieval Pipeline
650
  > **Goal:** Build the core data pipeline — load documents, chunk, embed, store, and retrieve.
651
 
652
+ - [ ] 2.1 Implement `server/ingest.py` — all 4 functions (load, chunk, embed, orchestrate)
653
+ - [ ] 2.2 Implement `server/retriever.py` — both functions (get_retriever, retrieve_with_scores)
654
  - [ ] 2.3 Implement `scripts/run_ingest.py` (CLI for ingestion)
655
  - [ ] 2.4 Add sample documents to `data/raw/`
656
  - [ ] 2.5 Verify: `python scripts/run_ingest.py --data-dir data/raw` processes documents and reports chunk count
 
662
  ### Phase 3: Memory & RAG Chain
663
  > **Goal:** Wire up conversation memory and the full RAG chain with Claude.
664
 
665
+ - [ ] 3.1 Implement `server/memory.py` — all 3 functions (create, get_as_string, clear)
666
+ - [ ] 3.2 Implement `server/chain.py` — both functions (build_qa_chain, run_query)
667
  - [ ] 3.3 Verify: chain answers a fintech question from CLI and returns source documents
668
 
669
  **Milestone:** End-to-end RAG pipeline works — query → retrieve → generate answer with sources.
 
674
  > **Goal:** Add self-scoring retrieval eval — Precision@K and faithfulness.
675
 
676
  - [ ] 4.1 Generate `data/ground_truth/eval_pairs.json` — 20 query/relevant-chunk pairs
677
+ - [ ] 4.2 Implement `server/eval/precision.py` — both functions (compute_precision_at_k, run_batch)
678
+ - [ ] 4.3 Implement `server/eval/faithfulness.py` — LLM-as-Judge scorer
679
  - [ ] 4.4 Implement `scripts/run_eval.py` (CLI for batch eval)
680
  - [ ] 4.5 Verify: `python scripts/run_eval.py` outputs mean Precision@5 and per-query scores
681
 
 
683
 
684
  ---
685
 
686
+ ### Phase 5a: FastAPI Backend API
687
+ > **Goal:** Build the REST API that serves the RAG chain and eval endpoints.
688
+
689
+ - [ ] 5a.1 Implement `server/main.py` (FastAPI app, CORS, startup event, static file serving)
690
+ - [ ] 5a.2 Implement `server/routes/chat.py` (POST /api/chat, DELETE /api/chat/memory)
691
+ - [ ] 5a.3 Implement `server/routes/eval.py` (GET /api/eval/session, POST /api/eval/precision)
692
+ - [ ] 5a.4 Verify: `uvicorn server.main:app` starts and API endpoints respond correctly
693
+
694
+ **Milestone:** All API endpoints work — chat returns answers with sources + faithfulness, eval returns scores.
695
+
696
+ ---
697
+
698
+ ### Phase 5b: React Frontend
699
+ > **Goal:** Build the React UI — Chat + Eval Dashboard tabs.
700
 
701
+ - [ ] 5b.1 Implement `frontend/src/api.js` (Axios client for backend)
702
+ - [ ] 5b.2 Implement `frontend/src/App.jsx` (tab navigation between Chat and Eval)
703
+ - [ ] 5b.3 Implement `frontend/src/components/ChatTab.jsx` (chat UI, sidebar, message input)
704
+ - [ ] 5b.4 Implement `frontend/src/components/MessageBubble.jsx` and `SourceExpander.jsx`
705
+ - [ ] 5b.5 Implement `frontend/src/components/EvalDashboard.jsx` (session log, batch Precision@K, retrieval health)
706
+ - [ ] 5b.6 Verify: `npm run dev` launches frontend, chat works end-to-end with backend
707
 
708
  **Milestone:** Full UI is functional — chat with sources, inline faithfulness badges, eval dashboard with bar chart.
709
 
 
731
  - [ ] 7.4 Verify live URL is accessible and functional
732
  - [ ] 7.5 Write `README.md` (problem, architecture, demo GIF, eval results, setup steps)
733
 
734
+ **Milestone:** App is live on Render, README is complete, project is portfolio-ready.
735
 
736
  ---
737
 
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
frontend/eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs.flat.recommended,
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ ecmaVersion: 2020,
18
+ globals: globals.browser,
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ ecmaFeatures: { jsx: true },
22
+ sourceType: 'module',
23
+ },
24
+ },
25
+ rules: {
26
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27
+ },
28
+ },
29
+ ])
frontend/index.html ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect x='4' y='18' width='6' height='10' rx='1' fill='%234f46e5'/><rect x='13' y='12' width='6' height='16' rx='1' fill='%236366f1'/><rect x='22' y='6' width='6' height='22' rx='1' fill='%234f46e5'/></svg>" type="image/svg+xml" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <meta name="description" content="FinRAG - Fintech Research Agent with RAG and retrieval evaluation" />
8
+ <title>FinRAG</title>
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ <script type="module" src="/src/main.jsx"></script>
13
+ </body>
14
+ </html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "axios": "^1.14.0",
14
+ "react": "^19.2.4",
15
+ "react-dom": "^19.2.4"
16
+ },
17
+ "devDependencies": {
18
+ "@eslint/js": "^9.39.4",
19
+ "@tailwindcss/vite": "^4.2.2",
20
+ "@types/react": "^19.2.14",
21
+ "@types/react-dom": "^19.2.3",
22
+ "@vitejs/plugin-react": "^6.0.1",
23
+ "eslint": "^9.39.4",
24
+ "eslint-plugin-react-hooks": "^7.0.1",
25
+ "eslint-plugin-react-refresh": "^0.5.2",
26
+ "globals": "^17.4.0",
27
+ "tailwindcss": "^4.2.2",
28
+ "vite": "^8.0.1"
29
+ }
30
+ }
frontend/public/favicon.svg ADDED
frontend/public/icons.svg ADDED
frontend/src/App.jsx ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from "react";
2
+ import { getDocuments, clearMemory } from "./api";
3
+ import Sidebar from "./components/Sidebar";
4
+ import ChatArea from "./components/ChatArea";
5
+
6
+ function App() {
7
+ const [documents, setDocuments] = useState([]);
8
+ const [evalLog, setEvalLog] = useState([]);
9
+
10
+ useEffect(() => {
11
+ getDocuments().then((d) => setDocuments(d.documents || []));
12
+ }, []);
13
+
14
+ async function handleNewConversation() {
15
+ await clearMemory();
16
+ setEvalLog([]);
17
+ window.location.reload();
18
+ }
19
+
20
+ return (
21
+ <div className="min-h-screen bg-gray-50 flex flex-col">
22
+ {/* Header */}
23
+ <header className="bg-white px-6 py-4 shrink-0 shadow-sm">
24
+ <div className="flex items-center gap-3">
25
+ <div className="w-1 h-8 bg-indigo-600 rounded-full" />
26
+ <div>
27
+ <h1 className="text-lg font-semibold text-gray-900 leading-tight">
28
+ FinRAG
29
+ </h1>
30
+ <p className="text-xs text-gray-400">Fintech Research Agent</p>
31
+ </div>
32
+ </div>
33
+ </header>
34
+
35
+ {/* Main */}
36
+ <main className="flex-1 flex">
37
+ <Sidebar
38
+ documents={documents}
39
+ setDocuments={setDocuments}
40
+ onNewConversation={handleNewConversation}
41
+ evalLog={evalLog}
42
+ />
43
+ <ChatArea
44
+ onEvalEntry={(entry) => setEvalLog((prev) => [...prev, entry])}
45
+ hasDocuments={documents.length > 0}
46
+ />
47
+ </main>
48
+ </div>
49
+ );
50
+ }
51
+
52
+ export default App;
frontend/src/api.js ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from "axios";
2
+
3
+ const api = axios.create({
4
+ baseURL: "/api",
5
+ });
6
+
7
+ export async function sendMessage(question) {
8
+ const { data } = await api.post("/chat", { question });
9
+ return data;
10
+ }
11
+
12
+ export async function clearMemory() {
13
+ const { data } = await api.delete("/chat/memory");
14
+ return data;
15
+ }
16
+
17
+ export async function getSessionEvalLog() {
18
+ const { data } = await api.get("/eval/session");
19
+ return data;
20
+ }
21
+
22
+ export async function runPrecisionEval() {
23
+ const { data } = await api.post("/eval/precision");
24
+ return data;
25
+ }
26
+
27
+ export async function uploadFiles(fileList) {
28
+ const formData = new FormData();
29
+ for (const file of fileList) {
30
+ formData.append("files", file);
31
+ }
32
+ const { data } = await api.post("/upload", formData, {
33
+ headers: { "Content-Type": "multipart/form-data" },
34
+ timeout: 300000,
35
+ });
36
+ return data;
37
+ }
38
+
39
+ export async function getDocuments() {
40
+ const { data } = await api.get("/documents");
41
+ return data;
42
+ }
frontend/src/assets/hero.png ADDED
frontend/src/assets/vite.svg ADDED
frontend/src/components/ChatArea.jsx ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { sendMessage } from "../api";
3
+ import MessageBubble from "./MessageBubble";
4
+
5
+ export default function ChatArea({ onEvalEntry, hasDocuments }) {
6
+ const [messages, setMessages] = useState([]);
7
+ const [input, setInput] = useState("");
8
+ const [loading, setLoading] = useState(false);
9
+
10
+ async function handleSend(e) {
11
+ e.preventDefault();
12
+ if (!input.trim() || loading || !hasDocuments) return;
13
+
14
+ const question = input.trim();
15
+ setInput("");
16
+ setMessages((prev) => [...prev, { role: "user", content: question }]);
17
+ setLoading(true);
18
+
19
+ try {
20
+ const data = await sendMessage(question);
21
+ setMessages((prev) => [
22
+ ...prev,
23
+ {
24
+ role: "assistant",
25
+ content: data.answer,
26
+ sources: data.sources,
27
+ faithfulness: data.faithfulness,
28
+ },
29
+ ]);
30
+ onEvalEntry({
31
+ query: question,
32
+ answer: data.answer,
33
+ faithfulness_score: data.faithfulness.score,
34
+ reason: data.faithfulness.reason,
35
+ });
36
+ } catch (err) {
37
+ const detail = err.response?.data?.detail || err.message;
38
+ setMessages((prev) => [
39
+ ...prev,
40
+ { role: "assistant", content: "Error: " + detail },
41
+ ]);
42
+ } finally {
43
+ setLoading(false);
44
+ }
45
+ }
46
+
47
+ return (
48
+ <div className="flex-1 flex flex-col bg-gray-50">
49
+ {/* Messages */}
50
+ <div className="flex-1 overflow-y-auto p-6 space-y-4">
51
+ {messages.length === 0 && (
52
+ <div className="flex flex-col items-center justify-center h-full text-center">
53
+ <svg
54
+ className="w-16 h-16 text-indigo-200 mb-4"
55
+ fill="none"
56
+ stroke="currentColor"
57
+ viewBox="0 0 24 24"
58
+ >
59
+ <path
60
+ strokeLinecap="round"
61
+ strokeLinejoin="round"
62
+ strokeWidth="1"
63
+ d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
64
+ />
65
+ </svg>
66
+ <p className="text-lg font-medium text-gray-500 mb-1">
67
+ {hasDocuments
68
+ ? "Ask a question about your documents"
69
+ : "Upload documents to get started"}
70
+ </p>
71
+ <p className="text-sm text-gray-400">
72
+ {hasDocuments
73
+ ? "FinRAG will find answers and cite sources"
74
+ : "Drop PDF, TXT, or CSV files in the sidebar"}
75
+ </p>
76
+ </div>
77
+ )}
78
+ {messages.map((msg, i) => (
79
+ <MessageBubble key={i} message={msg} />
80
+ ))}
81
+ {loading && (
82
+ <div className="flex items-center gap-2 text-indigo-400 text-sm">
83
+ <svg
84
+ className="w-4 h-4 animate-spin"
85
+ fill="none"
86
+ viewBox="0 0 24 24"
87
+ >
88
+ <circle
89
+ className="opacity-25"
90
+ cx="12"
91
+ cy="12"
92
+ r="10"
93
+ stroke="currentColor"
94
+ strokeWidth="4"
95
+ />
96
+ <path
97
+ className="opacity-75"
98
+ fill="currentColor"
99
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
100
+ />
101
+ </svg>
102
+ <span>Thinking...</span>
103
+ </div>
104
+ )}
105
+ </div>
106
+
107
+ {/* Input */}
108
+ <form onSubmit={handleSend} className="p-4 bg-white border-t border-gray-100">
109
+ <div className="flex gap-3 max-w-4xl mx-auto">
110
+ <input
111
+ type="text"
112
+ value={input}
113
+ onChange={(e) => setInput(e.target.value)}
114
+ placeholder={
115
+ hasDocuments
116
+ ? "Ask about your documents..."
117
+ : "Upload documents first to start chatting"
118
+ }
119
+ className="flex-1 px-4 py-3 border border-gray-200 rounded-xl text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent disabled:bg-gray-50 disabled:text-gray-400 transition-shadow"
120
+ disabled={loading || !hasDocuments}
121
+ />
122
+ <button
123
+ type="submit"
124
+ disabled={loading || !input.trim() || !hasDocuments}
125
+ className="px-6 py-3 bg-indigo-600 text-white rounded-xl text-sm font-medium hover:bg-indigo-700 disabled:bg-indigo-200 disabled:cursor-not-allowed transition-colors shadow-sm"
126
+ >
127
+ Send
128
+ </button>
129
+ </div>
130
+ </form>
131
+ </div>
132
+ );
133
+ }
frontend/src/components/FileUpload.jsx ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef } from "react";
2
+ import { uploadFiles } from "../api";
3
+
4
+ export default function FileUpload({ onUploadComplete }) {
5
+ const [uploading, setUploading] = useState(false);
6
+ const [dragOver, setDragOver] = useState(false);
7
+ const [error, setError] = useState(null);
8
+ const fileInputRef = useRef(null);
9
+
10
+ async function handleFiles(files) {
11
+ if (!files.length) return;
12
+ setUploading(true);
13
+ setError(null);
14
+ try {
15
+ const data = await uploadFiles(files);
16
+ onUploadComplete(data.documents);
17
+ } catch (err) {
18
+ setError(err.response?.data?.detail || "Upload failed");
19
+ } finally {
20
+ setUploading(false);
21
+ }
22
+ }
23
+
24
+ function handleDrop(e) {
25
+ e.preventDefault();
26
+ setDragOver(false);
27
+ handleFiles(Array.from(e.dataTransfer.files));
28
+ }
29
+
30
+ return (
31
+ <div>
32
+ <div
33
+ onClick={() => fileInputRef.current?.click()}
34
+ onDrop={handleDrop}
35
+ onDragOver={(e) => {
36
+ e.preventDefault();
37
+ setDragOver(true);
38
+ }}
39
+ onDragLeave={() => setDragOver(false)}
40
+ className={`border-2 border-dashed rounded-xl p-5 text-center cursor-pointer transition-all ${
41
+ dragOver
42
+ ? "border-indigo-500 bg-indigo-50"
43
+ : "border-gray-200 hover:border-indigo-300 hover:bg-indigo-50/50"
44
+ }`}
45
+ >
46
+ {uploading ? (
47
+ <div className="flex flex-col items-center gap-2">
48
+ <svg
49
+ className="w-6 h-6 text-indigo-500 animate-spin"
50
+ fill="none"
51
+ viewBox="0 0 24 24"
52
+ >
53
+ <circle
54
+ className="opacity-25"
55
+ cx="12"
56
+ cy="12"
57
+ r="10"
58
+ stroke="currentColor"
59
+ strokeWidth="4"
60
+ />
61
+ <path
62
+ className="opacity-75"
63
+ fill="currentColor"
64
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
65
+ />
66
+ </svg>
67
+ <p className="text-sm text-indigo-600 font-medium">
68
+ Processing...
69
+ </p>
70
+ </div>
71
+ ) : (
72
+ <>
73
+ <svg
74
+ className="w-8 h-8 text-indigo-400 mx-auto mb-2"
75
+ fill="none"
76
+ stroke="currentColor"
77
+ viewBox="0 0 24 24"
78
+ >
79
+ <path
80
+ strokeLinecap="round"
81
+ strokeLinejoin="round"
82
+ strokeWidth="1.5"
83
+ d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
84
+ />
85
+ </svg>
86
+ <p className="text-sm text-gray-600">Drop files here</p>
87
+ <p className="text-xs text-gray-400 mt-1">PDF, TXT, or CSV</p>
88
+ </>
89
+ )}
90
+ </div>
91
+ <input
92
+ ref={fileInputRef}
93
+ type="file"
94
+ multiple
95
+ accept=".pdf,.txt,.csv"
96
+ className="hidden"
97
+ onChange={(e) => handleFiles(Array.from(e.target.files))}
98
+ />
99
+ {error && <p className="text-xs text-red-500 mt-2">{error}</p>}
100
+ </div>
101
+ );
102
+ }
frontend/src/components/MessageBubble.jsx ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import SourceExpander from "./SourceExpander";
2
+
3
+ function FaithfulnessBadge({ faithfulness }) {
4
+ if (!faithfulness || faithfulness.score < 0) {
5
+ return (
6
+ <span className="inline-flex items-center gap-1.5 text-xs text-gray-400 bg-gray-100 px-2.5 py-1 rounded-full">
7
+ <span className="w-1.5 h-1.5 rounded-full bg-gray-400" />
8
+ Eval failed
9
+ </span>
10
+ );
11
+ }
12
+
13
+ const { score, reason } = faithfulness;
14
+
15
+ let dotColor, bgColor, textColor, label;
16
+ if (score >= 4) {
17
+ dotColor = "bg-green-500";
18
+ bgColor = "bg-green-50";
19
+ textColor = "text-green-700";
20
+ label = "Faithful";
21
+ } else if (score === 3) {
22
+ dotColor = "bg-yellow-500";
23
+ bgColor = "bg-yellow-50";
24
+ textColor = "text-yellow-700";
25
+ label = "Moderate";
26
+ } else {
27
+ dotColor = "bg-red-500";
28
+ bgColor = "bg-red-50";
29
+ textColor = "text-red-700";
30
+ label = "Low";
31
+ }
32
+
33
+ return (
34
+ <span
35
+ className={`inline-flex items-center gap-1.5 text-xs font-medium px-2.5 py-1 rounded-full ${bgColor} ${textColor}`}
36
+ title={reason}
37
+ >
38
+ <span className={`w-1.5 h-1.5 rounded-full ${dotColor}`} />
39
+ {label} ({score}/5)
40
+ </span>
41
+ );
42
+ }
43
+
44
+ export default function MessageBubble({ message }) {
45
+ const isUser = message.role === "user";
46
+
47
+ return (
48
+ <div className={`flex ${isUser ? "justify-end" : "justify-start"}`}>
49
+ <div
50
+ className={`max-w-2xl rounded-2xl px-5 py-3.5 ${
51
+ isUser
52
+ ? "bg-indigo-600 text-white"
53
+ : "bg-white text-gray-800 shadow-sm"
54
+ }`}
55
+ >
56
+ <p className="text-sm whitespace-pre-wrap leading-relaxed">
57
+ {message.content}
58
+ </p>
59
+
60
+ {!isUser && message.faithfulness && (
61
+ <div className="mt-3">
62
+ <FaithfulnessBadge faithfulness={message.faithfulness} />
63
+ </div>
64
+ )}
65
+
66
+ {!isUser && message.sources && (
67
+ <SourceExpander sources={message.sources} />
68
+ )}
69
+ </div>
70
+ </div>
71
+ );
72
+ }
frontend/src/components/Sidebar.jsx ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import FileUpload from "./FileUpload";
2
+
3
+ function TrafficLight({ faithfulnessMean }) {
4
+ let color, label;
5
+
6
+ if (faithfulnessMean === null) {
7
+ color = "bg-gray-300";
8
+ label = "No data";
9
+ } else {
10
+ const fNorm = faithfulnessMean / 5;
11
+ if (fNorm < 0.5) {
12
+ color = "bg-red-500";
13
+ label = "Poor";
14
+ } else if (fNorm < 0.7) {
15
+ color = "bg-yellow-500";
16
+ label = "Moderate";
17
+ } else {
18
+ color = "bg-green-500";
19
+ label = "Healthy";
20
+ }
21
+ }
22
+
23
+ return (
24
+ <div className="flex items-center gap-2">
25
+ <div className={`w-3 h-3 rounded-full ${color} ring-2 ring-offset-1 ring-${color === "bg-gray-300" ? "gray-200" : color.replace("bg-", "")}/30`} />
26
+ <span className="text-sm font-medium text-gray-700">{label}</span>
27
+ </div>
28
+ );
29
+ }
30
+
31
+ export default function Sidebar({
32
+ documents,
33
+ setDocuments,
34
+ onNewConversation,
35
+ evalLog,
36
+ }) {
37
+ const faithfulnessScores = evalLog.filter((e) => e.faithfulness_score > 0);
38
+ const faithfulnessMean =
39
+ faithfulnessScores.length > 0
40
+ ? faithfulnessScores.reduce((s, e) => s + e.faithfulness_score, 0) /
41
+ faithfulnessScores.length
42
+ : null;
43
+
44
+ return (
45
+ <aside className="w-80 bg-white border-r border-gray-100 flex flex-col shrink-0 h-[calc(100vh-65px)] overflow-y-auto">
46
+ <div className="p-4 space-y-4">
47
+ {/* New Conversation */}
48
+ <button
49
+ onClick={onNewConversation}
50
+ className="w-full px-4 py-2.5 bg-indigo-600 text-white rounded-xl text-sm font-medium hover:bg-indigo-700 transition-colors shadow-sm"
51
+ >
52
+ New Conversation
53
+ </button>
54
+
55
+ {/* File Upload */}
56
+ <div className="bg-gray-50/80 rounded-xl p-3.5">
57
+ <h3 className="text-xs font-semibold text-indigo-500 uppercase tracking-wider mb-2.5">
58
+ Upload Documents
59
+ </h3>
60
+ <FileUpload onUploadComplete={setDocuments} />
61
+ </div>
62
+
63
+ {/* Uploaded Documents */}
64
+ <div className="bg-gray-50/80 rounded-xl p-3.5">
65
+ <h3 className="text-xs font-semibold text-indigo-500 uppercase tracking-wider mb-2.5">
66
+ Documents ({documents.length})
67
+ </h3>
68
+ {documents.length === 0 ? (
69
+ <p className="text-sm text-gray-400">No documents yet</p>
70
+ ) : (
71
+ <ul className="space-y-1.5">
72
+ {documents.map((doc, i) => (
73
+ <li
74
+ key={i}
75
+ className="flex items-center justify-between text-sm bg-white rounded-lg px-3 py-2 shadow-xs"
76
+ >
77
+ <span className="text-gray-700 truncate">{doc.name}</span>
78
+ <span className="text-indigo-400 shrink-0 ml-2 text-xs font-medium">
79
+ {doc.chunk_count}
80
+ </span>
81
+ </li>
82
+ ))}
83
+ </ul>
84
+ )}
85
+ </div>
86
+
87
+ {/* Faithfulness Log */}
88
+ <div className="bg-gray-50/80 rounded-xl p-3.5">
89
+ <h3 className="text-xs font-semibold text-indigo-500 uppercase tracking-wider mb-2.5">
90
+ Faithfulness ({evalLog.length} queries)
91
+ </h3>
92
+ {evalLog.length > 0 ? (
93
+ <div className="flex gap-1.5 flex-wrap">
94
+ {evalLog.slice(-10).map((e, i) => (
95
+ <span
96
+ key={i}
97
+ title={e.reason}
98
+ className={`inline-flex items-center justify-center w-8 h-8 rounded-full text-xs font-bold text-white shadow-sm ${
99
+ e.faithfulness_score >= 4
100
+ ? "bg-green-500"
101
+ : e.faithfulness_score === 3
102
+ ? "bg-yellow-500"
103
+ : e.faithfulness_score > 0
104
+ ? "bg-red-500"
105
+ : "bg-gray-300"
106
+ }`}
107
+ >
108
+ {e.faithfulness_score > 0 ? e.faithfulness_score : "?"}
109
+ </span>
110
+ ))}
111
+ </div>
112
+ ) : (
113
+ <p className="text-sm text-gray-400">No queries yet</p>
114
+ )}
115
+ </div>
116
+
117
+ {/* Retrieval Health */}
118
+ <div className="bg-gray-50/80 rounded-xl p-3.5">
119
+ <h3 className="text-xs font-semibold text-indigo-500 uppercase tracking-wider mb-2.5">
120
+ Retrieval Health
121
+ </h3>
122
+ <div className="flex items-center justify-between">
123
+ <div>
124
+ <p className="text-xs text-gray-400">Mean Faithfulness</p>
125
+ <p className="text-lg font-semibold text-gray-800">
126
+ {faithfulnessMean !== null
127
+ ? `${faithfulnessMean.toFixed(1)}/5`
128
+ : "--"}
129
+ </p>
130
+ </div>
131
+ <TrafficLight faithfulnessMean={faithfulnessMean} />
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </aside>
136
+ );
137
+ }
frontend/src/components/SourceExpander.jsx ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+
3
+ export default function SourceExpander({ sources }) {
4
+ const [open, setOpen] = useState(false);
5
+
6
+ if (!sources || sources.length === 0) return null;
7
+
8
+ return (
9
+ <div className="mt-3">
10
+ <button
11
+ onClick={() => setOpen(!open)}
12
+ className="text-xs text-indigo-500 hover:text-indigo-700 flex items-center gap-1.5 font-medium transition-colors"
13
+ >
14
+ <span>
15
+ {open ? "Hide" : "Show"} sources ({sources.length})
16
+ </span>
17
+ <svg
18
+ className={`w-3.5 h-3.5 transition-transform ${
19
+ open ? "rotate-180" : ""
20
+ }`}
21
+ fill="none"
22
+ stroke="currentColor"
23
+ viewBox="0 0 24 24"
24
+ >
25
+ <path
26
+ strokeLinecap="round"
27
+ strokeLinejoin="round"
28
+ strokeWidth="2"
29
+ d="M19 9l-7 7-7-7"
30
+ />
31
+ </svg>
32
+ </button>
33
+
34
+ {open && (
35
+ <div className="mt-2.5 space-y-2">
36
+ {sources.map((src, i) => (
37
+ <div
38
+ key={i}
39
+ className="bg-white border-l-2 border-indigo-400 rounded-r-lg shadow-xs pl-3 pr-3 py-2.5"
40
+ >
41
+ <div className="flex items-center justify-between mb-1.5">
42
+ <span className="text-xs font-semibold text-indigo-700">
43
+ {src.source}
44
+ </span>
45
+ <div className="flex gap-2">
46
+ {src.page != null && (
47
+ <span className="text-[10px] text-gray-400 bg-gray-100 px-1.5 py-0.5 rounded">
48
+ Page {src.page + 1}
49
+ </span>
50
+ )}
51
+ {src.similarity_score != null && (
52
+ <span className="text-[10px] text-indigo-500 bg-indigo-50 px-1.5 py-0.5 rounded font-medium">
53
+ {src.similarity_score}
54
+ </span>
55
+ )}
56
+ </div>
57
+ </div>
58
+ <p className="text-xs text-gray-500 leading-relaxed">
59
+ {src.content.length > 200
60
+ ? src.content.slice(0, 200) + "..."
61
+ : src.content}
62
+ </p>
63
+ </div>
64
+ ))}
65
+ </div>
66
+ )}
67
+ </div>
68
+ );
69
+ }
frontend/src/index.css ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
2
+ @import "tailwindcss";
3
+
4
+ @theme {
5
+ --font-sans: "Inter", system-ui, sans-serif;
6
+ }
7
+
8
+ body {
9
+ font-family: var(--font-sans);
10
+ -webkit-font-smoothing: antialiased;
11
+ -moz-osx-font-smoothing: grayscale;
12
+ }
frontend/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.jsx'
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
frontend/vite.config.js ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import tailwindcss from '@tailwindcss/vite'
4
+
5
+ export default defineConfig({
6
+ plugins: [react(), tailwindcss()],
7
+ server: {
8
+ proxy: {
9
+ '/api': 'http://localhost:8000',
10
+ },
11
+ },
12
+ })
render.yaml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ - type: web
3
+ name: finrag
4
+ runtime: docker
5
+ plan: free
6
+ envVars:
7
+ - key: EURON_API_KEY
8
+ sync: false
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ openai>=1.0.0
2
+ langchain>=0.1.0
3
+ langchain-openai>=0.1.0
4
+ langchain-community>=0.0.20
5
+ langchain-chroma>=0.1.0
6
+ chromadb>=0.4.0
7
+ sentence-transformers>=2.2.0
8
+ pypdf>=3.0.0
9
+ fastapi>=0.110.0
10
+ uvicorn>=0.27.0
11
+ python-multipart>=0.0.6
12
+ pyyaml>=6.0
13
+ python-dotenv>=1.0.0
14
+ pytest>=7.0.0
sample_data/bajaj_finance_q3_2024_transcript.txt ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ BAJAJ FINANCE LIMITED — Q3 FY2024 EARNINGS CALL TRANSCRIPT
2
+ Date: January 30, 2024
3
+ Participants: Rajeev Jain (MD & CEO), Sandeep Jain (CFO), Analysts
4
+
5
+ OPENING REMARKS — RAJEEV JAIN, MD & CEO
6
+
7
+ Good evening, everyone. Thank you for joining Bajaj Finance's Q3 FY2024 earnings call. I'm pleased to report another strong quarter for the company.
8
+
9
+ Let me begin with the key highlights for Q3 FY2024:
10
+
11
+ Assets Under Management (AUM) grew 35% year-on-year to Rs 3,10,672 crore as of December 31, 2023. This represents a sequential increase of Rs 15,363 crore from Q2 FY2024. We continue to be on track to achieve our medium-term AUM target of Rs 5,00,000 crore by March 2026.
12
+
13
+ New loans booked during Q3 FY2024 stood at 91.3 lakh accounts, a 24% increase over Q3 FY2023. The customer franchise expanded to 7.86 crore, adding 39.6 lakh new customers during the quarter. Cross-sell franchise reached 5.12 crore, representing 65% of total customers.
14
+
15
+ FINANCIAL PERFORMANCE
16
+
17
+ Net Interest Income (NII) for Q3 FY2024 was Rs 8,845 crore, up 28% year-on-year. Net interest margin (NIM) for the quarter stood at 11.61% compared to 11.52% in Q2 FY2024 and 11.48% in Q3 FY2023.
18
+
19
+ Pre-provision operating profit was Rs 6,244 crore, a growth of 31% year-on-year. Profit after tax for Q3 FY2024 was Rs 3,639 crore, a growth of 22% year-on-year. Earnings per share for the quarter was Rs 58.8.
20
+
21
+ Cost-to-income ratio improved to 33.4% from 34.1% in Q3 FY2023, driven by operating leverage and continued investments in technology.
22
+
23
+ ASSET QUALITY
24
+
25
+ Gross NPA stood at 0.95% as of December 2023, compared to 1.14% in December 2022. Net NPA was at 0.36%, improving from 0.44% a year ago. Provision coverage ratio remained healthy at 62%.
26
+
27
+ Loan loss and provision for Q3 FY2024 was Rs 1,729 crore. We have maintained a management overlay provision of Rs 1,230 crore for macroeconomic uncertainties. Total provision buffer stands at Rs 5,840 crore, which is approximately 1.88% of AUM.
28
+
29
+ The RBI's advisory on unsecured lending has had limited impact on our portfolio. We proactively tightened underwriting standards for personal loans and credit card portfolios in October 2023.
30
+
31
+ SEGMENT PERFORMANCE
32
+
33
+ Consumer B2C Business:
34
+ - AUM: Rs 1,42,500 crore (46% of total AUM)
35
+ - Products: Personal loans, consumer durable loans, lifestyle finance, digital product finance
36
+ - Growth: 38% YoY
37
+
38
+ SME and Commercial Lending:
39
+ - AUM: Rs 58,300 crore (19% of total AUM)
40
+ - New SME accounts: 2.8 lakh in Q3
41
+ - Average ticket size: Rs 18.4 lakh
42
+ - Growth: 29% YoY
43
+
44
+ Rural Lending:
45
+ - AUM: Rs 22,800 crore (7% of total AUM)
46
+ - Presence: 1,140 rural locations
47
+ - Growth: 45% YoY
48
+
49
+ Mortgages (Bajaj Housing Finance):
50
+ - AUM: Rs 72,400 crore (23% of total AUM)
51
+ - New home loans: Rs 8,200 crore disbursed in Q3
52
+ - NPA: 0.28%
53
+
54
+ TECHNOLOGY AND DIGITAL INITIATIVES
55
+
56
+ Technology spend for 9M FY2024 was Rs 1,180 crore, approximately 12% of operating expenses. 72% of new personal loans originated through the app. Average loan disbursement time reduced to 14 seconds for pre-approved customers.
57
+
58
+ CAPITAL AND LIQUIDITY
59
+
60
+ Capital adequacy ratio (CRAR) stood at 23.8%, well above the regulatory requirement of 15%. Tier-I capital was 22.1%. Cost of funds for Q3 FY2024 was 7.82%.
61
+
62
+ GUIDANCE AND OUTLOOK
63
+
64
+ For FY2024 guidance: AUM growth 32-34%, new loans 35-37 million accounts, profit after tax growth 20-22%, GNPA below 1.1%, return on equity 21-23%.
65
+
66
+ For FY2025: AUM to cross Rs 4,00,000 crore, Bajaj Housing Finance IPO targeted for H1 FY2025.
sample_data/npci_upi_report_2024.txt ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ NATIONAL PAYMENTS CORPORATION OF INDIA — UPI ECOSYSTEM REPORT FY2024
2
+
3
+ EXECUTIVE SUMMARY
4
+
5
+ The Unified Payments Interface (UPI) completed another year of record-breaking growth in FY2024. Total UPI transaction volume reached 14.04 billion transactions per month by March 2024, with annual aggregate volume crossing 131 billion transactions valued at approximately Rs 200 lakh crore. UPI has firmly established itself as India's primary digital payment rail, processing more transactions than all other digital payment modes combined.
6
+
7
+ SECTION 1: UPI TRANSACTION STATISTICS
8
+
9
+ Monthly Transaction Trends:
10
+ - April 2023: 8.89 billion transactions, Rs 14.07 lakh crore value
11
+ - July 2023: 9.96 billion transactions, Rs 15.34 lakh crore value
12
+ - October 2023: 11.41 billion transactions, Rs 17.16 lakh crore value
13
+ - January 2024: 12.20 billion transactions, Rs 18.41 lakh crore value
14
+ - March 2024: 14.04 billion transactions, Rs 19.78 lakh crore value
15
+
16
+ Year-on-year growth: 56% in volume, 43% in value compared to FY2023.
17
+
18
+ Average ticket size decreased from Rs 1,730 in FY2023 to Rs 1,524 in FY2024, indicating increased adoption for small-value everyday transactions including transit, vending machines, and micropayments.
19
+
20
+ SECTION 2: UPI PARTICIPANT ECOSYSTEM
21
+
22
+ Active UPI handles crossed 400 million by March 2024, with approximately 300 million unique users transacting at least once per month. The merchant ecosystem expanded significantly:
23
+
24
+ - Registered UPI merchants: 3.2 crore (32 million)
25
+ - QR code deployments: 28 crore across India
26
+ - Third-party app providers (TPAPs): 57 approved apps
27
+ - Market share by volume: PhonePe (47%), Google Pay (34%), Paytm (14%), Others (5%)
28
+
29
+ Banking Infrastructure:
30
+ - 580 banks live on UPI as of March 2024 (up from 437 in March 2023)
31
+ - PSU banks accounted for 35% of UPI originating transactions
32
+ - Private banks accounted for 42% of UPI originating transactions
33
+ - Small Finance Banks and Payment Banks accounted for 23%
34
+
35
+ SECTION 3: UPI PRODUCT INNOVATIONS
36
+
37
+ UPI Lite:
38
+ UPI Lite was launched to enable small-value transactions (up to Rs 500) with near-zero decline rates. As of March 2024, UPI Lite had 5.2 crore enabled users processing 18 crore transactions monthly. UPI Lite X, the offline variant, was piloted in 12 cities.
39
+
40
+ UPI AutoPay:
41
+ Recurring mandates on UPI grew to 12.8 crore active mandates, commonly used for OTT subscriptions (38%), utility bills (28%), insurance premiums (18%), and mutual fund SIPs (16%).
42
+
43
+ Credit Line on UPI:
44
+ RBI permitted banks to offer pre-approved credit lines via UPI. By March 2024, 8 banks had launched credit-on-UPI products, disbursing Rs 4,200 crore in cumulative credit.
45
+
46
+ UPI International:
47
+ UPI acceptance was enabled in 7 countries: Singapore, UAE, France, Sri Lanka, Mauritius, Nepal, and Bhutan. Inbound UPI usage by foreign nationals was piloted during the G20 summit. Total cross-border UPI transactions reached 1.4 crore in FY2024.
48
+
49
+ SECTION 4: FRAUD AND RISK MANAGEMENT
50
+
51
+ UPI fraud rate remained low at 0.0006% of total transactions by volume. NPCI's Central Fraud Registry flagged 2.1 lakh suspicious accounts during FY2024. Key fraud mitigation measures included:
52
+
53
+ - Device binding and SIM verification for new UPI registrations
54
+ - AI-based transaction monitoring detecting anomalous patterns in real-time
55
+ - Cool-off period of 4 hours for first-time transfers exceeding Rs 2,000 to new beneficiaries
56
+ - Collaboration with telecom operators for SIM swap fraud detection
57
+
58
+ Total reported UPI fraud cases: 3.07 lakh in FY2024 (up from 2.22 lakh in FY2023), with a combined value of Rs 1,087 crore. The increase is attributed to higher transaction volumes and improved reporting mechanisms.
59
+
60
+ SECTION 5: BHARAT BILL PAYMENT SYSTEM (BBPS)
61
+
62
+ BBPS processed 1,243 crore transactions valued at Rs 15.4 lakh crore during FY2024, a 42% growth over FY2023. Biller categories expanded to include:
63
+ - Electricity and water utilities (largest category at 41%)
64
+ - Telecom and DTH (22%)
65
+ - Insurance premiums (14%)
66
+ - Loan EMI payments (12%)
67
+ - Municipal taxes and fees (6%)
68
+ - Education fees (5%)
69
+
70
+ SECTION 6: IMPS AND OTHER NPCI PRODUCTS
71
+
72
+ Immediate Payment Service (IMPS) processed 588 crore transactions worth Rs 69.4 lakh crore in FY2024. While IMPS growth has moderated (12% YoY) due to UPI substitution, it remains important for bank-to-bank high-value transfers.
73
+
74
+ RuPay card transactions grew 34% to reach 435 crore transactions in FY2024. RuPay's domestic debit card market share stood at 60%. RuPay credit cards on UPI were used in 8.4 crore transactions monthly by March 2024.
75
+
76
+ NETC FASTag processed 384 crore toll transactions valued at Rs 58,200 crore, covering 99% of national highway toll plazas.
77
+
78
+ SECTION 7: OUTLOOK AND STRATEGY
79
+
80
+ NPCI targets 100 billion monthly UPI transactions by 2028. Strategic priorities include:
81
+ - Expanding UPI to 20 countries for cross-border payments
82
+ - UPI Lite adoption target of 50 crore users
83
+ - Enabling UPI for capital markets (IPO, mutual funds, bonds)
84
+ - IoT-based payments integration for smart devices
85
+ - Carbon footprint tracking for UPI transactions
sample_data/rbi_annual_report_2024.txt ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ RESERVE BANK OF INDIA — ANNUAL REPORT 2023-24
2
+
3
+ CHAPTER 1: ASSESSMENT OF THE ECONOMY
4
+
5
+ India's real GDP growth for FY2024 is estimated at 7.6 per cent, driven by strong domestic demand and resilient services activity. The manufacturing sector showed signs of recovery, while agriculture growth moderated due to uneven monsoon distribution.
6
+
7
+ Inflation, as measured by the Consumer Price Index (CPI), averaged 5.4 per cent during FY2024, remaining within the RBI's target band of 2-6 per cent. Core inflation (excluding food and fuel) moderated to 4.2 per cent by March 2024, reflecting the impact of monetary policy tightening.
8
+
9
+ The current account deficit narrowed to 1.2 per cent of GDP in FY2024, supported by robust services exports and steady remittance inflows. Foreign exchange reserves stood at USD 645.6 billion as of March 2024.
10
+
11
+ CHAPTER 2: MONETARY POLICY OPERATIONS
12
+
13
+ The Monetary Policy Committee (MPC) maintained the policy repo rate at 6.50 per cent through FY2024, after cumulative hikes of 250 basis points since May 2022. The stance remained focused on withdrawal of accommodation to ensure inflation aligns with the 4 per cent target on a durable basis.
14
+
15
+ Liquidity conditions transitioned from surplus to deficit during the year. The RBI conducted variable rate repo (VRR) auctions and variable rate reverse repo (VRRR) auctions to manage liquidity. The weighted average call rate (WACR) remained closely aligned with the policy repo rate.
16
+
17
+ CHAPTER 3: FINANCIAL REGULATION AND SUPERVISION
18
+
19
+ The RBI strengthened its regulatory framework for Non-Banking Financial Companies (NBFCs). Key regulatory actions during FY2024 included:
20
+
21
+ - Scale-Based Regulation (SBR) framework implementation for NBFCs, classifying them into four layers: Base, Middle, Upper, and Top.
22
+ - Enhanced guidelines on digital lending, requiring all digital loans to be disbursed and repaid through borrower bank accounts. Lending Service Providers (LSPs) and Digital Lending Apps (DLAs) must comply with disclosure requirements.
23
+ - Revised guidelines on Fair Practices Code for NBFCs, emphasizing transparency in loan pricing and customer communication.
24
+ - Risk-based supervision adopted for systemically important NBFCs with asset size above Rs 1,000 crore.
25
+
26
+ The RBI also issued guidelines on climate-related financial risk management for regulated entities, requiring banks and NBFCs to integrate climate risk into their governance and risk management frameworks.
27
+
28
+ CHAPTER 4: PAYMENT AND SETTLEMENT SYSTEMS
29
+
30
+ The digital payments ecosystem in India continued its rapid expansion during FY2024. Total digital payment transactions grew by 44 per cent to reach 16,416 crore transactions valued at Rs 2,210 lakh crore.
31
+
32
+ Unified Payments Interface (UPI) remained the dominant payment mode, processing 11,761 crore transactions worth Rs 199.9 lakh crore during FY2024. This represents year-on-year growth of 56 per cent in volume and 43 per cent in value.
33
+
34
+ The RBI introduced the following initiatives to strengthen the payments infrastructure:
35
+ - UPI for secondary market investments, enabling retail investors to block funds in their bank accounts for IPO applications.
36
+ - Conversational payments on UPI, allowing users to initiate transactions through AI-powered conversational interfaces.
37
+ - UPI Lite X for offline transactions up to Rs 500, enabling payments without internet connectivity.
38
+ - Introduction of UPI for inbound travellers from G20 nations using their home country mobile numbers.
39
+
40
+ RTGS and NEFT systems processed 33.2 crore and 3,259 crore transactions respectively during FY2024. The RBI continued to operate RTGS on a 24x7x365 basis.
41
+
42
+ CHAPTER 5: FINANCIAL INCLUSION AND DIGITAL FINANCE
43
+
44
+ The Pradhan Mantri Jan Dhan Yojana (PMJDY) accounts reached 51.5 crore as of March 2024, with total deposits of Rs 2.18 lakh crore. The average deposit per account increased to Rs 4,234.
45
+
46
+ The RBI's financial literacy initiatives reached 8.7 crore participants through Centre for Financial Literacy (CFL) programmes across 812 districts.
47
+
48
+ Digital Rupee (e-Rupee) pilot programmes continued with both retail (e₹-R) and wholesale (e₹-W) variants. The retail CBDC pilot was expanded to cover 50 cities with participation from 13 banks. As of March 2024, approximately 10 lakh retail e₹ wallets were active.
49
+
50
+ CHAPTER 6: BANKING SECTOR DEVELOPMENTS
51
+
52
+ The banking sector demonstrated improved financial health during FY2024:
53
+ - Gross Non-Performing Assets (GNPA) ratio declined to 3.2 per cent from 3.9 per cent in March 2023.
54
+ - Net NPA ratio fell to 0.8 per cent from 1.0 per cent.
55
+ - Capital to Risk-Weighted Assets Ratio (CRAR) of scheduled commercial banks stood at 16.8 per cent, well above the regulatory minimum of 9 per cent.
56
+ - Return on Assets (ROA) improved to 1.3 per cent and Return on Equity (ROE) to 13.8 per cent.
57
+
58
+ Credit growth was broad-based at 16.3 per cent year-on-year as of March 2024. Personal loans grew at 29.8 per cent, services sector at 20.2 per cent, and industry at 8.5 per cent. The RBI issued advisories cautioning banks about rapid growth in unsecured personal loans and credit card outstanding.
59
+
60
+ CHAPTER 7: FOREIGN EXCHANGE MANAGEMENT
61
+
62
+ The Indian rupee exhibited relative stability during FY2024, depreciating marginally by 1.4 per cent against the US dollar. The RBI's intervention in the foreign exchange market was aimed at curbing excessive volatility rather than targeting any specific level.
63
+
64
+ Foreign Direct Investment (FDI) inflows stood at USD 44.4 billion during FY2024. The services sector, computer software, and telecommunications attracted the highest share of FDI.
65
+
66
+ The RBI permitted international trade settlements in Indian rupees (INR) with 22 countries, promoting INR as a settlement currency for cross-border transactions.
scripts/benchmark_chunks.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Benchmark Precision@K across different chunk sizes.
3
+ Wipes ChromaDB, re-ingests at each chunk size, runs eval, plots results.
4
+
5
+ Usage:
6
+ python scripts/benchmark_chunks.py
7
+ python scripts/benchmark_chunks.py --sizes 200 300 500 750 1000
8
+ python scripts/benchmark_chunks.py --data-dir data/raw --k 5
9
+ """
10
+
11
+ import argparse
12
+ import shutil
13
+ import sys
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+
17
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
18
+
19
+ import matplotlib
20
+ matplotlib.use("Agg")
21
+ import matplotlib.pyplot as plt
22
+
23
+ from server.ingest import load_documents, chunk_documents, embed_and_store
24
+ from server.eval.precision import run_batch_precision_eval
25
+ from server.utils import load_config
26
+
27
+
28
+ CHROMA_DIR = Path("./chroma_db")
29
+
30
+
31
+ def run_at_chunk_size(data_dir: str, chunk_size: int, chunk_overlap: int, eval_path: str, k: int):
32
+ """Wipe ChromaDB, re-ingest at given chunk_size, run Precision@K."""
33
+ # Wipe
34
+ if CHROMA_DIR.exists():
35
+ shutil.rmtree(CHROMA_DIR)
36
+
37
+ # Ingest
38
+ documents = load_documents(data_dir)
39
+ chunks = chunk_documents(documents, chunk_size=chunk_size, chunk_overlap=chunk_overlap)
40
+ embed_and_store(chunks)
41
+
42
+ chunk_count = len(chunks)
43
+
44
+ # Eval
45
+ results = run_batch_precision_eval(eval_path, k=k)
46
+
47
+ return {
48
+ "chunk_size": chunk_size,
49
+ "chunk_count": chunk_count,
50
+ "mean_precision": results["mean_precision_at_k"],
51
+ "per_query": results["per_query_results"],
52
+ }
53
+
54
+
55
+ def plot_results(results: list[dict], output_path: str):
56
+ """Generate a PNG chart showing Precision@K vs chunk size."""
57
+ sizes = [r["chunk_size"] for r in results]
58
+ precisions = [r["mean_precision"] for r in results]
59
+ chunk_counts = [r["chunk_count"] for r in results]
60
+
61
+ fig, ax1 = plt.subplots(figsize=(10, 6))
62
+
63
+ # Precision bars
64
+ bars = ax1.bar(
65
+ [str(s) for s in sizes],
66
+ precisions,
67
+ color=["#22c55e" if p >= 0.7 else "#eab308" if p >= 0.5 else "#ef4444" for p in precisions],
68
+ edgecolor="white",
69
+ linewidth=1.5,
70
+ )
71
+ ax1.set_xlabel("Chunk Size (characters)", fontsize=12)
72
+ ax1.set_ylabel("Mean Precision@K", fontsize=12, color="#1f2937")
73
+ ax1.set_ylim(0, 1.05)
74
+ ax1.tick_params(axis="y", labelcolor="#1f2937")
75
+
76
+ # Add value labels on bars
77
+ for bar, p, cc in zip(bars, precisions, chunk_counts):
78
+ ax1.text(
79
+ bar.get_x() + bar.get_width() / 2,
80
+ bar.get_height() + 0.02,
81
+ f"{p:.2f}\n({cc} chunks)",
82
+ ha="center",
83
+ va="bottom",
84
+ fontsize=9,
85
+ fontweight="bold",
86
+ )
87
+
88
+ # Chunk count line on secondary axis
89
+ ax2 = ax1.twinx()
90
+ ax2.plot(
91
+ [str(s) for s in sizes],
92
+ chunk_counts,
93
+ color="#6366f1",
94
+ marker="o",
95
+ linewidth=2,
96
+ label="Chunk count",
97
+ )
98
+ ax2.set_ylabel("Total Chunks", fontsize=12, color="#6366f1")
99
+ ax2.tick_params(axis="y", labelcolor="#6366f1")
100
+
101
+ ax1.set_title("Precision@K vs Chunk Size — FinRAG Benchmark", fontsize=14, fontweight="bold", pad=15)
102
+ ax2.legend(loc="upper right")
103
+
104
+ plt.tight_layout()
105
+ plt.savefig(output_path, dpi=150, bbox_inches="tight")
106
+ plt.close()
107
+ print(f"\nChart saved to: {output_path}")
108
+
109
+
110
+ def main():
111
+ config = load_config()
112
+ eval_config = config.get("eval", {})
113
+ default_eval_path = eval_config.get("ground_truth_path", "data/ground_truth/eval_pairs.json")
114
+ default_k = eval_config.get("precision_k", 5)
115
+
116
+ parser = argparse.ArgumentParser(description="Benchmark Precision@K across chunk sizes")
117
+ parser.add_argument("--sizes", nargs="+", type=int, default=[200, 300, 500, 750, 1000],
118
+ help="Chunk sizes to test")
119
+ parser.add_argument("--overlap-ratio", type=float, default=0.1,
120
+ help="Overlap as fraction of chunk size (default 0.1)")
121
+ parser.add_argument("--data-dir", type=str, default="data/raw",
122
+ help="Directory containing documents")
123
+ parser.add_argument("--eval-path", type=str, default=default_eval_path,
124
+ help="Path to eval_pairs.json")
125
+ parser.add_argument("--k", type=int, default=default_k, help="K for Precision@K")
126
+ args = parser.parse_args()
127
+
128
+ print(f"Benchmarking chunk sizes: {args.sizes}")
129
+ print(f"Data: {args.data_dir} | Eval: {args.eval_path} | K={args.k}")
130
+ print("=" * 60)
131
+
132
+ results = []
133
+ for size in args.sizes:
134
+ overlap = int(size * args.overlap_ratio)
135
+ print(f"\n--- Chunk size: {size} (overlap: {overlap}) ---")
136
+ result = run_at_chunk_size(args.data_dir, size, overlap, args.eval_path, args.k)
137
+ results.append(result)
138
+ print(f" Chunks: {result['chunk_count']} | Mean P@{args.k}: {result['mean_precision']:.4f}")
139
+
140
+ # Summary table
141
+ print("\n" + "=" * 60)
142
+ print(f"{'Chunk Size':>12} | {'Chunks':>8} | {'Mean P@K':>10}")
143
+ print("-" * 40)
144
+ for r in results:
145
+ print(f"{r['chunk_size']:>12} | {r['chunk_count']:>8} | {r['mean_precision']:>10.4f}")
146
+
147
+ # Best
148
+ best = max(results, key=lambda r: r["mean_precision"])
149
+ print(f"\nBest: chunk_size={best['chunk_size']} with P@{args.k}={best['mean_precision']:.4f}")
150
+
151
+ # Save chart
152
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
153
+ output_path = f"benchmark_precision_{timestamp}.png"
154
+ plot_results(results, output_path)
155
+
156
+ # Restore original chunk size
157
+ original_size = config.get("chunking", {}).get("chunk_size", 500)
158
+ original_overlap = config.get("chunking", {}).get("chunk_overlap", 50)
159
+ print(f"\nRestoring original config: chunk_size={original_size}, overlap={original_overlap}")
160
+ if CHROMA_DIR.exists():
161
+ shutil.rmtree(CHROMA_DIR)
162
+ documents = load_documents(args.data_dir)
163
+ chunks = chunk_documents(documents, chunk_size=original_size, chunk_overlap=original_overlap)
164
+ embed_and_store(chunks)
165
+ print(f"ChromaDB restored with {len(chunks)} chunks")
166
+
167
+
168
+ if __name__ == "__main__":
169
+ main()
scripts/run_eval.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import json
3
+ import sys
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+
7
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
8
+
9
+ from server.eval.precision import run_batch_precision_eval
10
+ from server.utils import load_config
11
+
12
+
13
+ def main():
14
+ config = load_config()
15
+ default_path = config.get("eval", {}).get("ground_truth_path", "data/ground_truth/eval_pairs.json")
16
+ default_k = config.get("eval", {}).get("precision_k", 5)
17
+
18
+ parser = argparse.ArgumentParser(description="Run batch Precision@K evaluation")
19
+ parser.add_argument("--queries", type=str, default=default_path, help="Path to eval_pairs.json")
20
+ parser.add_argument("--k", type=int, default=default_k, help="K for Precision@K")
21
+ args = parser.parse_args()
22
+
23
+ print(f"Running Precision@{args.k} eval on queries from {args.queries}...")
24
+ results = run_batch_precision_eval(args.queries, k=args.k)
25
+
26
+ print(f"\nMean Precision@{args.k}: {results['mean_precision_at_k']}")
27
+ print(f"\nPer-query breakdown:")
28
+ for r in results["per_query_results"]:
29
+ print(f" P@{args.k}={r['precision_at_k']:.2f} | {r['query'][:70]}")
30
+
31
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
32
+ output_path = f"eval_results_{timestamp}.json"
33
+ with open(output_path, "w") as f:
34
+ json.dump(results, f, indent=2)
35
+ print(f"\nResults saved to: {output_path}")
36
+
37
+
38
+ if __name__ == "__main__":
39
+ main()
scripts/run_ingest.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import shutil
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ # Add project root to path
7
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
8
+
9
+ from server.ingest import run_ingestion_pipeline
10
+
11
+
12
+ def main():
13
+ parser = argparse.ArgumentParser(description="Ingest documents into ChromaDB")
14
+ parser.add_argument("--data-dir", type=str, default="data/raw", help="Directory containing documents")
15
+ parser.add_argument("--reset", action="store_true", help="Wipe ChromaDB and re-ingest from scratch")
16
+ args = parser.parse_args()
17
+
18
+ if args.reset:
19
+ chroma_path = Path("./chroma_db")
20
+ if chroma_path.exists():
21
+ shutil.rmtree(chroma_path)
22
+ print("ChromaDB wiped.")
23
+
24
+ run_ingestion_pipeline(args.data_dir)
25
+
26
+
27
+ if __name__ == "__main__":
28
+ main()
server/__init__.py ADDED
File without changes
server/chain.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_openai import ChatOpenAI
2
+ from langchain_classic.chains import ConversationalRetrievalChain
3
+
4
+ from server.utils import load_config, setup_logger
5
+
6
+ logger = setup_logger(__name__)
7
+
8
+ SYSTEM_PROMPT = (
9
+ "You are FinRAG, a fintech research assistant. Answer questions using "
10
+ "only the provided context. If the answer is not in the context, say "
11
+ "'I could not find this in the loaded documents.' Do not hallucinate. "
12
+ "Be concise and cite your source document."
13
+ )
14
+
15
+
16
+ def _create_llm(llm_config: dict) -> ChatOpenAI:
17
+ """
18
+ Create ChatOpenAI instance pointed at Euron API.
19
+ Works with any model Euron supports (OpenAI, Anthropic, Google, Meta, etc.)
20
+ — just change the model name in config.yaml.
21
+ """
22
+ model = llm_config["model"]
23
+ logger.info(f"Using LLM: {model}")
24
+
25
+ return ChatOpenAI(
26
+ model=model,
27
+ base_url=llm_config.get("base_url", "https://api.euron.one/api/v1/euri"),
28
+ api_key=_get_api_key(),
29
+ temperature=llm_config.get("temperature", 0.1),
30
+ extra_body={"max_tokens": llm_config.get("max_tokens", 1000)},
31
+ )
32
+
33
+
34
+ def build_qa_chain(retriever, memory) -> ConversationalRetrievalChain:
35
+ """
36
+ Build LangChain ConversationalRetrievalChain:
37
+ - LLM: any model via Euron API (set in config.yaml)
38
+ - Retriever: from retriever.py
39
+ - Memory: from memory.py
40
+ - return_source_documents: True
41
+ """
42
+ config = load_config()
43
+ llm_config = config.get("llm", {})
44
+ llm = _create_llm(llm_config)
45
+
46
+ chain = ConversationalRetrievalChain.from_llm(
47
+ llm=llm,
48
+ retriever=retriever,
49
+ memory=memory,
50
+ return_source_documents=True,
51
+ verbose=False,
52
+ )
53
+
54
+ logger.info("QA chain built successfully")
55
+ return chain
56
+
57
+
58
+ def run_query(chain, question: str) -> dict:
59
+ """
60
+ Run chain on question.
61
+ Return dict with answer, source_documents, and question.
62
+ """
63
+ result = chain.invoke({"question": question})
64
+
65
+ source_docs = []
66
+ for doc in result.get("source_documents", []):
67
+ source_docs.append({
68
+ "content": doc.page_content,
69
+ "source": doc.metadata.get("source", ""),
70
+ "page": doc.metadata.get("page", None),
71
+ "chunk_index": doc.metadata.get("chunk_index", None),
72
+ })
73
+
74
+ return {
75
+ "answer": result.get("answer", ""),
76
+ "source_documents": source_docs,
77
+ "question": question,
78
+ }
79
+
80
+
81
+ def _get_api_key() -> str:
82
+ """Load Euron API key from environment."""
83
+ import os
84
+ from dotenv import load_dotenv
85
+ load_dotenv()
86
+ key = os.getenv("EURON_API_KEY", "")
87
+ if not key:
88
+ logger.warning("EURON_API_KEY not set in environment")
89
+ return key
server/eval/__init__.py ADDED
File without changes
server/eval/faithfulness.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ from langchain_openai import ChatOpenAI
4
+
5
+ from server.utils import load_config, setup_logger
6
+
7
+ logger = setup_logger(__name__)
8
+
9
+ FAITHFULNESS_PROMPT = """
10
+ You are an evaluation judge. Given a context and an answer, score how faithful
11
+ the answer is to the context on a scale of 1-5.
12
+
13
+ 1 = Answer contradicts or ignores the context entirely
14
+ 2 = Answer uses context minimally, adds significant unsupported claims
15
+ 3 = Answer mostly uses context with minor unsupported additions
16
+ 4 = Answer is well-grounded in context with trivial additions only
17
+ 5 = Answer is entirely and accurately derived from the context
18
+
19
+ Context:
20
+ {context}
21
+
22
+ Answer:
23
+ {answer}
24
+
25
+ Respond ONLY with valid JSON: {{"score": <int>, "reason": "<one sentence>"}}
26
+ """
27
+
28
+
29
+ def score_faithfulness(answer: str, source_chunks: list[dict]) -> dict:
30
+ """
31
+ Call LLM with FAITHFULNESS_PROMPT.
32
+ Parse JSON response.
33
+ Return dict with score, reason, raw_response.
34
+ Handle JSON parse errors gracefully — return score: -1 on failure.
35
+ """
36
+ config = load_config()
37
+ llm_config = config.get("llm", {})
38
+
39
+ context = "\n\n".join(chunk.get("content", "") for chunk in source_chunks)
40
+
41
+ prompt = FAITHFULNESS_PROMPT.format(context=context, answer=answer)
42
+
43
+ try:
44
+ llm = ChatOpenAI(
45
+ model=llm_config["model"],
46
+ base_url=llm_config.get("base_url", "https://api.euron.one/api/v1/euri"),
47
+ api_key=_get_api_key(),
48
+ temperature=0.0,
49
+ extra_body={"max_tokens": 200},
50
+ )
51
+
52
+ response = llm.invoke(prompt)
53
+ raw = response.content.strip()
54
+
55
+ parsed = json.loads(raw)
56
+ return {
57
+ "score": parsed.get("score", -1),
58
+ "reason": parsed.get("reason", ""),
59
+ "raw_response": raw,
60
+ }
61
+ except json.JSONDecodeError:
62
+ logger.warning(f"Failed to parse faithfulness JSON: {raw}")
63
+ return {"score": -1, "reason": "JSON parse error", "raw_response": raw}
64
+ except Exception as e:
65
+ logger.error(f"Faithfulness scoring failed: {e}")
66
+ return {"score": -1, "reason": str(e), "raw_response": ""}
67
+
68
+
69
+ def _get_api_key() -> str:
70
+ """Load Euron API key from environment."""
71
+ import os
72
+ from dotenv import load_dotenv
73
+ load_dotenv()
74
+ return os.getenv("EURON_API_KEY", "")
server/eval/precision.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ from server.retriever import retrieve_with_scores
4
+ from server.utils import setup_logger
5
+
6
+ logger = setup_logger(__name__)
7
+
8
+
9
+ def compute_precision_at_k(query: str, retrieved_chunks: list[dict], ground_truth: dict, k: int = 5) -> float:
10
+ """
11
+ Precision@K = (relevant chunks in top-K) / K
12
+
13
+ A chunk is "relevant" if:
14
+ - Its source matches ground_truth["relevant_sources"], OR
15
+ - Its content contains any keyword from ground_truth["relevant_chunk_keywords"]
16
+
17
+ Return float between 0 and 1.
18
+ """
19
+ relevant_sources = ground_truth.get("relevant_sources", [])
20
+ keywords = ground_truth.get("relevant_chunk_keywords", [])
21
+
22
+ top_k = retrieved_chunks[:k]
23
+ relevant_count = 0
24
+
25
+ for chunk in top_k:
26
+ source_match = chunk.get("source", "") in relevant_sources
27
+ keyword_match = any(
28
+ kw.lower() in chunk.get("content", "").lower() for kw in keywords
29
+ )
30
+ if source_match or keyword_match:
31
+ relevant_count += 1
32
+
33
+ precision = relevant_count / k if k > 0 else 0.0
34
+ return round(precision, 4)
35
+
36
+
37
+ def run_batch_precision_eval(eval_pairs_path: str, k: int = 5) -> dict:
38
+ """
39
+ Run precision@K for all queries in eval_pairs.json.
40
+ Return dict with mean_precision_at_k and per_query_results.
41
+ """
42
+ with open(eval_pairs_path, "r") as f:
43
+ eval_pairs = json.load(f)
44
+
45
+ per_query_results = []
46
+
47
+ for pair in eval_pairs:
48
+ query = pair["query"]
49
+ retrieved = retrieve_with_scores(query, k=k)
50
+ precision = compute_precision_at_k(query, retrieved, pair, k=k)
51
+ retrieved_sources = [c["source"] for c in retrieved]
52
+
53
+ per_query_results.append({
54
+ "query": query,
55
+ "precision_at_k": precision,
56
+ "retrieved_sources": retrieved_sources,
57
+ })
58
+
59
+ logger.info(f"P@{k}={precision:.2f} | {query[:60]}...")
60
+
61
+ mean_precision = sum(r["precision_at_k"] for r in per_query_results) / len(per_query_results)
62
+
63
+ return {
64
+ "mean_precision_at_k": round(mean_precision, 4),
65
+ "per_query_results": per_query_results,
66
+ }
server/ingest.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib
2
+ from pathlib import Path
3
+
4
+ from langchain_community.document_loaders import PyPDFLoader, TextLoader, CSVLoader
5
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
6
+ from langchain_huggingface import HuggingFaceEmbeddings
7
+ from langchain_chroma import Chroma
8
+
9
+ from server.utils import load_config, setup_logger
10
+
11
+ logger = setup_logger(__name__)
12
+
13
+
14
+ def load_documents(data_dir: str) -> list:
15
+ """
16
+ Load all PDFs and .txt files from data_dir.
17
+ Return list of LangChain Document objects with metadata:
18
+ - source: filename
19
+ - page: page number (PDFs only)
20
+ """
21
+ data_path = Path(data_dir)
22
+ documents = []
23
+
24
+ for file_path in sorted(data_path.iterdir()):
25
+ if file_path.suffix.lower() == ".pdf":
26
+ loader = PyPDFLoader(str(file_path))
27
+ docs = loader.load()
28
+ for doc in docs:
29
+ doc.metadata["source"] = file_path.name
30
+ documents.extend(docs)
31
+ elif file_path.suffix.lower() == ".txt":
32
+ loader = TextLoader(str(file_path), encoding="utf-8")
33
+ docs = loader.load()
34
+ for doc in docs:
35
+ doc.metadata["source"] = file_path.name
36
+ documents.extend(docs)
37
+ elif file_path.suffix.lower() == ".csv":
38
+ loader = CSVLoader(str(file_path), encoding="utf-8")
39
+ docs = loader.load()
40
+ for doc in docs:
41
+ doc.metadata["source"] = file_path.name
42
+ documents.extend(docs)
43
+
44
+ logger.info(f"Loaded {len(documents)} document pages from {data_dir}")
45
+ return documents
46
+
47
+
48
+ def load_documents_from_paths(file_paths: list[str]) -> list:
49
+ """Load documents from explicit file paths (not directory scan)."""
50
+ documents = []
51
+ for fp in file_paths:
52
+ file_path = Path(fp)
53
+ if file_path.suffix.lower() == ".pdf":
54
+ loader = PyPDFLoader(str(file_path))
55
+ docs = loader.load()
56
+ for doc in docs:
57
+ doc.metadata["source"] = file_path.name
58
+ documents.extend(docs)
59
+ elif file_path.suffix.lower() == ".txt":
60
+ loader = TextLoader(str(file_path), encoding="utf-8")
61
+ docs = loader.load()
62
+ for doc in docs:
63
+ doc.metadata["source"] = file_path.name
64
+ documents.extend(docs)
65
+ elif file_path.suffix.lower() == ".csv":
66
+ loader = CSVLoader(str(file_path), encoding="utf-8")
67
+ docs = loader.load()
68
+ for doc in docs:
69
+ doc.metadata["source"] = file_path.name
70
+ documents.extend(docs)
71
+ logger.info(f"Loaded {len(documents)} document pages from {len(file_paths)} files")
72
+ return documents
73
+
74
+
75
+ def ingest_files(file_paths: list[str]) -> Chroma:
76
+ """Ingest specific files: load -> chunk -> embed -> store."""
77
+ documents = load_documents_from_paths(file_paths)
78
+ chunks = chunk_documents(documents)
79
+ vectorstore = embed_and_store(chunks)
80
+ return vectorstore
81
+
82
+
83
+ def chunk_documents(documents: list, chunk_size: int = 500, chunk_overlap: int = 50) -> list:
84
+ """
85
+ Split documents using RecursiveCharacterTextSplitter.
86
+ Preserve metadata from parent document. Add chunk_index to metadata.
87
+ """
88
+ config = load_config()
89
+ chunk_size = config.get("chunking", {}).get("chunk_size", chunk_size)
90
+ chunk_overlap = config.get("chunking", {}).get("chunk_overlap", chunk_overlap)
91
+
92
+ splitter = RecursiveCharacterTextSplitter(
93
+ chunk_size=chunk_size,
94
+ chunk_overlap=chunk_overlap,
95
+ )
96
+
97
+ chunks = splitter.split_documents(documents)
98
+
99
+ for i, chunk in enumerate(chunks):
100
+ chunk.metadata["chunk_index"] = i
101
+
102
+ logger.info(f"Created {len(chunks)} chunks (size={chunk_size}, overlap={chunk_overlap})")
103
+ return chunks
104
+
105
+
106
+ def _chunk_id(chunk) -> str:
107
+ """Generate a deterministic ID from chunk content + metadata for idempotency."""
108
+ source = chunk.metadata.get("source", "")
109
+ page = str(chunk.metadata.get("page", ""))
110
+ content_hash = hashlib.md5((source + page + chunk.page_content).encode()).hexdigest()
111
+ return content_hash
112
+
113
+
114
+ def embed_and_store(chunks: list, collection_name: str = "finrag") -> Chroma:
115
+ """
116
+ Embed chunks using HuggingFaceEmbeddings (all-MiniLM-L6-v2).
117
+ Store in ChromaDB at ./chroma_db.
118
+ Idempotent: uses content hash as document ID to prevent duplicates.
119
+ """
120
+ config = load_config()
121
+ collection_name = config.get("retrieval", {}).get("collection_name", collection_name)
122
+
123
+ embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
124
+
125
+ ids = [_chunk_id(chunk) for chunk in chunks]
126
+ texts = [chunk.page_content for chunk in chunks]
127
+ metadatas = [chunk.metadata for chunk in chunks]
128
+
129
+ vectorstore = Chroma(
130
+ collection_name=collection_name,
131
+ embedding_function=embeddings,
132
+ persist_directory="./chroma_db",
133
+ )
134
+
135
+ # Filter out chunks that already exist
136
+ existing_ids = set()
137
+ try:
138
+ existing = vectorstore.get()
139
+ if existing and existing["ids"]:
140
+ existing_ids = set(existing["ids"])
141
+ except Exception:
142
+ pass
143
+
144
+ new_indices = [i for i, doc_id in enumerate(ids) if doc_id not in existing_ids]
145
+
146
+ if new_indices:
147
+ new_texts = [texts[i] for i in new_indices]
148
+ new_metadatas = [metadatas[i] for i in new_indices]
149
+ new_ids = [ids[i] for i in new_indices]
150
+ vectorstore.add_texts(texts=new_texts, metadatas=new_metadatas, ids=new_ids)
151
+ logger.info(f"Added {len(new_indices)} new chunks to ChromaDB (skipped {len(ids) - len(new_indices)} existing)")
152
+ else:
153
+ logger.info("All chunks already exist in ChromaDB, skipping")
154
+
155
+ return vectorstore
156
+
157
+
158
+ def run_ingestion_pipeline(data_dir: str) -> Chroma:
159
+ """
160
+ Orchestrates: load -> chunk -> embed -> store.
161
+ """
162
+ print(f"Loading documents from {data_dir}...")
163
+ documents = load_documents(data_dir)
164
+ print(f"Loaded {len(documents)} document pages")
165
+
166
+ print("Chunking...")
167
+ chunks = chunk_documents(documents)
168
+ print(f"{len(chunks)} chunks created")
169
+
170
+ print("Embedding and storing in ChromaDB...")
171
+ vectorstore = embed_and_store(chunks)
172
+
173
+ count = vectorstore._collection.count()
174
+ print(f"Collection 'finrag': {count} chunks ready")
175
+
176
+ return vectorstore
server/main.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from contextlib import asynccontextmanager
2
+ from pathlib import Path
3
+
4
+ from fastapi import FastAPI
5
+ from fastapi.middleware.cors import CORSMiddleware
6
+ from fastapi.staticfiles import StaticFiles
7
+
8
+ from server.retriever import get_retriever, has_documents
9
+ from server.memory import create_memory
10
+ from server.chain import build_qa_chain
11
+ from server.utils import setup_logger
12
+
13
+ logger = setup_logger(__name__)
14
+
15
+
16
+ @asynccontextmanager
17
+ async def lifespan(app: FastAPI):
18
+ """Initialize chain, memory, retriever on startup."""
19
+ logger.info("Starting FinRAG server...")
20
+ app.state.memory = create_memory()
21
+ app.state.retriever = None
22
+ app.state.chain = None
23
+ app.state.eval_log = []
24
+
25
+ if has_documents():
26
+ app.state.retriever = get_retriever()
27
+ app.state.chain = build_qa_chain(app.state.retriever, app.state.memory)
28
+ logger.info("Chain initialized with existing documents")
29
+ else:
30
+ logger.info("No documents found - chain will be built after first upload")
31
+
32
+ logger.info("FinRAG server ready")
33
+ yield
34
+
35
+
36
+ app = FastAPI(title="FinRAG API", lifespan=lifespan)
37
+
38
+ app.add_middleware(
39
+ CORSMiddleware,
40
+ allow_origins=["http://localhost:5173", "http://localhost:8000"],
41
+ allow_methods=["*"],
42
+ allow_headers=["*"],
43
+ )
44
+
45
+ # Import and include route modules
46
+ from server.routes import chat, eval, upload # noqa: E402
47
+
48
+ app.include_router(chat.router, prefix="/api")
49
+ app.include_router(eval.router, prefix="/api")
50
+ app.include_router(upload.router, prefix="/api")
51
+
52
+ # Serve React frontend build if it exists
53
+ frontend_dist = Path(__file__).resolve().parent.parent / "frontend" / "dist"
54
+ if frontend_dist.exists():
55
+ app.mount("/", StaticFiles(directory=str(frontend_dist), html=True), name="frontend")
server/memory.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_classic.memory import ConversationBufferWindowMemory
2
+
3
+ from server.utils import load_config, setup_logger
4
+
5
+ logger = setup_logger(__name__)
6
+
7
+
8
+ def create_memory(memory_key: str = "chat_history", max_token_limit: int = 2000) -> ConversationBufferWindowMemory:
9
+ """
10
+ Create LangChain ConversationBufferWindowMemory.
11
+ memory_key = "chat_history"
12
+ return_messages = True
13
+ k = number of recent conversation turns to keep
14
+ """
15
+ config = load_config()
16
+ max_token_limit = config.get("memory", {}).get("max_token_limit", max_token_limit)
17
+
18
+ # Use window memory with k turns (approximate: ~200 tokens per turn)
19
+ k_turns = max(1, max_token_limit // 200)
20
+
21
+ memory = ConversationBufferWindowMemory(
22
+ memory_key=memory_key,
23
+ return_messages=True,
24
+ output_key="answer",
25
+ k=k_turns,
26
+ )
27
+ logger.info(f"Created conversation memory (k={k_turns} turns)")
28
+ return memory
29
+
30
+
31
+ def get_memory_as_string(memory: ConversationBufferWindowMemory) -> str:
32
+ """Return conversation history as formatted string for display in UI."""
33
+ messages = memory.chat_memory.messages
34
+ lines = []
35
+ for msg in messages:
36
+ role = "User" if msg.type == "human" else "Assistant"
37
+ lines.append(f"{role}: {msg.content}")
38
+ return "\n".join(lines)
39
+
40
+
41
+ def clear_memory(memory: ConversationBufferWindowMemory) -> None:
42
+ """Clear all messages. Called on 'New Conversation' button."""
43
+ memory.clear()
44
+ logger.info("Conversation memory cleared")
server/retriever.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_huggingface import HuggingFaceEmbeddings
2
+ from langchain_chroma import Chroma
3
+
4
+ from server.utils import load_config, setup_logger
5
+
6
+ logger = setup_logger(__name__)
7
+
8
+
9
+ def get_retriever(collection_name: str = "finrag", k: int = 5):
10
+ """
11
+ Load existing ChromaDB collection.
12
+ Return LangChain retriever with k results.
13
+ """
14
+ config = load_config()
15
+ collection_name = config.get("retrieval", {}).get("collection_name", collection_name)
16
+ k = config.get("retrieval", {}).get("k", k)
17
+
18
+ embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
19
+
20
+ vectorstore = Chroma(
21
+ collection_name=collection_name,
22
+ embedding_function=embeddings,
23
+ persist_directory="./chroma_db",
24
+ )
25
+
26
+ return vectorstore.as_retriever(search_kwargs={"k": k})
27
+
28
+
29
+ def retrieve_with_scores(query: str, k: int = 5) -> list[dict]:
30
+ """
31
+ Return list of dicts with content, source, page, chunk_index, similarity_score.
32
+ """
33
+ config = load_config()
34
+ collection_name = config.get("retrieval", {}).get("collection_name", "finrag")
35
+ k = config.get("retrieval", {}).get("k", k)
36
+
37
+ embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
38
+
39
+ vectorstore = Chroma(
40
+ collection_name=collection_name,
41
+ embedding_function=embeddings,
42
+ persist_directory="./chroma_db",
43
+ )
44
+
45
+ results = vectorstore.similarity_search_with_relevance_scores(query, k=k)
46
+
47
+ output = []
48
+ for doc, score in results:
49
+ output.append({
50
+ "content": doc.page_content,
51
+ "source": doc.metadata.get("source", ""),
52
+ "page": doc.metadata.get("page", None),
53
+ "chunk_index": doc.metadata.get("chunk_index", None),
54
+ "similarity_score": round(score, 4),
55
+ })
56
+
57
+ return output
58
+
59
+
60
+ def get_document_stats(collection_name: str = "finrag") -> list[dict]:
61
+ """Return list of {name, chunk_count} for all unique sources in the collection."""
62
+ config = load_config()
63
+ collection_name = config.get("retrieval", {}).get("collection_name", collection_name)
64
+ embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
65
+ vectorstore = Chroma(
66
+ collection_name=collection_name,
67
+ embedding_function=embeddings,
68
+ persist_directory="./chroma_db",
69
+ )
70
+ try:
71
+ existing = vectorstore.get()
72
+ if not existing or not existing["ids"]:
73
+ return []
74
+ from collections import Counter
75
+ source_counts = Counter(m.get("source", "unknown") for m in existing["metadatas"])
76
+ return [{"name": name, "chunk_count": count} for name, count in sorted(source_counts.items())]
77
+ except Exception:
78
+ return []
79
+
80
+
81
+ def has_documents(collection_name: str = "finrag") -> bool:
82
+ """Check if any documents exist in the collection."""
83
+ return len(get_document_stats(collection_name)) > 0
server/routes/__init__.py ADDED
File without changes
server/routes/chat.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Request, HTTPException
2
+ from pydantic import BaseModel
3
+
4
+ from server.chain import run_query
5
+ from server.memory import clear_memory
6
+ from server.eval.faithfulness import score_faithfulness
7
+ from server.utils import setup_logger
8
+
9
+ logger = setup_logger(__name__)
10
+
11
+ router = APIRouter()
12
+
13
+
14
+ class ChatRequest(BaseModel):
15
+ question: str
16
+
17
+
18
+ @router.post("/chat")
19
+ async def chat(request: Request, body: ChatRequest):
20
+ """
21
+ Run a RAG query and return answer + sources + faithfulness score.
22
+ """
23
+ chain = request.app.state.chain
24
+ if chain is None:
25
+ raise HTTPException(status_code=400, detail="No documents uploaded yet. Please upload documents first.")
26
+ eval_log = request.app.state.eval_log
27
+
28
+ result = run_query(chain, body.question)
29
+
30
+ # Score faithfulness
31
+ faithfulness = score_faithfulness(result["answer"], result["source_documents"])
32
+
33
+ # Log to session eval
34
+ eval_log.append({
35
+ "query": body.question,
36
+ "answer": result["answer"],
37
+ "faithfulness_score": faithfulness["score"],
38
+ "reason": faithfulness["reason"],
39
+ })
40
+
41
+ return {
42
+ "answer": result["answer"],
43
+ "sources": result["source_documents"],
44
+ "faithfulness": faithfulness,
45
+ }
46
+
47
+
48
+ @router.delete("/chat/memory")
49
+ async def clear_chat_memory(request: Request):
50
+ """Clear conversation memory for a new conversation."""
51
+ memory = request.app.state.memory
52
+ clear_memory(memory)
53
+ return {"status": "cleared"}
server/routes/eval.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Request
2
+
3
+ from server.eval.precision import run_batch_precision_eval
4
+ from server.utils import load_config, setup_logger
5
+
6
+ logger = setup_logger(__name__)
7
+
8
+ router = APIRouter()
9
+
10
+
11
+ @router.get("/eval/session")
12
+ async def get_session_eval_log(request: Request):
13
+ """Return the session eval log: list of {query, answer, faithfulness_score, reason}."""
14
+ return {"eval_log": request.app.state.eval_log}
15
+
16
+
17
+ @router.post("/eval/precision")
18
+ async def run_precision_eval():
19
+ """
20
+ Run batch Precision@K eval against ground truth.
21
+ Returns mean_precision_at_k and per_query_results.
22
+ """
23
+ config = load_config()
24
+ eval_config = config.get("eval", {})
25
+ ground_truth_path = eval_config.get("ground_truth_path", "data/ground_truth/eval_pairs.json")
26
+ k = eval_config.get("precision_k", 5)
27
+
28
+ results = run_batch_precision_eval(ground_truth_path, k=k)
29
+ return results
server/routes/upload.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ from fastapi import APIRouter, Request, UploadFile, File, HTTPException
4
+
5
+ from server.ingest import ingest_files
6
+ from server.retriever import get_document_stats, get_retriever
7
+ from server.chain import build_qa_chain
8
+ from server.memory import create_memory
9
+ from server.utils import setup_logger
10
+
11
+ logger = setup_logger(__name__)
12
+
13
+ router = APIRouter()
14
+
15
+ UPLOAD_DIR = Path("data/raw")
16
+ ALLOWED_EXTENSIONS = {".pdf", ".txt", ".csv"}
17
+
18
+
19
+ @router.post("/upload")
20
+ async def upload_files(request: Request, files: list[UploadFile] = File(...)):
21
+ """
22
+ Accept file uploads (PDF, TXT, CSV).
23
+ Save to data/raw/, run ingestion, rebuild chain.
24
+ """
25
+ UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
26
+
27
+ saved_paths = []
28
+ for f in files:
29
+ ext = Path(f.filename).suffix.lower()
30
+ if ext not in ALLOWED_EXTENSIONS:
31
+ raise HTTPException(400, f"Unsupported file type: {ext}. Allowed: {ALLOWED_EXTENSIONS}")
32
+ dest = UPLOAD_DIR / f.filename
33
+ content = await f.read()
34
+ with open(dest, "wb") as out:
35
+ out.write(content)
36
+ saved_paths.append(str(dest))
37
+ logger.info(f"Saved uploaded file: {f.filename}")
38
+
39
+ # Run ingestion on uploaded files
40
+ ingest_files(saved_paths)
41
+
42
+ # Rebuild chain with new data
43
+ request.app.state.retriever = get_retriever()
44
+ request.app.state.memory = create_memory()
45
+ request.app.state.chain = build_qa_chain(
46
+ request.app.state.retriever, request.app.state.memory
47
+ )
48
+ logger.info("Chain rebuilt after upload")
49
+
50
+ docs = get_document_stats()
51
+ return {"uploaded": [f.filename for f in files], "documents": docs}
52
+
53
+
54
+ @router.get("/documents")
55
+ async def list_documents():
56
+ """Return list of uploaded documents with chunk counts."""
57
+ docs = get_document_stats()
58
+ return {"documents": docs}