Sebunya commited on
Commit
8d369b8
·
verified ·
1 Parent(s): 03c0f5d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +157 -63
app.py CHANGED
@@ -18,6 +18,7 @@ import re
18
  from typing import Dict, List, Tuple
19
  import time
20
  from contextlib import contextmanager
 
21
 
22
  import logging
23
  import traceback
@@ -79,7 +80,11 @@ class PipelineTimer:
79
  timer = PipelineTimer()
80
 
81
  # === Configuration ===
82
- genai.configure(api_key=os.environ["GEMINI_API_KEY"])
 
 
 
 
83
  embedding_model = "models/embedding-001"
84
  llm_model_name = "models/gemma-3-4b-it"
85
  collection_name = "xeno_collection"
@@ -94,24 +99,51 @@ def get_google_sheets_credentials():
94
  creds = Credentials.from_service_account_info(credentials_dict, scopes=scope)
95
  return creds
96
 
97
- client_gspread = gspread.authorize(get_google_sheets_credentials())
98
-
99
- # Open the Google Sheet and get both sheets
100
- spreadsheet = client_gspread.open("Response_Log")
101
- response_sheet = spreadsheet.sheet1 # Main response log
 
 
 
 
 
 
 
 
 
 
 
102
  try:
103
  timing_sheet = spreadsheet.worksheet("Timing_Log")
104
  except:
105
- # Create timing sheet if it doesn't exist
106
- timing_sheet = spreadsheet.add_worksheet(title="Timing_Log", rows="1000", cols="15")
107
- # Add headers
108
- headers = [
109
- "Timestamp", "Session_ID", "Question", "Total_Time_MS",
110
- "Intent_Classification_MS", "Memory_Retrieval_MS", "RAG_Retrieval_MS",
111
- "Embedding_Generation_MS", "Similarity_Calculation_MS", "Context_Processing_MS",
112
- "LLM_Generation_MS", "Memory_Update_MS", "Logging_MS", "Error_Step", "Notes"
113
- ]
114
- timing_sheet.append_row(headers)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  def log_response(question, answer, source_ids, knowledge_pairs, session_id):
117
  """Original response logging function"""
@@ -130,17 +162,19 @@ def log_response(question, answer, source_ids, knowledge_pairs, session_id):
130
  except Exception as e:
131
  print(f"Failed to log to Google Sheet: {e}")
132
  with open("/tmp/response_log.txt", "a") as f:
133
- f.write(f"{timestamp},{question},{answer},{source_ids},{knowledge_question_1},{knowledge_answer_1},{knowledge_question_2},{knowledge_answer_2}\n")
134
 
135
  def log_timing_data(question, session_id, timing_summary, error_step=None, notes=None):
136
  """Log timing data to the timing sheet"""
 
 
137
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
138
  step_times = timing_summary['step_times']
139
 
