Sebunya commited on
Commit
03c0f5d
·
verified ·
1 Parent(s): 3ca43b8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +284 -234
app.py CHANGED
@@ -18,7 +18,7 @@ import re
18
  from typing import Dict, List, Tuple
19
  import time
20
  from contextlib import contextmanager
21
- import threading # Required for background logging
22
  import logging
23
  import traceback
24
  import sys
@@ -59,26 +59,27 @@ class PipelineTimer:
59
  yield
60
  finally:
61
  step_end = time.time()
62
- self.step_times[step_name] = round((step_end - step_start) * 1000, 2)
63
  self.current_step = None
64
 
65
  def get_total_time(self):
 
66
  return round((time.time() - self.start_time) * 1000, 2)
67
 
68
  def get_timing_summary(self):
 
 
69
  return {
70
- 'total_time_ms': self.get_total_time(),
71
  'step_times': self.step_times,
72
  'timestamp': datetime.now().isoformat()
73
  }
74
 
 
75
  timer = PipelineTimer()
76
 
77
  # === Configuration ===
78
- if "GEMINI_API_KEY" not in os.environ:
79
- print("WARNING: GEMINI_API_KEY environment variable not found.")
80
-
81
- genai.configure(api_key=os.environ.get("GEMINI_API_KEY"))
82
  embedding_model = "models/embedding-001"
83
  llm_model_name = "models/gemma-3-4b-it"
84
  collection_name = "xeno_collection"
@@ -93,52 +94,27 @@ def get_google_sheets_credentials():
93
  creds = Credentials.from_service_account_info(credentials_dict, scopes=scope)
94
  return creds
95
 
96
- # Initialize Sheets
97
- try:
98
- client_gspread = gspread.authorize(get_google_sheets_credentials())
99
- spreadsheet = client_gspread.open("Response_Log")
100
- response_sheet = spreadsheet.sheet1
101
- except Exception as e:
102
- print(f"Error connecting to Google Sheets: {e}")
103
- # Dummy classes for dev/fallback
104
- class DummySheet:
105
- def append_row(self, *args, **kwargs): pass
106
- def worksheet(self, *args): return self
107
- def add_worksheet(self, *args, **kwargs): return self
108
- spreadsheet = DummySheet()
109
- response_sheet = DummySheet()
110
-
111
- # Timing Sheet
112
- try:
113
- timing_sheet = spreadsheet.worksheet("Timing_Log")
114
- except:
115
- try:
116
- timing_sheet = spreadsheet.add_worksheet(title="Timing_Log", rows="1000", cols="15")
117
- headers = [
118
- "Timestamp", "Session_ID", "Question", "Total_Time_MS",
119
- "Intent_Classification_MS", "Memory_Retrieval_MS", "RAG_Retrieval_MS",
120
- "Embedding_Generation_MS", "Similarity_Calculation_MS", "Context_Processing_MS",
121
- "LLM_Generation_MS", "Memory_Update_MS", "Logging_MS", "Error_Step", "Notes"
122
- ]
123
- timing_sheet.append_row(headers)
124
- except:
125
- timing_sheet = None
126
 
127
- # Feedback Sheet
 
 
128
  try:
129
- feedback_sheet = spreadsheet.worksheet("Feedback_Log")
130
  except:
131
- try:
132
- feedback_sheet = spreadsheet.add_worksheet(title="Feedback_Log", rows="1000", cols="6")
133
- headers = ["Timestamp", "Session_ID", "User_Message", "Bot_Response", "Rating", "Flag_Reason"]
134
- feedback_sheet.append_row(headers)
135
- except:
136
- feedback_sheet = None
137
-
138
- # === Logging Functions ===
 
 
139
 
140
  def log_response(question, answer, source_ids, knowledge_pairs, session_id):
141
- """Log the main chat interaction"""
142
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
143
  knowledge_question_1 = knowledge_pairs[0][0] if len(knowledge_pairs) > 0 else "N/A"
