sofzcc commited on
Commit
7fda255
Β·
verified Β·
1 Parent(s): 0ed57dc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +267 -34
app.py CHANGED
@@ -3,6 +3,7 @@ import re
3
  import json
4
  from pathlib import Path
5
  from typing import List, Dict, Tuple, Optional
 
6
 
7
  import numpy as np
8
  import faiss
@@ -10,6 +11,8 @@ import gradio as gr
10
 
11
  from transformers import pipeline, AutoTokenizer, AutoModelForQuestionAnswering
12
  from sentence_transformers import SentenceTransformer
 
 
13
 
14
  # ----------- Paths -----------
15
  KB_DIR = Path("./kb")
@@ -26,7 +29,52 @@ FAISS_PATH = INDEX_DIR / "kb_faiss.index"
26
 
27
  HEADING_RE = re.compile(r"^(#{1,6})\s+(.*)$", re.MULTILINE)
28
 
29
- # ----------- Load Markdown -----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  def read_markdown_files(kb_dir: Path) -> List[Dict]:
31
  """Read all markdown files from the knowledge base directory."""
32
  docs = []
@@ -101,6 +149,7 @@ class KBIndex:
101
  self.index = None
102
  self.embeddings = None
103
  self.metadata = []
 
104
 
105
  def build(self, kb_dir: Path):
106
  """Build the FAISS index from markdown files."""
@@ -131,12 +180,57 @@ class KBIndex:
131
  self.index = index
132
  self.embeddings = embeddings
133
  self.metadata = all_chunks
 
134
 
135
  np.save(EMBEDDINGS_PATH, embeddings)
136
  with open(METADATA_PATH, "w", encoding="utf-8") as f:
137
  json.dump(self.metadata, f, ensure_ascii=False, indent=2)
138
  faiss.write_index(index, str(FAISS_PATH))
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  def load(self) -> bool:
141
  """Load pre-built index from disk."""
142
  if not (EMBEDDINGS_PATH.exists() and METADATA_PATH.exists() and FAISS_PATH.exists()):
@@ -145,6 +239,7 @@ class KBIndex:
145
  with open(METADATA_PATH, "r", encoding="utf-8") as f:
146
  self.metadata = json.load(f)
147
  self.index = faiss.read_index(str(FAISS_PATH))
 
148
  return True
149
 
150
  def retrieve(self, query: str, top_k: int = 6) -> List[Tuple[int, float]]:
@@ -170,12 +265,18 @@ class KBIndex:
170
  score = float(out.get("score", 0.0))
171
  answer_text = out.get("answer", "").strip()
172
 
173
- if answer_text and len(answer_text) > 5:
 
 
 
 
174
  candidates.append({
175
- "text": answer_text,
 
176
  "score": score,
177
  "meta": meta,
178
- "sim": float(sim)
 
179
  })
180
  except Exception as e:
181
  continue
@@ -204,6 +305,52 @@ class KBIndex:
204
 
205
  best_sim = max([s for _, s in retrieved]) if retrieved else 0.0
206
  return best["text"], best["score"], citations, best_sim
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
  # Initialize KB
209
  kb = KBIndex()
@@ -239,31 +386,47 @@ def format_citations(citations: List[Dict]) -> str:
239
  lines.append(f"β€’ **{c['title']}** β€” _{c['section']}_")
240
  return "\n".join(lines)
241
 
242
- def respond(user_msg: str, history: List) -> str:
243
  """Generate response to user query using RAG pipeline."""
244
  user_msg = (user_msg or "").strip()
245
 
246
  if not user_msg:
247
  return "πŸ‘‹ How can I help? Ask me anything about the knowledge base, or use a quick action button below."
248
 
 
 
 
 
 
 
 
249
  # Retrieve relevant chunks
250
  retrieved = kb.retrieve(user_msg, top_k=6)
251
 
252
- if not retrieved:
253
- return "❌ I couldn't find any relevant information. Please try rephrasing your question or contact support."
 
 
 
 
254
 
255
  # Extract answer using QA model
256
  answer, qa_score, citations, best_sim = kb.answer(user_msg, retrieved)
257
 
258
- if not answer:
259
- # Fallback: show closest matches
260
- citations_md = format_citations(citations)
261
  return (
262
- f"πŸ€” I couldn't extract a specific answer, but here are the most relevant sections:\n\n"
263
- f"{citations_md}\n\n"
264
- f"πŸ’‘ Try rephrasing your question or ask me to show more details."
265
  )
