File size: 8,810 Bytes
74b9a8d 3e7e287 74b9a8d 3e7e287 c66dd86 41b9355 3e7e287 25bfc31 74b9a8d 41b9355 74b9a8d e048ca2 74b9a8d c66dd86 74b9a8d 3e7e287 74b9a8d f56ceca 74b9a8d 3e7e287 74b9a8d 3e7e287 f56ceca 74b9a8d 3e7e287 f56ceca 74b9a8d 3e7e287 74b9a8d 3e7e287 74b9a8d 3e7e287 74b9a8d b029948 3e7e287 41b9355 74b9a8d 3e7e287 74b9a8d 33d913e 74b9a8d c66dd86 74b9a8d 33d913e 74b9a8d 33d913e 74b9a8d 3e7e287 c66dd86 efe7e34 c66dd86 74b9a8d 12748e3 c66dd86 12748e3 f56ceca 74b9a8d 13a37a9 74b9a8d c66dd86 74b9a8d c66dd86 932be48 74b9a8d 3e7e287 74b9a8d 3e7e287 25bfc31 c66dd86 74b9a8d 3e7e287 efe7e34 74b9a8d c66dd86 efe7e34 3e7e287 74b9a8d 3e7e287 74b9a8d 3e7e287 74b9a8d 3e7e287 c66dd86 74b9a8d c66dd86 f56ceca 74b9a8d c66dd86 3e7e287 dd7779a f56ceca c66dd86 dd7779a 74b9a8d c66dd86 74b9a8d c66dd86 dd7779a 3e7e287 c66dd86 3e7e287 74b9a8d 3e7e287 f56ceca dd7779a f56ceca dd7779a 74b9a8d f56ceca |
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 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
# rag.py — Dual-company RAG pipeline (LD Events + Lamaki Designs)
from __future__ import annotations
import os, re, json, pickle, tempfile
from typing import List, Tuple
from functools import lru_cache
from supabase import create_client
from datasets import load_dataset
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_huggingface import HuggingFaceEndpoint
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain.chains import RetrievalQA
from langchain_core.prompts import PromptTemplate
# ---------------------------------------------------------------- CONFIG
DATASET_ID = "NimrodDev/LD_Events2"
LLM_MODEL = "mistralai/Mistral-7B-Instruct-v0.3"
CACHE_DIR = os.getenv("HF_HOME", tempfile.gettempdir())
FAISS_PATH = os.path.join(CACHE_DIR, "faiss_index.pkl")
HF_TOKEN = os.getenv("HF_TOKEN", os.getenv("HUGGINGFACEHUB_API_TOKEN", ""))
SUPABASE_URL = os.getenv("SUPABASE_URL")
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
supabase = None
if SUPABASE_URL and SUPABASE_KEY:
try:
supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
print("✅ Supabase client initialized.")
except Exception as e:
print(f"⚠️ Supabase init failed: {e}")
os.makedirs(CACHE_DIR, exist_ok=True)
os.environ.update({
"HF_HOME": CACHE_DIR,
"HF_HUB_CACHE": CACHE_DIR,
"TRANSFORMERS_CACHE": CACHE_DIR
})
# ---------------------------------------------------------------- INTENT DETECTION
GREETING_RE = re.compile(r"\b(hi|hello|hey|good morning|good afternoon|good evening)\b", re.I)
THANKS_RE = re.compile(r"\b(thank|thanks|appreciate)\b", re.I)
BYE_RE = re.compile(r"\b(bye|goodbye|see you|later)\b", re.I)
MONEY_RE = re.compile(r"\b(price|cost|budget|cheap|expensive|money|usd|ksh|payment|deposit|fee|quote)\b", re.I)
COMPLAIN_RE = re.compile(r"\b(complain|bad|terrible|awful|disappointed|angry|slow|rude|issue|problem)\b", re.I)
HUMAN_RE = re.compile(r"\b(agent|human|representative|manager|someone|person)\b", re.I)
# ---------------------------------------------------------------- COMPANY FALLBACKS
FALLBACKS = {
"LD Events": {
"greeting": "Hello! 👋 I’m *Amina*, your assistant for **LD Events** (weddings, graduations, corporate events) and **Lamaki Designs** (construction & architecture). How may I help you today?",
"money": "Our event packages vary depending on venue and number of guests. Could you share a few details so we can estimate a quote?",
"complain": "I’m sorry to hear that 😔. I’ll alert our support team — expect a call from a senior agent shortly.",
"thanks": "You’re most welcome! 💐",
"bye": "Thanks for chatting with LD Events. Have a beautiful day!",
"handoff": "Sure! I’ll connect you to a human representative now. Please hold on a moment.",
"default": "Let me get back to you on that. I’ve forwarded your question to a senior planner."
},
"Lamaki Designs": {
"greeting": "Karibu! 🏗️ I’m *Amina*, assistant for **Lamaki Designs** (construction, architectural plans, project management) and **LD Events** (weddings, graduations, corporate events). How may I assist?",
"money": "Construction costs depend on project scope and materials. Kindly share your plot size or design type for an accurate estimate.",
"complain": "We’re truly sorry for the inconvenience. Our site supervisor will reach out within 30 minutes to help.",
"thanks": "Asante! We appreciate your time.",
"bye": "Goodbye 👋 and thank you for trusting Lamaki Designs.",
"handoff": "No problem. A Lamaki Designs representative will join the chat soon.",
"default": "Let me get back to you on that — I’ll forward this to our design team."
}
}
# ---------------------------------------------------------------- HELPERS
def _company_from_text(text: str) -> str:
t = text.lower()
if any(k in t for k in ("lamaki", "construction", "architect", "plan", "bungalow", "site", "building")):
return "Lamaki Designs"
if any(k in t for k in ("ld events", "event", "wedding", "graduation", "venue", "party")):
return "LD Events"
return "LD Events"
def _detect_intent(text: str) -> str:
if GREETING_RE.search(text): return "greeting"
if THANKS_RE.search(text): return "thanks"
if BYE_RE.search(text): return "bye"
if MONEY_RE.search(text): return "money"
if COMPLAIN_RE.search(text): return "complain"
if HUMAN_RE.search(text): return "handoff"
return "normal"
def _fallback_answer(company: str, intent: str) -> str:
return FALLBACKS[company].get(intent, FALLBACKS[company]["default"])
# ---------------------------------------------------------------- DATA FETCH
@lru_cache(maxsize=1)
def get_texts() -> List[str]:
try:
print("🔍 Loading dataset from Parquet...")
ds = load_dataset(DATASET_ID, split="train", revision="refs/convert/parquet")
texts = [str(row["text"]) for row in ds if row.get("text")]
print(f"✅ Loaded {len(texts)} text chunks from {DATASET_ID}")
return texts
except Exception as e:
print(f"⚠️ Dataset load failed: {e}")
return []
# ---------------------------------------------------------------- VECTORSTORE
@lru_cache(maxsize=1)
def get_vectorstore() -> FAISS:
if os.path.exists(FAISS_PATH):
try:
with open(FAISS_PATH, "rb") as f:
print("📦 Using cached FAISS index.")
return pickle.load(f)
except Exception as e:
print(f"⚠️ Failed to load FAISS cache: {e}, rebuilding...")
texts = get_texts()
if not texts:
print("⚠️ No dataset found; using dummy FAISS index.")
return FAISS.from_texts(["No context available."],
HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2"))
splitter = RecursiveCharacterTextSplitter(chunk_size=700, chunk_overlap=100)
docs = splitter.create_documents(texts)
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
vs = FAISS.from_documents(docs, embeddings)
with open(FAISS_PATH, "wb") as f:
pickle.dump(vs, f)
print("✅ FAISS index created and cached.")
return vs
# ---------------------------------------------------------------- LLM
@lru_cache(maxsize=1)
def get_llm():
if not HF_TOKEN:
raise ValueError("Hugging Face token missing! Please set HF_TOKEN or HUGGINGFACEHUB_API_TOKEN.")
return HuggingFaceEndpoint(
repo_id=LLM_MODEL,
huggingfacehub_api_token=HF_TOKEN,
temperature=0.3,
max_new_tokens=300
)
PROMPT = PromptTemplate.from_template("""
You are Amina, a friendly virtual assistant for {company}.
Use the context below to answer questions concisely and politely.
If unsure, say: "Let me get back to you on that."
Context:
{context}
Question:
{question}
Answer:
""")
# ---------------------------------------------------------------- MAIN CHAT LOGIC
def ask_question(phone: str, question: str) -> Tuple[str, List]:
intent = _detect_intent(question)
company = _company_from_text(question)
# Fast fallback for simple intents
if intent in ("greeting", "thanks", "bye", "handoff"):
answer = _fallback_answer(company, intent)
_save_chat(phone, question, answer)
return answer, []
vs = get_vectorstore()
retriever = vs.as_retriever(search_kwargs={"k": 4})
qa = RetrievalQA.from_chain_type(
llm=get_llm(),
retriever=retriever,
chain_type_kwargs={"prompt": PROMPT.partial(company=company)},
return_source_documents=True,
)
try:
result = qa({"query": question})
answer = result.get("result", "").strip()
docs = result.get("source_documents", [])
except Exception as e:
print(f"❌ QA pipeline error: {e}")
answer, docs = "", []
# Smart fallback (pricing, complaints, or missing)
if not answer or len(answer.split()) < 4:
answer = _fallback_answer(company, intent if intent in ("money", "complain") else "default")
_save_chat(phone, question, answer)
return answer, docs
# ---------------------------------------------------------------- SUPABASE LOGGING
def _save_chat(phone: str, q: str, a: str) -> None:
if not supabase:
return
try:
data = [
{"user_phone": phone, "role": "user", "message": q},
{"user_phone": phone, "role": "assistant", "message": a}
]
supabase.table("chat_memory").insert(data).execute()
except Exception as e:
print(f"⚠️ Chat log failed: {e}")
|