144
  knowledge_answer_1 = knowledge_pairs[0][1] if len(knowledge_pairs) > 0 else "N/A"
@@ -150,194 +126,222 @@ def log_response(question, answer, source_ids, knowledge_pairs, session_id):
150
  ]
151
  try:
152
  response_sheet.append_row(row)
153
- print(f"Logged response: {question} | Sources: {source_ids}")
154
  except Exception as e:
155
- print(f"Failed to log response: {e}")
 
 
156
 
157
  def log_timing_data(question, session_id, timing_summary, error_step=None, notes=None):
158
- """Log performance metrics"""
159
- if timing_sheet is None: return
160
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
161
  step_times = timing_summary['step_times']
 
162
  row = [
163
- timestamp, session_id, question[:100], timing_summary['total_time_ms'],
164
- step_times.get('intent_classification', 0), step_times.get('memory_retrieval', 0),
165
- step_times.get('rag_retrieval', 0), step_times.get('embedding_generation', 0),
166
- step_times.get('similarity_calculation', 0), step_times.get('context_processing', 0),
167
- step_times.get('llm_generation', 0), step_times.get('memory_update', 0),
168
- step_times.get('response_logging', 0), error_step or "", notes or ""
 
 
 
 
 
 
 
 
 
169
  ]
170
- try:
171
- timing_sheet.append_row(row)
172
- except Exception as e:
173
- print(f"Failed to log timing: {e}")
174
-
175
- # === Feedback Functions ===
176
-
177
- def _log_feedback_background(row):
178
- """Background worker to send feedback to Google Sheets"""
179
- try:
180
- if feedback_sheet:
181
- feedback_sheet.append_row(row)
182
- print("Feedback logged successfully.")
183
- else:
184
- print("Feedback sheet not available.")
185
- except Exception as e:
186
- print(f"Failed to log feedback: {e}")
187
-
188
- def handle_vote(data: gr.LikeData, history, session_id):
189
- """
190
- Handles the Google AI Studio style Thumbs Up/Down events.
191
- Triggered when user clicks the icon on the chat bubble.
192
- """
193
- if not history: return
194
 
195
  try:
196
- # Determine rating
197
- rating = "Positive" if data.liked else "Negative"
198
-
199
- # Get the interaction from history using data.index
200
- # history is a list of [user_msg, bot_msg]
201
- interaction_index = data.index
202
-
203
- # Safety check on index
204
- if interaction_index < len(history):
205
- interaction = history[interaction_index]
206
- user_msg = interaction[0]
207
- bot_msg = interaction[1]
208
-
209
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
210
- row = [timestamp, session_id, user_msg, bot_msg, rating, "Quick Vote (Icon Click)"]
211
-
212
- # Run in background thread
213
- threading.Thread(target=_log_feedback_background, args=(row,)).start()
214
- print(f"Vote registered: {rating}")
215
-
216
- except Exception as e:
217
- print(f"Error handling vote: {e}")
218
-
219
- def submit_manual_flag(reason, history, session_id):
220
- """Handles the manual text feedback submission"""
221
- if not history: return "No conversation to flag."
222
-
223
- try:
224
- last_interaction = history[-1]
225
- user_msg = last_interaction[0]
226
- bot_msg = last_interaction[1]
227
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
228
-
229
- row = [timestamp, session_id, user_msg, bot_msg, "Negative", reason]
230
- threading.Thread(target=_log_feedback_background, args=(row,)).start()
231
-
232
- return "Report submitted. Thank you."
233
  except Exception as e:
234
- return f"Error submitting report: {str(e)}"
 
 
 
235
 
236
- # === Core Logic & Classes ===
237
  conn = sqlite3.connect("xeno_memory.db", check_same_thread=False)
238
  memory = SqliteSaver(conn=conn)
239
 
240
  def update_memory(config, user_message, assistant_message):
 
241
  with timer.time_step("memory_update"):
242
  full_checkpoint = memory.get(config) or {}
