junaid17 commited on
Commit
ca67025
·
verified ·
1 Parent(s): eff4a3d

Upload 13 files

Browse files
Notebook/Qwen2_5_financial_finetuning.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
app.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException
2
+ import shutil
3
+ from fastapi.responses import StreamingResponse
4
+ import json
5
+ import os
6
+ from pydantic import BaseModel
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from scripts.rag import RagPipeline
9
+ from scripts.main import set_rag_instance
10
+ from scripts.main import stream_chat_response
11
+
12
+
13
+ app = FastAPI(version='1.0', title='FinAI', description="A finetuned qwen model for financial QA with rag.")
14
+ rag = RagPipeline()
15
+ set_rag_instance(rag)
16
+
17
+ app.add_middleware(
18
+ CORSMiddleware,
19
+ allow_origins=["*"],
20
+ allow_credentials=True,
21
+ allow_methods=["*"],
22
+ allow_headers=["*"],
23
+ )
24
+
25
+
26
+ class ChatRequest(BaseModel):
27
+ message: str
28
+ thread_id: str = "default"
29
+
30
+
31
+ @app.get('/')
32
+ def health_check():
33
+ return {'status' : 'The api is live.'}
34
+
35
+
36
+ @app.post("/upload")
37
+ async def upload_document(file: UploadFile = File(...)):
38
+ try:
39
+ if not file.filename.endswith(".pdf"):
40
+ raise HTTPException(
41
+ status_code=400,
42
+ detail="Only PDF files allowed."
43
+ )
44
+
45
+ os.makedirs("temp_docs", exist_ok=True)
46
+
47
+ file_path = f"temp_docs/{file.filename}"
48
+
49
+ with open(file_path, "wb") as buffer:
50
+ shutil.copyfileobj(file.file, buffer)
51
+
52
+ rag.delete_all_docs()
53
+
54
+ docs = rag.load_docs(file_path)
55
+ split_docs = rag.split_docs(docs)
56
+
57
+ rag.add_docs(split_docs)
58
+ rag.create_bm25(split_docs)
59
+
60
+ return {
61
+ "status": "success",
62
+ "message": "Document uploaded successfully",
63
+ "chunks": len(split_docs),
64
+ "document": file.filename
65
+ }
66
+
67
+ except Exception as e:
68
+ raise HTTPException(
69
+ status_code=500,
70
+ detail=str(e)
71
+ )
72
+
73
+
74
+ @app.delete("/reset")
75
+ async def reset_docs():
76
+ try:
77
+ rag.delete_all_docs()
78
+
79
+ return {
80
+ "status": "success",
81
+ "message": "All docs deleted"
82
+ }
83
+
84
+ except Exception as e:
85
+ raise HTTPException(
86
+ status_code=500,
87
+ detail=str(e)
88
+ )
89
+
90
+
91
+
92
+ @app.post("/chat/stream")
93
+ async def chat_stream(request: ChatRequest):
94
+
95
+ def event_generator():
96
+ try:
97
+ metadata_sent = False
98
+
99
+ for chunk in stream_chat_response(
100
+ user_message=request.message,
101
+ thread_id=request.thread_id
102
+ ):
103
+ if not metadata_sent:
104
+ metadata_event = {
105
+ "type": "metadata",
106
+ "used_rag": chunk["metadata"]["used_rag"],
107
+ "sources": chunk["metadata"]["sources"],
108
+ "thread_id": chunk["metadata"]["thread_id"]
109
+ }
110
+
111
+ yield f"data: {json.dumps(metadata_event)}\n\n"
112
+
113
+ metadata_sent = True
114
+
115
+ token_event = {
116
+ "type": "token",
117
+ "content": chunk["token"]
118
+ }
119
+
120
+ yield f"data: {json.dumps(token_event)}\n\n"
121
+
122
+ done_event = {
123
+ "type": "done"
124
+ }
125
+
126
+ yield f"data: {json.dumps(done_event)}\n\n"
127
+
128
+ except Exception as e:
129
+ error_event = {
130
+ "type": "error",
131
+ "message": str(e)
132
+ }
133
+
134
+ yield f"data: {json.dumps(error_event)}\n\n"
135
+
136
+ return StreamingResponse(
137
+ event_generator(),
138
+ media_type="text/event-stream"
139
+ )
140
+
assets/improvements.png ADDED
assets/metrices.png ADDED
assets/workflow.png ADDED
index.html ADDED
@@ -0,0 +1,1418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>FinAI — Financial Intelligence</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
9
+ <style>
10
+ :root {
11
+ --bg: #050505;
12
+ --bg-elevated: #0a0a0a;
13
+ --bg-card: #0f0f0f;
14
+ --bg-hover: #161616;
15
+ --bg-active: #1a1a1a;
16
+ --border: #1f1f1f;
17
+ --border-light: #2a2a2a;
18
+ --border-hover: #333333;
19
+ --text: #f5f5f5;
20
+ --text-secondary: #a0a0a0;
21
+ --text-muted: #666666;
22
+ --text-dim: #444444;
23
+ --accent: #e8e8e8;
24
+ --accent-dim: #999999;
25
+ --radius-sm: 16px;
26
+ --radius: 20px;
27
+ --radius-lg: 24px;
28
+ --radius-pill: 999px;
29
+ }
30
+
31
+ * { margin: 0; padding: 0; box-sizing: border-box; }
32
+
33
+ body {
34
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
35
+ background: var(--bg);
36
+ color: var(--text);
37
+ height: 100vh;
38
+ overflow: hidden;
39
+ line-height: 1.5;
40
+ -webkit-font-smoothing: antialiased;
41
+ }
42
+
43
+ ::-webkit-scrollbar { width: 4px; }
44
+ ::-webkit-scrollbar-track { background: transparent; }
45
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
46
+
47
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
48
+ @keyframes fadeInScale { from { opacity: 0; transform: scale(0.97); } to { opacity: 1; transform: scale(1); } }
49
+ @keyframes slideInRight { from { opacity: 0; transform: translateX(30px); } to { opacity: 1; transform: translateX(0); } }
50
+ @keyframes slideInLeft { from { opacity: 0; transform: translateX(-30px); } to { opacity: 1; transform: translateX(0); } }
51
+ @keyframes pulse { 0%,100% { opacity: 0.3; } 50% { opacity: 1; } }
52
+ @keyframes breathe { 0%,100% { transform: scale(1); opacity: 0.6; } 50% { transform: scale(1.05); opacity: 1; } }
53
+ @keyframes spin { to { transform: rotate(360deg); } }
54
+ @keyframes typingDot { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
55
+ @keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
56
+ @keyframes progressFill { from { width: 0%; } to { width: 100%; } }
57
+ @keyframes cursorBlink { 0%,100% { opacity: 1; } 50% { opacity: 0; } }
58
+ @keyframes ripple { 0% { transform: scale(0); opacity: 0.5; } 100% { transform: scale(4); opacity: 0; } }
59
+ @keyframes float { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }
60
+
61
+ .app { display: flex; height: 100vh; width: 100vw; }
62
+
63
+ /* ===== SIDEBAR ===== */
64
+ .sidebar {
65
+ width: 300px;
66
+ background: var(--bg-elevated);
67
+ border-right: 1px solid var(--border);
68
+ display: flex; flex-direction: column;
69
+ padding: 20px;
70
+ gap: 20px;
71
+ overflow-y: auto;
72
+ transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
73
+ position: relative; z-index: 100;
74
+ }
75
+
76
+ .sidebar-logo {
77
+ display: flex; align-items: center; gap: 12px;
78
+ padding: 8px 4px;
79
+ }
80
+
81
+ .logo-ring {
82
+ width: 36px; height: 36px;
83
+ border-radius: var(--radius-sm);
84
+ background: var(--bg-card);
85
+ border: 1.5px solid var(--border-light);
86
+ display: flex; align-items: center; justify-content: center;
87
+ font-size: 16px;
88
+ position: relative;
89
+ overflow: hidden;
90
+ }
91
+
92
+ .logo-ring::after {
93
+ content: ''; position: absolute; inset: -2px;
94
+ border-radius: inherit;
95
+ background: conic-gradient(from 0deg, transparent, var(--text-dim), transparent);
96
+ opacity: 0.3;
97
+ animation: spin 8s linear infinite;
98
+ z-index: -1;
99
+ }
100
+
101
+ .logo-text {
102
+ font-size: 18px; font-weight: 700;
103
+ letter-spacing: -0.4px;
104
+ color: var(--text);
105
+ }
106
+
107
+ .new-chat {
108
+ display: flex; align-items: center; justify-content: center;
109
+ gap: 8px; padding: 12px;
110
+ background: var(--bg-card);
111
+ border: 1.5px solid var(--border);
112
+ border-radius: var(--radius-sm);
113
+ color: var(--text);
114
+ font-size: 13px; font-weight: 500;
115
+ font-family: inherit; cursor: pointer;
116
+ transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
117
+ }
118
+
119
+ .new-chat:hover {
120
+ background: var(--bg-hover);
121
+ border-color: var(--border-hover);
122
+ transform: translateY(-1px);
123
+ box-shadow: 0 4px 20px rgba(0,0,0,0.4);
124
+ }
125
+
126
+ .new-chat:active { transform: scale(0.98); }
127
+
128
+ .section-label {
129
+ font-size: 10px; font-weight: 600;
130
+ text-transform: uppercase;
131
+ letter-spacing: 2px;
132
+ color: var(--text-dim);
133
+ padding: 0 6px;
134
+ margin-bottom: 6px;
135
+ }
136
+
137
+ .recent-list { display: flex; flex-direction: column; gap: 2px; }
138
+
139
+ .recent-item {
140
+ display: flex; align-items: center; gap: 10px;
141
+ padding: 10px 12px;
142
+ border-radius: var(--radius-sm);
143
+ cursor: pointer;
144
+ transition: all 0.2s ease;
145
+ position: relative;
146
+ }
147
+
148
+ .recent-item:hover { background: var(--bg-hover); }
149
+ .recent-item.active { background: var(--bg-active); }
150
+ .recent-item.active::before {
151
+ content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
152
+ width: 3px; height: 14px; background: var(--text-secondary); border-radius: 2px;
153
+ }
154
+
155
+ .recent-dot {
156
+ width: 6px; height: 6px; border-radius: 50%;
157
+ background: var(--border-light); flex-shrink: 0;
158
+ }
159
+ .recent-item.active .recent-dot { background: var(--text-secondary); }
160
+
161
+ .recent-title {
162
+ font-size: 13px; font-weight: 500;
163
+ color: var(--text-secondary);
164
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
165
+ }
166
+ .recent-time { font-size: 11px; color: var(--text-dim); margin-top: 1px; }
167
+
168
+ /* Upload Area */
169
+ .upload-wrap { display: flex; flex-direction: column; gap: 10px; }
170
+
171
+ .upload-zone {
172
+ border: 1.5px dashed var(--border-light);
173
+ border-radius: var(--radius-sm);
174
+ padding: 24px 16px;
175
+ text-align: center;
176
+ cursor: pointer;
177
+ transition: all 0.3s ease;
178
+ background: var(--bg-card);
179
+ position: relative;
180
+ overflow: hidden;
181
+ }
182
+
183
+ .upload-zone:hover {
184
+ border-color: var(--border-hover);
185
+ background: var(--bg-hover);
186
+ }
187
+
188
+ .upload-zone.dragover {
189
+ border-color: var(--text-muted);
190
+ background: rgba(255,255,255,0.02);
191
+ }
192
+
193
+ .upload-zone input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
194
+
195
+ .upload-zone-icon { font-size: 28px; margin-bottom: 8px; display: block; filter: grayscale(1); opacity: 0.7; }
196
+ .upload-zone-text { font-size: 13px; color: var(--text-secondary); font-weight: 500; }
197
+ .upload-zone-hint { font-size: 11px; color: var(--text-dim); margin-top: 3px; }
198
+
199
+ /* Upload Progress */
200
+ .upload-progress {
201
+ display: none;
202
+ flex-direction: column;
203
+ gap: 8px;
204
+ padding: 12px;
205
+ background: var(--bg-card);
206
+ border: 1px solid var(--border);
207
+ border-radius: var(--radius-sm);
208
+ animation: fadeInScale 0.3s ease;
209
+ }
210
+
211
+ .upload-progress.show { display: flex; }
212
+
213
+ .progress-track {
214
+ height: 3px;
215
+ background: var(--border);
216
+ border-radius: 3px;
217
+ overflow: hidden;
218
+ }
219
+
220
+ .progress-fill {
221
+ height: 100%;
222
+ background: linear-gradient(90deg, var(--text-dim), var(--text-secondary), var(--text-dim));
223
+ background-size: 200% 100%;
224
+ border-radius: 3px;
225
+ width: 0%;
226
+ transition: width 0.4s cubic-bezier(0.16, 1, 0.3, 1);
227
+ animation: shimmer 2s linear infinite;
228
+ }
229
+
230
+ .progress-info {
231
+ display: flex; justify-content: space-between; align-items: center;
232
+ font-size: 11px;
233
+ }
234
+
235
+ .progress-step {
236
+ color: var(--text-secondary);
237
+ font-weight: 500;
238
+ display: flex; align-items: center; gap: 6px;
239
+ }
240
+
241
+ .progress-step .spinner {
242
+ width: 12px; height: 12px;
243
+ border: 1.5px solid transparent;
244
+ border-top-color: var(--text-secondary);
245
+ border-radius: 50%;
246
+ animation: spin 0.8s linear infinite;
247
+ }
248
+
249
+ .progress-pct { color: var(--text-dim); font-family: 'JetBrains Mono', monospace; font-size: 10px; }
250
+
251
+ .upload-done {
252
+ display: none;
253
+ padding: 12px;
254
+ background: var(--bg-card);
255
+ border: 1px solid var(--border);
256
+ border-radius: var(--radius-sm);
257
+ animation: fadeInScale 0.3s ease;
258
+ }
259
+
260
+ .upload-done.show { display: flex; align-items: center; gap: 10px; }
261
+
262
+ .upload-done-icon {
263
+ width: 28px; height: 28px;
264
+ border-radius: 50%;
265
+ background: var(--bg-hover);
266
+ display: flex; align-items: center; justify-content: center;
267
+ font-size: 13px;
268
+ flex-shrink: 0;
269
+ }
270
+
271
+ .upload-done-text { font-size: 12px; color: var(--text-secondary); font-weight: 500; }
272
+ .upload-done-meta { font-size: 10px; color: var(--text-dim); margin-top: 2px; }
273
+
274
+ .upload-btn {
275
+ padding: 10px;
276
+ background: var(--bg-card);
277
+ border: 1.5px solid var(--border);
278
+ border-radius: var(--radius-sm);
279
+ color: var(--text);
280
+ font-size: 13px; font-weight: 500;
281
+ font-family: inherit; cursor: pointer;
282
+ transition: all 0.25s ease;
283
+ width: 100%;
284
+ }
285
+
286
+ .upload-btn:hover {
287
+ background: var(--bg-hover);
288
+ border-color: var(--border-hover);
289
+ }
290
+
291
+ .upload-btn:disabled { opacity: 0.4; cursor: not-allowed; }
292
+
293
+ .reset-btn {
294
+ padding: 10px;
295
+ background: transparent;
296
+ border: 1.5px solid var(--border);
297
+ border-radius: var(--radius-sm);
298
+ color: var(--text-muted);
299
+ font-size: 12px; font-weight: 500;
300
+ font-family: inherit; cursor: pointer;
301
+ transition: all 0.2s;
302
+ width: 100%;
303
+ display: flex; align-items: center; justify-content: center; gap: 6px;
304
+ }
305
+
306
+ .reset-btn:hover {
307
+ border-color: rgba(239,68,68,0.3);
308
+ color: #ef4444;
309
+ background: rgba(239,68,68,0.03);
310
+ }
311
+
312
+ /* Session */
313
+ .session-box {
314
+ margin-top: auto;
315
+ padding-top: 16px;
316
+ border-top: 1px solid var(--border);
317
+ display: flex; flex-direction: column; gap: 10px;
318
+ }
319
+
320
+ .session-label {
321
+ display: flex; justify-content: space-between; align-items: center;
322
+ font-size: 10px; color: var(--text-dim);
323
+ text-transform: uppercase;
324
+ letter-spacing: 1.5px;
325
+ }
326
+
327
+ .session-id {
328
+ font-family: 'JetBrains Mono', monospace;
329
+ font-size: 10px; color: var(--text-dim);
330
+ word-break: break-all;
331
+ padding: 8px 10px;
332
+ background: var(--bg-card);
333
+ border: 1px solid var(--border);
334
+ border-radius: var(--radius-sm);
335
+ }
336
+
337
+ .copy-mini {
338
+ background: none; border: none;
339
+ color: var(--text-dim); cursor: pointer;
340
+ font-size: 10px; padding: 2px 6px;
341
+ border-radius: 4px; transition: all 0.2s;
342
+ }
343
+
344
+ .copy-mini:hover { color: var(--text-secondary); background: var(--bg-hover); }
345
+
346
+ .version { font-size: 10px; color: var(--text-dim); text-align: center; }
347
+
348
+ /* ===== MAIN ===== */
349
+ .main {
350
+ flex: 1;
351
+ display: flex; flex-direction: column;
352
+ position: relative; overflow: hidden;
353
+ background: var(--bg);
354
+ }
355
+
356
+ /* Empty State */
357
+ .empty {
358
+ flex: 1;
359
+ display: flex; flex-direction: column;
360
+ align-items: center; justify-content: center;
361
+ gap: 24px;
362
+ padding: 40px 24px;
363
+ animation: fadeInScale 0.6s ease;
364
+ }
365
+
366
+ .empty-orb {
367
+ width: 72px; height: 72px;
368
+ border-radius: var(--radius-lg);
369
+ background: var(--bg-card);
370
+ border: 1.5px solid var(--border);
371
+ display: flex; align-items: center; justify-content: center;
372
+ font-size: 28px;
373
+ position: relative;
374
+ animation: breathe 4s ease-in-out infinite;
375
+ }
376
+
377
+ .empty-orb::before {
378
+ content: ''; position: absolute; inset: -4px;
379
+ border-radius: inherit;
380
+ background: conic-gradient(from 0deg, transparent, var(--border-light), transparent);
381
+ opacity: 0.2;
382
+ animation: spin 12s linear infinite;
383
+ z-index: -1;
384
+ }
385
+
386
+ .empty-title {
387
+ font-size: 32px; font-weight: 700;
388
+ letter-spacing: -1px;
389
+ color: var(--text);
390
+ }
391
+
392
+ .empty-desc {
393
+ font-size: 14px; color: var(--text-muted);
394
+ text-align: center; max-width: 400px; line-height: 1.7;
395
+ }
396
+
397
+ .empty-chips {
398
+ display: flex; flex-wrap: wrap;
399
+ justify-content: center; gap: 8px;
400
+ max-width: 600px; margin-top: 4px;
401
+ }
402
+
403
+ .chip {
404
+ padding: 8px 16px;
405
+ background: var(--bg-card);
406
+ border: 1px solid var(--border);
407
+ border-radius: var(--radius-pill);
408
+ font-size: 12px; color: var(--text-secondary);
409
+ cursor: pointer;
410
+ transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
411
+ font-weight: 500;
412
+ }
413
+
414
+ .chip:hover {
415
+ border-color: var(--border-hover);
416
+ color: var(--text);
417
+ background: var(--bg-hover);
418
+ transform: translateY(-2px);
419
+ box-shadow: 0 4px 16px rgba(0,0,0,0.3);
420
+ }
421
+
422
+ /* Messages */
423
+ .messages {
424
+ flex: 1;
425
+ overflow-y: auto;
426
+ padding: 32px 24px 16px;
427
+ display: flex; flex-direction: column;
428
+ gap: 28px;
429
+ scroll-behavior: smooth;
430
+ }
431
+
432
+ .msg {
433
+ display: flex; gap: 14px;
434
+ max-width: 780px;
435
+ width: 100%;
436
+ align-self: center;
437
+ animation: fadeIn 0.35s ease-out;
438
+ }
439
+
440
+ .msg-avatar {
441
+ width: 32px; height: 32px;
442
+ border-radius: 50%;
443
+ display: flex; align-items: center; justify-content: center;
444
+ flex-shrink: 0;
445
+ font-size: 13px;
446
+ position: relative;
447
+ }
448
+
449
+ .msg-avatar.user {
450
+ background: var(--bg-card);
451
+ border: 1.5px solid var(--border-light);
452
+ color: var(--text-secondary);
453
+ }
454
+
455
+ .msg-avatar.assistant {
456
+ background: var(--bg-card);
457
+ border: 1.5px solid var(--border-light);
458
+ color: var(--text);
459
+ }
460
+
461
+ .msg-avatar::after {
462
+ content: ''; position: absolute; bottom: -2px; right: -2px;
463
+ width: 8px; height: 8px; border-radius: 50%;
464
+ background: var(--bg); border: 2px solid var(--bg);
465
+ }
466
+
467
+ .msg-avatar.user::after { background: var(--text-dim); }
468
+ .msg-avatar.assistant::after { background: var(--text-secondary); }
469
+
470
+ .msg-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
471
+
472
+ .msg-head {
473
+ display: flex; align-items: center; gap: 8px;
474
+ font-size: 12px;
475
+ }
476
+
477
+ .msg-name { font-weight: 600; color: var(--text); }
478
+ .msg-time { color: var(--text-dim); font-weight: 400; font-size: 11px; }
479
+
480
+ .msg-text {
481
+ font-size: 14.5px; line-height: 1.75;
482
+ color: var(--text-secondary);
483
+ word-wrap: break-word;
484
+ padding: 2px 0;
485
+ }
486
+
487
+ .msg-text strong { color: var(--text); font-weight: 600; }
488
+ .msg-text em { color: var(--text); font-style: italic; }
489
+ .msg-text code {
490
+ background: var(--bg-elevated);
491
+ padding: 2px 6px;
492
+ border-radius: 6px;
493
+ font-family: 'JetBrains Mono', monospace;
494
+ font-size: 12px; color: var(--text);
495
+ border: 1px solid var(--border);
496
+ }
497
+
498
+ .msg-text p { margin: 0 0 10px 0; }
499
+ .msg-text p:last-child { margin-bottom: 0; }
500
+
501
+ /* Typing */
502
+ .typing {
503
+ display: flex; gap: 4px; align-items: center;
504
+ padding: 6px 0;
505
+ }
506
+
507
+ .typing span {
508
+ width: 5px; height: 5px;
509
+ background: var(--text-dim);
510
+ border-radius: 50%;
511
+ animation: typingDot 1.4s ease-in-out infinite;
512
+ }
513
+
514
+ .typing span:nth-child(2) { animation-delay: 0.2s; }
515
+ .typing span:nth-child(3) { animation-delay: 0.4s; }
516
+
517
+ /* Cursor */
518
+ .stream-cursor {
519
+ display: inline-block;
520
+ width: 2px; height: 1.1em;
521
+ background: var(--text-secondary);
522
+ margin-left: 2px;
523
+ vertical-align: text-bottom;
524
+ animation: cursorBlink 1s step-end infinite;
525
+ }
526
+
527
+ /* RAG Card */
528
+ .rag {
529
+ margin-top: 10px;
530
+ background: var(--bg-elevated);
531
+ border: 1px solid var(--border);
532
+ border-radius: var(--radius-sm);
533
+ overflow: hidden;
534
+ animation: fadeInScale 0.35s ease;
535
+ }
536
+
537
+ .rag-toggle {
538
+ display: flex; align-items: center; gap: 10px;
539
+ padding: 10px 14px;
540
+ cursor: pointer;
541
+ user-select: none;
542
+ transition: background 0.15s;
543
+ }
544
+
545
+ .rag-toggle:hover { background: var(--bg-hover); }
546
+
547
+ .rag-badge {
548
+ display: inline-flex; align-items: center; gap: 4px;
549
+ padding: 3px 10px; border-radius: var(--radius-pill);
550
+ font-size: 10px; font-weight: 700;
551
+ text-transform: uppercase; letter-spacing: 0.8px;
552
+ }
553
+
554
+ .rag-badge.rag-active {
555
+ background: var(--bg-hover);
556
+ color: var(--text-secondary);
557
+ border: 1px solid var(--border-light);
558
+ }
559
+
560
+ .rag-badge.rag-direct {
561
+ background: var(--bg-hover);
562
+ color: var(--text-dim);
563
+ border: 1px solid var(--border);
564
+ }
565
+
566
+ .rag-label { font-size: 12px; color: var(--text-muted); font-weight: 500; }
567
+ .rag-arrow { margin-left: auto; font-size: 10px; color: var(--text-dim); transition: transform 0.25s ease; }
568
+ .rag-toggle.open .rag-arrow { transform: rotate(180deg); }
569
+
570
+ .rag-body {
571
+ display: none;
572
+ padding: 0 14px 12px;
573
+ flex-direction: column; gap: 6px;
574
+ }
575
+
576
+ .rag-body.open { display: flex; }
577
+
578
+ .source {
579
+ display: flex; align-items: center; gap: 10px;
580
+ padding: 10px 12px;
581
+ background: var(--bg);
582
+ border-radius: var(--radius-sm);
583
+ border: 1px solid var(--border);
584
+ transition: all 0.2s ease;
585
+ }
586
+
587
+ .source:hover {
588
+ border-color: var(--border-hover);
589
+ background: var(--bg-card);
590
+ transform: translateX(3px);
591
+ }
592
+
593
+ .source-icon {
594
+ width: 28px; height: 28px;
595
+ background: var(--bg-elevated);
596
+ border-radius: 8px;
597
+ display: flex; align-items: center; justify-content: center;
598
+ font-size: 12px; flex-shrink: 0;
599
+ border: 1px solid var(--border);
600
+ }
601
+
602
+ .source-info { flex: 1; min-width: 0; }
603
+ .source-name {
604
+ font-size: 12px; font-weight: 500;
605
+ color: var(--text-secondary);
606
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
607
+ }
608
+ .source-meta { font-size: 10px; color: var(--text-dim); margin-top: 1px; }
609
+
610
+ .source-score {
611
+ font-family: 'JetBrains Mono', monospace;
612
+ font-size: 10px; color: var(--text-secondary);
613
+ background: var(--bg-elevated);
614
+ padding: 3px 8px; border-radius: 6px;
615
+ border: 1px solid var(--border);
616
+ }
617
+
618
+ .rag-footer {
619
+ padding-top: 8px; margin-top: 4px;
620
+ border-top: 1px solid var(--border);
621
+ font-size: 10px; color: var(--text-dim);
622
+ font-family: 'JetBrains Mono', monospace;
623
+ display: flex; justify-content: space-between;
624
+ }
625
+
626
+ /* Performance */
627
+ .perf {
628
+ display: flex; align-items: center; gap: 14px;
629
+ padding: 4px 0;
630
+ font-size: 11px;
631
+ color: var(--text-dim);
632
+ font-family: 'JetBrains Mono', monospace;
633
+ }
634
+
635
+ .perf-item { display: flex; align-items: center; gap: 5px; }
636
+ .perf-dot {
637
+ width: 4px; height: 4px; border-radius: 50%;
638
+ background: var(--text-dim);
639
+ }
640
+
641
+ /* Input */
642
+ .input-area {
643
+ padding: 16px 24px 28px;
644
+ display: flex; flex-direction: column;
645
+ align-items: center; gap: 10px;
646
+ position: relative;
647
+ }
648
+
649
+ .input-box {
650
+ display: flex; align-items: flex-end; gap: 10px;
651
+ width: 100%; max-width: 720px;
652
+ background: var(--bg-elevated);
653
+ border: 1.5px solid var(--border);
654
+ border-radius: var(--radius);
655
+ padding: 6px 6px 6px 18px;
656
+ transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
657
+ }
658
+
659
+ .input-box:focus-within {
660
+ border-color: var(--border-hover);
661
+ box-shadow: 0 0 0 4px rgba(255,255,255,0.03), 0 8px 32px rgba(0,0,0,0.4);
662
+ }
663
+
664
+ .input-field {
665
+ flex: 1;
666
+ background: transparent; border: none;
667
+ color: var(--text); font-family: inherit;
668
+ font-size: 14.5px; padding: 10px 0;
669
+ resize: none; outline: none;
670
+ max-height: 160px; min-height: 24px;
671
+ line-height: 1.6;
672
+ }
673
+
674
+ .input-field::placeholder { color: var(--text-dim); }
675
+
676
+ .send-btn {
677
+ width: 32px; height: 32px;
678
+ border-radius: 50%;
679
+ background: var(--bg-card);
680
+ border: 1.5px solid var(--border);
681
+ color: var(--text-secondary);
682
+ display: flex; align-items: center; justify-content: center;
683
+ cursor: pointer;
684
+ transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
685
+ flex-shrink: 0; margin-bottom: 2px;
686
+ }
687
+
688
+ .send-btn:hover {
689
+ background: var(--bg-hover);
690
+ border-color: var(--border-hover);
691
+ color: var(--text);
692
+ transform: scale(1.08);
693
+ }
694
+
695
+ .send-btn:active { transform: scale(0.95); }
696
+ .send-btn:disabled { opacity: 0.3; cursor: not-allowed; transform: none; }
697
+
698
+ .send-btn svg { width: 15px; height: 15px; }
699
+
700
+ .input-hint {
701
+ font-size: 11px; color: var(--text-dim);
702
+ text-align: center;
703
+ }
704
+
705
+ /* Status */
706
+ .status-bar {
707
+ position: absolute; bottom: 0; left: 0; right: 0;
708
+ padding: 6px 24px;
709
+ display: flex; align-items: center; gap: 8px;
710
+ font-size: 11px; color: var(--text-dim);
711
+ pointer-events: none;
712
+ }
713
+
714
+ .status-pulse {
715
+ width: 5px; height: 5px;
716
+ background: var(--text-dim);
717
+ border-radius: 50%;
718
+ }
719
+
720
+ .status-pulse.active {
721
+ background: var(--text-secondary);
722
+ animation: pulse 2s ease-in-out infinite;
723
+ box-shadow: 0 0 6px rgba(160,160,160,0.3);
724
+ }
725
+
726
+ /* Toast */
727
+ .toasts {
728
+ position: fixed; top: 16px; right: 16px;
729
+ z-index: 1000;
730
+ display: flex; flex-direction: column; gap: 8px;
731
+ }
732
+
733
+ .toast {
734
+ padding: 12px 18px; border-radius: var(--radius-sm);
735
+ font-size: 13px; font-weight: 500;
736
+ display: flex; align-items: center; gap: 10px;
737
+ animation: slideInRight 0.35s cubic-bezier(0.16, 1, 0.3, 1);
738
+ box-shadow: 0 8px 32px rgba(0,0,0,0.5);
739
+ backdrop-filter: blur(16px);
740
+ max-width: 380px;
741
+ border: 1px solid var(--border);
742
+ }
743
+
744
+ .toast.ok { background: var(--bg-card); color: var(--text); }
745
+ .toast.err { background: var(--bg-card); color: #ef4444; }
746
+ .toast.info { background: var(--bg-card); color: var(--text-secondary); }
747
+
748
+ /* Mobile */
749
+ .mobile-toggle {
750
+ display: none;
751
+ position: absolute; left: 16px; top: 14px;
752
+ width: 36px; height: 36px;
753
+ border-radius: var(--radius-sm);
754
+ border: 1.5px solid var(--border);
755
+ background: var(--bg-elevated);
756
+ color: var(--text-secondary);
757
+ align-items: center; justify-content: center;
758
+ cursor: pointer; font-size: 16px; z-index: 60;
759
+ }
760
+
761
+ .sidebar-overlay {
762
+ display: none; position: fixed; inset: 0;
763
+ background: rgba(0,0,0,0.6);
764
+ z-index: 99; backdrop-filter: blur(4px);
765
+ }
766
+
767
+ @media (max-width: 768px) {
768
+ .sidebar { position: fixed; left: 0; top: 0; height: 100vh; transform: translateX(-100%); width: 280px; }
769
+ .sidebar.open { transform: translateX(0); }
770
+ .sidebar-overlay.show { display: block; }
771
+ .mobile-toggle { display: flex; }
772
+ .messages { padding: 56px 16px 16px; }
773
+ .msg { max-width: 100%; }
774
+ .input-area { padding: 12px 16px 20px; }
775
+ .empty-title { font-size: 26px; }
776
+ }
777
+
778
+ /* Chat header */
779
+ .chat-header {
780
+ padding: 14px 24px;
781
+ border-bottom: 1px solid var(--border);
782
+ display: flex; align-items: center; justify-content: center;
783
+ position: relative;
784
+ background: rgba(5,5,5,0.8);
785
+ backdrop-filter: blur(16px);
786
+ }
787
+
788
+ .chat-header-text {
789
+ font-size: 13px; font-weight: 600;
790
+ color: var(--text-muted);
791
+ letter-spacing: 0.5px;
792
+ }
793
+
794
+ .spinner {
795
+ width: 14px; height: 14px;
796
+ border: 1.5px solid transparent;
797
+ border-top-color: currentColor;
798
+ border-radius: 50%;
799
+ animation: spin 0.8s linear infinite;
800
+ }
801
+ </style>
802
+ <base target="_blank">
803
+ </head>
804
+ <body>
805
+ <div class="app">
806
+ <!-- Sidebar -->
807
+ <aside class="sidebar" id="sidebar">
808
+ <div class="sidebar-logo">
809
+ <div class="logo-ring">💹</div>
810
+ <div class="logo-text">FinAI</div>
811
+ </div>
812
+
813
+ <button class="new-chat" id="newChatBtn">
814
+ <span style="font-size:16px;">+</span> New Chat
815
+ </button>
816
+
817
+ <div>
818
+ <div class="section-label">Recent Chats</div>
819
+ <div class="recent-list" id="recentList"></div>
820
+ </div>
821
+
822
+ <div class="upload-wrap">
823
+ <div class="section-label">Upload Document</div>
824
+ <div class="upload-zone" id="uploadZone">
825
+ <input type="file" id="fileInput" accept=".pdf" />
826
+ <span class="upload-zone-icon">📄</span>
827
+ <div class="upload-zone-text" id="uzText">Drop PDF or click to browse</div>
828
+ <div class="upload-zone-hint" id="uzHint">Financial reports, statements, tax docs</div>
829
+ </div>
830
+
831
+ <div class="upload-progress" id="uploadProgress">
832
+ <div class="progress-track"><div class="progress-fill" id="progressFill"></div></div>
833
+ <div class="progress-info">
834
+ <span class="progress-step" id="progressStep"><div class="spinner"></div> Uploading PDF...</span>
835
+ <span class="progress-pct" id="progressPct">0%</span>
836
+ </div>
837
+ </div>
838
+
839
+ <div class="upload-done" id="uploadDone">
840
+ <div class="upload-done-icon">✓</div>
841
+ <div>
842
+ <div class="upload-done-text" id="udText">Document uploaded</div>
843
+ <div class="upload-done-meta" id="udMeta">Ready for queries</div>
844
+ </div>
845
+ </div>
846
+
847
+ <button class="upload-btn" id="uploadBtn">Upload Document</button>
848
+ </div>
849
+
850
+ <div>
851
+ <div class="section-label">Danger Zone</div>
852
+ <button class="reset-btn" id="resetBtn">
853
+ <span>🗑</span> Delete All Documents
854
+ </button>
855
+ </div>
856
+
857
+ <div class="session-box">
858
+ <div class="session-label">
859
+ <span>Session ID</span>
860
+ <button class="copy-mini" id="copyThread">Copy</button>
861
+ </div>
862
+ <div class="session-id" id="threadDisplay"></div>
863
+ <div class="version">FinAI v1.0 · Qwen + RAG</div>
864
+ </div>
865
+ </aside>
866
+
867
+ <div class="sidebar-overlay" id="sidebarOverlay"></div>
868
+
869
+ <!-- Main -->
870
+ <main class="main" id="main">
871
+ <button class="mobile-toggle" id="mobileToggle">☰</button>
872
+
873
+ <header class="chat-header" id="chatHeader" style="display:none;">
874
+ <div class="chat-header-text">Conversation</div>
875
+ </header>
876
+
877
+ <div class="messages" id="messages">
878
+ <div class="empty" id="emptyState">
879
+ <div class="empty-orb">💹</div>
880
+ <div class="empty-title">FinAI</div>
881
+ <div class="empty-desc">
882
+ Your intelligent financial assistant. Upload a document and ask about dividends,
883
+ taxes, retirement accounts, business valuation, or market analysis.
884
+ </div>
885
+ <div class="empty-chips" id="emptyChips">
886
+ <div class="chip" data-q="What are the risks of dividend-yielding stocks?">Dividend risks</div>
887
+ <div class="chip" data-q="15-year vs 30-year mortgage paid in 15 — what's the difference?">Mortgage comparison</div>
888
+ <div class="chip" data-q="How do taxes work for a new LLC business owner?">LLC taxes</div>
889
+ <div class="chip" data-q="Can I have both a 401(k) and a SEP IRA?">Retirement accounts</div>
890
+ <div class="chip" data-q="How to value a small business before investing?">Business valuation</div>
891
+ <div class="chip" data-q="Why do companies have fiscal years different from calendar years?">Fiscal years</div>
892
+ <div class="chip" data-q="What's wrong with taking money from your own business?">Owner withdrawals</div>
893
+ <div class="chip" data-q="Is there a candlestick pattern that guarantees profit?">Trading patterns</div>
894
+ </div>
895
+ </div>
896
+ </div>
897
+
898
+ <div class="input-area">
899
+ <div class="input-box">
900
+ <textarea class="input-field" id="inputField" rows="1" placeholder="Ask a finance question..."></textarea>
901
+ <button class="send-btn" id="sendBtn">
902
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
903
+ <line x1="22" y1="2" x2="11" y2="13"></line>
904
+ <polygon points="22,2 15,22 11,13 2,9 22,2"></polygon>
905
+ </svg>
906
+ </button>
907
+ </div>
908
+ <div class="input-hint">Enter to send · Shift+Enter for new line</div>
909
+ </div>
910
+
911
+ <div class="status-bar">
912
+ <span class="status-pulse" id="statusDot"></span>
913
+ <span id="statusText">Ready</span>
914
+ </div>
915
+ </main>
916
+ </div>
917
+
918
+ <div class="toasts" id="toasts"></div>
919
+
920
+ <script>
921
+ // ================= CONFIG =================
922
+ const API_BASE = "http://127.0.0.1:8000";
923
+
924
+ // ================= STATE =================
925
+ let threadId = localStorage.getItem("finai_tid") || generateId();
926
+ let messages = JSON.parse(localStorage.getItem("finai_msgs") || "[]");
927
+ let isStreaming = false;
928
+ let currentFile = null;
929
+ let streamStart = 0;
930
+ let tokCount = 0;
931
+
932
+ localStorage.setItem("finai_tid", threadId);
933
+
934
+ // ================= DOM =================
935
+ const sidebar = document.getElementById("sidebar");
936
+ const sidebarOverlay = document.getElementById("sidebarOverlay");
937
+ const mobileToggle = document.getElementById("mobileToggle");
938
+ const messagesEl = document.getElementById("messages");
939
+ const emptyState = document.getElementById("emptyState");
940
+ const inputField = document.getElementById("inputField");
941
+ const sendBtn = document.getElementById("sendBtn");
942
+ const uploadZone = document.getElementById("uploadZone");
943
+ const fileInput = document.getElementById("fileInput");
944
+ const uploadBtn = document.getElementById("uploadBtn");
945
+ const uploadProgress = document.getElementById("uploadProgress");
946
+ const progressFill = document.getElementById("progressFill");
947
+ const progressStep = document.getElementById("progressStep");
948
+ const progressPct = document.getElementById("progressPct");
949
+ const uploadDone = document.getElementById("uploadDone");
950
+ const udText = document.getElementById("udText");
951
+ const udMeta = document.getElementById("udMeta");
952
+ const uzText = document.getElementById("uzText");
953
+ const uzHint = document.getElementById("uzHint");
954
+ const resetBtn = document.getElementById("resetBtn");
955
+ const newChatBtn = document.getElementById("newChatBtn");
956
+ const threadDisplay = document.getElementById("threadDisplay");
957
+ const copyThread = document.getElementById("copyThread");
958
+ const toasts = document.getElementById("toasts");
959
+ const chatHeader = document.getElementById("chatHeader");
960
+ const statusDot = document.getElementById("statusDot");
961
+ const statusText = document.getElementById("statusText");
962
+ const recentList = document.getElementById("recentList");
963
+
964
+ threadDisplay.textContent = threadId;
965
+
966
+ // ================= INIT =================
967
+ function generateId() {
968
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
969
+ const r = Math.random() * 16 | 0;
970
+ return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16);
971
+ });
972
+ }
973
+
974
+ function saveMsgs() {
975
+ localStorage.setItem("finai_msgs", JSON.stringify(messages));
976
+ updateRecent();
977
+ }
978
+
979
+ function updateRecent() {
980
+ let chats = JSON.parse(localStorage.getItem("finai_chats") || "[]");
981
+ if (messages.length > 0) {
982
+ const first = messages.find(m => m.role === "user");
983
+ const title = first ? first.content.slice(0, 32) + (first.content.length > 32 ? "..." : "") : "New Chat";
984
+ const idx = chats.findIndex(c => c.id === threadId);
985
+ const entry = { id: threadId, title, time: Date.now() };
986
+ if (idx >= 0) chats[idx] = entry; else chats.unshift(entry);
987
+ chats = chats.slice(0, 15);
988
+ localStorage.setItem("finai_chats", JSON.stringify(chats));
989
+ }
990
+ recentList.innerHTML = "";
991
+ chats.forEach(c => {
992
+ const el = document.createElement("div");
993
+ el.className = `recent-item${c.id === threadId ? " active" : ""}`;
994
+ const d = new Date(c.time);
995
+ const ts = d.toLocaleDateString() === new Date().toLocaleDateString()
996
+ ? d.toLocaleTimeString([], {hour:"2-digit", minute:"2-digit"})
997
+ : d.toLocaleDateString([], {month:"short", day:"numeric"});
998
+ el.innerHTML = `<div class="recent-dot"></div><div><div class="recent-title">${esc(c.title)}</div><div class="recent-time">${ts}</div></div>`;
999
+ el.addEventListener("click", () => loadChat(c.id));
1000
+ recentList.appendChild(el);
1001
+ });
1002
+ }
1003
+
1004
+ function loadChat(id) {
1005
+ const data = localStorage.getItem("finai_msgs_" + id);
1006
+ if (!data) { toast("Chat not found", "err"); return; }
1007
+ threadId = id;
1008
+ messages = JSON.parse(data);
1009
+ localStorage.setItem("finai_tid", threadId);
1010
+ threadDisplay.textContent = threadId;
1011
+ renderMsgs();
1012
+ updateRecent();
1013
+ sidebar.classList.remove("open");
1014
+ sidebarOverlay.classList.remove("show");
1015
+ }
1016
+
1017
+ function renderMsgs() {
1018
+ messagesEl.innerHTML = "";
1019
+ if (messages.length === 0) {
1020
+ messagesEl.appendChild(emptyState);
1021
+ chatHeader.style.display = "none";
1022
+ return;
1023
+ }
1024
+ chatHeader.style.display = "flex";
1025
+ messages.forEach(m => appendMsg(m.role, m.content, m.meta, m.perf, false));
1026
+ scrollBottom();
1027
+ }
1028
+
1029
+ // ================= HELPERS =================
1030
+ function toast(msg, type="info") {
1031
+ const el = document.createElement("div");
1032
+ el.className = `toast ${type}`;
1033
+ const icon = type === "ok" ? "✓" : type === "err" ? "✕" : "•";
1034
+ el.innerHTML = `<span style="font-weight:700;font-size:14px;">${icon}</span> ${msg}`;
1035
+ toasts.appendChild(el);
1036
+ setTimeout(() => { el.style.opacity="0"; el.style.transform="translateX(20px)"; setTimeout(()=>el.remove(),300); }, 4000);
1037
+ }
1038
+
1039
+ function setStatus(text, active=false) {
1040
+ statusText.textContent = text;
1041
+ statusDot.classList.toggle("active", active);
1042
+ }
1043
+
1044
+ function scrollBottom() { messagesEl.scrollTop = messagesEl.scrollHeight; }
1045
+ function esc(t) { const d=document.createElement("div"); d.textContent=t; return d.innerHTML; }
1046
+
1047
+ function fmtMd(t) {
1048
+ if (!t) return "";
1049
+ return t
1050
+ .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
1051
+ .replace(/\*(.+?)\*/g, "<em>$1</em>")
1052
+ .replace(/`([^`]+)`/g, "<code>$1</code>")
1053
+ .replace(/\n/g, "<p></p>");
1054
+ }
1055
+
1056
+ // ================= MESSAGES =================
1057
+ function appendMsg(role, content, meta=null, perf=null, animate=true) {
1058
+ if (messages.length === 0 && emptyState.parentNode) {
1059
+ emptyState.remove();
1060
+ chatHeader.style.display = "flex";
1061
+ }
1062
+ const msg = document.createElement("div");
1063
+ msg.className = `msg${animate?" fade-in":""}`;
1064
+
1065
+ const av = document.createElement("div");
1066
+ av.className = `msg-avatar ${role}`;
1067
+ av.textContent = role === "user" ? "👤" : "🤖";
1068
+
1069
+ const body = document.createElement("div");
1070
+ body.className = "msg-body";
1071
+
1072
+ const head = document.createElement("div");
1073
+ head.className = "msg-head";
1074
+ const tm = new Date().toLocaleTimeString([], {hour:"2-digit", minute:"2-digit"});
1075
+ head.innerHTML = `<span class="msg-name">${role==="user"?"You":"FinAI"}</span><span class="msg-time">${tm}</span>`;
1076
+ body.appendChild(head);
1077
+
1078
+ const bubble = document.createElement("div");
1079
+ bubble.className = "msg-text";
1080
+ bubble.innerHTML = fmtMd(content);
1081
+ body.appendChild(bubble);
1082
+
1083
+ if (perf && role === "assistant") {
1084
+ const p = document.createElement("div");
1085
+ p.className = "perf";
1086
+ p.innerHTML = `
1087
+ <div class="perf-item"><span class="perf-dot"></span>${perf.tokens} tokens</div>
1088
+ <div class="perf-item">${perf.tps.toFixed(1)} tok/s</div>
1089
+ <div class="perf-item">${perf.latency.toFixed(2)}s</div>
1090
+ `;
1091
+ body.appendChild(p);
1092
+ }
1093
+
1094
+ if (meta && role === "assistant") {
1095
+ body.appendChild(makeRag(meta));
1096
+ }
1097
+
1098
+ msg.appendChild(av);
1099
+ msg.appendChild(body);
1100
+ messagesEl.appendChild(msg);
1101
+ scrollBottom();
1102
+ return msg;
1103
+ }
1104
+
1105
+ function makeRag(meta) {
1106
+ const card = document.createElement("div");
1107
+ card.className = "rag";
1108
+ const used = meta.used_rag;
1109
+ const sources = meta.sources || [];
1110
+
1111
+ const toggle = document.createElement("div");
1112
+ toggle.className = "rag-toggle";
1113
+ const bCls = used ? "rag-active" : "rag-direct";
1114
+ const bTxt = used ? `📄 RAG · ${sources.length} source${sources.length!==1?"s":""}` : "🧠 Direct";
1115
+ toggle.innerHTML = `<span class="rag-badge ${bCls}">${bTxt}</span><span class="rag-arrow">▼</span>`;
1116
+
1117
+ const body = document.createElement("div");
1118
+ body.className = "rag-body";
1119
+
1120
+ if (used && sources.length) {
1121
+ sources.forEach((s,i) => {
1122
+ const row = document.createElement("div");
1123
+ row.className = "source";
1124
+ row.style.animationDelay = `${i*0.06}s`;
1125
+ row.innerHTML = `
1126
+ <div class="source-icon">📄</div>
1127
+ <div class="source-info">
1128
+ <div class="source-name">${esc(s.document||"Unknown")}</div>
1129
+ <div class="source-meta">Page ${s.page||"?"}${s.chunk_index!==undefined?` · Chunk ${s.chunk_index}`:""}</div>
1130
+ </div>
1131
+ <div class="source-score">${(s.score||0.95).toFixed(3)}</div>
1132
+ `;
1133
+ body.appendChild(row);
1134
+ });
1135
+ } else if (!used) {
1136
+ const info = document.createElement("div");
1137
+ info.style.cssText = "font-size:11px;color:var(--text-dim);padding:4px 0;";
1138
+ info.textContent = "Generated from fine-tuned Qwen model knowledge without document retrieval.";
1139
+ body.appendChild(info);
1140
+ }
1141
+
1142
+ const foot = document.createElement("div");
1143
+ foot.className = "rag-footer";
1144
+ foot.innerHTML = `<span>Thread: ${meta.thread_id||threadId}</span><span>${new Date().toLocaleTimeString()}</span>`;
1145
+ body.appendChild(foot);
1146
+
1147
+ toggle.addEventListener("click", () => {
1148
+ const open = body.classList.contains("open");
1149
+ body.classList.toggle("open");
1150
+ toggle.classList.toggle("open");
1151
+ toggle.querySelector(".rag-arrow").textContent = open ? "▼" : "▲";
1152
+ });
1153
+
1154
+ card.appendChild(toggle);
1155
+ card.appendChild(body);
1156
+ return card;
1157
+ }
1158
+
1159
+ // ================= CHAT =================
1160
+ async function sendMsg() {
1161
+ const text = inputField.value.trim();
1162
+ if (!text || isStreaming) return;
1163
+
1164
+ inputField.value = "";
1165
+ inputField.style.height = "auto";
1166
+ isStreaming = true;
1167
+ sendBtn.disabled = true;
1168
+ streamStart = performance.now();
1169
+ tokCount = 0;
1170
+ setStatus("Generating...", true);
1171
+
1172
+ messages.push({role:"user", content:text});
1173
+ saveMsgs();
1174
+ appendMsg("user", text);
1175
+
1176
+ const asst = {role:"assistant", content:"", meta:null, perf:null};
1177
+ messages.push(asst);
1178
+ const msgDiv = appendMsg("assistant", "", null, null);
1179
+ const bubble = msgDiv.querySelector(".msg-text");
1180
+ bubble.innerHTML = `<div class="typing"><span></span><span></span><span></span></div>`;
1181
+
1182
+ try {
1183
+ const res = await fetch(`${API_BASE}/chat/stream`, {
1184
+ method: "POST",
1185
+ headers: {"Content-Type":"application/json"},
1186
+ body: JSON.stringify({message:text, thread_id:threadId})
1187
+ });
1188
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
1189
+
1190
+ const reader = res.body.getReader();
1191
+ const decoder = new TextDecoder();
1192
+ let buf = "", full = "", meta = null;
1193
+
1194
+ while (true) {
1195
+ const {done, value} = await reader.read();
1196
+ if (done) break;
1197
+ buf += decoder.decode(value, {stream:true});
1198
+ const lines = buf.split("\n");
1199
+ buf = lines.pop();
1200
+ for (const line of lines) {
1201
+ if (!line.trim()) continue;
1202
+ const clean = line.replace(/^data: /,"").trim();
1203
+ if (!clean) continue;
1204
+ try {
1205
+ const ev = JSON.parse(clean);
1206
+ if (ev.type === "token") {
1207
+ if (full === "") bubble.innerHTML = "";
1208
+ full += ev.content;
1209
+ tokCount++;
1210
+ const elapsed = (performance.now()-streamStart)/1000;
1211
+ const tps = elapsed>0 ? tokCount/elapsed : 0;
1212
+ setStatus(`${tokCount} tokens · ${tps.toFixed(1)} tok/s`, true);
1213
+ bubble.innerHTML = fmtMd(full) + '<span class="stream-cursor"></span>';
1214
+ scrollBottom();
1215
+ } else if (ev.type === "metadata") {
1216
+ meta = ev;
1217
+ } else if (ev.type === "done") {
1218
+ break;
1219
+ } else if (ev.type === "error") {
1220
+ throw new Error(ev.message);
1221
+ }
1222
+ } catch(e) {}
1223
+ }
1224
+ }
1225
+
1226
+ const total = (performance.now()-streamStart)/1000;
1227
+ const tps = total>0 ? tokCount/total : 0;
1228
+ bubble.innerHTML = fmtMd(full);
1229
+ asst.content = full;
1230
+ asst.meta = meta;
1231
+ asst.perf = {tokens:tokCount, tps, latency:total};
1232
+ saveMsgs();
1233
+ localStorage.setItem("finai_msgs_"+threadId, JSON.stringify(messages));
1234
+
1235
+ if (meta) {
1236
+ const rag = makeRag(meta);
1237
+ const body = msgDiv.querySelector(".msg-body");
1238
+ const perfEl = body.querySelector(".perf");
1239
+ body.insertBefore(rag, perfEl?.nextSibling || null);
1240
+ }
1241
+ setStatus("Ready", false);
1242
+
1243
+ } catch(err) {
1244
+ bubble.innerHTML = `<span style="color:#ef4444">Error: ${esc(err.message)}</span>`;
1245
+ asst.content = `Error: ${err.message}`;
1246
+ saveMsgs();
1247
+ toast(err.message, "err");
1248
+ setStatus("Error", false);
1249
+ } finally {
1250
+ isStreaming = false;
1251
+ sendBtn.disabled = false;
1252
+ inputField.focus();
1253
+ }
1254
+ }
1255
+
1256
+ // ================= UPLOAD =================
1257
+ uploadZone.addEventListener("click", () => fileInput.click());
1258
+ uploadZone.addEventListener("dragover", e => { e.preventDefault(); uploadZone.classList.add("dragover"); });
1259
+ uploadZone.addEventListener("dragleave", () => uploadZone.classList.remove("dragover"));
1260
+ uploadZone.addEventListener("drop", e => {
1261
+ e.preventDefault();
1262
+ uploadZone.classList.remove("dragover");
1263
+ if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]);
1264
+ });
1265
+ fileInput.addEventListener("change", () => { if (fileInput.files.length) handleFile(fileInput.files[0]); });
1266
+
1267
+ function handleFile(file) {
1268
+ if (file.type !== "application/pdf" && !file.name.endsWith(".pdf")) {
1269
+ toast("Only PDF files are supported", "err");
1270
+ return;
1271
+ }
1272
+ currentFile = file;
1273
+ uzText.textContent = file.name;
1274
+ uzHint.textContent = `${(file.size/1024).toFixed(1)} KB · Click Upload to index`;
1275
+ uploadZone.style.borderColor = "var(--border-hover)";
1276
+ uploadDone.classList.remove("show");
1277
+ }
1278
+
1279
+ uploadBtn.addEventListener("click", async () => {
1280
+ if (!currentFile) { toast("Select a PDF file first", "info"); return; }
1281
+
1282
+ uploadProgress.classList.add("show");
1283
+ uploadDone.classList.remove("show");
1284
+ uploadBtn.disabled = true;
1285
+
1286
+ const steps = [
1287
+ {text:"Uploading PDF...", pct:"15%"},
1288
+ {text:"Extracting text...", pct:"30%"},
1289
+ {text:"Splitting into chunks...", pct:"50%"},
1290
+ {text:"Creating embeddings...", pct:"70%"},
1291
+ {text:"Storing in vector DB...", pct:"90%"},
1292
+ {text:"Finalizing...", pct:"98%"}
1293
+ ];
1294
+
1295
+ let idx = 0;
1296
+ const interval = setInterval(() => {
1297
+ if (idx < steps.length) {
1298
+ progressStep.innerHTML = `<div class="spinner"></div> ${steps[idx].text}`;
1299
+ progressPct.textContent = steps[idx].pct;
1300
+ progressFill.style.width = steps[idx].pct;
1301
+ idx++;
1302
+ }
1303
+ }, 500);
1304
+
1305
+ const formData = new FormData();
1306
+ formData.append("file", currentFile);
1307
+
1308
+ try {
1309
+ const res = await fetch(`${API_BASE}/upload`, {method:"POST", body:formData});
1310
+ const data = await res.json();
1311
+ clearInterval(interval);
1312
+
1313
+ if (res.ok) {
1314
+ progressFill.style.width = "100%";
1315
+ progressStep.innerHTML = `<div class="spinner"></div> Done!`;
1316
+ progressPct.textContent = "100%";
1317
+
1318
+ setTimeout(() => {
1319
+ uploadProgress.classList.remove("show");
1320
+ udText.textContent = `Uploaded: ${data.filename || currentFile.name}`;
1321
+ udMeta.textContent = `${data.chunks || "?"} chunks indexed · Ready for queries`;
1322
+ uploadDone.classList.add("show");
1323
+ toast(`"${currentFile.name}" indexed successfully`, "ok");
1324
+
1325
+ currentFile = null;
1326
+ uzText.textContent = "Drop PDF or click to browse";
1327
+ uzHint.textContent = "Financial reports, statements, tax docs";
1328
+ uploadZone.style.borderColor = "";
1329
+ fileInput.value = "";
1330
+ uploadBtn.disabled = false;
1331
+ }, 600);
1332
+ } else {
1333
+ throw new Error(data.detail || data.message || "Upload failed");
1334
+ }
1335
+ } catch(err) {
1336
+ clearInterval(interval);
1337
+ uploadProgress.classList.remove("show");
1338
+ progressFill.style.width = "0%";
1339
+ toast(err.message, "err");
1340
+ uploadBtn.disabled = false;
1341
+ }
1342
+ });
1343
+
1344
+ // ================= RESET =================
1345
+ resetBtn.addEventListener("click", async () => {
1346
+ if (!confirm("Delete all uploaded documents? This cannot be undone.")) return;
1347
+ try {
1348
+ const res = await fetch(`${API_BASE}/reset`, {method:"DELETE"});
1349
+ if (res.ok) { toast("All documents deleted", "ok"); uploadDone.classList.remove("show"); }
1350
+ else throw new Error("Failed");
1351
+ } catch(err) { toast(err.message, "err"); }
1352
+ });
1353
+
1354
+ // ================= NEW CHAT =================
1355
+ newChatBtn.addEventListener("click", () => {
1356
+ if (messages.length > 0) {
1357
+ localStorage.setItem("finai_msgs_"+threadId, JSON.stringify(messages));
1358
+ }
1359
+ messages = [];
1360
+ threadId = generateId();
1361
+ localStorage.setItem("finai_tid", threadId);
1362
+ localStorage.removeItem("finai_msgs");
1363
+ threadDisplay.textContent = threadId;
1364
+ messagesEl.innerHTML = "";
1365
+ messagesEl.appendChild(emptyState);
1366
+ chatHeader.style.display = "none";
1367
+ updateRecent();
1368
+ toast("New chat started", "info");
1369
+ });
1370
+
1371
+ // ================= COPY =================
1372
+ copyThread.addEventListener("click", () => {
1373
+ navigator.clipboard.writeText(threadId).then(() => {
1374
+ copyThread.textContent = "Copied!";
1375
+ setTimeout(() => copyThread.textContent = "Copy", 1200);
1376
+ });
1377
+ });
1378
+
1379
+ // ================= INPUT =================
1380
+ inputField.addEventListener("input", () => {
1381
+ inputField.style.height = "auto";
1382
+ inputField.style.height = Math.min(inputField.scrollHeight, 160) + "px";
1383
+ });
1384
+
1385
+ inputField.addEventListener("keydown", e => {
1386
+ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMsg(); }
1387
+ });
1388
+
1389
+ sendBtn.addEventListener("click", sendMsg);
1390
+
1391
+ // ================= CHIPS =================
1392
+ document.getElementById("emptyChips").addEventListener("click", e => {
1393
+ const chip = e.target.closest(".chip");
1394
+ if (chip) {
1395
+ inputField.value = chip.dataset.q;
1396
+ inputField.focus();
1397
+ inputField.style.height = "auto";
1398
+ inputField.style.height = Math.min(inputField.scrollHeight, 160) + "px";
1399
+ }
1400
+ });
1401
+
1402
+ // ================= MOBILE =================
1403
+ mobileToggle.addEventListener("click", () => {
1404
+ sidebar.classList.toggle("open");
1405
+ sidebarOverlay.classList.toggle("show");
1406
+ });
1407
+
1408
+ sidebarOverlay.addEventListener("click", () => {
1409
+ sidebar.classList.remove("open");
1410
+ sidebarOverlay.classList.remove("show");
1411
+ });
1412
+
1413
+ // ================= BOOT =================
1414
+ updateRecent();
1415
+ if (messages.length > 0) renderMsgs();
1416
+ </script>
1417
+ </body>
1418
+ </html>
requirements.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #llama-cpp-python
2
+ uvicorn
3
+ fastapi
4
+ pydantic
5
+ langchain
6
+ langgraph
7
+ langchain-community
8
+ langchain-core
9
+ python-multipart
10
+ langchain-huggingface
11
+ huggingface_hub
12
+ rank-bm25
13
+ sentence-transformers
14
+ pypdf
15
+ pinecone-client
16
+ pinecone
17
+ langchain-pinecone
18
+ python-dotenv
scripts/__pycache__/load_llm.cpython-311.pyc ADDED
Binary file (1.91 kB). View file
 
scripts/__pycache__/main.cpython-311.pyc ADDED
Binary file (6.21 kB). View file
 
scripts/__pycache__/rag.cpython-311.pyc ADDED
Binary file (9.61 kB). View file
 
scripts/load_llm.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # file: model_loader.py
2
+ from langchain_community.chat_models import ChatLlamaCpp
3
+ from huggingface_hub import hf_hub_download
4
+ from langchain_core.callbacks import StreamingStdOutCallbackHandler
5
+ import logging
6
+ import os
7
+
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
+
11
+ _llm_instance = None
12
+
13
+ def get_model():
14
+ try:
15
+ global _llm_instance
16
+
17
+ if _llm_instance is None:
18
+ model_path = hf_hub_download(
19
+ repo_id="junaid17/qwen2.5-finance-assistant-gguf",
20
+ filename="qwen2.5-finance-assistant-q4_k_m.gguf",
21
+ )
22
+
23
+ logger.info(f"Loading model from: {model_path}")
24
+
25
+ _llm_instance = ChatLlamaCpp(
26
+ model_path=model_path,
27
+ temperature=0.5,
28
+ max_tokens=2048,
29
+ n_ctx=4096,
30
+ n_batch=512,
31
+ n_threads=max(4, os.cpu_count() // 2),
32
+ n_gpu_layers=0,
33
+ verbose=False,
34
+ streaming=True,
35
+ callbacks=[StreamingStdOutCallbackHandler()]
36
+ )
37
+
38
+ logger.info("Model loaded successfully!")
39
+ except Exception as e:
40
+ logger.exception(f"Error while loading the model, {str(e)}")
41
+
42
+ return _llm_instance
scripts/main.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langgraph.graph import StateGraph, START, END
2
+ from typing import TypedDict, Annotated
3
+ from scripts.rag import RagPipeline
4
+ from scripts.load_llm import get_model
5
+ from langchain_openai import OpenAIEmbeddings, ChatOpenAI
6
+ from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
7
+ from langgraph.graph.message import add_messages
8
+ from langgraph.checkpoint.memory import MemorySaver
9
+
10
+
11
+ # Initialized Rag and llm
12
+ RAG = None
13
+ llm = get_model()
14
+
15
+ def set_rag_instance(rag_instance):
16
+ global RAG
17
+ RAG = rag_instance
18
+
19
+ # Initializing ChatState
20
+ class ChatState(TypedDict):
21
+ query: str
22
+ retrieved_docs: list
23
+ context: str
24
+ use_rag: bool
25
+ final_prompt: str
26
+ sources: list
27
+
28
+
29
+ # Making Nodes
30
+
31
+ def retrieve_node(state):
32
+ query = state["query"]
33
+
34
+ docs = RAG.hybrid_retrieve(query=query, dense_k=3, top_k=3)
35
+
36
+ return {"retrieved_docs": docs}
37
+
38
+
39
+ def relevance_node(state):
40
+ docs = state["retrieved_docs"]
41
+
42
+ use_rag = False
43
+
44
+ if docs:
45
+ meaningful_docs = [
46
+ d for d in docs
47
+ if len(d.page_content.strip()) > 50
48
+ ]
49
+
50
+ use_rag = len(meaningful_docs) > 0
51
+
52
+ return {"use_rag": use_rag}
53
+
54
+
55
+ def build_context_node(state):
56
+ docs = state["retrieved_docs"]
57
+
58
+ context = ""
59
+ sources = []
60
+
61
+ for doc in docs:
62
+ source = doc.metadata.get("source", "unknown")
63
+ page = doc.metadata.get("page", "unknown")
64
+
65
+ sources.append({
66
+ "document": source,
67
+ "page": page
68
+ })
69
+
70
+ context += f"""
71
+ SOURCE: {source}
72
+ PAGE: {page}
73
+
74
+ CONTENT:
75
+ {doc.page_content}
76
+ """
77
+
78
+ return {
79
+ "context": context,
80
+ "sources": sources
81
+ }
82
+
83
+
84
+ def rag_prompt_node(state):
85
+ query = state["query"]
86
+ context = state["context"]
87
+
88
+ prompt = f"""
89
+ You are a financial intelligence assistant.
90
+
91
+ Use ONLY the provided context.
92
+
93
+ If context is insufficient, say so.
94
+
95
+ Context:
96
+ {context}
97
+
98
+ Question:
99
+ {query}
100
+ """
101
+
102
+ return {
103
+ "final_prompt": prompt
104
+ }
105
+
106
+ def direct_prompt_node(state):
107
+ query = state["query"]
108
+
109
+ prompt = f"""
110
+ You are a financial intelligence assistant.
111
+ your job is to answer the user's question to the best of your ability.
112
+
113
+ Question:
114
+ {query}
115
+ """
116
+
117
+ return {
118
+ "final_prompt": prompt,
119
+ "sources": []
120
+ }
121
+
122
+ def route_decision(state):
123
+ if state["use_rag"]:
124
+ return "build_context"
125
+
126
+ return "direct_prompt"
127
+
128
+
129
+
130
+ memory = MemorySaver()
131
+ workflow = StateGraph(ChatState)
132
+
133
+ workflow.add_node("retrieve", retrieve_node)
134
+ workflow.add_node("relevance", relevance_node)
135
+ workflow.add_node("build_context", build_context_node)
136
+ workflow.add_node("rag_prompt", rag_prompt_node)
137
+ workflow.add_node("direct_prompt", direct_prompt_node)
138
+
139
+ workflow.set_entry_point("retrieve")
140
+
141
+ workflow.add_edge("retrieve", "relevance")
142
+
143
+ workflow.add_conditional_edges(
144
+ "relevance",
145
+ route_decision,
146
+ {
147
+ "build_context": "build_context",
148
+ "direct_prompt": "direct_prompt"
149
+ }
150
+ )
151
+
152
+ workflow.add_edge("build_context", "rag_prompt")
153
+ workflow.add_edge("rag_prompt", END)
154
+ workflow.add_edge("direct_prompt", END)
155
+
156
+ graph_app = workflow.compile(checkpointer=memory)
157
+
158
+ def stream_chat_response(user_message: str, thread_id: str):
159
+ config = {
160
+ "configurable": {
161
+ "thread_id": thread_id
162
+ }
163
+ }
164
+
165
+ state = graph_app.invoke(
166
+ {"query": user_message},
167
+ config=config
168
+ )
169
+
170
+ metadata = {
171
+ "used_rag": state["use_rag"],
172
+ "sources": state["sources"],
173
+ "thread_id": thread_id
174
+ }
175
+
176
+ for chunk in llm.stream(
177
+ [HumanMessage(content=state["final_prompt"])]
178
+ ):
179
+ if chunk.content:
180
+ yield {
181
+ "token": chunk.content,
182
+ "metadata": metadata
183
+ }
scripts/rag.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from pinecone import Pinecone, ServerlessSpec
4
+ from dotenv import load_dotenv
5
+ from langchain_huggingface import HuggingFaceEmbeddings
6
+ from langchain_pinecone import PineconeVectorStore
7
+ from langchain_community.retrievers import BM25Retriever
8
+ from langchain_community.document_loaders import PyPDFLoader
9
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
10
+
11
+ load_dotenv()
12
+
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
+
16
+ INDEX_NAME = "finance-rag"
17
+
18
+
19
+ class RagPipeline:
20
+
21
+ def __init__(self, index_name=INDEX_NAME, embedding_model="BAAI/bge-base-en-v1.5"):
22
+ api_key = os.getenv("PINECONE_API_KEY")
23
+
24
+ if not api_key:
25
+ raise ValueError("PINECONE_API_KEY not found in environment variables.")
26
+
27
+ self.pc = Pinecone(api_key=api_key)
28
+ self.index_name = index_name
29
+ self.bm25_retriever = None
30
+ self.cached_docs = [] # FIX
31
+
32
+ self._ensure_index()
33
+
34
+ logger.info("Loading embedding model...")
35
+ self.embeddings = HuggingFaceEmbeddings(model_name=embedding_model)
36
+ logger.info("Embedding model loaded successfully.")
37
+
38
+ def _ensure_index(self):
39
+ existing_indexes = self.pc.list_indexes().names()
40
+
41
+ if self.index_name not in existing_indexes:
42
+ logger.info(f"Creating Pinecone index: {self.index_name}")
43
+
44
+ self.pc.create_index(
45
+ name=self.index_name,
46
+ dimension=768,
47
+ metric="cosine",
48
+ spec=ServerlessSpec(
49
+ cloud="aws",
50
+ region="us-east-1"
51
+ )
52
+ )
53
+
54
+ logger.info("Pinecone index created successfully.")
55
+
56
+ else:
57
+ logger.info(f"Pinecone index '{self.index_name}' already exists.")
58
+
59
+ def vector_store(self):
60
+ return PineconeVectorStore(
61
+ index=self.pc.Index(self.index_name),
62
+ embedding=self.embeddings
63
+ )
64
+
65
+ def load_docs(self, pdf_path: str):
66
+ try:
67
+ logger.info(f"Loading PDF: {pdf_path}")
68
+
69
+ loader = PyPDFLoader(pdf_path)
70
+ documents = loader.load()
71
+
72
+ logger.info(f"Loaded {len(documents)} pages.")
73
+
74
+ return documents
75
+
76
+ except Exception as e:
77
+ logger.exception("Error loading PDF.")
78
+ raise e
79
+
80
+ def split_docs(self, docs, chunk_size=1000, chunk_overlap=250):
81
+ try:
82
+ logger.info("Splitting documents into chunks...")
83
+
84
+ splitter = RecursiveCharacterTextSplitter(
85
+ chunk_size=chunk_size,
86
+ chunk_overlap=chunk_overlap
87
+ )
88
+
89
+ split_documents = splitter.split_documents(docs)
90
+
91
+ self.cached_docs = split_documents # FIX
92
+
93
+ logger.info(f"Generated {len(split_documents)} chunks.")
94
+
95
+ return split_documents
96
+
97
+ except Exception:
98
+ logger.exception("Error splitting documents.")
99
+ return None
100
+
101
+ def add_docs(self, split_docs):
102
+ try:
103
+ logger.info("Uploading chunks to Pinecone...")
104
+
105
+ vectorstore = self.vector_store()
106
+ vectorstore.add_documents(split_docs)
107
+
108
+ logger.info("Documents uploaded to Pinecone successfully.")
109
+
110
+ except Exception:
111
+ logger.exception("Error uploading documents to Pinecone.")
112
+
113
+ def delete_all_docs(self):
114
+ try:
115
+ logger.info("Deleting ALL documents from Pinecone index...")
116
+
117
+ index = self.pc.Index(self.index_name)
118
+
119
+ index.delete(delete_all=True)
120
+
121
+ logger.info("All documents deleted successfully.")
122
+
123
+ self.bm25_retriever = None
124
+ self.cached_docs = [] # FIX
125
+
126
+ except Exception:
127
+ logger.exception("Error deleting all documents.")
128
+
129
+ def create_bm25(self, split_docs=None, k=4): # FIX
130
+ try:
131
+ logger.info("Creating BM25 retriever...")
132
+
133
+ docs = split_docs if split_docs is not None else self.cached_docs
134
+
135
+ self.bm25_retriever = BM25Retriever.from_documents(docs)
136
+ self.bm25_retriever.k = k
137
+
138
+ logger.info("BM25 retriever ready.")
139
+
140
+ except Exception:
141
+ logger.exception("Error creating BM25 retriever.")
142
+
143
+ def dense_retriever(self, k=4):
144
+ vectorstore = self.vector_store()
145
+
146
+ return vectorstore.as_retriever(
147
+ search_kwargs={"k": k}
148
+ )
149
+
150
+ def hybrid_retrieve(self, query, dense_k=4, top_k=6):
151
+ try:
152
+ dense_docs = []
153
+
154
+ try:
155
+ dense_docs = self.dense_retriever(k=dense_k).invoke(query)
156
+ except Exception:
157
+ logger.warning("Dense retrieval unavailable.")
158
+
159
+ bm25_docs = []
160
+
161
+ if self.bm25_retriever is None:
162
+ if self.cached_docs:
163
+ logger.info("Rebuilding BM25 retriever.")
164
+ self.create_bm25()
165
+ else:
166
+ logger.warning("No uploaded docs found. Using direct LLM fallback.")
167
+ return []
168
+
169
+ if self.bm25_retriever:
170
+ bm25_docs = self.bm25_retriever.invoke(query)
171
+
172
+ combined = bm25_docs + dense_docs
173
+
174
+ seen = set()
175
+ unique = []
176
+
177
+ for doc in combined:
178
+ text = doc.page_content.strip()
179
+
180
+ if text not in seen:
181
+ seen.add(text)
182
+ unique.append(doc)
183
+
184
+ return unique[:top_k]
185
+
186
+ except Exception:
187
+ logger.exception("Error during hybrid retrieval.")
188
+ return []