File size: 7,675 Bytes
25eb473
b233df3
25eb473
 
 
 
 
 
 
ca2bd98
 
 
 
b233df3
2d2f030
25eb473
2d2f030
 
 
b233df3
ca2bd98
 
 
 
 
 
 
 
 
 
 
 
 
46186e5
ca2bd98
 
46186e5
ca2bd98
46186e5
ca2bd98
 
 
 
 
46186e5
ca2bd98
 
46186e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ca2bd98
 
 
 
 
46186e5
 
ca2bd98
2d2f030
25eb473
 
b233df3
25eb473
 
 
 
 
 
 
 
 
 
ca2bd98
46186e5
25eb473
 
 
b233df3
25eb473
 
b233df3
2d2f030
25eb473
46186e5
 
 
 
 
 
 
 
 
 
 
b233df3
25eb473
 
b233df3
2d2f030
eeea84a
 
 
 
 
 
46186e5
2d2f030
 
 
 
ca2bd98
46186e5
2d2f030
 
 
46186e5
 
2d2f030
46186e5
 
2d2f030
ca2bd98
46186e5
 
2d2f030
 
25eb473
 
2d2f030
 
 
 
 
 
 
 
25eb473
 
2d2f030
e576071
2d2f030
46186e5
 
 
2d2f030
e576071
25eb473
e576071
25eb473
 
 
 
 
 
 
 
e576071
3b25149
46186e5
 
 
2d2f030
46186e5
ca2bd98
46186e5
ca2bd98
25eb473
2d2f030
25eb473
 
f4db8e9
25eb473
 
 
b233df3
 
46186e5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import os
import gradio as gr
import pandas as pd
import torch
import numpy as np
from sentence_transformers import util
import google.generativeai as genai
import chromadb
from langchain_chroma import Chroma
import gspread
from google.oauth2.service_account import Credentials
from datetime import datetime
import json

# === Configuration ===
genai.configure(api_key=os.environ["GEMINI_API_KEY"])
embedding_model = "models/embedding-001"
llm_model_name = "models/gemma-3-4b-it"
collection_name = "xeno_collection"

# === Google Sheets Setup for Hugging Face ===
def get_google_sheets_credentials():
    credentials_json = os.environ.get("GOOGLE_SHEETS_CREDENTIALS")
    if not credentials_json:
        raise ValueError("GOOGLE_SHEETS_CREDENTIALS environment variable not set.")
    credentials_dict = json.loads(credentials_json)
    scope = ["https://spreadsheets.google.com/feeds", "https://www.googleapis.com/auth/drive"]
    creds = Credentials.from_service_account_info(credentials_dict, scopes=scope)
    return creds

# Authenticate with Google Sheets
client_gspread = gspread.authorize(get_google_sheets_credentials())

# Open the Google Sheet
sheet = client_gspread.open("Response_Log").sheet1

def log_response(question, answer, source_ids, knowledge_pairs):
    """
    Log a question, answer, source IDs, and knowledge base question-answer pairs to the Google Sheet.
    
    Args:
        question (str): The question asked by the user.
        answer (str): The answer provided by the model.
        source_ids (str): Comma-separated list of source IDs used.
        knowledge_pairs (list): List of tuples containing (question, answer) from the knowledge base.
    """
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    # Prepare row with user question, answer, source IDs, and knowledge base pairs
    knowledge_question_1 = knowledge_pairs[0][0] if len(knowledge_pairs) > 0 else "N/A"
    knowledge_answer_1 = knowledge_pairs[0][1] if len(knowledge_pairs) > 0 else "N/A"
    knowledge_question_2 = knowledge_pairs[1][0] if len(knowledge_pairs) > 1 else "N/A"
    knowledge_answer_2 = knowledge_pairs[1][1] if len(knowledge_pairs) > 1 else "N/A"
    row = [
        timestamp,
        question,
        answer,
        source_ids,
        knowledge_question_1,
        knowledge_answer_1,
        knowledge_question_2,
        knowledge_answer_2
    ]
    try:
        sheet.append_row(row)
        print(f"Logged: {question} | Source IDs: {source_ids}")
    except Exception as e:
        print(f"Failed to log to Google Sheet: {e}")
        with open("/tmp/response_log.txt", "a") as f:
            f.write(f"{timestamp},{question},{answer},{source_ids},{knowledge_question_1},{knowledge_answer_1},{knowledge_question_2},{knowledge_answer_2}\n")

