flowbar-support-chatbot / rag_engine.py
J-Barrert
Fix: embed Q+A for search coverage + visible accent colors on chrome elements
30ad198
Raw
History Blame Contribute Delete
4.06 kB
"""
RAG engine for the chatbot demo.
Uses sentence-transformers for embeddings and FAISS for retrieval.
"""
import os
import numpy as np
from faq_data import FAQ_LIST
def build_retriever(model_name="all-MiniLM-L6-v2"):
"""Load sentence transformer model and build FAISS index."""
from sentence_transformers import SentenceTransformer
import faiss
# Prepare texts for embedding — embed question + answer together so all terms are searchable
texts = [f"{item['question']} {item['answer']}" for item in FAQ_LIST]
answers = [item["answer"] for item in FAQ_LIST]
categories = [item["category"] for item in FAQ_LIST]
# Load model (cached locally after first download)
model = SentenceTransformer(model_name)
# Create embeddings
embeddings = model.encode(texts, convert_to_numpy=True, show_progress_bar=False)
dimension = embeddings.shape[1]
# Build FAISS index
index = faiss.IndexFlatL2(dimension)
index.add(embeddings.astype(np.float32))
return {
"model": model,
"index": index,
"texts": texts,
"answers": answers,
"categories": categories,
}
def search(retriever, query, top_k=3):
"""Search for the most relevant FAQ entries."""
query_vec = retriever["model"].encode([query], convert_to_numpy=True)
distances, indices = retriever["index"].search(query_vec.astype(np.float32), top_k)
results = []
for i, idx in enumerate(indices[0]):
results.append({
"question": retriever["texts"][idx],
"answer": retriever["answers"][idx],
"category": retriever["categories"][idx],
"score": float(distances[0][i]),
})
return results
def generate_response(query, retrieved, api_key, model="openrouter/owl-alpha"):
"""Generate a natural response using OpenRouter API with retrieved context."""
import httpx
if not api_key:
return "I need an OpenRouter API key to generate responses. Set it in Settings above.", []
# Build context from retrieved docs
context_parts = []
for i, r in enumerate(retrieved, 1):
context_parts.append(f"[{i}] Q: {r['question']}\n A: {r['answer']}")
context = "\n\n".join(context_parts)
system_prompt = (
"You are a helpful customer support assistant for FlowBar, a project management SaaS."
" Answer the user's question using ONLY the information provided in the context below."
" If the context doesn't contain the answer, say 'I don't have that information in my knowledge base."
" Would you like me to connect you with a human agent?'"
" Be concise, friendly, and professional."
" Reference the source number when appropriate."
)
user_prompt = f"Context:\n{context}\n\nUser Question: {query}"
payload = {
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
"temperature": 0.3,
"max_tokens": 500,
}
try:
resp = httpx.post(
"https://openrouter.ai/api/v1/chat/completions",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"HTTP-Referer": "https://huggingface.co/spaces",
},
json=payload,
timeout=30,
)
resp.raise_for_status()
data = resp.json()
return data["choices"][0]["message"]["content"], retrieved
except Exception as e:
return f"Sorry, I couldn't reach the AI. Error: {str(e)}", retrieved
def generate_response_light(query, retrieved):
"""Fallback: generate response without external API using template."""
best = retrieved[0]
return (
f"Based on our knowledge base, here's what I found:\n\n"
f"**{best['question']}**\n\n{best['answer']}\n\n"
f"*Did that answer your question? If not, please rephrase or ask something else.*"
), retrieved