NimrodDev commited on
Commit
3e7e287
·
0 Parent(s):

working rag + filters + fallbacks

Browse files
Files changed (5) hide show
  1. .gitignore +4 -0
  2. Dockerfile +6 -0
  3. app.py +16 -0
  4. rag.py +161 -0
  5. requirements.txt +10 -0
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ .env
2
+ faiss_db
3
+ data/
4
+ __pycache__/
Dockerfile ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+ WORKDIR /code
3
+ COPY requirements.txt .
4
+ RUN pip install -r requirements.txt
5
+ COPY . .
6
+ CMD ["gunicorn", "app:app", "-b", "0.0.0.0:7860"]
app.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ from flask import Flask, request, jsonify
3
+ from rag import ask_question
4
+
5
+ app = Flask(__name__)
6
+
7
+ @app.route("/webhook", methods=["POST"])
8
+ def webhook():
9
+ payload = request.get_json(force=True)
10
+ phone = payload["phone"]
11
+ question = payload["question"]
12
+ answer, docs = ask_question(phone, question)
13
+ return jsonify({"answer": answer, "docs": len(docs)})
14
+
15
+ if __name__ == "__main__":
16
+ app.run(host="0.0.0.0", port=7860)
rag.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # rag.py (v2 – with filters + fallbacks)
2
+ from __future__ import annotations
3
+ import os, uuid, tempfile, requests, shutil, re
4
+ from pathlib import Path
5
+ from functools import lru_cache
6
+ from typing import List, Tuple
7
+ from datasets import load_dataset
8
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
9
+ from langchain_community.vectorstores import FAISS
10
+ from langchain_huggingface import HuggingFaceEmbeddings
11
+ from langchain_core.prompts import PromptTemplate
12
+ from langchain.chains import RetrievalQA
13
+ from langchain_huggingface import HuggingFaceEndpoint
14
+ from supabase import create_client
15
+
16
+ # ---------- config ----------
17
+ HF_DS = "NimrodDev/LD_Events2"
18
+ EMBED_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
19
+ LLM_MODEL = "microsoft/DialoGPT-medium"
20
+ FAISS_PATH = Path("faiss_db")
21
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
22
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY")
23
+ HF_TOKEN = os.getenv("HF_TOKEN")
24
+
25
+ supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
26
+
27
+ # ---------- keyword filters ----------
28
+ GREETING_RE = re.compile(r"\b(hi|hello|hey|good morning|good afternoon|good evening)\b", re.I)
29
+ THANKS_RE = re.compile(r"\b(thank|thanks|appreciate)\b", re.I)
30
+ BYE_RE = re.compile(r"\b(bye|goodbye|see you|later)\b", re.I)
31
+ MONEY_RE = re.compile(r"\b(price|cost|budget|cheap|expensive|money|usd|ksh|payment|deposit)\b", re.I)
32
+ COMPLAIN_RE = re.compile(r"\b(complain|bad|terrible|awful|disappointed|angry|slow|rude)\b", re.I)
33
+
34
+ # ---------- company-specific fallbacks ----------
35
+ FALLBACKS = {
36
+ "LD Events": {
37
+ "greeting": "Hello! 👋 Welcome to LD Events – your trusted partner for weddings, graduations and corporate events.",
38
+ "money": "Our pricing depends on venue, guest count and package. Please share a few details so we can give you a tailored quote.",
39
+ "complain": "We’re sorry to hear this. A senior agent will contact you within 30 minutes to resolve the issue.",
40
+ "thanks": "You’re welcome! If you need anything else, just text back.",
41
+ "bye": "Thanks for chatting with LD Events. Have a lovely day!",
42
+ "default": "I’m not sure about that, but a human agent will follow up shortly."
43
+ },
44
+ "Lamaki Designs": {
45
+ "greeting": "Karibu! 🏗️ Lamaki Designs here – quality construction, architectural plans and project management.",
46
+ "money": "Cost varies by project size and materials. Kindly share your plot size / plan so we can estimate for you.",
47
+ "complain": "We apologise for the inconvenience. Our site manager will call you within 30 minutes to sort it out.",
48
+ "thanks": "Asante! Feel free to text any time.",
49
+ "bye": "Good-bye and stay safe!",
50
+ "default": "Let me get back to you on that."
51
+ }
52
+ }
53
+
54
+ # ---------- helpers ----------
55
+ def _company_from_text(text: str) -> str:
56
+ t = text.lower()
57
+ if any(k in t for k in ("ld events", "event", "wedding", "venue", "graduation")):
58
+ return "LD Events"
59
+ if any(k in t for k in ("lamaki", "construction", "build", "site", "bungalow", "architect")):
60
+ return "Lamaki Designs"
61
+ return "LD Events" # default
62
+
63
+ def _detect_intent(text: str) -> str:
64
+ if GREETING_RE.search(text):
65
+ return "greeting"
66
+ if THANKS_RE.search(text):
67
+ return "thanks"
68
+ if BYE_RE.search(text):
69
+ return "bye"
70
+ if MONEY_RE.search(text):
71
+ return "money"
72
+ if COMPLAIN_RE.search(text):
73
+ return "complain"
74
+ return "normal"
75
+
76
+ def _fallback_answer(company: str, intent: str) -> str:
77
+ return FALLBACKS[company].get(intent, FALLBACKS[company]["default"])
78
+
79
+ # ---------- pdf loader ----------
80
+ def download_pdfs() -> List[Path]:
81
+ data_dir = Path("data")
82
+ data_dir.mkdir(exist_ok=True)
83
+ ds = load_dataset(HF_DS, split="train", streaming=True)
84
+ paths = []
85
+ for row in ds:
86
+ url = row["pdf_url"]
87
+ name = row.get("name", uuid.uuid4().hex) + ".pdf"
88
+ dest = data_dir / name
89
+ if not dest.exists():
90
+ r = requests.get(url, stream=True, timeout=30)
91
+ r.raise_for_status()
92
+ with open(dest, "wb") as f:
93
+ shutil.copyfileobj(r.raw, f)
94
+ paths.append(dest)
95
+ return paths
96
+
97
+ # ---------- vector store ----------
98
+ @lru_cache(maxsize=1)
99
+ def get_vectorstore() -> FAISS:
100
+ if FAISS_PATH.exists():
101
+ return FAISS.load_local(str(FAISS_PATH), HuggingFaceEmbeddings(model_name=EMBED_MODEL),
102
+ allow_dangerous_deserialization=True)
103
+ docs = []
104
+ splitter = RecursiveCharacterTextSplitter(chunk_size=600, chunk_overlap=50)
105
+ for pdf in download_pdfs():
106
+ text = Path(pdf).read_text(encoding="utf-8", errors="ignore")
107
+ docs.extend(splitter.create_documents([text], metadatas=[{"source": pdf.name}]))
108
+ vs = FAISS.from_documents(docs, HuggingFaceEmbeddings(model_name=EMBED_MODEL))
109
+ vs.save_local(str(FAISS_PATH))
110
+ return vs
111
+
112
+ # ---------- llm ----------
113
+ @lru_cache(maxsize=1)
114
+ def get_llm():
115
+ return HuggingFaceEndpoint(
116
+ repo_id=LLM_MODEL,
117
+ temperature=0.1,
118
+ max_new_tokens=150,
119
+ huggingfacehub_api_token=HF_TOKEN
120
+ )
121
+
122
+ PROMPT = PromptTemplate.from_template("""You are Amina, assistant for {company}.
123
+ Use only the context below. If unsure, say: “A human agent will follow up.”
124
+ Context: {context}
125
+ Question: {question}
126
+ Answer:""")
127
+
128
+ # ---------- main entry ----------
129
+ def ask_question(phone: str, question: str) -> Tuple[str, List]:
130
+ intent = _detect_intent(question)
131
+ company = _company_from_text(question)
132
+
133
+ # short-circuit greetings/thanks/bye – no LLM, no context needed
134
+ if intent in ("greeting", "thanks", "bye"):
135
+ answer = _fallback_answer(company, intent)
136
+ _save_chat(phone, question, answer)
137
+ return answer, []
138
+
139
+ # money or complaints: fallback if no docs
140
+ vs = get_vectorstore()
141
+ docs = vs.similarity_search(question, k=3)
142
+ if not docs:
143
+ answer = _fallback_answer(company, intent if intent in ("money", "complain") else "default")
144
+ _save_chat(phone, question, answer)
145
+ return answer, []
146
+
147
+ # normal RAG
148
+ qa = RetrievalQA.from_chain_type(
149
+ llm=get_llm(),
150
+ retriever=vs.as_retriever(search_kwargs={"k": 3}),
151
+ return_source_documents=True,
152
+ chain_type_kwargs={"prompt": PROMPT}
153
+ )
154
+ result = qa({"query": question, "company": company})
155
+ answer = result["result"].strip()
156
+ _save_chat(phone, question, answer)
157
+ return answer, result.get("source_documents", [])
158
+
159
+ def _save_chat(phone: str, q: str, a: str) -> None:
160
+ supabase.table("chat_memory").insert({"user_phone": phone, "role": "user", "message": q}).execute()
161
+ supabase.table("chat_memory").insert({"user_phone": phone, "role": "assistant", "message": a}).execute()
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ flask==3.0.2
2
+ langchain==0.3.0
3
+ langchain-community==0.3.0
4
+ langchain-huggingface==0.3.0
5
+ sentence-transformers==3.0.0
6
+ faiss-cpu==1.8.0
7
+ datasets==2.20.0
8
+ supabase==2.6.1
9
+ python-dotenv==1.0.1
10
+ gunicorn==22.0.0