NimrodDev commited on
Commit
74b9a8d
·
1 Parent(s): c66dd86
Files changed (2) hide show
  1. app.py +15 -8
  2. rag.py +87 -76
app.py CHANGED
@@ -1,16 +1,23 @@
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)
 
1
+ #from flask import Flask, request, jsonify
 
2
  from rag import ask_question
3
 
4
  app = Flask(__name__)
5
 
6
  @app.route("/webhook", methods=["POST"])
7
  def webhook():
8
+ try:
9
+ data = request.get_json(force=True)
10
+ phone = data.get("phone", "")
11
+ question = data.get("question", "")
12
+ answer, docs = ask_question(phone, question)
13
+ return jsonify({"answer": answer, "docs": len(docs)})
14
+ except Exception as e:
15
+ print(f"❌ Webhook error: {e}")
16
+ return jsonify({"answer": "Let me get back to you on that.", "docs": 0})
17
+
18
+ @app.route("/healthcheck")
19
+ def healthcheck():
20
+ return jsonify({"status": "ok", "message": "Amina bot running!"})
21
 
22
  if __name__ == "__main__":
23
+ app.run(host="0.0.0.0", port=7860)
rag.py CHANGED
@@ -1,73 +1,80 @@
1
- # rag.py — production-grade, Parquet-ready, FAISS-cached RAG pipeline
2
  from __future__ import annotations
3
- import os, re, json, pickle
4
  from typing import List, Tuple
5
  from functools import lru_cache
6
- import requests
7
  from supabase import create_client
8
-
9
- # LangChain + HF
10
  from datasets import load_dataset
11
  from langchain.text_splitter import RecursiveCharacterTextSplitter
12
  from langchain_community.vectorstores import FAISS
13
- from langchain_community.embeddings import HuggingFaceEmbeddings
14
  from langchain_huggingface import HuggingFaceEndpoint
15
- from langchain_core.prompts import PromptTemplate
16
  from langchain.chains import RetrievalQA
 
17
 
18
- # ------------------------------------------------------------------ CONFIG
19
  DATASET_ID = "NimrodDev/LD_Events2"
20
- HF_TOKEN = os.getenv("HF_TOKEN")
 
 
 
 
21
  SUPABASE_URL = os.getenv("SUPABASE_URL")
22
  SUPABASE_KEY = os.getenv("SUPABASE_KEY")
23
- LLM_MODEL = "mistralai/Mistral-7B-Instruct-v0.3" # stronger model
24
- CACHE_DIR = os.getenv("HF_HOME", "/tmp/hf_cache")
25
- FAISS_PATH = "/tmp/faiss_index.pkl"
26
-
27
- os.makedirs(CACHE_DIR, exist_ok=True)
28
- os.environ["TRANSFORMERS_CACHE"] = CACHE_DIR
29
- os.environ["HF_HOME"] = CACHE_DIR
30
- os.environ["HF_HUB_CACHE"] = CACHE_DIR
31
-
32
- supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
33
 
34
- # ------------------------------------------------------------------ INTENT DETECTION
35
- GREETING_RE = re.compile(r"\b(hi|hello|hey|good morning|good afternoon|good evening)\b", re.I)
36
- THANKS_RE = re.compile(r"\b(thank|thanks|appreciate)\b", re.I)
37
- BYE_RE = re.compile(r"\b(bye|goodbye|see you|later)\b", re.I)
38
- MONEY_RE = re.compile(r"\b(price|cost|budget|cheap|expensive|money|usd|ksh|payment|deposit)\b", re.I)
39
- COMPLAIN_RE = re.compile(r"\b(complain|bad|terrible|awful|disappointed|angry|slow|rude)\b", re.I)
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  FALLBACKS = {
42
  "LD Events": {
43
- "greeting": "Hello! 👋 I’m Amina, your assistant for *LD Events* (weddings, graduations, corporate events) "
44
- "and *Lamaki Designs* (construction & architectural plans).\n\n"
45
- "Which service would you like to know about?\n\n",
46
- "money": "Our pricing depends on venue / project size. Please share a few details so we can give you a tailored quote.",
47
- "complain": "We’re sorry to hear this. A senior agent will contact you within 30 minutes to resolve the issue.",
48
- "thanks": "You’re welcome! If you need anything else, just text back.",
49
- "bye": "Thanks for chatting. Have a lovely day!",
50
- "default": "I’m not sure about that, but a human agent will follow up shortly."
51
  },
52
  "Lamaki Designs": {
53
- "greeting": "Karibu! 🏗️ I’m Amina, your assistant for *Lamaki Designs* (construction, architectural plans, project management) "
54
- "and *LD Events* (weddings, graduations, corporate events).\n\n"
55
- "Which service would you like to know about?\n\n",
56
- "money": "Cost varies by project size and materials. Kindly share your plot size / plan so we can estimate for you.",
57
- "complain": "We apologise for the inconvenience. Our site manager will call you within 30 minutes to sort it out.",
58
- "thanks": "Asante! Feel free to text any time.",
59
- "bye": "Good-bye and stay safe!",
60
- "default": "Let me get back to you on that."
61
  }
62
  }
