Add Dockerfile and Render config
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .env.example +1 -0
- .gitignore +36 -0
- Dockerfile +25 -0
- config.yaml +23 -0
- data/ground_truth/eval_pairs.json +102 -0
- data/raw/bajaj_finance_q3_2024_transcript.txt +66 -0
- data/raw/npci_upi_report_2024.txt +85 -0
- data/raw/rbi_annual_report_2024.txt +66 -0
- finrag.md +152 -102
- frontend/.gitignore +24 -0
- frontend/README.md +16 -0
- frontend/eslint.config.js +29 -0
- frontend/index.html +14 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +30 -0
- frontend/public/favicon.svg +1 -0
- frontend/public/icons.svg +24 -0
- frontend/src/App.jsx +52 -0
- frontend/src/api.js +42 -0
- frontend/src/assets/hero.png +0 -0
- frontend/src/assets/vite.svg +1 -0
- frontend/src/components/ChatArea.jsx +133 -0
- frontend/src/components/FileUpload.jsx +102 -0
- frontend/src/components/MessageBubble.jsx +72 -0
- frontend/src/components/Sidebar.jsx +137 -0
- frontend/src/components/SourceExpander.jsx +69 -0
- frontend/src/index.css +12 -0
- frontend/src/main.jsx +10 -0
- frontend/vite.config.js +12 -0
- render.yaml +8 -0
- requirements.txt +14 -0
- sample_data/bajaj_finance_q3_2024_transcript.txt +66 -0
- sample_data/npci_upi_report_2024.txt +85 -0
- sample_data/rbi_annual_report_2024.txt +66 -0
- scripts/benchmark_chunks.py +169 -0
- scripts/run_eval.py +39 -0
- scripts/run_ingest.py +28 -0
- server/__init__.py +0 -0
- server/chain.py +89 -0
- server/eval/__init__.py +0 -0
- server/eval/faithfulness.py +74 -0
- server/eval/precision.py +66 -0
- server/ingest.py +176 -0
- server/main.py +55 -0
- server/memory.py +44 -0
- server/retriever.py +83 -0
- server/routes/__init__.py +0 -0
- server/routes/chat.py +53 -0
- server/routes/eval.py +29 -0
- 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 |
-
|
|
|
|
|
| 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 |
-
├──
|
| 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 +
|
| 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 |
-
├──
|
| 82 |
-
│ ├──
|
| 83 |
-
│ ├──
|
| 84 |
-
│
|
| 85 |
-
│
|
| 86 |
-
│
|
| 87 |
-
│
|
| 88 |
-
│
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 `
|
| 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 `
|
| 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 `
|
| 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 `
|
| 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 `
|
| 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 `
|
| 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 `
|
| 325 |
|
| 326 |
```python
|
| 327 |
def load_config(config_path: str = "config.yaml") -> dict:
|
|
@@ -363,79 +376,90 @@ eval:
|
|
| 363 |
|
| 364 |
---
|
| 365 |
|
| 366 |
-
## 5.
|
| 367 |
|
| 368 |
-
### 5.1 `
|
| 369 |
|
| 370 |
-
**Entry point.**
|
| 371 |
|
| 372 |
```python
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
|
| 379 |
-
|
| 380 |
-
render_chat_tab()
|
| 381 |
|
| 382 |
-
|
| 383 |
-
render_eval_dashboard()
|
| 384 |
-
```
|
| 385 |
|
| 386 |
-
**Session state to initialise:**
|
| 387 |
```python
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
|
|
|
|
|
|
| 396 |
```
|
| 397 |
|
| 398 |
---
|
| 399 |
|
| 400 |
-
### 5.
|
| 401 |
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 420 |
|
| 421 |
-
**
|
| 422 |
|
| 423 |
-
**
|
| 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 |
-
**
|
| 430 |
-
-
|
| 431 |
-
-
|
| 432 |
-
-
|
| 433 |
-
-
|
|
|
|
| 434 |
|
| 435 |
-
**
|
| 436 |
-
-
|
| 437 |
-
-
|
| 438 |
-
-
|
| 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 |
-
|
|
|
|
| 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
|
| 559 |
|
| 560 |
-
CMD ["
|
| 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
|
| 605 |
-
- [ ] 1.4 Implement `
|
| 606 |
-
- [ ] 1.5
|
|
|
|
| 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 `
|
| 616 |
-
- [ ] 2.2 Implement `
|
| 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 `
|
| 629 |
-
- [ ] 3.2 Implement `
|
| 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 `
|
| 641 |
-
- [ ] 4.3 Implement `
|
| 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
|
| 650 |
-
> **Goal:** Build the
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 651 |
|
| 652 |
-
- [ ]
|
| 653 |
-
- [ ]
|
| 654 |
-
- [ ]
|
| 655 |
-
- [ ]
|
| 656 |
-
- [ ]
|
|
|
|
| 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
|
| 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}
|