Sebunya commited on
Commit
8ac3f9d
·
verified ·
1 Parent(s): 6253fff

updated app.py file so improve the ui

Browse files
Files changed (1) hide show
  1. app.py +124 -239
app.py CHANGED
@@ -1,21 +1,23 @@
1
- import uuid
2
  import os
3
- import gradio as gr
4
- import pandas as pd
 
 
 
5
  import torch
6
  import numpy as np
 
 
 
7
  from sentence_transformers import util
 
 
8
  import google.generativeai as genai
9
  import chromadb
10
  from langchain_chroma import Chroma
11
  import gspread
12
  from google.oauth2.service_account import Credentials
13
  from langgraph.checkpoint.sqlite import SqliteSaver
14
- import sqlite3
15
- import json
16
- from datetime import datetime
17
- import re
18
- from typing import Dict, List, Tuple
19
 
20
  # === Configuration ===
21
  genai.configure(api_key=os.environ["GEMINI_API_KEY"])
@@ -23,154 +25,102 @@ embedding_model = "models/embedding-001"
23
  llm_model_name = "models/gemma-3-4b-it"
24
  collection_name = "xeno_collection"
25
 
26
- # === Google Sheets Setup for Hugging Face ===
27
  def get_google_sheets_credentials():
28
  credentials_json = os.environ.get("GOOGLE_SHEETS_CREDENTIALS")
29
  if not credentials_json:
30
  raise ValueError("GOOGLE_SHEETS_CREDENTIALS environment variable not set.")
31
  credentials_dict = json.loads(credentials_json)
32
- scope = ["https://spreadsheets.google.com/feeds", "https://www.googleapis.com/auth/drive"]
33
- creds = Credentials.from_service_account_info(credentials_dict, scopes=scope)
34
- return creds
 
 
35
 
36
- # Authenticate with Google Sheets
37
  client_gspread = gspread.authorize(get_google_sheets_credentials())
38
-
39
- # Open the Google Sheet
40
  sheet = client_gspread.open("Response_Log").sheet1
41
 
42
  def log_response(question, answer, source_ids, knowledge_pairs, session_id):
43
- """
44
- Log a question, answer, source IDs, and knowledge base question-answer pairs to the Google Sheet.
45
-
46
- Args:
47
- question (str): The question asked by the user.
48
- answer (str): The answer provided by the model.
49
- source_ids (str): Comma-separated list of source IDs used.
50
- knowledge_pairs (list): List of tuples containing (question, answer) from the knowledge base.
51
- """
52
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
53
- knowledge_question_1 = knowledge_pairs[0][0] if len(knowledge_pairs) > 0 else "N/A"
54
- knowledge_answer_1 = knowledge_pairs[0][1] if len(knowledge_pairs) > 0 else "N/A"
55
- knowledge_question_2 = knowledge_pairs[1][0] if len(knowledge_pairs) > 1 else "N/A"
56
- knowledge_answer_2 = knowledge_pairs[1][1] if len(knowledge_pairs) > 1 else "N/A"
57
  row = [
58
- timestamp,
59
- session_id,
60
- question,
61
- answer,
62
- source_ids,
63
- knowledge_question_1,
64
- knowledge_answer_1,
65
- knowledge_question_2,
66
- knowledge_answer_2
67
  ]
68
  try:
69
  sheet.append_row(row)
70
- print(f"Logged: {question} | Source IDs: {source_ids}")
71
  except Exception as e:
72
- print(f"Failed to log to Google Sheet: {e}")
73
  with open("/tmp/response_log.txt", "a") as f:
74
- f.write(f"{timestamp},{question},{answer},{source_ids},{knowledge_question_1},{knowledge_answer_1},{knowledge_question_2},{knowledge_answer_2}\n")
75
 
76
- # === LangGraph Memory Setup ===
77
  conn = sqlite3.connect("xeno_memory.db", check_same_thread=False)
78
  memory = SqliteSaver(conn=conn)
79
 
80
  def update_memory(config, user_message, assistant_message):
81
- full_checkpoint = memory.get(config) or {}
82
- messages = full_checkpoint.get("channel_values", {}).get("messages", [])
83
-
84
  messages.append({"role": "user", "content": user_message})