243
  messages = full_checkpoint.get("channel_values", {}).get("messages", [])
 
244
  messages.append({"role": "user", "content": user_message})
245
  messages.append({"role": "assistant", "content": assistant_message})
246
- checkpoint = {
247
- "v": 1, "id": str(uuid.uuid4()), "ts": datetime.now().isoformat(),
 
 
 
248
  "channel_values": {"messages": messages},
249
- "channel_versions": {}, "versions_seen": {},
 
250
  }
251
- memory.put(config, checkpoint, {}, {})
 
252
 
253
  def retrieve_memory(config):
 
254
  with timer.time_step("memory_retrieval"):
255
  full_checkpoint = memory.get(config) or {}
256
  return full_checkpoint.get("channel_values", {}).get("messages", [])
257
 
 
258
  class IntentClassifier:
259
  def __init__(self):
260
  self.intent_patterns = {
261
  'greeting': {
262
- 'patterns': [r'\b(hi|hello|hey|greetings)\b', r'^(hi|hello)[\s!.]*$'],
263
- 'responses': ["Hello! I'm XENO Assistant. How can I help you with XENO financial services?"]
 
 
 
 
 
 
 
 
264
  },
265
  'thanks': {
266
- 'patterns': [r'\b(thank|thanks)\b'],
267
- 'responses': ["You're welcome! Let me know if you need anything else."]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  }
269
  }
270
 
271
  def classify_intent(self, message: str) -> Tuple[str, str]:
 
272
  message_lower = message.lower().strip()
 
273
  for intent_name, intent_data in self.intent_patterns.items():
274
  for pattern in intent_data['patterns']:
275
  if re.search(pattern, message_lower, re.IGNORECASE):
276
- return intent_name, intent_data['responses'][0]
 
 
 
277
  return 'query', ''
 
 
 
 
278
 
279
  intent_classifier = IntentClassifier()
280
 
281
- # === Knowledge Base & ChromaDB ===
282
- try:
283
- df_kb = pd.read_json("XENO_Uganda_KnowledgeBase_Advisory.json")
284
- df_kb.dropna(subset=['Content'], inplace=True)
285
- xeno_data_list = df_kb.to_dict('records')
286
-
287
  documents, metadatas, ids = [], [], []
288
- for item in xeno_data_list:
289
  documents.append(f"Question: {item['Question']}\nAnswer: {item['Content']}")
290
- metadatas.append({"question": item["Question"], "content": item["Content"], "id": str(item["ID"])})
291
- ids.append(str(item["ID"]))
292
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  client = chromadb.PersistentClient(path="/tmp/xeno_db")
294
  try:
295
  collection = client.get_collection(name=collection_name)
 
296
  except:
 
297
  collection = client.create_collection(name=collection_name)
298
- if documents: collection.add(documents=documents, metadatas=metadatas, ids=ids)
299
-
300
- vector_store = Chroma(client=client, collection_name=collection_name)
301
- retriever = vector_store.as_retriever(search_type="similarity", search_kwargs={"k": 4})
302
  except Exception as e:
303
- print(f"DB Init Error: {e}")
304
- # Define dummy retriever to allow UI to load even if DB fails
305
- class DummyRetriever:
306
- def invoke(self, *args): return []
307
- retriever = DummyRetriever()
308
 
309
- # === Prompt & Generation ===
310
- SYSTEM_PROMPT = """You are a friendly XENO Support Assistant.
311
- Use only the information provided in the context to answer.
312
- If context is missing, apologize and say you cannot assist. Do not hallucinate."""
313
 
 
 
 
 
 
 
 
 
 
314
  def process_context(results, cosine_scores, max_results=2):
 
315
  with timer.time_step("context_processing"):
316
- if not results: return "", [], []
317
  sorted_indices = np.argsort(cosine_scores)[::-1][:max_results]
318
  formatted_context = ""
319
  source_ids = []
320
  knowledge_pairs = []