140
  row = [
141
  timestamp,
142
  session_id,
143
- question[:100] + "..." if len(question) > 100 else question, # Truncate long questions
144
  timing_summary['total_time_ms'],
145
  step_times.get('intent_classification', 0),
146
  step_times.get('memory_retrieval', 0),
@@ -160,16 +194,55 @@ def log_timing_data(question, session_id, timing_summary, error_step=None, notes
160
  print(f"Logged timing data: Total {timing_summary['total_time_ms']}ms")
161
  except Exception as e:
162
  print(f"Failed to log timing data: {e}")
163
- # Fallback to local file
164
- with open("/tmp/timing_log.txt", "a") as f:
165
- f.write(f"{timestamp},{session_id},{question},{timing_summary}\n")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
  # === LangGraph Memory Setup ===
168
  conn = sqlite3.connect("xeno_memory.db", check_same_thread=False)
169
  memory = SqliteSaver(conn=conn)
170
 
171
  def update_memory(config, user_message, assistant_message):
172
- """Update memory with timing"""
173
  with timer.time_step("memory_update"):
174
  full_checkpoint = memory.get(config) or {}
175
  messages = full_checkpoint.get("channel_values", {}).get("messages", [])
@@ -189,7 +262,6 @@ def update_memory(config, user_message, assistant_message):
189
  memory.put(config, checkpoint_to_save, {}, {})
190
 
191
  def retrieve_memory(config):
192
- """Retrieve memory with timing"""
193
  with timer.time_step("memory_retrieval"):
194
  full_checkpoint = memory.get(config) or {}
195
  return full_checkpoint.get("channel_values", {}).get("messages", [])
@@ -237,46 +309,39 @@ class IntentClassifier:
237
  }
238
 
239
  def classify_intent(self, message: str) -> Tuple[str, str]:
240
- """Classify intent with timing"""
241
  message_lower = message.lower().strip()
242
-
243
  for intent_name, intent_data in self.intent_patterns.items():
244
  for pattern in intent_data['patterns']:
245
  if re.search(pattern, message_lower, re.IGNORECASE):
246
  import random
247
  response = random.choice(intent_data['responses'])
248
  return intent_name, response
249
-
250
  return 'query', ''
251
-
252
- def is_simple_intent(self, intent: str) -> bool:
253
- simple_intents = ['greeting', 'thanks']
254
- return intent in simple_intents
255
 
256
  intent_classifier = IntentClassifier()
257
 
258
  # === Load and Clean Knowledge Base ===
259
- df_kb = pd.read_json("XENO_Uganda_KnowledgeBase_Advisory.json")
260
- df_kb.dropna(subset=['Content'], inplace=True)
261
-
262
- def prepare_documents(data):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  documents, metadatas, ids = [], [], []
264
- for item in data:
265
- documents.append(f"Question: {item['Question']}\nAnswer: {item['Content']}")
266
- metadatas.append({
267
- "question": item["Question"],
268
- "content": item["Content"],
269
- "section": item.get("Section", ""),
270
- "source": item.get("Source", ""),
271
- "owner": item.get("Owner", ""),
272
- "tag": item.get("Tag", ""),
273
- "id": item["ID"]
274
- })
275
- ids.append(item["ID"])
276
- return documents, metadatas, ids
277
-
278
- xeno_data_list = df_kb.to_dict('records')
279
- documents, metadatas, ids = prepare_documents(xeno_data_list)
280
 
281
  # === Setup ChromaDB ===
282
  try:
@@ -287,7 +352,8 @@ try:
287
  except:
288
  print(f"Creating new ChromaDB collection: {collection_name}")
289
  collection = client.create_collection(name=collection_name)
290
- collection.add(documents=documents, metadatas=metadatas, ids=ids)
 
291
  except Exception as e:
292
  print(f"Failed to initialize ChromaDB: {e}")
293
  raise
@@ -305,7 +371,6 @@ remember previous conversations."""
305
 
306
  # === Context Processing ===
307
  def process_context(results, cosine_scores, max_results=2):
308
- """Process context with timing"""
309
  with timer.time_step("context_processing"):
310
  sorted_indices = np.argsort(cosine_scores)[::-1][:max_results]
311
  formatted_context = ""
@@ -320,13 +385,12 @@ def process_context(results, cosine_scores, max_results=2):
320
  formatted_context += f"Q: {question}\n"
321
  formatted_context += f"A: {answer}\n"
322
  formatted_context += "-" * 40 + "\n"
323
- source_ids.append(result.metadata.get('id', 'N/A'))
324
  knowledge_pairs.append((question, answer))
325
  return formatted_context, source_ids, knowledge_pairs
326
 
327
  # === LLM Generation ===
328
  def generate_xeno_response(context, question, chat_history):
329
- """Generate response with timing"""
330
  with timer.time_step("llm_generation"):
331
  model = genai.GenerativeModel(llm_model_name)
332
  formatted_history = "\n".join(
@@ -340,7 +404,6 @@ def generate_xeno_response(context, question, chat_history):
340
 
341
  # === Main Interface Logic ===
342
  def get_context_and_answer(message, history, session_id="default"):
343
- """Main pipeline with comprehensive timing"""
344
  # Reset timer for new request
345
  timer.reset()
346
  error_step = None
@@ -396,16 +459,16 @@ def get_context_and_answer(message, history, session_id="default"):
396
  torch.tensor(query_embedding).float(),
397
  torch.tensor(doc_embeddings).float()
398
  )[0].tolist()
399
- max_score = max(cosine_scores)
400
 
401
  if max_score < 0.4:
402
  answer = "I'm sorry, I couldn't find specific information for your question. Could you try rephrasing it, or contact XENO support directly?"
403
  notes.append(f"Low similarity score: {max_score:.3f}")
404
  else:
405
- # Step 6: Context Processing (timed within function)
406
  context, source_ids_list, knowledge_pairs = process_context(queried_results, cosine_scores)
407
 
408
- # Step 7: LLM Generation (timed within function)
409
  answer = generate_xeno_response(context, message, chat_history)
410
  source_ids = ", ".join(source_ids_list)
411
  notes.append(f"Max similarity: {max_score:.3f}")
@@ -413,10 +476,11 @@ def get_context_and_answer(message, history, session_id="default"):
413
  except Exception as e:
414
  error_step = timer.current_step or "rag_processing"
415
  print(f"Error during RAG processing: {e}")
 
416
  answer = "I apologize, but I'm having a technical issue. Please try again shortly or contact XENO support."
417
  notes.append(f"Error: {str(e)}")
418
 
419
- # Step 8: Memory Update (timed within function)
420
  update_memory(config, message, answer)
421
 
422
  # Step 9: Response Logging
@@ -440,7 +504,6 @@ def get_context_and_answer(message, history, session_id="default"):
440
  logging.error(f"Error in main pipeline: {e}")
441
  logging.error(traceback.format_exc())
442
 
443
- # Still log timing data even on error
444
  timing_summary = timer.get_timing_summary()
445
  log_timing_data(
446
  message,
@@ -478,22 +541,53 @@ def create_interface():
478
  *Simply type your question below to get started!*
479
  """)
