CarsonNg commited on
Commit
23a973e
·
verified ·
1 Parent(s): 8d5e36f

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +328 -481
app.py CHANGED
@@ -1,481 +1,328 @@
1
- """
2
- RAG Chat Interface for Hugging Face Space.
3
- Connects to Modal backend for document indexing and question answering.
4
- """
5
- import gradio as gr
6
- import requests
7
- import os
8
- import time
9
- from typing import Tuple, Optional, Generator
10
-
11
- # ---------- Configuration Constants ----------
12
- BACKEND_URLS = {
13
- "query": "https://carsonbytes--query.modal.run",
14
- "debug": "https://carsonbytes--debug.modal.run",
15
- "upload": "https://carsonbytes--upload.modal.run",
16
- "health": "https://carsonbytes--health.modal.run",
17
- }
18
-
19
- WARMUP_TIMEOUT = 20 # seconds
20
- QUERY_TIMEOUT = 60 # seconds
21
- UPLOAD_TIMEOUT = 120 # seconds
22
- MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB in bytes
23
- MAX_QUESTION_LENGTH = 1000 # characters
24
-
25
-
26
- # ---------- Utility Functions ----------
27
- def get_backend_url(endpoint: str) -> str:
28
- """Get the URL for a specific backend endpoint."""
29
- return BACKEND_URLS.get(endpoint, BACKEND_URLS["query"])
30
-
31
-
32
- def format_file_size(size_bytes: int) -> str:
33
- """Format file size in human-readable format."""
34
- if size_bytes < 1024:
35
- return f"{size_bytes} bytes"
36
- elif size_bytes < 1024 * 1024:
37
- return f"{size_bytes / 1024:.1f} KB"
38
- else:
39
- return f"{size_bytes / (1024 * 1024):.1f} MB"
40
-
41
-
42
- # ---------- Backend Communication Functions ----------
43
- def check_health() -> bool:
44
- """Check if backend is running and responsive."""
45
- try:
46
- resp = requests.get(
47
- get_backend_url("health"),
48
- timeout=WARMUP_TIMEOUT
49
- )
50
- return resp.status_code == 200
51
- except Exception:
52
- return False
53
-
54
-
55
- def check_index_status() -> Tuple[bool, Optional[str]]:
56
- """
57
- Check if an index exists by sending a test query.
58
- Returns tuple: (index_exists: bool, filename: str or None)
59
- """
60
- try:
61
- response = requests.post(
62
- get_backend_url("query"),
63
- json={"question": "__check_index_status__"},
64
- timeout=QUERY_TIMEOUT
65
- )
66
- response.raise_for_status()
67
- data = response.json()
68
-
69
- answer = data.get("answer", "")
70
-
71
- # Check if "No document uploaded" is in the response
72
- if "no document uploaded" in answer.lower():
73
- return False, None
74
-
75
- # Index exists - try to get filename from response metadata if available
76
- filename = data.get("indexed_filename", None)
77
- return True, filename
78
-
79
- except Exception:
80
- # On error, assume no index
81
- return False, None
82
-
83
-
84
- def upload_document(file_obj) -> Tuple[bool, Optional[str], str]:
85
- """
86
- Upload a document to the backend for indexing.
87
- Accepts file object, extracts text content.
88
- Returns tuple: (success: bool, filename: str, message: str)
89
- """
90
- if file_obj is None:
91
- return False, None, "❌ No file selected."
92
-
93
- # Check file size
94
- file_size = os.path.getsize(file_obj.name) if os.path.exists(file_obj.name) else 0
95
- if file_size > MAX_FILE_SIZE:
96
- return False, None, f"❌ File too large. Maximum size is {format_file_size(MAX_FILE_SIZE)}."
97
-
98
- try:
99
- # Read file content as text
100
- with open(file_obj.name, "r", encoding="utf-8") as f:
101
- text_content = f.read()
102
-
103
- if not text_content or not text_content.strip():
104
- return False, None, "❌ File is empty."
105
-
106
- filename = os.path.basename(file_obj.name)
107
-
108
- # Send JSON payload to backend
109
- payload = {
110
- "text": text_content,
111
- "filename": filename
112
- }
113
- response = requests.post(
114
- get_backend_url("upload"),
115
- json=payload,
116
- timeout=UPLOAD_TIMEOUT
117
- )
118
- response.raise_for_status()
119
- data = response.json()
120
-
121
- if data.get("status") == "success":
122
- indexed_name = data.get("message", filename)
123
- return True, indexed_name, f"✅ Indexed {indexed_name}"
124
- else:
125
- return False, None, f"❌ {data.get('message', 'Upload failed')}"
126
-
127
- except requests.exceptions.Timeout:
128
- return False, None, "❌ Upload timed out. Please try a smaller file."
129
- except UnicodeDecodeError:
130
- return False, None, "❌ Could not decode file. Please use a text file."
131
- except Exception as e:
132
- return False, None, f"❌ Upload failed: {str(e)}"
133
-
134
-
135
- def query_backend(message: str, history: list) -> Generator[Tuple[str, list], None, None]:
136
- """
137
- Send a question to the backend and yield responses.
138
- Yields status updates and final answer.
139
- """
140
- if not message or not message.strip():
141
- yield "", history
142
- return
143
-
144
- # Validate question length
145
- if len(message) > MAX_QUESTION_LENGTH:
146
- message = message[:MAX_QUESTION_LENGTH]
147
-
148
- # Add user message to history
149
- history.append((message, None))
150
-
151
- # Initial thinking message
152
- yield "🤔 Thinking...", history
153
-
154
- try:
155
- response = requests.post(
156
- get_backend_url("query"),
157
- json={"question": message},
158
- timeout=QUERY_TIMEOUT
159
- )
160
- response.raise_for_status()
161
- data = response.json()
162
-
163
- answer = data.get("answer", "⚠️ Received an empty response.")
164
-
165
- # Update history with the answer
166
- history[-1] = (message, answer)
167
- yield "", history
168
-
169
- except requests.exceptions.Timeout:
170
- history[-1] = (message, "❌ Query timed out. Please try again.")
171
- yield "", history
172
- except requests.exceptions.ConnectionError:
173
- history[-1] = (message, "❌ Cannot connect to backend. Please try again later.")
174
- yield "", history
175
- except Exception as e:
176
- history[-1] = (message, f"❌ Error: {str(e)}")
177
- yield "", history
178
-
179
-
180
- def update_index_display(filename: Optional[str], is_indexed: bool) -> str:
181
- """Update the index status display based on current state."""
182
- if is_indexed and filename:
183
- return f"**Indexed file:** 📄 {filename}"
184
- elif is_indexed:
185
- return "**Indexed file:** ✅ Document indexed (filename unknown)"
186
- return "**Indexed file:** No document indexed"
187
-
188
-
189
- def initialize_index_state() -> Tuple[bool, Optional[str], str]:
190
- """Check index status on page load and return state values."""
191
- is_indexed, filename = check_index_status()
192
-
193
- # Update display text based on index status
194
- if is_indexed and filename:
195
- display_text = f"**Indexed file:** 📄 {filename}"
196
- elif is_indexed:
197
- display_text = "**Indexed file:** ✅ Document indexed (filename unknown)"
198
- else:
199
- display_text = "**Indexed file:** No document indexed"
200
-
201
- return is_indexed, filename, display_text
202
-
203
-
204
- def warmup_with_status(start_time: float) -> Generator[Tuple[str, bool, str, float], None, None]:
205
- """
206
- Warmup function to initialize the backend on Space startup.
207
- Returns tuple: (status_message: str, success: bool, loading_text: str, current_time: float)
208
- Only calls the health endpoint - no query endpoint.
209
- Displays elapsed time during warmup.
210
- Waits indefinitely for backend to become ready.
211
- """
212
- elapsed = time.time() - start_time
213
- yield f"⏳ Loading GPU snapshots to speed up cold startup. It would take < 20 seconds... ({elapsed:.1f}s elapsed)", False, f"⏳ Warming up... ({elapsed:.1f}s)", start_time
214
-
215
- try:
216
- resp = requests.get(
217
- get_backend_url("health"),
218
- timeout=None # Wait indefinitely
219
- )
220
-
221
- elapsed = time.time() - start_time
222
- if resp.status_code == 200:
223
- yield f"✅ Backend ready ({elapsed:.1f}s)", True, f"✅ Backend ready ({elapsed:.1f}s)", start_time
224
- else:
225
- yield f"❌ Backend returned status {resp.status_code} ({elapsed:.1f}s)", False, f"❌ Backend returned status ({elapsed:.1f}s)", start_time
226
-
227
- except requests.exceptions.ConnectionError:
228
- elapsed = time.time() - start_time
229
- yield f"❌ Cannot connect to backend. Please check your connection. ({elapsed:.1f}s)", False, f"❌ Cannot connect ({elapsed:.1f}s)", start_time
230
- except Exception as e:
231
- elapsed = time.time() - start_time
232
- yield f"❌ Warm-up failed: {str(e)} ({elapsed:.1f}s)", False, f"❌ Warm-up failed: {str(e)} ({elapsed:.1f}s)", start_time
233
-
234
-
235
- # ---------- UI Components ----------
236
- def create_warmup_page() -> Tuple[gr.Blocks, gr.State]:
237
- """Create the warm-up landing page."""
238
- with gr.Blocks(fill_height=True) as demo:
239
- # Header
240
- gr.Markdown(
241
- """
242
- <div style="text-align: center; margin-top: 15vh;">
243
- <h1 style="font-size: 3em; margin-bottom: 0.5em;">📚 RAG Chat Interface</h1>
244
- <p style="font-size: 1.2em; color: #666;">Powered by Qwen 2.5 & Modal</p>
245
- <br>
246
- <p style="color: #555;">Click the button below to warm up the backend and get started.</p>
247
- </div>
248
- """,
249
- elem_classes=["center"]
250
- )
251
-
252
- with gr.Row():
253
- with gr.Column():
254
- warmup_btn = gr.Button(
255
- "🔥 Warm Up Backend",
256
- variant="primary",
257
- size="lg",
258
- elem_classes=["warmup-btn"]
259
- )
260
-
261
- warmup_status = gr.Markdown("", elem_classes=["status-msg"])
262
-
263
- # Hidden state for navigation
264
- ready = gr.State(False)
265
- start_time = gr.State(time.time())
266
-
267
- warmup_btn.click(
268
- fn=lambda: time.time(),
269
- outputs=[start_time]
270
- ).then(
271
- fn=warmup_with_status,
272
- inputs=[start_time],
273
- outputs=[warmup_status, ready, warmup_btn, start_time]
274
- )
275
-
276
- return demo, ready
277
-
278
-
279
- def create_chat_page() -> gr.Blocks:
280
- """Create the main chat interface page."""
281
- with gr.Blocks() as demo:
282
- # Header
283
- gr.Markdown(
284
- """
285
- <div style="text-align: center; margin-bottom: 2em;">
286
- <h1>📚 RAG Chat Interface</h1>
287
- <p style="color: #666;">Upload a document, then ask questions about its content.</p>
288
- </div>
289
- """
290
- )
291
-
292
- # ---------- State ----------
293
- document_indexed = gr.State(False)
294
- indexed_filename = gr.State(None)
295
-
296
- # ---------- Index Status Display ----------
297
- with gr.Row():
298
- with gr.Column():
299
- index_status = gr.Markdown("**Indexed file:** No document indexed")
300
-
301
- # ---------- File Upload Section ----------
302
- with gr.Row():
303
- with gr.Column(scale=2):
304
- file_input = gr.File(
305
- label="Upload .txt file",
306
- file_types=[".txt"],
307
- file_count="single",
308
- show_label=True
309
- )
310
- with gr.Column(scale=1):
311
- upload_btn = gr.Button("📤 Build Index", variant="primary", size="lg")
312
-
313
- with gr.Row():
314
- upload_status = gr.Markdown("**Status:** Waiting for document...")
315
-
316
- upload_btn.click(
317
- fn=upload_document,
318
- inputs=[file_input],
319
- outputs=[document_indexed, indexed_filename, upload_status]
320
- ).then(
321
- fn=update_index_display,
322
- inputs=[indexed_filename, document_indexed],
323
- outputs=[index_status]
324
- )
325
-
326
- # ---------- Chat Interface Section ----------
327
- gr.Markdown(
328
- """
329
- <div style="margin-top: 2em; margin-bottom: 1em;">
330
- <h3>💬 Ask Questions</h3>
331
- </div>
332
- """
333
- )
334
-
335
- def chat_wrapper(message: str, history: list) -> Generator[Tuple[str, list], None, None]:
336
- """Wrapper that checks if document is indexed before querying."""
337
- if not document_indexed.value:
338
- yield "⚠️ Please upload a document first using the 'Build Index' button.", history
339
- return
340
- yield from query_backend(message, history)
341
-
342
- gr.ChatInterface(
343
- fn=chat_wrapper,
344
- title=None,
345
- description=None,
346
- )
347
-
348
- return demo
349
-
350
-
351
- # Main application
352
- def create_app() -> gr.Blocks:
353
- """Create the main application with page navigation."""
354
- with gr.Blocks() as demo:
355
- # Page state
356
- page = gr.State("warmup")
357
-
358
- # Index state
359
- document_indexed = gr.State(False)
360
- indexed_filename = gr.State(None)
361
-
362
- # Container for page content
363
- page_container = gr.Column()
364
-
365
- with page_container:
366
- # Warmup page
367
- with gr.Column(elem_id="warmup-page") as warmup_page:
368
- gr.Markdown(
369
- """
370
- <div style="text-align: center; margin-top: 15vh;">
371
- <h1 style="font-size: 3em; margin-bottom: 0.5em;">📚 RAG Chat Interface</h1>
372
- <p style="font-size: 1.2em; color: #666;">Powered by Qwen 2.5 & Modal</p>
373
- <br>
374
- <p style="color: #555;">Click the button below to warm up the backend and get started.</p>
375
- </div>
376
- """,
377
- elem_classes=["center"]
378
- )
379
-
380
- with gr.Row():
381
- with gr.Column():
382
- warmup_btn = gr.Button(
383
- "🔥 Warm Up Backend",
384
- variant="primary",
385
- size="lg",
386
- elem_classes=["warmup-btn"]
387
- )
388
-
389
- warmup_status = gr.Markdown("", elem_classes=["status-msg"])
390
-
391
- # Hidden state for navigation
392
- ready = gr.State(False)
393
- start_time = gr.State(time.time())
394
-
395
- warmup_btn.click(
396
- fn=lambda: time.time(),
397
- outputs=[start_time]
398
- ).then(
399
- fn=warmup_with_status,
400
- inputs=[start_time],
401
- outputs=[warmup_status, ready, warmup_btn, start_time]
402
- )
403
-
404
- # Chat page (initially hidden)
405
- with gr.Column(elem_id="chat-page", visible=False) as chat_page:
406
- # Header
407
- gr.Markdown(
408
- """
409
- <div style="text-align: center; margin-bottom: 2em;">
410
- <h1>📚 RAG Chat Interface</h1>
411
- <p style="color: #666;">Upload a document, then ask questions about its content.</p>
412
- </div>
413
- """
414
- )
415
-
416
- # ---------- Index Status Display ----------
417
- with gr.Row():
418
- with gr.Column():
419
- index_status = gr.Markdown("**Indexed file:** No document indexed")
420
-
421
- # ---------- File Upload Section ----------
422
- with gr.Row():
423
- with gr.Column(scale=2):
424
- file_input = gr.File(
425
- label="Upload .txt file",
426
- file_types=[".txt"],
427
- file_count="single",
428
- show_label=True
429
- )
430
- with gr.Column(scale=1):
431
- upload_btn = gr.Button("📤 Build Index", variant="primary", size="lg")
432
-
433
- with gr.Row():
434
- upload_status = gr.Markdown("**Status:** Waiting for document...")
435
-
436
- upload_btn.click(
437
- fn=upload_document,
438
- inputs=[file_input],
439
- outputs=[document_indexed, indexed_filename, upload_status]
440
- ).then(
441
- fn=update_index_display,
442
- inputs=[indexed_filename, document_indexed],
443
- outputs=[index_status]
444
- )
445
-
446
- # ---------- Chat Interface Section ----------
447
- gr.Markdown(
448
- """
449
- <div style="margin-top: 2em; margin-bottom: 1em;">
450
- <h3>💬 Ask Questions</h3>
451
- </div>
452
- """
453
- )
454
-
455
- def chat_wrapper(message: str, history: list) -> Generator[Tuple[str, list], None, None]:
456
- """Wrapper that checks if document is indexed before querying."""
457
- if not document_indexed.value:
458
- yield "⚠️ Please upload a document first using the 'Build Index' button.", history
459
- return
460
- yield from query_backend(message, history)
461
-
462
- gr.ChatInterface(
463
- fn=chat_wrapper,
464
- title=None,
465
- description=None,
466
- )
467
-
468
- # Navigation logic - only trigger when ready changes from False to True
469
- ready.change(
470
- fn=lambda r: (gr.update(visible=False), gr.update(visible=True), "chat" if r else "warmup"),
471
- inputs=[ready],
472
- outputs=[warmup_page, chat_page, page]
473
- )
474
-
475
- return demo
476
-
477
-
478
- # Launch the application
479
- if __name__ == "__main__":
480
- app = create_app()
481
- app.launch(server_name="0.0.0.0", server_port=7860, theme=gr.themes.Soft())
 