321
  for i, idx in enumerate(sorted_indices, 1):
322
- if idx < len(results):
323
- result = results[idx]
324
- question = result.metadata.get('question', 'N/A')
325
- answer = result.metadata.get('content', 'N/A')
326
- formatted_context += f"Info {i}: Q: {question}\n A: {answer}\n---\n"
327
- source_ids.append(str(result.metadata.get('id', 'N/A')))
328
- knowledge_pairs.append((question, answer))
 
 
 
329
  return formatted_context, source_ids, knowledge_pairs
330
 
 
331
  def generate_xeno_response(context, question, chat_history):
 
332
  with timer.time_step("llm_generation"):
333
  model = genai.GenerativeModel(llm_model_name)
334
- hist_text = "\n".join([f"{m['role']}: {m['content']}" for m in chat_history]) if chat_history else ""
335
- prompt = f"{SYSTEM_PROMPT}\nHistory:\n{hist_text}\nContext:\n{context}\nQuestion:\n{question}"
 
 
 
 
336
  response = model.generate_content(prompt)
337
  return response.text.strip()
338
 
339
- # === Main Pipeline ===
340
- def get_context_and_answer(message, history, session_id):
 
 
341
  timer.reset()
342
  error_step = None
343
  notes = []
@@ -345,108 +349,154 @@ def get_context_and_answer(message, history, session_id):
345
  try:
346
  config = {"configurable": {"thread_id": str(session_id), "checkpoint_ns": ""}}
347
 
 
348
  with timer.time_step("intent_classification"):
349
  intent, direct_response = intent_classifier.classify_intent(message)
350
 
 
351
  chat_history = retrieve_memory(config)
352
- answer, source_ids, knowledge_pairs = "", "N/A", []
 
 
 
353
 
354
  if intent != 'query':
355
  answer = direct_response
356
- notes.append(f"Intent: {intent}")
357
- else:
358
- try:
359
- with timer.time_step("rag_retrieval"):
360
- queried_results = retriever.invoke(message)
361
-
362
- with timer.time_step("embedding_generation"):
363
- q_embed = genai.embed_content(model=embedding_model, content=message, task_type="retrieval_query")['embedding']
364
- d_embeds = [genai.embed_content(model=embedding_model, content=d.page_content, task_type="retrieval_document")['embedding'] for d in queried_results]
365
-
366
- with timer.time_step("similarity_calculation"):
367
- if d_embeds:
368
- cosine_scores = util.cos_sim(torch.tensor(q_embed).float(), torch.tensor(d_embeds).float())[0].tolist()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  max_score = max(cosine_scores)
370
- else:
371
- cosine_scores, max_score = [], 0
372
-
373
- if max_score < 0.4:
374
- answer = "I'm sorry, I couldn't find specific information for your question.Could You please specify exactly what seems to be the issue"
375
- notes.append(f"Low score: {max_score}")
376
- else:
377
- context, source_ids_list, knowledge_pairs = process_context(queried_results, cosine_scores)
378
- answer = generate_xeno_response(context, message, chat_history)
379
- source_ids = ", ".join(source_ids_list)
380
- notes.append(f"Score: {max_score:.2f}")
381
-
382
- except Exception as e:
383
- error_step = "rag_pipeline"
384
- answer = "I apologize, but I'm having a technical issue."
385
- print(f"RAG Error: {e}")
386
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  update_memory(config, message, answer)
388
 
 
389
  with timer.time_step("response_logging"):
390
  log_response(message, answer, source_ids, knowledge_pairs, session_id)
391
 
392
- log_timing_data(message, session_id, timer.get_timing_summary(), error_step, "; ".join(notes))
 
 
 
 
 
 
 
 
 
393
  return answer
394
-
395
  except Exception as e:
396
- log_timing_data(message, session_id, timer.get_timing_summary(), "pipeline_crash", str(e))
397
- return "System Error. Please try again."
 
 
 
 
 
 
 
 
 
 
 
 
 