480
 
481
- session_id_box = gr.Textbox(label="Session ID", value=str(uuid.uuid4()), interactive=True)
 
482
 
483
  chatbot = gr.Chatbot(
484
  label="XENO Assistant",
485
  bubble_full_width=False,
486
- height=500
487
  )
488
 
489
  with gr.Row():
490
  msg = gr.Textbox(
491
  label="Your Message",
492
  placeholder="Type your question here...",
493
- scale=3,
494
  )
495
  send_button = gr.Button("Send", variant="primary", scale=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
496
 
 
497
  send_button.click(respond, [msg, chatbot, session_id_box], [msg, chatbot])
498
  msg.submit(respond, [msg, chatbot, session_id_box], [msg, chatbot])
499
 
 
18
  from typing import Dict, List, Tuple
19
  import time
20
  from contextlib import contextmanager
21
+ import threading # <--- Added for non-blocking feedback logging
22
 
23
  import logging
24
  import traceback
 
80
  timer = PipelineTimer()
81
 
82
  # === Configuration ===
83
+ # Ensure API Key is set
84
+ if "GEMINI_API_KEY" not in os.environ:
85
+ print("WARNING: GEMINI_API_KEY environment variable not found.")
86
+
87
+ genai.configure(api_key=os.environ.get("GEMINI_API_KEY"))
88
  embedding_model = "models/embedding-001"
89
  llm_model_name = "models/gemma-3-4b-it"
90
  collection_name = "xeno_collection"
 
99
  creds = Credentials.from_service_account_info(credentials_dict, scopes=scope)
100
  return creds
101
 
102
+ # Authenticate
103
+ try:
104
+ client_gspread = gspread.authorize(get_google_sheets_credentials())
105
+ spreadsheet = client_gspread.open("Response_Log")
106
+ response_sheet = spreadsheet.sheet1
107
+ except Exception as e:
108
+ print(f"Error connecting to Google Sheets: {e}")
109
+ # Create dummy objects if connection fails to prevent app crash during dev
110
+ class DummySheet:
111
+ def append_row(self, *args, **kwargs): pass
112
+ def worksheet(self, *args): return self
113
+ def add_worksheet(self, *args, **kwargs): return self
114
+ spreadsheet = DummySheet()
115
+ response_sheet = DummySheet()
116
+
117
+ # Setup Timing Sheet
118
  try:
119
  timing_sheet = spreadsheet.worksheet("Timing_Log")
120
  except:
121
+ try:
122
+ timing_sheet = spreadsheet.add_worksheet(title="Timing_Log", rows="1000", cols="15")
123
+ headers = [
124
+ "Timestamp", "Session_ID", "Question", "Total_Time_MS",
125
+ "Intent_Classification_MS", "Memory_Retrieval_MS", "RAG_Retrieval_MS",
126
+ "Embedding_Generation_MS", "Similarity_Calculation_MS", "Context_Processing_MS",
127
+ "LLM_Generation_MS", "Memory_Update_MS", "Logging_MS", "Error_Step", "Notes"
128
+ ]
129
+ timing_sheet.append_row(headers)
130
+ except Exception as e:
131
+ print(f"Could not create Timing_Log sheet: {e}")
132
+ timing_sheet = None
133
+
134
+ # === NEW: Setup Feedback Sheet ===
135
+ try:
136
+ feedback_sheet = spreadsheet.worksheet("Feedback_Log")
137
+ except:
138
+ try:
139
+ feedback_sheet = spreadsheet.add_worksheet(title="Feedback_Log", rows="1000", cols="6")
140
+ headers = ["Timestamp", "Session_ID", "User_Message", "Bot_Response", "Rating", "Flag_Reason"]
141
+ feedback_sheet.append_row(headers)
142
+ except Exception as e:
143
+ print(f"Could not create Feedback_Log sheet: {e}")
144
+ feedback_sheet = None
145
+
146
+ # === Logging Functions ===
147
 
148
  def log_response(question, answer, source_ids, knowledge_pairs, session_id):
149
  """Original response logging function"""
 
162
  except Exception as e:
163
  print(f"Failed to log to Google Sheet: {e}")
164
  with open("/tmp/response_log.txt", "a") as f:
165
+ f.write(f"{timestamp},{question},{answer},{source_ids}\n")
166
 
167
  def log_timing_data(question, session_id, timing_summary, error_step=None, notes=None):
168
  """Log timing data to the timing sheet"""
169
+ if timing_sheet is None: return
170
+
171
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
172
  step_times = timing_summary['step_times']
173
 
174
  row = [
175
  timestamp,
176
  session_id,
177
+ question[:100] + "..." if len(question) > 100 else question,
178
  timing_summary['total_time_ms'],
179
  step_times.get('intent_classification', 0),
180
  step_times.get('memory_retrieval', 0),
 
194
  print(f"Logged timing data: Total {timing_summary['total_time_ms']}ms")
195
  except Exception as e:
196
  print(f"Failed to log timing data: {e}")
197
+
198
+ # === NEW: Feedback Functions ===
199
+
200
+ def _log_feedback_background(row):
201
+ """Helper to run network request in background thread"""
202
+ try:
203
+ if feedback_sheet:
204
+ feedback_sheet.append_row(row)
205
+ print("Feedback logged successfully.")
206
+ else:
207
+ print("Feedback sheet not available.")
208
+ except Exception as e:
209
+ print(f"Failed to log feedback: {e}")
210
+
211
+ def submit_feedback(rating, reason, history, session_id):
212
+ """
213
+ Handles user feedback submission.
214
+ rating: 'Positive' or 'Negative'
215
+ reason: User provided text
216
+ history: Gradio chat history list
217
+ """
218
+ if not history or len(history) == 0:
219
+ return "No conversation to rate yet."
220
+
221
+ # Get the last interaction (Gradio history is a list of lists: [[user, bot], ...])
222
+ last_interaction = history[-1]
223
+
224
+ # Safety check for history format
225
+ if isinstance(last_interaction, list) and len(last_interaction) >= 2:
226
+ user_msg = last_interaction[0]
227
+ bot_msg = last_interaction[1]
228
+ else:
229
+ return "Error reading conversation history."
230
+
231
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
232
+
233
+ # Prepare row data
234
+ row = [timestamp, session_id, user_msg, bot_msg, rating, reason]
235
+
236
+ # Run in thread to prevent UI blocking
237
+ threading.Thread(target=_log_feedback_background, args=(row,)).start()
238
+
239
+ return f"Feedback received ({rating}). Thank you!"
240
 
241
  # === LangGraph Memory Setup ===
242
  conn = sqlite3.connect("xeno_memory.db", check_same_thread=False)
243
  memory = SqliteSaver(conn=conn)
244
 
245
  def update_memory(config, user_message, assistant_message):
 
246
  with timer.time_step("memory_update"):
247
  full_checkpoint = memory.get(config) or {}
248
  messages = full_checkpoint.get("channel_values", {}).get("messages", [])
 
262
  memory.put(config, checkpoint_to_save, {}, {})
263
 
264
  def retrieve_memory(config):
 
265
  with timer.time_step("memory_retrieval"):
266
  full_checkpoint = memory.get(config) or {}
267
  return full_checkpoint.get("channel_values", {}).get("messages", [])
 
309
  }
310
 
311
  def classify_intent(self, message: str) -> Tuple[str, str]:
 
312
  message_lower = message.lower().strip()
 
313
  for intent_name, intent_data in self.intent_patterns.items():
314
  for pattern in intent_data['patterns']:
315
  if re.search(pattern, message_lower, re.IGNORECASE):
316
  import random
317
  response = random.choice(intent_data['responses'])
318
  return intent_name, response
 
319
  return 'query', ''
 
 
 
 
320
 
321
  intent_classifier = IntentClassifier()
322
 
323
  # === Load and Clean Knowledge Base ===
324
+ try:
325
+ df_kb = pd.read_json("XENO_Uganda_KnowledgeBase_Advisory.json")
326
+ df_kb.dropna(subset=['Content'], inplace=True)
327
+
328
+ def prepare_documents(data):
329
+ documents, metadatas, ids = [], [], []
330
+ for item in data:
331
+ documents.append(f"Question: {item['Question']}\nAnswer: {item['Content']}")
332
+ metadatas.append({
333
+ "question": item["Question"],
334
+ "content": item["Content"],
335
+ "id": str(item["ID"])
336
+ })
337
+ ids.append(str(item["ID"]))
338
+ return documents, metadatas, ids
339
+
340
+ xeno_data_list = df_kb.to_dict('records')
341
+ documents, metadatas, ids = prepare_documents(xeno_data_list)
342
+ except Exception as e:
343
+ print(f"Warning: Could not load JSON knowledge base: {e}")
344
  documents, metadatas, ids = [], [], []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
 
346
  # === Setup ChromaDB ===
347
  try:
 
352
  except:
353
  print(f"Creating new ChromaDB collection: {collection_name}")
354
  collection = client.create_collection(name=collection_name)
355
+ if documents:
356
+ collection.add(documents=documents, metadatas=metadatas, ids=ids)
357
  except Exception as e:
358
  print(f"Failed to initialize ChromaDB: {e}")
359
  raise
 
371
 
372
  # === Context Processing ===
373
  def process_context(results, cosine_scores, max_results=2):
 
374
  with timer.time_step("context_processing"):
375
  sorted_indices = np.argsort(cosine_scores)[::-1][:max_results]
376
  formatted_context = ""
 
385
  formatted_context += f"Q: {question}\n"
386
  formatted_context += f"A: {answer}\n"
387
  formatted_context += "-" * 40 + "\n"
388
+ source_ids.append(str(result.metadata.get('id', 'N/A')))
389
  knowledge_pairs.append((question, answer))
390
  return formatted_context, source_ids, knowledge_pairs
391
 
392
  # === LLM Generation ===
393
  def generate_xeno_response(context, question, chat_history):
 
394
  with timer.time_step("llm_generation"):
395
  model = genai.GenerativeModel(llm_model_name)
396
  formatted_history = "\n".join(
 
404
 
405
  # === Main Interface Logic ===
406
  def get_context_and_answer(message, history, session_id="default"):
 
407
  # Reset timer for new request
408
  timer.reset()
409
  error_step = None
 
459
  torch.tensor(query_embedding).float(),
460
  torch.tensor(doc_embeddings).float()
461
  )[0].tolist()
462
+ max_score = max(cosine_scores) if cosine_scores else 0
463
 
464
  if max_score < 0.4:
465
  answer = "I'm sorry, I couldn't find specific information for your question. Could you try rephrasing it, or contact XENO support directly?"
466
  notes.append(f"Low similarity score: {max_score:.3f}")
467
  else:
468
+ # Step 6: Context Processing
469
  context, source_ids_list, knowledge_pairs = process_context(queried_results, cosine_scores)
470
 
471
+ # Step 7: LLM Generation
472
  answer = generate_xeno_response(context, message, chat_history)
473
  source_ids = ", ".join(source_ids_list)
474
  notes.append(f"Max similarity: {max_score:.3f}")
 
476
  except Exception as e:
477
  error_step = timer.current_step or "rag_processing"
478
  print(f"Error during RAG processing: {e}")
479
+ traceback.print_exc()
480
  answer = "I apologize, but I'm having a technical issue. Please try again shortly or contact XENO support."
481
  notes.append(f"Error: {str(e)}")
482
 
483
+ # Step 8: Memory Update
484
  update_memory(config, message, answer)
485
 
486
  # Step 9: Response Logging
 
504
  logging.error(f"Error in main pipeline: {e}")
505
  logging.error(traceback.format_exc())
506
 
 
507
  timing_summary = timer.get_timing_summary()
508
  log_timing_data(
509
  message,
 
541
  *Simply type your question below to get started!*
542
  """)
543
 
544
+ # Hidden state for session
545
+ session_id_box = gr.Textbox(label="Session ID", value=str(uuid.uuid4()), visible=False)
546
 
547
  chatbot = gr.Chatbot(
548
  label="XENO Assistant",
549
  bubble_full_width=False,
550
+ height=450
551
  )
552
 
553
  with gr.Row():
554
  msg = gr.Textbox(
555
  label="Your Message",
556
  placeholder="Type your question here...",
557
+ scale=4,
558
  )
559
  send_button = gr.Button("Send", variant="primary", scale=1)
560
+
561
+ # ===== FEEDBACK SECTION =====
562
+ with gr.Row():
563
+ with gr.Accordion("Rate this response / Flag Issue", open=False):
564
+ with gr.Row():
565
+ thumbs_up = gr.Button("👍 Good Answer")
566
+ thumbs_down = gr.Button("👎 Bad / Flag")
567
+
568
+ feedback_reason = gr.Textbox(
569
+ label="Reason (Optional for Like, Required for Flag)",
570
+ placeholder="E.g., Incorrect fees, hallucination, rude..."
571
+ )
572
+ feedback_status = gr.Label(value="", label="Status", show_label=False)
573
+
574
+ # Feedback Event Listeners
575
+ # Logic: If Thumbs Up is clicked, send 'Positive'. If Textbox is empty, reason defaults to "Good".
576
+ thumbs_up.click(
577
+ fn=lambda h, s, r: submit_feedback("Positive", r if r else "Good", h, s),
578
+ inputs=[chatbot, session_id_box, feedback_reason],
579
+ outputs=[feedback_status]
580
+ )
581
+
582
+ # Logic: If Thumbs Down is clicked, send 'Negative' with the content of the textbox.
583
+ thumbs_down.click(
584
+ fn=lambda r, h, s: submit_feedback("Negative", r, h, s),
585
+ inputs=[feedback_reason, chatbot, session_id_box],
586
+ outputs=[feedback_status]
587
+ )
588
+ # =============================
589
 
590
+ # Chat Event Listeners
591
  send_button.click(respond, [msg, chatbot, session_id_box], [msg, chatbot])
592
  msg.submit(respond, [msg, chatbot, session_id_box], [msg, chatbot])
593