85
  messages.append({"role": "assistant", "content": assistant_message})
86
-
87
- checkpoint_to_save = {
88
  "v": 1,
89
  "id": str(uuid.uuid4()),
90
  "ts": datetime.now().isoformat(),
91
  "channel_values": {"messages": messages},
92
  "channel_versions": {},
93
- "versions_seen": {},
94
- }
95
-
96
- memory.put(config, checkpoint_to_save, {}, {})
97
-
98
- # === Intent Classification System ===
99
  class IntentClassifier:
100
  def __init__(self):
101
- # Define intent patterns and responses
102
  self.intent_patterns = {
103
- 'greeting': {
104
- 'patterns': [
105
- r'\b(hi|hello|hey|good morning|good afternoon|good evening|greetings)\b',
106
- r'^(hi|hello|hey)[\s!.]*$',
107
- r'\b(how are you|how do you do)\b'
108
- ],
109
- 'responses': [
110
- "Hello! I'm XENO Assistant. How can I help you with XENO financial services today?",
111
- "Hi there! I'm here to assist you with any questions about XENO services. What can I help you with?",
112
- "Good day! Welcome to XENO Support. How may I assist you today?"
113
  ]
114
  },
115
- 'thanks': {
116
- 'patterns': [
117
- r'\b(thank you|thanks|thank u|thx|appreciate|grateful)\b',
118
- r'^(thanks|thank you)[\s!.]*$',
119
- r'\b(much appreciated|thanks a lot|thank you so much)\b'
120
- ],
121
- 'responses': [
122
- "You're welcome! Is there anything else I can help you with regarding XENO services?",
123
- "Happy to help! Feel free to ask if you have any other questions about XENO.",
124
- "Glad I could assist you! Let me know if you need help with anything else."
125
  ]
126
  },
127
- 'goodbye': {
128
- 'patterns': [
129
- r'\b(bye|goodbye|see you|farewell|take care|have a good day)\b',
130
- r'^(bye|goodbye)[\s!.]*$',
131
- r'\b(talk to you later|see you later|until next time)\b'
132
- ],
133
- 'responses': [
134
- "Goodbye! Thank you for using XENO services. Have a great day!",
135
- "Take care! Feel free to return anytime you need help with XENO services.",
136
- "Have a wonderful day! Don't hesitate to reach out if you need assistance with XENO."
137
  ]
138
  }
139
  }
140
-
141
  def classify_intent(self, message: str) -> Tuple[str, str]:
142
- """
143
- Classify the intent of a message and return appropriate response if it's a simple intent.
144
- Returns: (intent_name, response) - response is empty string if intent requires RAG
145
- """
146
  message_lower = message.lower().strip()
147
-
148
  for intent_name, intent_data in self.intent_patterns.items():
149
- for pattern in intent_data['patterns']:
150
- if re.search(pattern, message_lower, re.IGNORECASE):
151
- import random
152
- response = random.choice(intent_data['responses'])
153
- return intent_name, response
154
-
155
- return 'query', ''
156
-
157
- def is_simple_intent(self, intent: str) -> bool:
158
- """Check if intent can be handled without RAG"""
159
- simple_intents = ['greeting', 'thanks']
160
- return intent in simple_intents
161
 
162
- # Initialize intent classifier
163
  intent_classifier = IntentClassifier()
164
 
165
- # === Load and Clean Knowledge Base ===
166
  df_kb = pd.read_json("XENO_Uganda_KnowledgeBase_Advisory.json")
167
- df_kb.dropna(subset=['Content'], inplace=True)
168
 
169
  def prepare_documents(data):
170
- documents, metadatas, ids = [], [], []
171
  for item in data:
172
- documents.append(f"Question: {item['Question']}\nAnswer: {item['Content']}")
173
- metadatas.append({
174
  "question": item["Question"],
175
  "content": item["Content"],
176
  "section": item.get("Section", ""),
@@ -180,150 +130,85 @@ def prepare_documents(data):
180
  "id": item["ID"]
181
  })
182
  ids.append(item["ID"])
183
- return documents, metadatas, ids
184
 