398
 
399
- # === UI Logic ===
400
  def respond(message, history, session_id):
401
- if not session_id: session_id = str(uuid.uuid4())
 
 
 
402
  bot_response = get_context_and_answer(message, history, session_id)
403
  history.append([message, bot_response])
 
404
  return "", history
405
 
406
  def create_interface():
407
- # 'fill_height=True' is key for the modern full-screen chat look
408
- with gr.Blocks(theme=gr.themes.Soft(), fill_height=True) as demo:
409
- gr.Markdown("## ASKXENO Support")
 
 
 
 
 
 
 
410
 
411
- session_id_box = gr.Textbox(label="Session ID", value=str(uuid.uuid4()), visible=False)
 
 
 
412
 
413
- # likeable=True adds the Thumbs Up/Down icons to bubbles
414
  chatbot = gr.Chatbot(
415
  label="XENO Assistant",
416
- scale=1,
417
- likeable=True,
418
- show_copy_button=True,
419
- bubble_full_width=False
420
  )
421
 
422
- with gr.Row(variant="compact"):
423
  msg = gr.Textbox(
424
- placeholder="Ask about XENO services...",
425
- scale=6,
426
- lines=1,
427
- show_label=False,
428
- autofocus=True,
429
- container=False
430
  )
431
- send_btn = gr.Button("Send", variant="primary", scale=1, min_width=80)
432
-
433
- # Collapsible Flagging Section
434
- with gr.Accordion("Report an Issue", open=False):
435
- with gr.Row():
436
- flag_reason = gr.Textbox(placeholder="Describe the issue (e.g. incorrect fees)", show_label=False, scale=4)
437
- flag_btn = gr.Button("Submit Report", scale=1)
438
- flag_status = gr.Label(value="", show_label=False)
439
 
440
- # Event Wiring
441
  msg.submit(respond, [msg, chatbot, session_id_box], [msg, chatbot])
442
- send_btn.click(respond, [msg, chatbot, session_id_box], [msg, chatbot])
443
-
444
- # Handle the native Google AI Studio style likes
445
- chatbot.like(handle_vote, [chatbot, session_id_box], None)
446
-
447
- # Handle manual text flagging
448
- flag_btn.click(submit_manual_flag, [flag_reason, chatbot, session_id_box], [flag_status])
449
-
450
  return demo
451
 
452
  if __name__ == "__main__":
 
18
  from typing import Dict, List, Tuple
19
  import time
20
  from contextlib import contextmanager
21
+
22
  import logging
23
  import traceback
24
  import sys
 
59
  yield
60
  finally:
61
  step_end = time.time()
62
+ self.step_times[step_name] = round((step_end - step_start) * 1000, 2) # Convert to milliseconds
63
  self.current_step = None
64
 
65
  def get_total_time(self):
66
+ """Get total elapsed time since reset"""
67
  return round((time.time() - self.start_time) * 1000, 2)
68
 
69
  def get_timing_summary(self):
70
+ """Get a summary of all timing data"""
71
+ total_time = self.get_total_time()
72
  return {
73
+ 'total_time_ms': total_time,
74
  'step_times': self.step_times,
75
  'timestamp': datetime.now().isoformat()
76
  }
77
 
