Deevyankar commited on
Commit
628fcee
·
verified ·
1 Parent(s): d7ffc39

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +519 -23
app.py CHANGED
@@ -1,4 +1,9 @@
1
  import os
 
 
 
 
 
2
  import gradio as gr
3
  import chromadb
4
 
@@ -11,11 +16,14 @@ COLLECTION_NAME = "neuro_course"
11
  INDEX = None
12
 
13
 
14
- def get_persist_dir():
 
 
 
15
  return "storage/chroma"
16
 
17
 
18
- def load_index():
19
  persist_dir = get_persist_dir()
20
 
21
  if not os.path.exists(persist_dir):
@@ -34,6 +42,7 @@ def load_index():
34
  vector_store = ChromaVectorStore(chroma_collection=collection)
35
  storage_context = StorageContext.from_defaults(vector_store=vector_store)
36
 
 
37
  embed_model = HuggingFaceEmbedding(
38
  model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
39
  )
@@ -45,19 +54,141 @@ def load_index():
45
  )
46
 
47
 
48
- def get_index():
49
  global INDEX
50
  if INDEX is None:
51
  INDEX = load_index()
52
  return INDEX
53
 
54
 
55
- def ask_brainchat(message, history):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  if not message or not message.strip():
57
- return "Please type a question."
 
 
 
58
 
59
  if not os.getenv("OPENAI_API_KEY"):
60
- return "OPENAI_API_KEY is missing. Add it in Hugging Face Space Secrets."
 
 
61
 
62
  try:
63
  index = get_index()
@@ -74,10 +205,10 @@ def ask_brainchat(message, history):
74
  )
75
 
76
  prompt = f"""
77
- You are BrainChat, a neurology and neuroanatomy tutor.
78
 
79
  Rules:
80
- - Answer only from the retrieved textbook/course material.
81
  - If the answer is not supported by the retrieved material, say:
82
  "Not found in the course material."
83
  - Keep the answer clear and concise unless the user asks for more detail.
@@ -85,29 +216,394 @@ Rules:
85
  - If the question is in English, answer in English.
86
 
87
  Question:
88
- {message}
89
  """
90
 
91
  response = query_engine.query(prompt)
92
- return str(response)
93
 
94
  except Exception as e:
95
- return f"Error: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
 
 
 
 
 
 
 
 
 
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  with gr.Blocks() as demo:
99
- gr.Markdown("# 🧠 BrainChat")
100
- gr.Markdown("Ask questions from the uploaded neuroscience and neuroanatomy books.")
101
-
102
- gr.ChatInterface(
103
- fn=ask_brainchat,
104
- title="Neurology Tutor",
105
- description="This Space loads a prebuilt Chroma database from storage/chroma.",
106
- textbox=gr.Textbox(
107
- placeholder="Ask a question...",
108
- lines=1
109
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  )
111
 
112
  if __name__ == "__main__":
113
- demo.launch()
 
1
  import os
2
+ import re
3
+ import html
4
+ from urllib.parse import quote
5
+ from typing import List, Dict, Any, Optional
6
+
7
  import gradio as gr
8
  import chromadb
9
 
 
16
  INDEX = None
17
 
18
 
19
+ # ---------------------------------------------------
20
+ # DB loading
21
+ # ---------------------------------------------------
22
+ def get_persist_dir() -> str:
23
  return "storage/chroma"
24
 
25
 
26
+ def load_index() -> VectorStoreIndex:
27
  persist_dir = get_persist_dir()
28
 
29
  if not os.path.exists(persist_dir):
 
42
  vector_store = ChromaVectorStore(chroma_collection=collection)
43
  storage_context = StorageContext.from_defaults(vector_store=vector_store)
44
 
45
+ # MUST match the embedding model used when the DB was built
46
  embed_model = HuggingFaceEmbedding(
47
  model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
48
  )
 
54
  )
55
 
56
 
57
+ def get_index() -> VectorStoreIndex:
58
  global INDEX
59
  if INDEX is None:
60
  INDEX = load_index()
61
  return INDEX
62
 
63
 