185
- xeno_data_list = df_kb.to_dict('records')
186
- documents, metadatas, ids = prepare_documents(xeno_data_list)
187
 
188
- # === Setup ChromaDB ===
 
189
  try:
190
- client = chromadb.PersistentClient(path="/tmp/xeno_db")
191
- try:
192
- collection = client.get_collection(name=collection_name)
193
- print(f"Loaded existing ChromaDB collection: {collection_name}")
194
- except:
195
- print(f"Creating new ChromaDB collection: {collection_name}")
196
- collection = client.create_collection(name=collection_name)
197
- collection.add(documents=documents, metadatas=metadatas, ids=ids)
198
- except Exception as e:
199
- print(f"Failed to initialize ChromaDB: {e}")
200
- raise
201
 
202
- vector_store = Chroma(client=client, collection_name=collection_name)
203
- retriever = vector_store.as_retriever(search_type="similarity", search_kwargs={"k": 4})
204
 
205
  # === Prompt System ===
206
- SYSTEM_PROMPT = """You are a friendly XENO Support Assistant, an AI-powered helpful and professional customer service representative.
207
- Use only the information provided in the knowledge base context to answer user queries.
208
- 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.
209
- Only use context that is clearly relevant to the user's question.
210
- For greetings like “hi” or “hello”, respond politely without using the context.
211
- remember previous conversations."""
212
 
213
- # === Context Processing ===
214
  def process_context(results, cosine_scores, max_results=2):
215
- sorted_indices = np.argsort(cosine_scores)[::-1][:max_results]
216
- formatted_context = ""
217
- source_ids = []
218
- knowledge_pairs = []
219
- for i, idx in enumerate(sorted_indices, 1):
220
- result = results[idx]
221
- score = cosine_scores[idx]
222
- question = result.metadata.get('question', 'N/A')
223
- answer = result.metadata.get('content', 'N/A')
224
- formatted_context += f"Knowledge Entry {i}:\n"
225
- formatted_context += f"Q: {question}\n"
226
- formatted_context += f"A: {answer}\n"
227
- formatted_context += "-" * 40 + "\n"
228
- source_ids.append(result.metadata.get('id', 'N/A'))
229
- knowledge_pairs.append((question, answer))
230
- return formatted_context, source_ids, knowledge_pairs
231
-
232
- # === LLM Generation (Refactored) ===
233
- def generate_xeno_response(context, question, chat_history):
234
- """Generates a response but does NOT handle memory."""
235
  model = genai.GenerativeModel(llm_model_name)
236
- formatted_history = "\n".join(
237
- [f"{msg['role'].capitalize()}: {msg['content']}" for msg in chat_history]
238
- ) if chat_history else "None"
239
-
240
- prompt = f"{SYSTEM_PROMPT}\n### HISTORY ###\n{formatted_history}\n### CONTEXT ###\n{context}\n### QUESTION ###\n{question}"
241
-
242
- response = model.generate_content(prompt)
243
- return response.text.strip()
244
-
245
-
246
- # === Main Interface Logic (Refactored) ===
247
- def get_context_and_answer(message, history, session_id="default"):
248
- """
249
- Handles intent classification, RAG, and memory updates in one place.
250
- """
251
- config = {"configurable": {"thread_id": str(session_id), "checkpoint_ns": ""}}
252
-
253
- full_checkpoint = memory.get(config) or {}
254
- chat_history = full_checkpoint.get("channel_values", {}).get("messages", [])
255
- intent, direct_response = intent_classifier.classify_intent(message)
256
-
257
- answer = ""
258
- source_ids = "N/A"
259
- knowledge_pairs = []
260
-
261
- if intent != 'query':
262
- answer = direct_response
263
- else:
264
- if len(message.strip()) < 3:
265
- answer = "I'd be happy to help! Could you please provide more details about what you'd like to know?"
266
- else:
267
- try:
268
- queried_results = retriever.invoke(message)
269
- query_embedding = genai.embed_content(model=embedding_model, content=message, task_type="retrieval_query")['embedding']
270
-
271
- doc_embeddings = [genai.embed_content(model=embedding_model, content=doc.page_content, task_type="retrieval_document")['embedding'] for doc in queried_results]
272
-
273
- cosine_scores = util.cos_sim(torch.tensor(query_embedding).float(), torch.tensor(doc_embeddings).float())[0].tolist()
274
-
275
- if max(cosine_scores) < 0.4:
276
- answer = "I'm sorry, I couldn't find specific information for your question. Could you try rephrasing it, or contact XENO support directly?"
277
- else:
278
- context, source_ids_list, knowledge_pairs = process_context(queried_results, cosine_scores)
279
- answer = generate_xeno_response(context, message, chat_history)
280
- source_ids = ", ".join(source_ids_list)
281
-
282
- except Exception as e:
283
- print(f"Error during RAG processing: {e}")
284
- answer = "I apologize, but I'm having a technical issue. Please try again shortly or contact XENO support."
285
 
