Sebunya commited on
Commit
e11fe89
·
verified ·
1 Parent(s): 97f6b52

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +308 -189
app.py CHANGED
@@ -1,15 +1,3 @@
1
- # ==========================================
2
- # CRITICAL FIX FOR CHROMADB / SQLITE
3
- # This block must be at the very top before other imports
4
- try:
5
- __import__('pysqlite3')
6
- import sys
7
- sys.modules['sqlite3'] = sys.modules.pop('pysqlite3')
8
- print("Successfully patched sqlite3 for ChromaDB")
9
- except ImportError:
10
- print("Warning: pysqlite3-binary not installed. ChromaDB might fail if system sqlite is old.")
11
- # ==========================================
12
-
13
  import uuid
14
  import os
15
  import gradio as gr
@@ -30,7 +18,7 @@ import re
30
  from typing import Dict, List, Tuple
31
  import time
32
  from contextlib import contextmanager
33
- import threading
34
  import logging
35
  import traceback
36
  import sys
@@ -56,12 +44,15 @@ class PipelineTimer:
56
  self.reset()
57
 
58
  def reset(self):
 
59
  self.start_time = time.time()
60
  self.step_times = {}
 
61
  self.current_step = None
62
 
63
  @contextmanager
64
  def time_step(self, step_name: str):
 
65
  step_start = time.time()
66
  self.current_step = step_name
67
  try:
@@ -71,9 +62,12 @@ class PipelineTimer:
71
  self.step_times[step_name] = round((step_end - step_start) * 1000, 2)
72
  self.current_step = None
73
 
 
 
 
74
  def get_timing_summary(self):
75
  return {
76
- 'total_time_ms': round((time.time() - self.start_time) * 1000, 2),
77
  'step_times': self.step_times,
78
  'timestamp': datetime.now().isoformat()
79
  }
@@ -81,11 +75,10 @@ class PipelineTimer:
81
  timer = PipelineTimer()
82
 
83
  # === Configuration ===
84
- api_key = os.environ.get("GEMINI_API_KEY")
85
- if not api_key:
86
- print("WARNING: GEMINI_API_KEY not set. App may crash.")
87
-
88
- genai.configure(api_key=api_key)
89
  embedding_model = "models/embedding-001"
90
  llm_model_name = "models/gemma-3-4b-it"
91
  collection_name = "xeno_collection"
@@ -94,156 +87,210 @@ collection_name = "xeno_collection"
94
  def get_google_sheets_credentials():
95
  credentials_json = os.environ.get("GOOGLE_SHEETS_CREDENTIALS")
96
  if not credentials_json:
97
- # Return None to handle gracefully later instead of crashing
98
- print("WARNING: GOOGLE_SHEETS_CREDENTIALS not set.")
99
- return None
100
-
101
- try:
102
- credentials_dict = json.loads(credentials_json)
103
- scope = ["https://spreadsheets.google.com/feeds", "https://www.googleapis.com/auth/drive"]
104
- return Credentials.from_service_account_info(credentials_dict, scopes=scope)
105
- except Exception as e:
106
- print(f"Error parsing Google Credentials: {e}")
107
- return None
108
-
109
- # Initialize Sheets with Robust Failover
110
- spreadsheet = None
111
- response_sheet = None
112
- timing_sheet = None
113
- feedback_sheet = None
114
 
 
115
  try:
116
- creds = get_google_sheets_credentials()
117
- if creds:
118
- client_gspread = gspread.authorize(creds)
119
- try:
120
- spreadsheet = client_gspread.open("Response_Log")
121
- response_sheet = spreadsheet.sheet1
122
- except Exception as e:
123
- print(f"Could not open spreadsheet: {e}")
124
-
125
- if spreadsheet:
126
- # Init Timing Sheet
127
- try:
128
- timing_sheet = spreadsheet.worksheet("Timing_Log")
129
- except:
130
- try:
131
- timing_sheet = spreadsheet.add_worksheet(title="Timing_Log", rows="1000", cols="15")
132
- timing_sheet.append_row(["Timestamp", "Session_ID", "Question", "Total_Time_MS", "Details"])
133
- except: pass
134
-
135
- # Init Feedback Sheet
136
- try:
137
- feedback_sheet = spreadsheet.worksheet("Feedback_Log")
138
- except:
139
- try:
140
- feedback_sheet = spreadsheet.add_worksheet(title="Feedback_Log", rows="1000", cols="6")
141
- feedback_sheet.append_row(["Timestamp", "Session_ID", "User_Message", "Bot_Response", "Rating", "Reason"])
142
- except: pass
143
  except Exception as e:
144
- print(f"Google Sheets init failed: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
 
146
- # === Logging Helper Functions ===
147
  def log_response(question, answer, source_ids, knowledge_pairs, session_id):
148
- if not response_sheet: return
 
 
 
 
 
 
 
 
 
149
  try:
150
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
151
- kq1 = knowledge_pairs[0][0] if len(knowledge_pairs) > 0 else ""
152
- ka1 = knowledge_pairs[0][1] if len(knowledge_pairs) > 0 else ""
153
- kq2 = knowledge_pairs[1][0] if len(knowledge_pairs) > 1 else ""
154
- ka2 = knowledge_pairs[1][1] if len(knowledge_pairs) > 1 else ""
155
-
156
- row = [timestamp, session_id, question, answer, source_ids, kq1, ka1, kq2, ka2]
157
  response_sheet.append_row(row)
 
158
  except Exception as e:
159
- print(f"Log response failed: {e}")
160
 
161
  def log_timing_data(question, session_id, timing_summary, error_step=None, notes=None):
162
- if not timing_sheet: return
 
 
 
 
 
 
 
 
 
 
 
163
  try:
164
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
165
- row = [
166
- timestamp, session_id, question[:50],
167
- timing_summary['total_time_ms'],
168
- json.dumps(timing_summary['step_times']),
169
- error_step, notes
170
- ]
171
  timing_sheet.append_row(row)
172
- except Exception: pass
 
 
 
173
 
174
  def _log_feedback_background(row):
 
175
  try:
176
- if feedback_sheet: feedback_sheet.append_row(row)
177
- except Exception as e: print(f"Feedback log failed: {e}")
 
 
 
 
 
178
 
179
- # === Feedback Logic ===
180
  def handle_vote(data: gr.LikeData, history, session_id):
 
 
 
 
181
  if not history: return
 
182
  try:
 
183
  rating = "Positive" if data.liked else "Negative"
184
- idx = data.index
185
- if idx < len(history):
186
- interaction = history[idx]
187
- row = [datetime.now().strftime("%Y-%m-%d %H:%M:%S"), session_id, interaction[0], interaction[1], rating, "Quick Vote"]
 
 
 
 
 
 
 
 
 
 
 
188
  threading.Thread(target=_log_feedback_background, args=(row,)).start()
189
- except Exception as e: print(f"Vote error: {e}")
 
 
 
190
 
191
  def submit_manual_flag(reason, history, session_id):
192
- if not history: return "No history."
 
 
193
  try:
194
- interaction = history[-1]
195
- row = [datetime.now().strftime("%Y-%m-%d %H:%M:%S"), session_id, interaction[0], interaction[1], "Negative", reason]
 
 
 
 
196
  threading.Thread(target=_log_feedback_background, args=(row,)).start()
197
- return "Report submitted."
198
- except Exception as e: return f"Error: {e}"
 
 
199
 
200
- # === Core Logic ===
201
- # Use a file-based DB that persists in the container's storage
202
- db_path = "xeno_memory.db"
203
- conn = sqlite3.connect(db_path, check_same_thread=False)
204
  memory = SqliteSaver(conn=conn)
205
 
206
- def update_memory(config, user_msg, bot_msg):
207
- checkpoint = {
208
- "v": 1, "id": str(uuid.uuid4()), "ts": datetime.now().isoformat(),
209
- "channel_values": {"messages": [{"role": "user", "content": user_msg}, {"role": "assistant", "content": bot_msg}]},
210
- "channel_versions": {}, "versions_seen": {}
211
- }
212
- memory.put(config, checkpoint, {}, {})
 
 
 
 
 
213
 
214
  def retrieve_memory(config):
215
- res = memory.get(config)
216
- return res.get("channel_values", {}).get("messages", []) if res else []
 
217
 
218
  class IntentClassifier:
219
- def classify_intent(self, msg):
220
- msg = msg.lower()
221
- if re.search(r'\b(hi|hello|hey)\b', msg): return 'greeting', "Hello! How can I help with XENO?"
222
- if re.search(r'\b(thanks|thank)\b', msg): return 'thanks', "You're welcome!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  return 'query', ''
224
 
225
  intent_classifier = IntentClassifier()
226
 
227
- # === Knowledge Base ===
228
- # Wrapped in try-except to prevent crash on file read error
229
- documents, metadatas, ids = [], [], []
230
  try:
231
- if os.path.exists("XENO_Uganda_KnowledgeBase_Advisory.json"):
232
- df_kb = pd.read_json("XENO_Uganda_KnowledgeBase_Advisory.json")
233
- df_kb.dropna(subset=['Content'], inplace=True)
234
- for r in df_kb.to_dict('records'):
235
- documents.append(f"Q: {r['Question']}\nA: {r['Content']}")
236
- metadatas.append({"question": r["Question"], "content": r["Content"], "id": str(r["ID"])})
237
- ids.append(str(r["ID"]))
238
- else:
239
- print("Warning: Knowledge base JSON file not found.")
240
- except Exception as e:
241
- print(f"Error loading KB: {e}")
242
 
243
- # === ChromaDB ===
244
- try:
245
- # Use a persistent path that is writable in most containers
246
- client = chromadb.PersistentClient(path="./xeno_db_storage")
247
  try:
248
  collection = client.get_collection(name=collection_name)
249
  except:
@@ -253,83 +300,155 @@ try:
253
  vector_store = Chroma(client=client, collection_name=collection_name)
254
  retriever = vector_store.as_retriever(search_type="similarity", search_kwargs={"k": 4})
255
  except Exception as e:
256
- print(f"ChromaDB Fatal Error: {e}")
257
- # Fallback to prevent crash
258
  class DummyRetriever:
259
- def invoke(self, x): return []
260
  retriever = DummyRetriever()
261
 
262
- # === Generation ===
263
- def generate_response(context, question, history):
264
- try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  model = genai.GenerativeModel(llm_model_name)
266
- hist_str = "\n".join([f"{m['role']}: {m['content']}" for m in history])
267
- prompt = f"System: You are XENO Support.\nHistory:{hist_str}\nContext:{context}\nUser:{question}"
268
- return model.generate_content(prompt).text.strip()
269
- except Exception as e:
270
- return f"I'm having trouble connecting to my brain right now. ({str(e)})"
271
 
272
  # === Main Pipeline ===
273
- def process_message(message, history, session_id):
274
  timer.reset()
275
- if not session_id: session_id = str(uuid.uuid4())
276
- config = {"configurable": {"thread_id": str(session_id)}}
277
 
278
  try:
279
- with timer.time_step("intent"):
280
- intent, direct_resp = intent_classifier.classify_intent(message)
 
 
281
 
 
 
 
282
  if intent != 'query':
283
- resp = direct_resp
 
284
  else:
285
- with timer.time_step("retrieval"):
286
- docs = retriever.invoke(message)
287
-
288
- context = "\n".join([d.page_content for d in docs])
289
- chat_hist = retrieve_memory(config)
290
-
291
- with timer.time_step("generation"):
292
- resp = generate_response(context, message, chat_hist)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
 
294
- update_memory(config, message, resp)
295
- log_timing_data(message, session_id, timer.get_timing_summary())
296
- return resp
297
-
 
 
298
  except Exception as e:
299
- print(f"Pipeline Error: {e}")
300
- traceback.print_exc()
301
- return "I encountered a system error. Please try again."
302
-
303
- # === UI ===
304
- def respond(msg, hist, sid):
305
- if not sid: sid = str(uuid.uuid4())
306
- resp = process_message(msg, hist, sid)
307
- hist.append([msg, resp])
308
- return "", hist
309
-
310
- def create_demo():
311
  with gr.Blocks(theme=gr.themes.Soft(), fill_height=True) as demo:
312
- gr.Markdown("## ASKXENO")
313
- sid = gr.Textbox(value=str(uuid.uuid4()), visible=False)
314
- cb = gr.Chatbot(scale=1, likeable=True, show_copy_button=True, bubble_full_width=False)
 
 
 
 
 
 
 
 
 
315
 
316
  with gr.Row(variant="compact"):
317
- txt = gr.Textbox(placeholder="Ask XENO...", scale=6, container=False, autofocus=True)
318
- btn = gr.Button("Send", scale=1)
319
-
320
- with gr.Accordion("Report Issue", open=False):
 
 
 
 
 
 
 
 
321
  with gr.Row():
322
- ftxt = gr.Textbox(placeholder="Issue details", show_label=False, scale=4)
323
- fbtn = gr.Button("Submit", scale=1)
324
- flbl = gr.Label(show_label=False)
325
-
326
- txt.submit(respond, [txt, cb, sid], [txt, cb])
327
- btn.click(respond, [txt, cb, sid], [txt, cb])
328
- cb.like(handle_vote, [cb, sid], None)
329
- fbtn.click(submit_manual_flag, [ftxt, cb, sid], [flbl])
330
 
 
 
 
 
 
 
331
  return demo
332
 
333
  if __name__ == "__main__":
334
- demo = create_demo()
335
- demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import uuid
2
  import os
3
  import gradio as gr
 
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
 
44
  self.reset()
45
 
46
  def reset(self):
47
+ """Reset all timing data for a new request"""
48
  self.start_time = time.time()
49
  self.step_times = {}
50
+ self.step_start = None
51
  self.current_step = None
52
 
53
  @contextmanager
54
  def time_step(self, step_name: str):
55
+ """Context manager to time a specific step"""
56
  step_start = time.time()
57
  self.current_step = step_name
58
  try:
 
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
  }
 
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"
 
87
  def get_google_sheets_credentials():
88
  credentials_json = os.environ.get("GOOGLE_SHEETS_CREDENTIALS")
89
  if not credentials_json:
90
+ raise ValueError("GOOGLE_SHEETS_CREDENTIALS environment variable not set.")
91
+ credentials_dict = json.loads(credentials_json)
92
+ scope = ["https://spreadsheets.google.com/feeds", "https://www.googleapis.com/auth/drive"]
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"
145
+ knowledge_question_2 = knowledge_pairs[1][0] if len(knowledge_pairs) > 1 else "N/A"
146
+ knowledge_answer_2 = knowledge_pairs[1][1] if len(knowledge_pairs) > 1 else "N/A"
147
+ row = [
148
+ timestamp, session_id, question, answer, source_ids,
149
+ knowledge_question_1, knowledge_answer_1, knowledge_question_2, knowledge_answer_2
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:
 
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 = []
344
 
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."
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__":
453
+ iface = create_interface()
454
+ iface.launch(share=False, server_name="0.0.0.0", server_port=7860, ssr_mode=False)