1
+ """
2
+ RAG Chat Interface for Hugging Face Space.
3
+ Connects to Modal backend for document indexing and question answering.
4
+ """
5
+ import gradio as gr
6
+ import requests
7
+ import os
8
+ import time
9
+ import threading
10
+ import re
11
+ from typing import Tuple, Optional, Generator
12
+
13
+ # ---------- Configuration Constants ----------
14
+ BACKEND_URLS = {
15
+ "query": "https://carsonbytes--query.modal.run/",
16
+ "debug": "https://carsonbytes--debug.modal.run/",
17
+ "upload": "https://carsonbytes--upload.modal.run/",
18
+ "health": "https://carsonbytes--health.modal.run/",
19
+ }
20
+
21
+ WARMUP_TIMEOUT = 15 # seconds
22
+ QUERY_TIMEOUT = 60 # seconds
23
+ UPLOAD_TIMEOUT = 120 # seconds
24
+ MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB in bytes
25
+ MAX_QUESTION_LENGTH = 1000 # characters
26
+
27
+
28
+ # ---------- Utility Functions ----------
29
+ def get_backend_url(endpoint: str) -> str:
30
+ """Get the URL for a specific backend endpoint."""
31
+ return BACKEND_URLS.get(endpoint, BACKEND_URLS["query"])
32
+
33
+
34
+ def format_file_size(size_bytes: int) -> str:
35
+ """Format file size in human-readable format."""
36
+ if size_bytes < 1024:
37
+ return f"{size_bytes} bytes"
38
+ elif size_bytes < 1024 * 1024:
39
+ return f"{size_bytes / 1024:.1f} KB"
40
+ else:
41
+ return f"{size_bytes / (1024 * 1024):.1f} MB"
42
+
43
+
44
+ def clean_answer(answer: str) -> str:
45
+ """Remove prompt-template bleed-through from the model's response."""
46
+ # Truncate at [INST] — everything after is a leaked prompt
47
+ if '[INST]' in answer:
48
+ answer = answer[:answer.index('[INST]')]
49
+ # Truncate at any remaining SYS markers
50
+ for marker in ['<<SYS>>', '<</SYS>>', '<</SYS']:
51
+ if marker in answer:
52
+ answer = answer[:answer.index(marker)]
53
+ return answer.strip()
54
+
55
+
56
+ # ---------- Backend Communication Functions ----------
57
+ def check_health() -> Tuple[bool, Optional[str]]:
58
+ """Check if backend is running and return (index_exists, indexed_filename)."""
59
+ try:
60
+ resp = requests.get(get_backend_url("health"), timeout=WARMUP_TIMEOUT)
61
+ if resp.status_code == 200:
62
+ data = resp.json()
63
+ index_exists = data.get("index_exists", False)
64
+ filename = data.get("indexed_filename", None)
65
+ return index_exists, filename
66
+ return False, None
67
+ except Exception:
68
+ return False, None
69
+
70
+
71
+ def upload_document(file_obj) -> Tuple[bool, Optional[str], str]:
72
+ """
73
+ Upload a document to the backend for indexing.
74
+ Returns tuple: (success: bool, filename: str, message: str)
75
+ """
76
+ if file_obj is None:
77
+ return False, None, "❌ No file selected."
78
+
79
+ file_size = os.path.getsize(file_obj.name) if os.path.exists(file_obj.name) else 0
80
+ if file_size > MAX_FILE_SIZE:
81
+ return False, None, f"❌ File too large. Maximum size is {format_file_size(MAX_FILE_SIZE)}."
82
+
83
+ try:
84
+ with open(file_obj.name, "r", encoding="utf-8") as f:
85
+ text_content = f.read()
86
+
87
+ if not text_content or not text_content.strip():
88
+ return False, None, "❌ File is empty."
89
+
90
+ filename = os.path.basename(file_obj.name)
91
+
92
+ payload = {"text": text_content, "filename": filename}
93
+ response = requests.post(
94
+ get_backend_url("upload"),
95
+ json=payload,
96
+ timeout=UPLOAD_TIMEOUT
97
+ )
98
+ response.raise_for_status()
99
+ data = response.json()
100
+
101
+ if data.get("status") == "success":
102
+ return True, filename, f"✅ Indexed {filename}"
103
+ else:
104
+ return False, None, f"❌ {data.get('message', 'Upload failed')}"
105
+
106
+ except requests.exceptions.Timeout:
107
+ return False, None, "❌ Upload timed out. Please try a smaller file."
108
+ except UnicodeDecodeError:
109
+ return False, None, "❌ Could not decode file. Please use a text file."
110
+ except Exception as e:
111
+ return False, None, f"❌ Upload failed: {str(e)}"
112
+
113
+
114
+ def query_backend(message: str, history: list) -> Generator[Tuple[str, list], None, None]:
115
+ """Send a question to the backend and yield responses."""
116
+ if not message or not message.strip():
117
+ yield "", history
118
+ return
119
+
120
+ if len(message) > MAX_QUESTION_LENGTH:
121
+ message = message[:MAX_QUESTION_LENGTH]
122
+
123
+ history = history + [(message, None)]
124
+ yield "🤔 Thinking...", history
125
+
126
+ try:
127
+ response = requests.post(
128
+ get_backend_url("query"),
129
+ json={"question": message},
130
+ timeout=QUERY_TIMEOUT
131
+ )
132
+ response.raise_for_status()
133
+ data = response.json()
134
+
135
+ answer = data.get("answer", "⚠️ Received an empty response.")
136
+ answer = clean_answer(answer)
137
+
138
+ history[-1] = (message, answer)
139
+ yield "", history
140
+
141
+ except requests.exceptions.Timeout:
142
+ history[-1] = (message, "❌ Query timed out. Please try again.")
143
+ yield "", history
144
+ except requests.exceptions.ConnectionError:
145
+ history[-1] = (message, "❌ Cannot connect to backend. Please try again later.")
146
+ yield "", history
147
+ except Exception as e:
148
+ history[-1] = (message, f"❌ Error: {str(e)}")
149
+ yield "", history
150
+
151
+
152
+ def update_index_display(filename: Optional[str], is_indexed: bool) -> str:
153
+ if is_indexed and filename:
154
+ return f"**Indexed file:** 📄 {filename}"
155
+ elif is_indexed:
156
+ return "**Indexed file:** ✅ Document indexed (filename unknown)"
157
+ return "**Indexed file:** No document indexed"
158
+
159
+
160
+ def load_index_state() -> Tuple[bool, Optional[str], str]:
161
+ """Called on page load — uses /health to get current index state."""
162
+ is_indexed, filename = check_health()
163
+ display = update_index_display(filename, is_indexed)
164
+ return is_indexed, filename, display
165
+
166
+
167
+ def warmup_with_status(start_time: float = None) -> Generator[Tuple[str, bool, str, float], None, None]:
168
+ """
169
+ Polls the health endpoint in a background thread and ticks the timer every second.
170
+ Yields: (status_message, success, button_label, start_time)
171
+ """
172
+ if start_time is None:
173
+ start_time = time.time()
174
+
175
+ result = {"done": False, "success": False, "status_code": None, "error": None}
176
+
177
+ def do_request():
178
+ try:
179
+ resp = requests.get(get_backend_url("health"), timeout=None)
180
+ result["status_code"] = resp.status_code
181
+ result["success"] = resp.status_code == 200
182
+ except Exception as e:
183
+ result["error"] = str(e)
184
+ finally:
185
+ result["done"] = True
186
+
187
+ thread = threading.Thread(target=do_request, daemon=True)
188
+ thread.start()
189
+
190
+ while not result["done"]:
191
+ elapsed = time.time() - start_time
192
+ msg = f"⏳ Loading GPU snapshots... ({elapsed:.1f}s elapsed)"
193
+ yield msg, False, f"⏳ Warming up... ({elapsed:.1f}s)", start_time
194
+ time.sleep(1)
195
+
196
+ elapsed = time.time() - start_time
197
+ if result["success"]:
198
+ msg = f"✅ Backend ready ({elapsed:.1f}s)"
199
+ yield msg, True, msg, start_time
200
+ elif result["error"]:
201
+ msg = f"❌ Cannot connect: {result['error']} ({elapsed:.1f}s)"
202
+ yield msg, False, f"❌ Failed ({elapsed:.1f}s)", start_time
203
+ else:
204
+ msg = f"❌ Backend returned status {result['status_code']} ({elapsed:.1f}s)"
205
+ yield msg, False, f"❌ Failed ({elapsed:.1f}s)", start_time
206
+
207
+
208
+ # ---------- Main App ----------
209
+ def create_app() -> gr.Blocks:
210
+ with gr.Blocks() as demo:
211
+ page = gr.State("warmup")
212
+ document_indexed = gr.State(False)
213
+ indexed_filename = gr.State(None)
214
+
215
+ # --- Warmup page ---
216
+ with gr.Column(elem_id="warmup-page") as warmup_page:
217
+ gr.Markdown(
218
+ """
219
+ <div style="text-align: center; margin-top: 15vh;">
220
+ <h1 style="font-size: 3em; margin-bottom: 0.5em;">📚 RAG Chat Interface</h1>
221
+ <p style="font-size: 1.2em; color: #666;">Powered by Qwen 2.5 & Modal</p>
222
+ <br>
223
+ <p style="color: #555;">Click the button below to warm up the backend and get started.</p>
224
+ </div>
225
+ """
226
+ )
227
+
228
+ with gr.Row():
229
+ with gr.Column():
230
+ warmup_btn = gr.Button(
231
+ "🔥 Warm Up Backend",
232
+ variant="primary",
233
+ size="lg",
234
+ )
235
+
236
+ warmup_status = gr.Markdown("")
237
+ ready = gr.State(False)
238
+ start_time_state = gr.State(0.0)
239
+
240
+ warmup_btn.click(
241
+ fn=lambda: time.time(),
242
+ outputs=[start_time_state]
243
+ ).then(
244
+ fn=warmup_with_status,
245
+ inputs=[start_time_state],
246
+ outputs=[warmup_status, ready, warmup_btn, start_time_state]
247
+ )
248
+
249
+ # --- Chat page (initially hidden) ---
250
+ with gr.Column(elem_id="chat-page", visible=False) as chat_page:
251
+ gr.Markdown(
252
+ """
253
+ <div style="text-align: center; margin-bottom: 2em;">
254
+ <h1>📚 RAG Chat Interface</h1>
255
+ <p style="color: #666;">Upload a document, then ask questions about its content.</p>
256
+ </div>
257
+ """
258
+ )
259
+
260
+ with gr.Row():
261
+ with gr.Column():
262
+ index_status = gr.Markdown("**Indexed file:** No document indexed")
263
+
264
+ with gr.Row():
265
+ with gr.Column(scale=2):
266
+ file_input = gr.File(
267
+ label="Upload .txt file",
268
+ file_types=[".txt"],
269
+ file_count="single",
270
+ show_label=True
271
+ )
272
+ with gr.Column(scale=1):
273
+ upload_btn = gr.Button("📤 Build Index", variant="primary", size="lg")
274
+
275
+ with gr.Row():
276
+ upload_status = gr.Markdown("**Status:** Waiting for document...")
277
+
278
+ upload_btn.click(
279
+ fn=upload_document,
280
+ inputs=[file_input],
281
+ outputs=[document_indexed, indexed_filename, upload_status]
282
+ ).then(
283
+ fn=update_index_display,
284
+ inputs=[indexed_filename, document_indexed],
285
+ outputs=[index_status]
286
+ )
287
+
288
+ gr.Markdown(
289
+ """
290
+ <div style="margin-top: 2em; margin-bottom: 1em;">
291
+ <h3>💬 Ask Questions</h3>
292
+ </div>
293
+ """
294
+ )
295
+
296
+ def chat_wrapper(message: str, history: list, is_indexed: bool) -> Generator[Tuple[str, list], None, None]:
297
+ if not is_indexed:
298
+ history = history + [(message, "⚠️ Please upload a document first using the 'Build Index' button.")]
299
+ yield "", history
300
+ return
301
+ yield from query_backend(message, history)
302
+
303
+ gr.ChatInterface(
304
+ fn=chat_wrapper,
305
+ additional_inputs=[document_indexed],
306
+ title=None,
307
+ description=None,
308
+ )
309
+
310
+ # Switch pages when warmup completes
311
+ ready.change(
312
+ fn=lambda r: (gr.update(visible=not r), gr.update(visible=r), "chat" if r else "warmup"),
313
+ inputs=[ready],
314
+ outputs=[warmup_page, chat_page, page]
315
+ )
316
+
317
+ # On page load, check /health and populate index state for chat page
318
+ demo.load(
319
+ fn=load_index_state,
320
+ outputs=[document_indexed, indexed_filename, index_status]
321
+ )
322
+
323
+ return demo
324
+
325
+
326
+ if __name__ == "__main__":
327
+ app = create_app()
328
+ app.launch(server_name="0.0.0.0", server_port=7860, theme=gr.themes.Soft())