266
 
 
 
 
 
 
 
267
  # Check confidence
268
  low_confidence = (qa_score < CONFIDENCE_THRESHOLD) or (best_sim < SIMILARITY_THRESHOLD)
269
  citations_md = format_citations(citations)
@@ -271,39 +434,72 @@ def respond(user_msg: str, history: List) -> str:
271
  # Format response based on confidence
272
  if low_confidence:
273
  return (
274
- f"⚠️ **Answer (Low Confidence):**\n{answer}\n\n"
275
  f"---\n"
276
  f"πŸ“š **Related Sources:**\n{citations_md}\n\n"
277
- f"πŸ’¬ *If this doesn't help, please say \"escalate to support\" for human assistance.*"
278
  )
279
  else:
280
  return (
281
- f"βœ… **Answer:**\n{answer}\n\n"
282
  f"---\n"
283
  f"πŸ“š **Sources:**\n{citations_md}\n\n"
284
  f"πŸ’‘ *Say \"show more details\" to see the full context.*"
285
  )
286
 
287
- def process_message(user_input: str, history: List) -> Tuple[List, Dict]:
288
  """Process user message and return updated chat history."""
289
  user_input = (user_input or "").strip()
290
  if not user_input:
291
  return history, gr.update(value="")
292
 
293
- reply = respond(user_input, history or [])
294
  new_history = (history or []) + [
295
  {"role": "user", "content": user_input},
296
  {"role": "assistant", "content": reply}
297
  ]
298
  return new_history, gr.update(value="")
299
 
300
- def process_quick(label: str, history: List) -> Tuple[List, Dict]:
301
  """Process quick action button click."""
302
  for btn_label, query in QUICK_ACTIONS:
303
  if label == btn_label:
304
- return process_message(query, history)
305
  return history, gr.update(value="")
306
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  def rebuild_index_handler():
308
  """Rebuild the search index from KB directory."""
309
  try:
@@ -319,18 +515,35 @@ with gr.Blocks(
319
  css="""
320
  .contain { max-width: 1200px; margin: auto; }
321
  .quick-btn { min-width: 180px !important; }
 
322
  """
323
  ) as demo:
324
 
 
 
 
325
  # Header
326
  gr.Markdown(
327
  """
328
  # πŸ€– RAG Knowledge Assistant
329
  ### AI-powered Q&A with document retrieval and citation
330
- Ask questions about your knowledge base, and get answers backed by relevant sources.
331
  """
332
  )
333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  # Main chat interface
335
  with gr.Row():
336
  with gr.Column(scale=1):