78
+ # Initialize global timer
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
  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"""
118
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
119
  knowledge_question_1 = knowledge_pairs[0][0] if len(knowledge_pairs) > 0 else "N/A"
120
  knowledge_answer_1 = knowledge_pairs[0][1] if len(knowledge_pairs) > 0 else "N/A"
 
126
  ]
127
  try:
128
  response_sheet.append_row(row)
129
+ print(f"Logged response: {question} | Source IDs: {source_ids}")
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),
147
+ step_times.get('rag_retrieval', 0),
148
+ step_times.get('embedding_generation', 0),
149
+ step_times.get('similarity_calculation', 0),
150
+ step_times.get('context_processing', 0),
151
+ step_times.get('llm_generation', 0),
152
+ step_times.get('memory_update', 0),
153
+ step_times.get('response_logging', 0),
154
+ error_step or "",
155
+ notes or ""
156
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
  try:
159
+ timing_sheet.append_row(row)
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", [])
176
+
177
  messages.append({"role": "user", "content": user_message})
178
  messages.append({"role": "assistant", "content": assistant_message})
179
+
180
+ checkpoint_to_save = {
181
+ "v": 1,
182
+ "id": str(uuid.uuid4()),
183
+ "ts": datetime.now().isoformat(),
184
  "channel_values": {"messages": messages},
185
+ "channel_versions": {},
186
+ "versions_seen": {},
187
  }
188
+
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", [])
196
 
197
+ # === Intent Classification System ===
198
  class IntentClassifier:
199
  def __init__(self):
200
  self.intent_patterns = {
201
  'greeting': {
202
+ 'patterns': [
203
+ r'\b(hi|hello|hey|good morning|good afternoon|good evening|greetings)\b',
204
+ r'^(hi|hello|hey)[\s!.]*$',
205
+ r'\b(how are you|how do you do)\b'
206
+ ],
207
+ 'responses': [
208
+ "Hello! I'm XENO Assistant. How can I help you with XENO financial services today?",
209
+ "Hi there! I'm here to assist you with any questions about XENO services. What can I help you with?",
210
+ "Good day! Welcome to XENO Support. How may I assist you today?"
211
+ ]
212
  },
213
  'thanks': {
214
+ 'patterns': [
215
+ r'\b(thank you|thanks|thank u|thx|appreciate|grateful)\b',
216
+ r'^(thanks|thank you)[\s!.]*$',
217
+ r'\b(much appreciated|thanks a lot|thank you so much)\b'
218
+ ],
219
+ 'responses': [
220
+ "You're welcome! Is there anything else I can help you with regarding XENO services?",
221
+ "Happy to help! Feel free to ask if you have any other questions about XENO.",
222
+ "Glad I could assist you! Let me know if you need help with anything else."
223
+ ]
224
+ },
225
+ 'goodbye': {
226
+ 'patterns': [
227
+ r'\b(bye|goodbye|see you|farewell|take care|have a good day)\b',
228
+ r'^(bye|goodbye)[\s!.]*$',
229
+ r'\b(talk to you later|see you later|until next time)\b'
230
+ ],
231
+ 'responses': [
232
+ "Goodbye! Thank you for using XENO services. Have a great day!",
233
+ "Take care! Feel free to return anytime you need help with XENO services.",
234
+ "Have a wonderful day! Don't hesitate to reach out if you need assistance with XENO."
235
+ ]
236
  }
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:
283
  client = chromadb.PersistentClient(path="/tmp/xeno_db")
284
  try:
285
  collection = client.get_collection(name=collection_name)
286
+ print(f"Loaded existing ChromaDB collection: {collection_name}")
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
 
 
 
294
 
295
+ vector_store = Chroma(client=client, collection_name=collection_name)
296
+ retriever = vector_store.as_retriever(search_type="similarity", search_kwargs={"k": 4})
 
 
297
 
298
+ # === Prompt System ===
299
+ SYSTEM_PROMPT = """You are a friendly XENO Support Assistant, an AI-powered helpful and professional customer service representative.
300
+ Use only the information provided in the knowledge base context to answer user queries.
301
+ Do not hallucinate. If context doesn't contain relevant info, say so in a calm polite manner by saying I'm sorry, I can't assist with that.
302
+ Only use context that is clearly relevant to the user's question.
303
+ For greetings like "hi" or "hello", respond politely without using the context.
304
+ 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 = ""
312
  source_ids = []
313
  knowledge_pairs = []
314
  for i, idx in enumerate(sorted_indices, 1):