63
 
64
- # ------------------------------------------------------------------ HELPERS
65
  def _company_from_text(text: str) -> str:
66
  t = text.lower()
67
- if any(k in t for k in ("ld events", "event", "wedding", "venue", "graduation")):
68
- return "LD Events"
69
- if any(k in t for k in ("lamaki", "construction", "build", "site", "bungalow", "architect")):
70
  return "Lamaki Designs"
 
 
71
  return "LD Events"
72
 
73
  def _detect_intent(text: str) -> str:
@@ -76,65 +83,67 @@ def _detect_intent(text: str) -> str:
76
  if BYE_RE.search(text): return "bye"
77
  if MONEY_RE.search(text): return "money"
78
  if COMPLAIN_RE.search(text): return "complain"
 
79
  return "normal"
80
 
81
  def _fallback_answer(company: str, intent: str) -> str:
82
  return FALLBACKS[company].get(intent, FALLBACKS[company]["default"])
83
 
84
- # ------------------------------------------------------------------ DATA FETCH (Parquet)
85
  @lru_cache(maxsize=1)
86
  def get_texts() -> List[str]:
87
  try:
88
- print("🔍 Loading dataset from Parquet branch...")
89
  ds = load_dataset(DATASET_ID, split="train", revision="refs/convert/parquet")
90
  texts = [str(row["text"]) for row in ds if row.get("text")]
91
- print(f"✅ Loaded {len(texts)} rows from {DATASET_ID}")
92
  return texts
93
  except Exception as e:
94
- print(f"⚠️ Failed to load Parquet dataset: {e}")
95
  return []
96
 
97
- # ------------------------------------------------------------------ VECTORSTORE
98
  @lru_cache(maxsize=1)
99
  def get_vectorstore() -> FAISS:
100
  if os.path.exists(FAISS_PATH):
101
  try:
102
  with open(FAISS_PATH, "rb") as f:
103
- vs = pickle.load(f)
104
- print("📦 Loaded cached FAISS index.")
105
- return vs
106
- except Exception:
107
- print("⚠️ Cached FAISS corrupted, rebuilding...")
108
 
109
  texts = get_texts()
110
  if not texts:
111
- print("⚠️ No dataset found; creating dummy FAISS index.")
112
- return FAISS.from_texts(["No context available."], HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2"))
 
113
 
114
- embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
115
- splitter = RecursiveCharacterTextSplitter(chunk_size=600, chunk_overlap=100)
116
  docs = splitter.create_documents(texts)
 
117
  vs = FAISS.from_documents(docs, embeddings)
118
-
119
  with open(FAISS_PATH, "wb") as f:
120
  pickle.dump(vs, f)
121
  print("✅ FAISS index created and cached.")
122
  return vs
123
 
124
- # ------------------------------------------------------------------ LLM
125
  @lru_cache(maxsize=1)
126
  def get_llm():
 
 
127
  return HuggingFaceEndpoint(
128
  repo_id=LLM_MODEL,
129
  huggingfacehub_api_token=HF_TOKEN,
130
- temperature=0.1,
131
- max_new_tokens=200
132
  )
133
 
134
  PROMPT = PromptTemplate.from_template("""
135
- You are Amina, assistant for {company}.
136
- Answer clearly and politely using the context below.
137
- If context lacks information, say: "Let me get back to you on that."
138
 
139
  Context:
140
  {context}
@@ -145,24 +154,25 @@ Question:
145
  Answer:
146
  """)
147
 
148
- # ------------------------------------------------------------------ MAIN LOGIC
149
  def ask_question(phone: str, question: str) -> Tuple[str, List]:
150
- intent = _detect_intent(question)
151
  company = _company_from_text(question)
152
 
153
- # Quick intents skip RAG
154
- if intent in ("greeting", "thanks", "bye"):
155
  answer = _fallback_answer(company, intent)
156
  _save_chat(phone, question, answer)
157
  return answer, []
158
 
159
  vs = get_vectorstore()
160
  retriever = vs.as_retriever(search_kwargs={"k": 4})
 
161
  qa = RetrievalQA.from_chain_type(
162
  llm=get_llm(),
163
  retriever=retriever,
164
  chain_type_kwargs={"prompt": PROMPT},
165
- return_source_documents=True
166
  )
167
 
168
  try:
@@ -170,20 +180,21 @@ def ask_question(phone: str, question: str) -> Tuple[str, List]:
170
  answer = result.get("result", "").strip()
171
  docs = result.get("source_documents", [])
172
  except Exception as e:
173
- print(f"❌ QA error: {e}")
174
  answer, docs = "", []
175
 
176
- # Fallback logic
177
- if not answer or len(answer.split()) < 3:
178
  answer = _fallback_answer(company, intent if intent in ("money", "complain") else "default")
179
 
180
  _save_chat(phone, question, answer)
181
  return answer, docs
182
 
183
- # ------------------------------------------------------------------ SUPABASE LOGGING
184
  def _save_chat(phone: str, q: str, a: str) -> None:
 
185
  try:
186
  supabase.table("chat_memory").insert({"user_phone": phone, "role": "user", "message": q}).execute()
187
  supabase.table("chat_memory").insert({"user_phone": phone, "role": "assistant", "message": a}).execute()
188
  except Exception as e:
189
- print(f"⚠️ Supabase logging failed: {e}")
 
1
+ # rag.py — Dual-company RAG pipeline (LD Events + Lamaki Designs)
2
  from __future__ import annotations
3
+ import os, re, json, pickle, tempfile
4
  from typing import List, Tuple
5
  from functools import lru_cache
 
6
  from supabase import create_client
 
 
7
  from datasets import load_dataset
8
  from langchain.text_splitter import RecursiveCharacterTextSplitter
9
  from langchain_community.vectorstores import FAISS
 
10
  from langchain_huggingface import HuggingFaceEndpoint
11
+ from langchain_community.embeddings import HuggingFaceEmbeddings
12
  from langchain.chains import RetrievalQA
13
+ from langchain_core.prompts import PromptTemplate
14
 
15
+ # ---------------------------------------------------------------- CONFIG
16
  DATASET_ID = "NimrodDev/LD_Events2"
17
+ LLM_MODEL = "mistralai/Mistral-7B-Instruct-v0.3"
18
+ CACHE_DIR = os.getenv("HF_HOME", tempfile.gettempdir())
19
+ FAISS_PATH = os.path.join(CACHE_DIR, "faiss_index.pkl")
20
+
21
+ HF_TOKEN = os.getenv("HF_TOKEN", os.getenv("HUGGINGFACEHUB_API_TOKEN", ""))
22
  SUPABASE_URL = os.getenv("SUPABASE_URL")
23
  SUPABASE_KEY = os.getenv("SUPABASE_KEY")
 
 
 
 
 
 
 
 
 
 
24
 
25
+ supabase = None
26
+ if SUPABASE_URL and SUPABASE_KEY:
27
+ try:
28
+ supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
29
+ except Exception as e:
30
+ print(f"⚠️ Supabase init failed: {e}")
31
 
32
+ os.makedirs(CACHE_DIR, exist_ok=True)
33
+ os.environ.update({
34
+ "HF_HOME": CACHE_DIR,
35
+ "HF_HUB_CACHE": CACHE_DIR,
36
+ "TRANSFORMERS_CACHE": CACHE_DIR
37
+ })
38
+
39
+ # ---------------------------------------------------------------- INTENT DETECTION
40
+ GREETING_RE = re.compile(r"\b(hi|hello|hey|good morning|good afternoon|good evening)\b", re.I)
41
+ THANKS_RE = re.compile(r"\b(thank|thanks|appreciate)\b", re.I)
42
+ BYE_RE = re.compile(r"\b(bye|goodbye|see you|later)\b", re.I)
43
+ MONEY_RE = re.compile(r"\b(price|cost|budget|cheap|expensive|money|usd|ksh|payment|deposit|fee|quote)\b", re.I)
44
+ COMPLAIN_RE = re.compile(r"\b(complain|bad|terrible|awful|disappointed|angry|slow|rude|issue|problem)\b", re.I)
45
+ HUMAN_RE = re.compile(r"\b(agent|human|representative|manager|someone|person)\b", re.I)
46
+
47
+ # ---------------------------------------------------------------- COMPANY FALLBACKS
48
  FALLBACKS = {
49
  "LD Events": {
50
+ "greeting": "Hello! 👋 I’m *Amina*, your assistant for **LD Events** (weddings, graduations, corporate events) "
51
+ "and **Lamaki Designs** (construction & architecture). How may I help you today?",
52
+ "money": "Our event packages vary depending on venue and number of guests. Could you share a few details so we can estimate a quote?",
53
+ "complain": "I’m sorry to hear that 😔. I’ll alert our support team expect a call from a senior agent shortly.",
54
+ "thanks": "You’re most welcome! 💐",
55
+ "bye": "Thanks for chatting with LD Events. Have a beautiful day!",
56
+ "handoff": "Sure! I’ll connect you to a human representative now. Please hold on a moment.",
57
+ "default": "Let me get back to you on that. I’ve forwarded your question to a senior planner."
58
  },
59
  "Lamaki Designs": {
60
+ "greeting": "Karibu! 🏗️ I’m *Amina*, assistant for **Lamaki Designs** (construction, architectural plans, project management) "
61
+ "and **LD Events** (weddings, graduations, corporate events). How may I assist?",
62
+ "money": "Construction costs depend on project scope and materials. Kindly share your plot size or design type for an accurate estimate.",
63
+ "complain": "We’re truly sorry for the inconvenience. Our site supervisor will reach out within 30 minutes to help.",
64
+ "thanks": "Asante! We appreciate your time.",
65
+ "bye": "Goodbye 👋 and thank you for trusting Lamaki Designs.",
66
+ "handoff": "No problem. A Lamaki Designs representative will join the chat soon.",
67
+ "default": "Let me get back to you on that — I’ll forward this to our design team."
68
  }
69
  }
70
 
71
+ # ---------------------------------------------------------------- HELPERS
72
  def _company_from_text(text: str) -> str:
73
  t = text.lower()
74
+ if any(k in t for k in ("lamaki", "construction", "architect", "plan", "bungalow", "site", "building")):
 
 
75
  return "Lamaki Designs"
76
+ if any(k in t for k in ("ld events", "event", "wedding", "graduation", "venue", "party")):
77
+ return "LD Events"
78
  return "LD Events"
79
 
80
  def _detect_intent(text: str) -> str:
 
83
  if BYE_RE.search(text): return "bye"
84
  if MONEY_RE.search(text): return "money"
85
  if COMPLAIN_RE.search(text): return "complain"
86
+ if HUMAN_RE.search(text): return "handoff"
87
  return "normal"
88
 
89
  def _fallback_answer(company: str, intent: str) -> str:
90
  return FALLBACKS[company].get(intent, FALLBACKS[company]["default"])
91
 
92
+ # ---------------------------------------------------------------- DATA FETCH
93
  @lru_cache(maxsize=1)
94
  def get_texts() -> List[str]:
95
  try:
96
+ print("🔍 Loading dataset from Parquet...")
97
  ds = load_dataset(DATASET_ID, split="train", revision="refs/convert/parquet")
98
  texts = [str(row["text"]) for row in ds if row.get("text")]
99
+ print(f"✅ Loaded {len(texts)} text chunks from {DATASET_ID}")
100
  return texts
101
  except Exception as e:
102
+ print(f"⚠️ Dataset load failed: {e}")
103
  return []
104
 
105
+ # ---------------------------------------------------------------- VECTORSTORE
106
  @lru_cache(maxsize=1)
107
  def get_vectorstore() -> FAISS:
108
  if os.path.exists(FAISS_PATH):
109
  try:
110
  with open(FAISS_PATH, "rb") as f:
111
+ print("📦 Using cached FAISS index.")
112
+ return pickle.load(f)
113
+ except Exception as e:
114
+ print(f"⚠️ Failed to load FAISS cache: {e}, rebuilding...")
 
115
 
116
  texts = get_texts()
117
  if not texts:
118
+ print("⚠️ No dataset found; building dummy FAISS index.")
119
+ return FAISS.from_texts(["No context available."],
120
+ HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2"))
121
 
122
+ splitter = RecursiveCharacterTextSplitter(chunk_size=700, chunk_overlap=100)
 
123
  docs = splitter.create_documents(texts)
124
+ embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
125
  vs = FAISS.from_documents(docs, embeddings)
 
126
  with open(FAISS_PATH, "wb") as f:
127
  pickle.dump(vs, f)
128
  print("✅ FAISS index created and cached.")
129
  return vs
130
 
131
+ # ---------------------------------------------------------------- LLM
132
  @lru_cache(maxsize=1)
133
  def get_llm():
134
+ if not HF_TOKEN:
135
+ raise ValueError("Hugging Face token missing! Please set HF_TOKEN or HUGGINGFACEHUB_API_TOKEN.")
136
  return HuggingFaceEndpoint(
137
  repo_id=LLM_MODEL,
138
  huggingfacehub_api_token=HF_TOKEN,
139
+ temperature=0.3,
140
+ max_new_tokens=300
141
  )
142
 
143
  PROMPT = PromptTemplate.from_template("""
144
+ You are Amina, a friendly virtual assistant for {company}.
145
+ Use the context below to answer questions concisely and politely.
146
+ If unsure, say: "Let me get back to you on that."
147
 
148
  Context:
149
  {context}
 
154
  Answer:
155
  """)
156
 
157
+ # ---------------------------------------------------------------- MAIN CHAT LOGIC
158
  def ask_question(phone: str, question: str) -> Tuple[str, List]:
159
+ intent = _detect_intent(question)
160
  company = _company_from_text(question)
161
 
162
+ # Fast fallback for simple intents
163
+ if intent in ("greeting", "thanks", "bye", "handoff"):
164
  answer = _fallback_answer(company, intent)
165
  _save_chat(phone, question, answer)
166
  return answer, []
167
 
168
  vs = get_vectorstore()
169
  retriever = vs.as_retriever(search_kwargs={"k": 4})
170
+
171
  qa = RetrievalQA.from_chain_type(
172
  llm=get_llm(),
173
  retriever=retriever,
174
  chain_type_kwargs={"prompt": PROMPT},
175
+ return_source_documents=True,
176
  )
177
 
178
  try:
 
180
  answer = result.get("result", "").strip()
181
  docs = result.get("source_documents", [])
182
  except Exception as e:
183
+ print(f"❌ QA pipeline error: {e}")
184
  answer, docs = "", []
185
 
186
+ # Smart fallback (pricing, complaints, or missing)
187
+ if not answer or len(answer.split()) < 4:
188
  answer = _fallback_answer(company, intent if intent in ("money", "complain") else "default")
189
 
190
  _save_chat(phone, question, answer)
191
  return answer, docs
192
 
193
+ # ---------------------------------------------------------------- SUPABASE LOGGING
194
  def _save_chat(phone: str, q: str, a: str) -> None:
195
+ if not supabase: return
196
  try:
197
  supabase.table("chat_memory").insert({"user_phone": phone, "role": "user", "message": q}).execute()
198
  supabase.table("chat_memory").insert({"user_phone": phone, "role": "assistant", "message": a}).execute()
199
  except Exception as e:
200
+ print(f"⚠️ Chat log failed: {e}")