Sebunya commited on
Commit
5615ee1
·
verified ·
1 Parent(s): fe6a7ce

Update app.py

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