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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +174 -40
app.py CHANGED
@@ -2,6 +2,11 @@ import uuid
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,6 +15,27 @@ import sqlite3
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
 
@@ -17,6 +43,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,6 +60,16 @@ def log_response(question, answer, source_ids, knowledge_pairs, session_id):
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
 
@@ -52,6 +98,134 @@ def update_memory(config, user_message, assistant_message):
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
 
@@ -75,12 +249,10 @@ def get_context_and_answer(message, history, session_id="default"):
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"
@@ -100,22 +272,6 @@ def get_context_and_answer(message, history, session_id="default"):
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:
@@ -127,17 +283,6 @@ def get_context_and_answer(message, history, session_id="default"):
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
 
@@ -153,16 +298,6 @@ def respond(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})
@@ -189,7 +324,6 @@ def create_interface():
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)
 
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
  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"])
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 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
 
 
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,
 
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
 
 
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", ""),
177
+ "source": item.get("Source", ""),
178
+ "owner": item.get("Owner", ""),
179
+ "tag": item.get("Tag", ""),
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
 
 
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"
 
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:
 
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
 
 
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})
 
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)