315
+ result = results[idx]
316
+ score = cosine_scores[idx]
317
+ question = result.metadata.get('question', 'N/A')
318
+ answer = result.metadata.get('content', 'N/A')
319
+ formatted_context += f"Knowledge Entry {i}:\n"
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(
333
+ [f"{msg['role'].capitalize()}: {msg['content']}" for msg in chat_history]
334
+ ) if chat_history else "None"
335
+
336
+ prompt = f"{SYSTEM_PROMPT}\n### HISTORY ###\n{formatted_history}\n### CONTEXT ###\n{context}\n### QUESTION ###\n{question}"
337
+
338
  response = model.generate_content(prompt)
339
  return response.text.strip()
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
347
  notes = []
 
349
  try:
350
  config = {"configurable": {"thread_id": str(session_id), "checkpoint_ns": ""}}
351
 
352
+ # Step 1: Intent Classification
353
  with timer.time_step("intent_classification"):
354
  intent, direct_response = intent_classifier.classify_intent(message)
355
 
356
+ # Step 2: Memory Retrieval
357
  chat_history = retrieve_memory(config)
358
+
359
+ answer = ""
360
+ source_ids = "N/A"
361
+ knowledge_pairs = []
362
 
363
  if intent != 'query':
364
  answer = direct_response
365
+ notes.append(f"Simple intent: {intent}")
366
+ else:
367
+ if len(message.strip()) < 3:
368
+ answer = "I'd be happy to help! Could you please provide more details about what you'd like to know?"
369
+ notes.append("Message too short")
370
+ else:
371
+ try:
372
+ # Step 3: RAG Retrieval
373
+ with timer.time_step("rag_retrieval"):
374
+ queried_results = retriever.invoke(message)
375
+
376
+ # Step 4: Embedding Generation
377
+ with timer.time_step("embedding_generation"):
378
+ query_embedding = genai.embed_content(
379
+ model=embedding_model,
380
+ content=message,
381
+ task_type="retrieval_query"
382
+ )['embedding']
383
+
384
+ doc_embeddings = [
385
+ genai.embed_content(
386
+ model=embedding_model,
387
+ content=doc.page_content,
388
+ task_type="retrieval_document"
389
+ )['embedding']
390
+ for doc in queried_results
391
+ ]
392
+
393
+ # Step 5: Similarity Calculation
394
+ with timer.time_step("similarity_calculation"):
395
+ cosine_scores = util.cos_sim(
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}")
412
+
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
423
  with timer.time_step("response_logging"):
424
  log_response(message, answer, source_ids, knowledge_pairs, session_id)
425
 
426
+ # Log timing data
427
+ timing_summary = timer.get_timing_summary()
428
+ log_timing_data(
429
+ message,
430
+ session_id,
431
+ timing_summary,
432
+ error_step=error_step,
433
+ notes="; ".join(notes) if notes else None
434
+ )
435
+
436
  return answer
437
+
438
  except Exception as e:
439
+ error_step = timer.current_step or "main_pipeline"
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,
447
+ session_id,
448
+ timing_summary,
449
+ error_step=error_step,
450
+ notes=f"Pipeline error: {str(e)}"
451
+ )
452
+
453
+ return "I apologize, but I encountered an error processing your request. Please try again."
454
 
455
+ # === Enhanced Gradio UI ===
456
  def respond(message, history, session_id):
457
+ """Gradio's main response function"""
458
+ if not session_id:
459
+ session_id = str(uuid.uuid4())
460
+
461
  bot_response = get_context_and_answer(message, history, session_id)
462
  history.append([message, bot_response])
463
+
464
  return "", history
465
 
466
  def create_interface():
467
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
468
+ gr.Markdown("""
469
+ # ASKXENO
470
+ **Welcome to XENO AI Support!**
471
+
472
+ I can help you with questions about XENO financial services including:
473
+ - Account management and setup
474
+ - Transaction processes and fees
475
+ - Platform features and troubleshooting
476
+ - General service information
477
 
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
+
 
 
 
 
 
 
 
500
  return demo
501
 
502
  if __name__ == "__main__":