286
  update_memory(config, message, answer)
287
- log_response(message, answer, source_ids, knowledge_pairs, session_id)
288
-
289
  return answer
290
 
291
- # === Enhanced Gradio UI ===
292
- def respond(message, history, session_id):
293
- """Gradio's main response function."""
294
- if not session_id:
295
- session_id = str(uuid.uuid4())
296
-
297
- response = get_context_and_answer(message, history, session_id)
298
-
299
- config = {"configurable": {"thread_id": str(session_id), "checkpoint_ns": ""}}
300
- updated_messages = (memory.get(config) or {}).get("messages", [])
301
-
302
- history.append({"role": "user", "content": message})
303
- history.append({"role": "assistant", "content": response})
304
-
305
- return "", history
306
- def create_interface():
307
- with gr.Blocks() as demo:
308
- gr.Markdown("""ASKXENO
309
-
310
- **Welcome to XENO AI Support!**
311
- I can help you with questions about XENO financial services including:
312
- • Account management and setup
313
- • Transaction processes and fees
314
- • Platform features and troubleshooting
315
- • General service information
316
- *Simply type your question below to get started!*
317
- """)
318
-
319
- session_id_box = gr.Textbox(label="Session ID", value=str(uuid.uuid4()), interactive=True)
320
-
321
- chatbot = gr.Chatbot(label="XENO Assistant", bubble_full_width=False, height=500, type="messages")
322
- msg = gr.Textbox(label="Your Message", placeholder="Type your question here...")
323
-
324
- msg.submit(respond, [msg, chatbot, session_id_box], [msg, chatbot])
325
- return demo
326
 
327
  if __name__ == "__main__":
328
- iface = create_interface()
329
- iface.launch(share=False, server_name="0.0.0.0", server_port=7860)
 
 
1
  import os
2
+ import uuid
3
+ import re
4
+ import json
5
+ import sqlite3
6
+ import random
7
  import torch
8
  import numpy as np
9
+ import pandas as pd
10
+ import gradio as gr
11
+ from datetime import datetime
12
  from sentence_transformers import util
13
+ from typing import Tuple
14
+
15
  import google.generativeai as genai
16
  import chromadb
17
  from langchain_chroma import Chroma
18
  import gspread
19
  from google.oauth2.service_account import Credentials
20
  from langgraph.checkpoint.sqlite import SqliteSaver
 
 
 
 
 
21
 
22
  # === Configuration ===
23
  genai.configure(api_key=os.environ["GEMINI_API_KEY"])
 
25
  llm_model_name = "models/gemma-3-4b-it"
26
  collection_name = "xeno_collection"
27
 
28
+ # === Google Sheets Setup ===
29
  def get_google_sheets_credentials():
30
  credentials_json = os.environ.get("GOOGLE_SHEETS_CREDENTIALS")
31
  if not credentials_json:
32
  raise ValueError("GOOGLE_SHEETS_CREDENTIALS environment variable not set.")
33
  credentials_dict = json.loads(credentials_json)
34
+ scope = [
35
+ "https://spreadsheets.google.com/feeds",
36
+ "https://www.googleapis.com/auth/drive"
37
+ ]
38
+ return Credentials.from_service_account_info(credentials_dict, scopes=scope)
39
 
 
40
  client_gspread = gspread.authorize(get_google_sheets_credentials())
 
 
41
  sheet = client_gspread.open("Response_Log").sheet1