@@ -343,20 +556,20 @@ with gr.Blocks(
343
 
344
  with gr.Row():
345
  txt = gr.Textbox(
346
- placeholder="πŸ’¬ Ask a question (e.g., How do I connect WhatsApp?)",
347
  scale=9,
348
  show_label=False,
349
  container=False
350
  )
351
  send = gr.Button("Send", variant="primary", scale=1)
352
 
353
- # Quick action buttons
354
- gr.Markdown("### ⚑ Quick Actions")
355
- with gr.Row():
356
- quick_buttons = []
357
- for label, _ in QUICK_ACTIONS:
358
- btn = gr.Button(label, elem_classes="quick-btn", size="sm")
359
- quick_buttons.append((btn, label))
360
 
361
  # Admin section
362
  with gr.Accordion("πŸ”§ Admin Panel", open=False):
@@ -367,17 +580,36 @@ with gr.Blocks(
367
  """
368
  )
369
  with gr.Row():
370
- rebuild_btn = gr.Button("πŸ”„ Rebuild Index", variant="secondary")
371
  status_msg = gr.Markdown("")
372
 
373
  # Event handlers
374
- send.click(process_message, inputs=[txt, chat], outputs=[chat, txt])
375
- txt.submit(process_message, inputs=[txt, chat], outputs=[chat, txt])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
 
377
  for btn, label in quick_buttons:
378
  btn.click(
379
  process_quick,
380
- inputs=[gr.State(label), chat],
381
  outputs=[chat, txt]
382
  )
383
 
@@ -388,9 +620,10 @@ with gr.Blocks(
388
  """
389
  ---
390
  πŸ’‘ **Tips:**
 
 
391
  - Be specific in your questions for better results
392
  - Check the cited sources for full context
393
- - Use quick actions for common tasks
394
  """
395
  )
396
 
 
3
  import json
4
  from pathlib import Path
5
  from typing import List, Dict, Tuple, Optional
6
+ import tempfile
7
 
8
  import numpy as np
9
  import faiss
 
11
 
12
  from transformers import pipeline, AutoTokenizer, AutoModelForQuestionAnswering
13
  from sentence_transformers import SentenceTransformer
14
+ import PyPDF2
15
+ import docx
16
 
17
  # ----------- Paths -----------
18
  KB_DIR = Path("./kb")
 
29
 
30
  HEADING_RE = re.compile(r"^(#{1,6})\s+(.*)$", re.MULTILINE)
31
 
32
+ # ----------- Load Documents -----------
33
+ def extract_text_from_pdf(file_path: str) -> str:
34
+ """Extract text from PDF file."""
35
+ text = ""
36
+ try:
37
+ with open(file_path, 'rb') as file:
38
+ pdf_reader = PyPDF2.PdfReader(file)
39
+ for page in pdf_reader.pages:
40
+ text += page.extract_text() + "\n"
41
+ except Exception as e:
42
+ raise RuntimeError(f"Error reading PDF: {str(e)}")
43
+ return text
44
+
45
+ def extract_text_from_docx(file_path: str) -> str:
46
+ """Extract text from DOCX file."""
47
+ try:
48
+ doc = docx.Document(file_path)
49
+ text = "\n".join([paragraph.text for paragraph in doc.paragraphs])
50
+ return text
51
+ except Exception as e:
52
+ raise RuntimeError(f"Error reading DOCX: {str(e)}")
53
+
54
+ def extract_text_from_txt(file_path: str) -> str:
55
+ """Extract text from TXT file."""
56
+ try:
57
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as file:
58
+ return file.read()
59
+ except Exception as e:
60
+ raise RuntimeError(f"Error reading TXT: {str(e)}")
61
+
62
+ def extract_text_from_file(file_path: str) -> Tuple[str, str]:
63
+ """
64
+ Extract text from uploaded file based on extension.
65
+ Returns: (text_content, file_type)
66
+ """
67
+ ext = Path(file_path).suffix.lower()
68
+
69
+ if ext == '.pdf':
70
+ return extract_text_from_pdf(file_path), 'PDF'
71
+ elif ext == '.docx':
72
+ return extract_text_from_docx(file_path), 'DOCX'
73
+ elif ext in ['.txt', '.md']:
74
+ return extract_text_from_txt(file_path), 'Text'
75
+ else:
76
+ raise ValueError(f"Unsupported file type: {ext}. Supported: .pdf, .docx, .txt, .md")
77
+
78
  def read_markdown_files(kb_dir: Path) -> List[Dict]:
79
  """Read all markdown files from the knowledge base directory."""
80
  docs = []
 
149
  self.index = None
150
  self.embeddings = None
151
  self.metadata = []
152
+ self.uploaded_file_active = False # Track if using uploaded file
153
 
154
  def build(self, kb_dir: Path):
155
  """Build the FAISS index from markdown files."""
 
180
  self.index = index
181
  self.embeddings = embeddings
182
  self.metadata = all_chunks
183
+ self.uploaded_file_active = False
184
 
185
  np.save(EMBEDDINGS_PATH, embeddings)
186
  with open(METADATA_PATH, "w", encoding="utf-8") as f:
187
  json.dump(self.metadata, f, ensure_ascii=False, indent=2)
188
  faiss.write_index(index, str(FAISS_PATH))
189
 
190
+ def build_from_uploaded_file(self, file_path: str, filename: str):
191
+ """Build temporary index from an uploaded file."""
192
+ # Extract text from file
193
+ text_content, file_type = extract_text_from_file(file_path)
194
+
195
+ if not text_content or len(text_content.strip()) < 100:
196
+ raise RuntimeError("File appears to be empty or too short.")
197
+
198
+ # Create document structure
199
+ doc = {
200
+ "filepath": file_path,
201
+ "filename": filename,
202
+ "title": Path(filename).stem.replace("_", " ").title(),
203
+ "text": text_content
204
+ }
205
+
206
+ # Chunk the document
207
+ all_chunks = chunk_markdown(doc)
208
+
209
+ if not all_chunks:
210
+ raise RuntimeError("Could not extract meaningful content from file.")
211
+
212
+ # Build embeddings
213
+ texts = [c["content"] for c in all_chunks]
214
+ embeddings = self.embedder.encode(
215
+ texts,
216
+ batch_size=32,
217
+ convert_to_numpy=True,
218
+ show_progress_bar=False
219
+ )
220
+ faiss.normalize_L2(embeddings)
221
+
222
+ # Create new index
223
+ dim = embeddings.shape[1]
224
+ index = faiss.IndexFlatIP(dim)
225
+ index.add(embeddings)
226
+
227
+ self.index = index
228
+ self.embeddings = embeddings
229
+ self.metadata = all_chunks
230
+ self.uploaded_file_active = True
231
+
232
+ return len(all_chunks), file_type
233
+
234
  def load(self) -> bool:
235
  """Load pre-built index from disk."""
236
  if not (EMBEDDINGS_PATH.exists() and METADATA_PATH.exists() and FAISS_PATH.exists()):
 
239
  with open(METADATA_PATH, "r", encoding="utf-8") as f:
240
  self.metadata = json.load(f)
241
  self.index = faiss.read_index(str(FAISS_PATH))
242
+ self.uploaded_file_active = False
243
  return True
244
 
245
  def retrieve(self, query: str, top_k: int = 6) -> List[Tuple[int, float]]:
 
265
  score = float(out.get("score", 0.0))
266
  answer_text = out.get("answer", "").strip()
267
 
268
+ # Enhanced answer extraction with context
269
+ if answer_text and len(answer_text) > 3:
270
+ # Try to expand the answer with surrounding context
271
+ expanded_answer = self._expand_answer(answer_text, ctx)
272
+
273
  candidates.append({
274
+ "text": expanded_answer,
275
+ "original": answer_text,
276
  "score": score,
277
  "meta": meta,
278
+ "sim": float(sim),
279
+ "context": ctx
280
  })
281
  except Exception as e:
282
  continue
 
305
 
306
  best_sim = max([s for _, s in retrieved]) if retrieved else 0.0
307
  return best["text"], best["score"], citations, best_sim
308
+
309
+ def _expand_answer(self, answer: str, context: str, max_chars: int = 300) -> str:
310
+ """
311
+ Expand the extracted answer with surrounding context to make it more complete.
312
+ """
313
+ # Find the answer in the context
314
+ answer_pos = context.lower().find(answer.lower())
315
+
316
+ if answer_pos == -1:
317
+ return answer
318
+
319
+ # Get sentence boundaries around the answer
320
+ start = answer_pos
321
+ end = answer_pos + len(answer)
322
+
323
+ # Expand backwards to sentence start
324
+ while start > 0 and context[start - 1] not in '.!?\n':
325
+ start -= 1
326
+ if answer_pos - start > max_chars // 2:
327
+ break
328
+
329
+ # Expand forwards to sentence end
330
+ while end < len(context) and context[end] not in '.!?\n':
331
+ end += 1
332
+ if end - answer_pos > max_chars // 2:
333
+ break
334
+
335
+ # Include the punctuation
336
+ if end < len(context) and context[end] in '.!?':
337
+ end += 1
338
+
339
+ expanded = context[start:end].strip()
340
+
341
+ # If still too short, try to get the full sentence(s)
342
+ if len(expanded) < 50:
343
+ # Look for complete sentences around the answer
344
+ sentences = context.split('.')
345
+ for i, sent in enumerate(sentences):
346
+ if answer.lower() in sent.lower():
347
+ # Get this sentence and maybe the next one
348
+ result = sent.strip()
349
+ if i + 1 < len(sentences) and len(result) < 100:
350
+ result += ". " + sentences[i + 1].strip()
351
+ return result + ("." if not result.endswith(".") else "")
352
+
353
+ return expanded
354
 
355
  # Initialize KB
356
  kb = KBIndex()
 
386
  lines.append(f"β€’ **{c['title']}** β€” _{c['section']}_")
387
  return "\n".join(lines)
388
 
389
+ def respond(user_msg: str, history: List, uploaded_file_info: str = None) -> str:
390
  """Generate response to user query using RAG pipeline."""
391
  user_msg = (user_msg or "").strip()
392
 
393
  if not user_msg:
394
  return "πŸ‘‹ How can I help? Ask me anything about the knowledge base, or use a quick action button below."
395
 
396
+ # Check if we have an index
397
+ if kb.index is None or len(kb.metadata) == 0:
398
+ return "❌ I don't know the answer to that but if you have any document with details I can learn about it. Please upload a file using the upload section above."
399
+
400
+ # Add context about uploaded file
401
+ source_info = f" in the uploaded file" if kb.uploaded_file_active and uploaded_file_info else " in the knowledge base"
402
+
403
  # Retrieve relevant chunks
404
  retrieved = kb.retrieve(user_msg, top_k=6)
405
 
406
+ if not retrieved or (retrieved and max([s for _, s in retrieved]) < 0.20):
407
+ # Very low similarity - clearly don't know the answer
408
+ return (
409
+ f"❌ **I don't know the answer to that** but if you have any document with details I can learn about it.\n\n"
410
+ f"πŸ“€ Upload a relevant document above, and I'll be able to help you find the information you need!"
411
+ )
412
 
413
  # Extract answer using QA model
414
  answer, qa_score, citations, best_sim = kb.answer(user_msg, retrieved)
415
 
416
+ # Stricter threshold for "I don't know" response
417
+ if not answer or qa_score < 0.15 or best_sim < 0.25:
 
418
  return (
419
+ f"❌ **I don't know the answer to that** but if you have any document with details I can learn about it.\n\n"
420
+ f"The question seems outside the scope of what I currently know{source_info}. "
421
+ f"Try uploading a relevant document, or rephrase your question if you think the information might be here."
422
  )
423
 
424
+ # Clean up the answer text
425
+ answer = answer.strip()
426
+ # Ensure answer ends with proper punctuation
427
+ if answer and answer[-1] not in '.!?':
428
+ answer += "."
429
+
430
  # Check confidence
431
  low_confidence = (qa_score < CONFIDENCE_THRESHOLD) or (best_sim < SIMILARITY_THRESHOLD)
432
  citations_md = format_citations(citations)
 
434
  # Format response based on confidence
435
  if low_confidence:
436
  return (
437
+ f"⚠️ **Answer (Low Confidence):**\n\n{answer}\n\n"
438
  f"---\n"
439
  f"πŸ“š **Related Sources:**\n{citations_md}\n\n"
440
+ f"πŸ’¬ *I'm not entirely certain about this answer. If you have a more detailed document about this topic, please upload it for better accuracy.*"
441
  )
442
  else:
443
  return (
444
+ f"βœ… **Answer:**\n\n{answer}\n\n"
445
  f"---\n"
446
  f"πŸ“š **Sources:**\n{citations_md}\n\n"
447
  f"πŸ’‘ *Say \"show more details\" to see the full context.*"
448
  )
449
 
450
+ def process_message(user_input: str, history: List, uploaded_file_info: str) -> Tuple[List, Dict]:
451
  """Process user message and return updated chat history."""
452
  user_input = (user_input or "").strip()
453
  if not user_input:
454
  return history, gr.update(value="")
455
 
456
+ reply = respond(user_input, history or [], uploaded_file_info)
457
  new_history = (history or []) + [
458
  {"role": "user", "content": user_input},
459
  {"role": "assistant", "content": reply}
460
  ]
461
  return new_history, gr.update(value="")
462
 
463
+ def process_quick(label: str, history: List, uploaded_file_info: str) -> Tuple[List, Dict]:
464
  """Process quick action button click."""
465
  for btn_label, query in QUICK_ACTIONS:
466
  if label == btn_label:
467
+ return process_message(query, history, uploaded_file_info)
468
  return history, gr.update(value="")
469
 
470
+ def handle_file_upload(file):
471
+ """Process uploaded file and build index."""
472
+ if file is None:
473
+ return "ℹ️ No file uploaded.", ""
474
+
475
+ try:
476
+ filename = Path(file.name).name
477
+ num_chunks, file_type = kb.build_from_uploaded_file(file.name, filename)
478
+
479
+ return (
480
+ f"βœ… **File processed successfully!**\n\n"
481
+ f"πŸ“„ **File:** {filename}\n"
482
+ f"πŸ“‹ **Type:** {file_type}\n"
483
+ f"πŸ”’ **Chunks:** {num_chunks}\n\n"
484
+ f"You can now ask questions about this document!"
485
+ ), filename
486
+ except Exception as e:
487
+ return f"❌ **Error processing file:** {str(e)}\n\nPlease ensure the file is a valid PDF, DOCX, TXT, or MD file.", ""
488
+
489
+ def clear_uploaded_file():
490
+ """Clear uploaded file and reload KB index."""
491
+ try:
492
+ if kb.load():
493
+ return "βœ… Switched back to knowledge base.", "", None
494
+ else:
495
+ kb.index = None
496
+ kb.embeddings = None
497
+ kb.metadata = []
498
+ kb.uploaded_file_active = False
499
+ return "ℹ️ No knowledge base found. Please upload a file or build the KB index.", "", None
500
+ except Exception as e:
501
+ return f"⚠️ Error: {str(e)}", "", None
502
+
503
  def rebuild_index_handler():
504
  """Rebuild the search index from KB directory."""
505
  try:
 
515
  css="""
516
  .contain { max-width: 1200px; margin: auto; }
517
  .quick-btn { min-width: 180px !important; }
518
+ .upload-section { border: 2px dashed #ccc; padding: 20px; border-radius: 8px; }
519
  """
520
  ) as demo:
521
 
522
+ # State to track uploaded file
523
+ uploaded_file_state = gr.State("")
524
+
525
  # Header
526
  gr.Markdown(
527
  """
528
  # πŸ€– RAG Knowledge Assistant
529
  ### AI-powered Q&A with document retrieval and citation
530
+ Upload a document or use the knowledge base to get answers backed by relevant sources.
531
  """
532
  )
533
 
534
+ # File upload section
535
+ with gr.Row():
536
+ with gr.Column(scale=1):
537
+ gr.Markdown("### πŸ“€ Upload Document")
538
+ file_upload = gr.File(
539
+ label="Upload PDF, DOCX, TXT, or MD file",
540
+ file_types=[".pdf", ".docx", ".txt", ".md"],
541
+ type="filepath"
542
+ )
543
+ upload_status = gr.Markdown("ℹ️ Upload a file to ask questions about it.")
544
+ with gr.Row():
545
+ clear_btn = gr.Button("πŸ”„ Clear & Use KB", variant="secondary", size="sm")
546
+
547
  # Main chat interface
548
  with gr.Row():
549
  with gr.Column(scale=1):
 
556
 
557
  with gr.Row():
558
  txt = gr.Textbox(
559
+ placeholder="πŸ’¬ Ask a question about the document or knowledge base...",
560
  scale=9,
561
  show_label=False,
562
  container=False
563
  )
564
  send = gr.Button("Send", variant="primary", scale=1)
565
 
566
+ # Quick action buttons (only for KB mode)
567
+ with gr.Accordion("⚑ Quick Actions (Knowledge Base)", open=False):
568
+ with gr.Row():
569
+ quick_buttons = []
570
+ for label, _ in QUICK_ACTIONS:
571
+ btn = gr.Button(label, elem_classes="quick-btn", size="sm")
572
+ quick_buttons.append((btn, label))
573
 
574
  # Admin section
575
  with gr.Accordion("πŸ”§ Admin Panel", open=False):
 
580
  """
581
  )
582
  with gr.Row():
583
+ rebuild_btn = gr.Button("πŸ”„ Rebuild KB Index", variant="secondary")
584
  status_msg = gr.Markdown("")
585
 
586
  # Event handlers
587
+ file_upload.change(
588
+ handle_file_upload,
589
+ inputs=[file_upload],
590
+ outputs=[upload_status, uploaded_file_state]
591
+ )
592
+
593
+ clear_btn.click(
594
+ clear_uploaded_file,
595
+ outputs=[upload_status, uploaded_file_state, file_upload]
596
+ )
597
+
598
+ send.click(
599
+ process_message,
600
+ inputs=[txt, chat, uploaded_file_state],
601
+ outputs=[chat, txt]
602
+ )
603
+ txt.submit(
604
+ process_message,
605
+ inputs=[txt, chat, uploaded_file_state],
606
+ outputs=[chat, txt]
607
+ )
608
 
609
  for btn, label in quick_buttons:
610
  btn.click(
611
  process_quick,
612
+ inputs=[gr.State(label), chat, uploaded_file_state],
613
  outputs=[chat, txt]
614
  )
615
 
 
620
  """
621
  ---
622
  πŸ’‘ **Tips:**
623
+ - Upload a document to ask questions specifically about that file
624
+ - Use "Clear & Use KB" to switch back to the knowledge base
625
  - Be specific in your questions for better results
626
  - Check the cited sources for full context
 
627
  """
628
  )
629