64
+ # ---------------------------------------------------
65
+ # Logo helpers
66
+ # ---------------------------------------------------
67
+ def detect_logo_url() -> Optional[str]:
68
+ candidates = [
69
+ "brainchat_logo.png",
70
+ "Brain chat-09.png",
71
+ "Brain Chat Imagen.svg",
72
+ "BrainChatLogo.png",
73
+ ]
74
+ for name in candidates:
75
+ if os.path.exists(name):
76
+ return f"/gradio_api/file={quote(name)}"
77
+ return None
78
+
79
+
80
+ # ---------------------------------------------------
81
+ # Chat rendering helpers
82
+ # ---------------------------------------------------
83
+ def simple_markdown_to_html(text: str) -> str:
84
+ text = html.escape(text)
85
+
86
+ text = re.sub(r"^### (.+)$", r"<h3>\1</h3>", text, flags=re.MULTILINE)
87
+ text = re.sub(r"^## (.+)$", r"<h2>\1</h2>", text, flags=re.MULTILINE)
88
+ text = re.sub(r"^# (.+)$", r"<h1>\1</h1>", text, flags=re.MULTILINE)
89
+ text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
90
+
91
+ lines = text.split("\n")
92
+ converted = []
93
+ in_list = False
94
+
95
+ for line in lines:
96
+ if line.startswith("- "):
97
+ if not in_list:
98
+ converted.append("<ul>")
99
+ in_list = True
100
+ converted.append(f"<li>{line[2:]}</li>")
101
+ else:
102
+ if in_list:
103
+ converted.append("</ul>")
104
+ in_list = False
105
+ converted.append(line)
106
+
107
+ if in_list:
108
+ converted.append("</ul>")
109
+
110
+ text = "\n".join(converted)
111
+ text = text.replace("\n", "<br>")
112
+ return text
113
+
114
+
115
+ def render_chat_html(history: List[Dict[str, Any]]) -> str:
116
+ rows = []
117
+
118
+ for msg in history:
119
+ role = msg.get("role", "")
120
+ content = msg.get("content", "")
121
+
122
+ if not isinstance(content, str):
123
+ content = str(content)
124
+
125
+ content_html = simple_markdown_to_html(content)
126
+
127
+ if role == "user":
128
+ rows.append(
129
+ f"""
130
+ <div class="msg-row user-row">
131
+ <div class="msg-bubble user-bubble">{content_html}</div>
132
+ </div>
133
+ """
134
+ )
135
+ elif role == "assistant":
136
+ rows.append(
137
+ f"""
138
+ <div class="msg-row bot-row">
139
+ <div class="msg-bubble bot-bubble">{content_html}</div>
140
+ </div>
141
+ """
142
+ )
143
+
144
+ if not rows:
145
+ rows.append(
146
+ """
147
+ <div class="empty-chat">
148
+ <div class="empty-text">
149
+ Ask me anything about neurology and neuroanatomy.
150
+ </div>
151
+ </div>
152
+ """
153
+ )
154
+
155
+ return f"""
156
+ <div class="chat-inner" id="chat-inner">
157
+ {''.join(rows)}
158
+ </div>
159
+ <script>
160
+ const chatEl = document.getElementById("chat-inner");
161
+ if (chatEl) {{
162
+ chatEl.scrollTop = chatEl.scrollHeight;
163
+ }}
164
+ </script>
165
+ """
166
+
167
+
168
+ def make_logo_html() -> str:
169
+ logo_url = detect_logo_url()
170
+ if logo_url:
171
+ return f'<img src="{logo_url}" class="logo-img" alt="BrainChat Logo">'
172
+ return '<div class="logo-fallback">BRAIN<br>CHAT</div>'
173
+
174
+
175
+ # ---------------------------------------------------
176
+ # Chat logic
177
+ # ---------------------------------------------------
178
+ def respond(message: str, history: List[Dict[str, Any]]):
179
+ if history is None:
180
+ history = []
181
+
182
  if not message or not message.strip():
183
+ return history, render_chat_html(history), ""
184
+
185
+ user_text = message.strip()
186
+ history = history + [{"role": "user", "content": user_text}]
187
 