42
 
43
  def log_response(question, answer, source_ids, knowledge_pairs, session_id):
 
 
 
 
 
 
 
 
 
44
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
45
+ kq1, ka1 = knowledge_pairs[0] if len(knowledge_pairs) > 0 else ("N/A", "N/A")
46
+ kq2, ka2 = knowledge_pairs[1] if len(knowledge_pairs) > 1 else ("N/A", "N/A")
47
+
 
48
  row = [
49
+ timestamp, session_id, question, answer, source_ids, kq1, ka1, kq2, ka2
 
 
 
 
 
 
 
 
50
  ]
51
  try:
52
  sheet.append_row(row)
 
53
  except Exception as e:
 
54
  with open("/tmp/response_log.txt", "a") as f:
55
+ f.write(",".join(map(str, row)) + "\n")
56
 
57
+ # === Memory Setup ===
58
  conn = sqlite3.connect("xeno_memory.db", check_same_thread=False)
59
  memory = SqliteSaver(conn=conn)
60
 
61
  def update_memory(config, user_message, assistant_message):
62
+ checkpoint = memory.get(config) or {}
63
+ messages = checkpoint.get("channel_values", {}).get("messages", [])
 
64
  messages.append({"role": "user", "content": user_message})
65
  messages.append({"role": "assistant", "content": assistant_message})
66
+ memory.put(config, {
 
67
  "v": 1,
68
  "id": str(uuid.uuid4()),
69
  "ts": datetime.now().isoformat(),
70
  "channel_values": {"messages": messages},
71
  "channel_versions": {},
72
+ "versions_seen": {}
73
+ }, {}, {})
74
+
75
+ # === Intent Classifier ===
 
 
76
  class IntentClassifier:
77
  def __init__(self):
 
78
  self.intent_patterns = {
79
+ "greeting": {
80
+ "patterns": [r"\b(hi|hello|hey|good morning|good afternoon|good evening)\b"],
81
+ "responses": [
82
+ "Hello! I'm XENO Assistant. How can I help you today?",
83
+ "Hi there! What can I assist you with?",
84
+ "Good day! How may I assist you?"
 
 
 
 
85
  ]
86
  },
87
+ "thanks": {
88
+ "patterns": [r"\b(thank you|thanks|appreciate)\b"],
89
+ "responses": [
90
+ "You're welcome! Anything else I can help you with?",
91
+ "Happy to help!",
92
+ "Glad I could assist!"
 
 
 
 
93
  ]
94
  },
95
+ "goodbye": {
96
+ "patterns": [r"\b(bye|goodbye|see you)\b"],
97
+ "responses": [
98
+ "Goodbye! Have a great day!",
99
+ "Take care! Come back anytime.",
100
+ "See you later!"
 
 
 
 
101
  ]
102
  }
103
  }
104
+
105
  def classify_intent(self, message: str) -> Tuple[str, str]:
 
 
 
 
106
  message_lower = message.lower().strip()
 
107
  for intent_name, intent_data in self.intent_patterns.items():
108
+ for pattern in intent_data["patterns"]:
109
+ if re.search(pattern, message_lower):
110
+ return intent_name, random.choice(intent_data["responses"])
111
+ return "query", ""
 
 
 
 
 
 
 
 
112
 
 
113
  intent_classifier = IntentClassifier()
114
 
115
+ # === Load Knowledge Base ===
116
  df_kb = pd.read_json("XENO_Uganda_KnowledgeBase_Advisory.json")
117
+ df_kb.dropna(subset=["Content"], inplace=True)
118
 
119
  def prepare_documents(data):
120
+ docs, metas, ids = [], [], []
121
  for item in data:
122
+ docs.append(f"Question: {item['Question']}\nAnswer: {item['Content']}")
123
+ metas.append({
124
  "question": item["Question"],
125
  "content": item["Content"],
126
  "section": item.get("Section", ""),
 
130
  "id": item["ID"]
131
  })
132
  ids.append(item["ID"])
133
+ return docs, metas, ids
134
 
135
+ docs, metas, ids = prepare_documents(df_kb.to_dict("records"))
 
136
 