# === Load and Clean Knowledge Base ===
df_kb = pd.read_json("XENO_Uganda_KnowledgeBase_Advisory.json")
df_kb.dropna(subset=['Content'], inplace=True)

def prepare_documents(data):
    documents, metadatas, ids = [], [], []
    for item in data:
        documents.append(f"Question: {item['Question']}\nAnswer: {item['Content']}")
        metadatas.append({
            "question": item["Question"],
            "content": item["Content"],
            "section": item.get("Section", ""),
            "source": item.get("Source", ""),
            "owner": item.get("Owner", ""),
            "tag": item.get("Tag", ""),
            "id": item["ID"]
        })
        ids.append(item["ID"])
    return documents, metadatas, ids

xeno_data_list = df_kb.to_dict('records')
documents, metadatas, ids = prepare_documents(xeno_data_list)

# === Setup ChromaDB ===
try:
    client = chromadb.PersistentClient(path="/tmp/xeno_db")
    try:
        collection = client.get_collection(name=collection_name)
        print(f"Loaded existing ChromaDB collection: {collection_name}")
    except:
        print(f"Creating new ChromaDB collection: {collection_name}")
        collection = client.create_collection(name=collection_name)
        collection.add(documents=documents, metadatas=metadatas, ids=ids)
except Exception as e:
    print(f"Failed to initialize ChromaDB: {e}")
    raise

vector_store = Chroma(client=client, collection_name=collection_name)
retriever = vector_store.as_retriever(search_type="similarity", search_kwargs={"k": 4})

# === Prompt System ===
SYSTEM_PROMPT = """You are a friendly XENO Support Assistant, an AI-powered helpful and professional customer service representative.
Use only the information provided in the knowledge base context to answer user queries.
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.
Only use context that is clearly relevant to the user's question.
For greetings like “hi” or “hello”, respond politely without using the context.
remember previous conversations."""

# === Context Processing ===
def process_context(results, cosine_scores, max_results=2):
    sorted_indices = np.argsort(cosine_scores)[::-1][:max_results]
    formatted_context = ""
    source_ids = []
    knowledge_pairs = []
    for i, idx in enumerate(sorted_indices, 1):
        result = results[idx]
        score = cosine_scores[idx]
        question = result.metadata.get('question', 'N/A')
        answer = result.metadata.get('content', 'N/A')
        formatted_context += f"Knowledge Entry {i}:\n"
        formatted_context += f"Q: {question}\n"
        formatted_context += f"A: {answer}\n"
        formatted_context += "-" * 40 + "\n"
        source_ids.append(result.metadata.get('id', 'N/A'))
        knowledge_pairs.append((question, answer))
    return formatted_context, source_ids, knowledge_pairs

# === LLM Generation ===
def generate_xeno_response(context, question):
    model = genai.GenerativeModel(llm_model_name)
    prompt = f"""{SYSTEM_PROMPT}

### CONTEXT ###
{context}

### QUESTION ###
{question}"""
    response = model.generate_content(prompt)
    return response.text.strip()

# === Main Interface Logic ===
def get_context_and_answer(message, history):
    if message.lower().strip() in {"hi", "hello", "hey"}:
        answer = "Hello! How can I assist you with XENO services today?"
        log_response(message, answer, "N/A", [])
        return answer

    queried_results = retriever.invoke(message)
    query_embedding = genai.embed_content(model=embedding_model,
                                          content=message,
                                          task_type="retrieval_query")['embedding']
    cosine_scores = []
    for doc in queried_results:
        doc_embedding = genai.embed_content(model=embedding_model,
                                            content=doc.page_content,
                                            task_type="retrieval_document")['embedding']
        cos_sim = util.cos_sim(torch.tensor(query_embedding).float(), torch.tensor(doc_embedding).float())[0][0].item()
        cosine_scores.append(cos_sim)

    if max(cosine_scores) < 0.6:
        answer = "I'm sorry, I couldn't find the specific information you're looking for in my knowledge base."
        log_response(message, answer, "N/A", [])
        return answer

    context, source_ids, knowledge_pairs = process_context(queried_results, cosine_scores)
    answer = generate_xeno_response(context, message)
    log_response(message, answer, ", ".join(source_ids), knowledge_pairs)
    return answer

# === Gradio UI ===
iface = gr.ChatInterface(
    fn=get_context_and_answer,
    title="ASKXENO",
    description="Ask anything about XENO's financial services.",
    theme="soft"
)

if __name__ == "__main__":
    iface.launch(share=False)