188
  if not os.getenv("OPENAI_API_KEY"):
189
+ reply = "OPENAI_API_KEY is missing. Add it in Hugging Face Space Secrets."
190
+ history = history + [{"role": "assistant", "content": reply}]
191
+ return history, render_chat_html(history), ""
192
 
193
  try:
194
  index = get_index()
 
205
  )
206
 
207
  prompt = f"""
208
+ You are BrainChat, a friendly neurology and neuroanatomy tutor.
209
 
210
  Rules:
211
+ - Answer only from the retrieved course and textbook material.
212
  - If the answer is not supported by the retrieved material, say:
213
  "Not found in the course material."
214
  - Keep the answer clear and concise unless the user asks for more detail.
 
216
  - If the question is in English, answer in English.
217
 
218
  Question:
219
+ {user_text}
220
  """
221
 
222
  response = query_engine.query(prompt)
223
+ answer = str(response)
224
 
225
  except Exception as e:
226
+ answer = f"Error: {str(e)}"
227
+
228
+ history = history + [{"role": "assistant", "content": answer}]
229
+ return history, render_chat_html(history), ""
230
+
231
+
232
+ def clear_chat():
233
+ empty = []
234
+ return empty, render_chat_html(empty)
235
+
236
+
237
+ def noop():
238
+ return None
239
+
240
+
241
+ # ---------------------------------------------------
242
+ # CSS
243
+ # ---------------------------------------------------
244
+ CUSTOM_CSS = """
245
+ html, body, .gradio-container {
246
+ margin: 0 !important;
247
+ padding: 0 !important;
248
+ background: #dedede !important;
249
+ font-family: Arial, Helvetica, sans-serif !important;
250
+ }
251
+
252
+ .gradio-container {
253
+ min-height: 100vh !important;
254
+ }
255
+
256
+ #app-wrap {
257
+ display: flex;
258
+ justify-content: center;
259
+ padding: 18px 0 28px 0;
260
+ }
261
+
262
+ /* PHONE */
263
+ #phone-shell {
264
+ width: 430px;
265
+ max-width: 95vw;
266
+ height: 860px;
267
+ background: linear-gradient(180deg, #e8c7d4 0%, #a55ca2 48%, #2b0c46 100%);
268
+ border: 16px solid #000;
269
+ border-radius: 42px;
270
+ box-shadow: 0 0 0 4px #555;
271
+ position: relative;
272
+ overflow: hidden;
273
+ }
274
+
275
+ /* notch */
276
+ #top-decor {
277
+ position: absolute;
278
+ inset: 0 0 auto 0;
279
+ pointer-events: none;
280
+ z-index: 10;
281
+ }
282
+
283
+ .notch {
284
+ width: 170px;
285
+ height: 30px;
286
+ background: #000;
287
+ position: absolute;
288
+ top: 0;
289
+ left: 50%;
290
+ transform: translateX(-50%);
291
+ border-bottom-left-radius: 20px;
292
+ border-bottom-right-radius: 20px;
293
+ }
294
+
295
+ .speaker {
296
+ width: 56px;
297
+ height: 8px;
298
+ background: #5a5a5a;
299
+ border-radius: 999px;
300
+ position: absolute;
301
+ top: 11px;
302
+ left: 50%;
303
+ transform: translateX(-50%);
304
+ }
305
+
306
+ .camera {
307
+ width: 12px;
308
+ height: 12px;
309
+ background: #5a5a5a;
310
+ border-radius: 50%;
311
+ position: absolute;
312
+ top: 9px;
313
+ left: calc(50% + 56px);
314
+ }
315
+
316
+ /* logo */
317
+ #logo-holder {
318
+ margin-top: 72px;
319
+ display: flex;
320
+ justify-content: center;
321
+ align-items: center;
322
+ min-height: 130px;
323
+ background: transparent !important;
324
+ border: none !important;
325
+ box-shadow: none !important;
326
+ }
327
+
328
+ .logo-img {
329
+ width: 122px;
330
+ height: 122px;
331
+ object-fit: contain;
332
+ display: block;
333
+ margin: 0 auto;
334
+ }
335
+
336
+ .logo-fallback {
337
+ width: 122px;
338
+ height: 122px;
339
+ border-radius: 50%;
340
+ background: #f3ec4a;
341
+ color: #111;
342
+ font-weight: 800;
343
+ font-size: 24px;
344
+ line-height: 1.05;
345
+ display: flex;
346
+ align-items: center;
347
+ justify-content: center;
348
+ text-align: center;
349
+ }
350
 
351
+ /* chat area */
352
+ #chat-render {
353
+ height: 565px;
354
+ margin: 8px 18px 0 18px;
355
+ overflow: hidden;
356
+ background: transparent !important;
357
+ border: none !important;
358
+ box-shadow: none !important;
359
+ }
360
 
361
+ .chat-inner {
362
+ height: 100%;
363
+ overflow-y: auto;
364
+ padding: 6px 2px 10px 2px;
365
+ box-sizing: border-box;
366
+ scroll-behavior: smooth;
367
+ }
368
+
369
+ .chat-inner::-webkit-scrollbar {
370
+ width: 6px;
371
+ }
372
+
373
+ .chat-inner::-webkit-scrollbar-thumb {
374
+ background: rgba(255,255,255,0.35);
375
+ border-radius: 999px;
376
+ }
377
+
378
+ .msg-row {
379
+ display: flex;
380
+ width: 100%;
381
+ margin: 12px 0;
382
+ }
383
+
384
+ .user-row {
385
+ justify-content: flex-start;
386
+ }
387
+
388
+ .bot-row {
389
+ justify-content: flex-end;
390
+ }
391
+
392
+ .msg-bubble {
393
+ max-width: 78%;
394
+ padding: 15px 18px;
395
+ font-size: 17px;
396
+ line-height: 1.45;
397
+ box-sizing: border-box;
398
+ word-wrap: break-word;
399
+ overflow-wrap: break-word;
400
+ }
401
+
402
+ .user-bubble {
403
+ background: #f5f5f5;
404
+ color: #111;
405
+ border-radius: 24px;
406
+ border-bottom-left-radius: 6px;
407
+ }
408
+
409
+ .bot-bubble {
410
+ background: #efe88f;
411
+ color: #111;
412
+ border-radius: 24px;
413
+ border-bottom-right-radius: 6px;
414
+ }
415
+
416
+ .msg-bubble h1,
417
+ .msg-bubble h2,
418
+ .msg-bubble h3 {
419
+ margin: 6px 0 8px 0;
420
+ font-size: 18px;
421
+ }
422
+
423
+ .msg-bubble ul {
424
+ margin-top: 6px;
425
+ margin-bottom: 6px;
426
+ padding-left: 20px;
427
+ }
428
+
429
+ .empty-chat {
430
+ height: 100%;
431
+ display: flex;
432
+ align-items: center;
433
+ justify-content: center;
434
+ }
435
+
436
+ .empty-text {
437
+ color: rgba(255,255,255,0.95);
438
+ text-align: center;
439
+ font-size: 16px;
440
+ max-width: 250px;
441
+ }
442
+
443
+ /* bottom bar */
444
+ #input-wrap {
445
+ position: absolute;
446
+ left: 0;
447
+ right: 0;
448
+ bottom: 0;
449
+ background: #e7e163;
450
+ border-top-left-radius: 34px;
451
+ border-top-right-radius: 34px;
452
+ padding: 14px 16px 24px 16px;
453
+ box-sizing: border-box;
454
+ z-index: 20;
455
+ }
456
+
457
+ #input-row {
458
+ display: flex !important;
459
+ align-items: center !important;
460
+ gap: 10px !important;
461
+ }
462
+
463
+ #plus-btn button {
464
+ min-width: 40px !important;
465
+ width: 40px !important;
466
+ height: 40px !important;
467
+ border: none !important;
468
+ background: transparent !important;
469
+ color: #b01773 !important;
470
+ font-size: 42px !important;
471
+ line-height: 1 !important;
472
+ padding: 0 !important;
473
+ box-shadow: none !important;
474
+ }
475
+
476
+ #plus-btn button:hover {
477
+ background: transparent !important;
478
+ }
479
+
480
+ #msg-box {
481
+ flex: 1 !important;
482
+ background: transparent !important;
483
+ border: none !important;
484
+ box-shadow: none !important;
485
+ }
486
+
487
+ #msg-box textarea {
488
+ border: none !important;
489
+ border-radius: 999px !important;
490
+ min-height: 46px !important;
491
+ max-height: 90px !important;
492
+ padding: 10px 18px !important;
493
+ background: #f5f5f5 !important;
494
+ color: #111 !important;
495
+ font-size: 16px !important;
496
+ box-shadow: none !important;
497
+ }
498
+
499
+ #send-btn button {
500
+ min-width: 44px !important;
501
+ width: 44px !important;
502
+ height: 44px !important;
503
+ border: none !important;
504
+ background: transparent !important;
505
+ color: #b01773 !important;
506
+ font-size: 30px !important;
507
+ padding: 0 !important;
508
+ box-shadow: none !important;
509
+ }
510
+
511
+ #send-btn button:hover {
512
+ background: transparent !important;
513
+ }
514
+
515
+ #home-bar {
516
+ position: absolute;
517
+ left: 50%;
518
+ transform: translateX(-50%);
519
+ bottom: 8px;
520
+ width: 120px;
521
+ height: 7px;
522
+ border-radius: 999px;
523
+ background: #000;
524
+ z-index: 30;
525
+ }
526
+
527
+ /* external controls */
528
+ #actions-wrap {
529
+ width: 430px;
530
+ max-width: 95vw;
531
+ margin: 12px auto 0 auto;
532
+ display: flex;
533
+ justify-content: center;
534
+ gap: 12px;
535
+ }
536
+
537
+ #clear-btn button {
538
+ border-radius: 999px !important;
539
+ }
540
+
541
+ /* hide footer */
542
+ footer {
543
+ display: none !important;
544
+ }
545
+ """
546
+
547
+
548
+ # ---------------------------------------------------
549
+ # UI
550
+ # ---------------------------------------------------
551
  with gr.Blocks() as demo:
552
+ history_state = gr.State([])
553
+
554
+ with gr.Column(elem_id="app-wrap"):
555
+ with gr.Column(elem_id="phone-shell"):
556
+ gr.HTML(
557
+ """
558
+ <div id="top-decor">
559
+ <div class="notch"></div>
560
+ <div class="speaker"></div>
561
+ <div class="camera"></div>
562
+ </div>
563
+ """
564
+ )
565
+
566
+ gr.HTML(make_logo_html(), elem_id="logo-holder")
567
+
568
+ chat_view = gr.HTML(render_chat_html([]), elem_id="chat-render")
569
+
570
+ with gr.Row(elem_id="input-wrap"):
571
+ with gr.Row(elem_id="input-row"):
572
+ plus_btn = gr.Button("+", elem_id="plus-btn")
573
+ msg = gr.Textbox(
574
+ placeholder="Escribe tu mensaje...",
575
+ show_label=False,
576
+ lines=1,
577
+ elem_id="msg-box",
578
+ container=False,
579
+ scale=8,
580
+ )
581
+ send = gr.Button("➤", elem_id="send-btn", scale=1)
582
+
583
+ gr.HTML('<div id="home-bar"></div>')
584
+
585
+ with gr.Row(elem_id="actions-wrap"):
586
+ clear_btn = gr.Button("Clear Chat", elem_id="clear-btn")
587
+
588
+ plus_btn.click(noop, inputs=[], outputs=[])
589
+
590
+ msg.submit(
591
+ respond,
592
+ inputs=[msg, history_state],
593
+ outputs=[history_state, chat_view, msg],
594
+ )
595
+
596
+ send.click(
597
+ respond,
598
+ inputs=[msg, history_state],
599
+ outputs=[history_state, chat_view, msg],
600
+ )
601
+
602
+ clear_btn.click(
603
+ clear_chat,
604
+ inputs=[],
605
+ outputs=[history_state, chat_view],
606
  )
607
 
608
  if __name__ == "__main__":
609
+ demo.launch(css=CUSTOM_CSS)