137
+ # === ChromaDB ===
138
+ client = chromadb.PersistentClient(path="/tmp/xeno_db")
139
  try:
140
+ collection = client.get_collection(name=collection_name)
141
+ except:
142
+ collection = client.create_collection(name=collection_name)
143
+ collection.add(documents=docs, metadatas=metas, ids=ids)
 
 
 
 
 
 
 
144
 
145
+ retriever = Chroma(client=client, collection_name=collection_name) \
146
+ .as_retriever(search_type="similarity", search_kwargs={"k": 4})
147
 
148
  # === Prompt System ===
149
+ SYSTEM_PROMPT = """You are a friendly XENO Support Assistant.
150
+ Use only the knowledge base context to answer questions.
151
+ Do not hallucinate. If no relevant info, politely decline.
152
+ Remember previous conversations."""
 
 
153
 
154
+ # === Context Processor ===
155
  def process_context(results, cosine_scores, max_results=2):
156
+ sorted_idx = np.argsort(cosine_scores)[::-1][:max_results]
157
+ ctx, src_ids, kpairs = "", [], []
158
+ for i, idx in enumerate(sorted_idx, 1):
159
+ q = results[idx].metadata.get("question", "N/A")
160
+ a = results[idx].metadata.get("content", "N/A")
161
+ ctx += f"Knowledge Entry {i}:\nQ: {q}\nA: {a}\n" + "-" * 40 + "\n"
162
+ src_ids.append(results[idx].metadata.get("id", "N/A"))
163
+ kpairs.append((q, a))
164
+ return ctx, src_ids, kpairs
165
+
166
+ # === LLM Generation ===
167
+ def generate_xeno_response(context, question, history):
 
 
 
 
 
 
 
 
168
  model = genai.GenerativeModel(llm_model_name)
169
+ hist_text = "\n".join([f"{m['role'].capitalize()}: {m['content']}" for m in history]) if history else "None"
170
+ prompt = f"{SYSTEM_PROMPT}\n### HISTORY ###\n{hist_text}\n### CONTEXT ###\n{context}\n### QUESTION ###\n{question}"
171
+ return model.generate_content(prompt).text.strip()
172
+
173
+ # === Chat Handler ===
174
+ def chat_handler(message, history):
175
+ session_id = "default" # could be made dynamic if needed
176
+ config = {"configurable": {"thread_id": session_id, "checkpoint_ns": ""}}
177
+ checkpoint = memory.get(config) or {}
178
+ chat_history = checkpoint.get("channel_values", {}).get("messages", [])
179
+
180
+ intent, quick_reply = intent_classifier.classify_intent(message)
181
+ answer, src_ids, kpairs = "", "N/A", []
182
+
183
+ if intent != "query":
184
+ answer = quick_reply
185
+ else:
186
+ try:
187
+ results = retriever.invoke(message)
188
+ query_emb = genai.embed_content(model=embedding_model, content=message, task_type="retrieval_query")['embedding']
189
+ doc_embs = [genai.embed_content(model=embedding_model, content=doc.page_content, task_type="retrieval_document")['embedding'] for doc in results]
190
+ cos_scores = util.cos_sim(torch.tensor(query_emb).float(), torch.tensor(doc_embs).float())[0].tolist()
191
+
192
+ if max(cos_scores) < 0.4:
193
+ answer = "I'm sorry, I couldn't find specific information for your question."
194
+ else:
195
+ ctx, src_ids_list, kpairs = process_context(results, cos_scores)
196
+ answer = generate_xeno_response(ctx, message, chat_history)
197
+ src_ids = ", ".join(src_ids_list)
198
+ except Exception as e:
199
+ answer = "I’m having a technical issue. Please try again later."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
  update_memory(config, message, answer)
202
+ log_response(message, answer, src_ids, kpairs, session_id)
 
203
  return answer
204
 
205
+ # === Clean ChatInterface UI ===
206
+ iface = gr.ChatInterface(
207
+ fn=chat_handler,
208
+ title="ASKXENO",
209
+ description="Ask anything about XENO's financial services.",
210
+ theme="soft"
211
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
  if __name__ == "__main__":
214
+ iface.launch(share=False, server_name="0.0.0.0", server_port=7860)