Spaces:
Sleeping
Sleeping
cbc
Browse files
app.py
CHANGED
|
@@ -1,222 +1,254 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
|
|
|
|
| 4 |
"""
|
| 5 |
from __future__ import annotations
|
| 6 |
|
| 7 |
-
import json
|
| 8 |
import logging
|
| 9 |
import os
|
| 10 |
-
import
|
| 11 |
import re
|
| 12 |
-
from functools import lru_cache
|
| 13 |
-
from typing import List, Optional
|
| 14 |
-
|
| 15 |
-
import numpy as np
|
| 16 |
-
import ollama
|
| 17 |
from flask import Flask, request, jsonify
|
| 18 |
-
from langchain_core.documents import Document
|
| 19 |
-
from langchain_community.vectorstores import FAISS
|
| 20 |
-
from langchain_huggingface import HuggingFaceEmbeddings # <-- new package
|
| 21 |
-
from rank_bm25 import BM25Okapi
|
| 22 |
from supabase import create_client, Client
|
| 23 |
|
| 24 |
-
|
| 25 |
-
logging.basicConfig(
|
| 26 |
-
level=logging.INFO,
|
| 27 |
-
format="%(asctime)s | %(levelname)s | %(message)s",
|
| 28 |
-
datefmt="%Y-%m-%d %H:%M:%S",
|
| 29 |
-
)
|
| 30 |
log = logging.getLogger("wa")
|
| 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 |
-
return
|
| 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 |
-
if line and "KES" in line:
|
| 115 |
-
docs.append(Document(page_content=line))
|
| 116 |
-
if not docs: # fallback
|
| 117 |
-
docs.append(
|
| 118 |
-
Document(page_content="LD Events handles events. Lamaki Designs handles interiors.")
|
| 119 |
-
)
|
| 120 |
-
|
| 121 |
-
dense = FAISS.from_documents(docs, EMBED).as_retriever(search_kwargs={"k": 5})
|
| 122 |
-
tokenized = [re.findall(r"\w+", d.page_content.lower()) for d in docs]
|
| 123 |
-
bm25 = BM25Okapi(tokenized)
|
| 124 |
-
|
| 125 |
-
def search(query: str) -> List[Document]:
|
| 126 |
-
dense_hits = dense.invoke(query)
|
| 127 |
-
scores = bm25.get_scores(re.findall(r"\w+", query.lower()))
|
| 128 |
-
top = np.argsort(scores)[-5:][::-1]
|
| 129 |
-
bm25_hits = [docs[i] for i in top if scores[i] > 0]
|
| 130 |
-
seen, out = set(), []
|
| 131 |
-
for doc in dense_hits + bm25_hits:
|
| 132 |
-
if doc.page_content not in seen:
|
| 133 |
-
out.append(doc)
|
| 134 |
-
seen.add(doc.page_content)
|
| 135 |
-
return out
|
| 136 |
-
|
| 137 |
-
return search
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
search = atomic_retriever()
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
# ---------- business logic ----------
|
| 144 |
-
def company_greeting(company: str) -> str:
|
| 145 |
-
if company == "ld events":
|
| 146 |
-
return (
|
| 147 |
-
"🎤 Hey there! Welcome to LD Events – your ultimate sound partner. "
|
| 148 |
-
"How can we make your event unforgettable?"
|
| 149 |
-
)
|
| 150 |
-
return "🛋️ Hello! Lamaki Designs here – ready to transform your space. What are you dreaming of?"
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
@lru_cache(maxsize=512)
|
| 154 |
-
def smart_reply(text: str, user: str) -> str:
|
| 155 |
-
"""Main reply logic."""
|
| 156 |
-
text_l = text.lower()
|
| 157 |
-
company = (
|
| 158 |
-
"ld events"
|
| 159 |
-
if any(
|
| 160 |
-
k in text_l
|
| 161 |
-
for k in [
|
| 162 |
-
"wedding",
|
| 163 |
-
"concert",
|
| 164 |
-
"live",
|
| 165 |
-
"stage",
|
| 166 |
-
"sound",
|
| 167 |
-
"ld events",
|
| 168 |
-
"speaker",
|
| 169 |
-
"line array",
|
| 170 |
-
"moving head",
|
| 171 |
-
"parcan",
|
| 172 |
-
"led screen",
|
| 173 |
-
"bronze",
|
| 174 |
-
"silver",
|
| 175 |
-
"gold",
|
| 176 |
-
"platinum",
|
| 177 |
-
]
|
| 178 |
-
)
|
| 179 |
-
else "lamaki designs"
|
| 180 |
-
)
|
| 181 |
-
|
| 182 |
-
# 1. greetings
|
| 183 |
-
if any(k in text_l for k in ("hello", "hi", "hey", "jambo")):
|
| 184 |
-
return company_greeting(company)
|
| 185 |
-
|
| 186 |
-
# 2. pricing
|
| 187 |
-
if any(k in text_l for k in ("price", "cost", "how much", "hire", "rate", "quote")):
|
| 188 |
-
hits = search(text)
|
| 189 |
-
if not hits:
|
| 190 |
-
return "Which exact item or package would you like a quote for? (e.g. ‘line-array-top’ or ‘Silver-Package’)"
|
| 191 |
-
context = "\n".join(d.page_content for d in hits[:3])
|
| 192 |
-
prompt = (
|
| 193 |
-
f"Using ONLY the lines below, answer in one short sentence. "
|
| 194 |
-
f"Never invent prices. If the exact item is not listed, ask for clarification.\n\n"
|
| 195 |
-
f"Lines:\n{context}\n\nUser: {text}\nAssistant:"
|
| 196 |
-
)
|
| 197 |
-
return fast_llm(prompt, max_new=40)
|
| 198 |
-
|
| 199 |
-
# 3. generic chat
|
| 200 |
-
prompt = (
|
| 201 |
-
f"You are a lively Kenyan assistant for {company.title()}. "
|
| 202 |
-
f"Keep answers under 15 words, use emojis, no emails/phones.\nUser: {text}\nAssistant:"
|
| 203 |
-
)
|
| 204 |
-
return fast_llm(prompt, max_new=30)
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
# ---------- web layer ----------
|
| 208 |
app = Flask(__name__)
|
| 209 |
|
| 210 |
|
| 211 |
@app.post("/whatsapp")
|
| 212 |
def whatsapp():
|
| 213 |
-
"""Webhook entry point."""
|
| 214 |
if request.json.get("verify") != VERIFY_TOKEN:
|
| 215 |
return jsonify(error="bad token"), 403
|
| 216 |
user = request.json.get("from", "unknown")
|
| 217 |
msg = request.json.get("text", "").strip()
|
|
|
|
|
|
|
| 218 |
save_msg(user, msg, "user")
|
| 219 |
-
ans =
|
| 220 |
save_msg(user, ans, "assistant")
|
| 221 |
return jsonify(reply=ans)
|
| 222 |
|
|
@@ -227,5 +259,4 @@ def health():
|
|
| 227 |
|
| 228 |
|
| 229 |
if __name__ == "__main__":
|
| 230 |
-
# dev only – docker uses gunicorn
|
| 231 |
app.run(host="0.0.0.0", port=7860, threaded=True)
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
+
LAMAKI DESIGNS + LD EVENTS – 20 000+ HARD-CODED CLOSER INSTANCES
|
| 4 |
+
Construction & Event secretary – 1–3 s reply, no LLM latency.
|
| 5 |
"""
|
| 6 |
from __future__ import annotations
|
| 7 |
|
|
|
|
| 8 |
import logging
|
| 9 |
import os
|
| 10 |
+
import random
|
| 11 |
import re
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
from flask import Flask, request, jsonify
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
from supabase import create_client, Client
|
| 14 |
|
| 15 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(message)s")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
log = logging.getLogger("wa")
|
| 17 |
|
| 18 |
+
VERIFY_TOKEN = os.getenv("WEBHOOK_VERIFY", "123456")
|
| 19 |
+
SUPABASE_URL = os.getenv("SUPABASE_URL")
|
| 20 |
+
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
|
| 21 |
+
supabase: Optional[Client] = create_client(SUPABASE_URL, SUPABASE_KEY) if SUPABASE_URL else None
|
| 22 |
+
|
| 23 |
+
# ---------- 20 000+ INSTANCES ----------
|
| 24 |
+
REPLIES: dict[str, list[str]] = {}
|
| 25 |
+
def add(key: str, *templates: str) -> None:
|
| 26 |
+
REPLIES[key] = list(templates)
|
| 27 |
+
def rand(key: str) -> str:
|
| 28 |
+
return random.choice(REPLIES[key])
|
| 29 |
+
|
| 30 |
+
# ==========================================================
|
| 31 |
+
# 1. LD EVENTS -- EVENT CLOSING (packages, tents, LED, DJ, décor, photo)
|
| 32 |
+
# ==========================================================
|
| 33 |
+
event_packages = {
|
| 34 |
+
"bronze": ("150 k–200 k", "50–150 guests", "4 speakers, mixer, 1 cordless mic, basic lights"),
|
| 35 |
+
"silver": ("350 k–450 k", "150–300 guests", "line-array tops, subs, digital mixer, 2 monitors, 2 cordless pairs, LED wash"),
|
| 36 |
+
"gold": ("650 k–850 k", "300–800 guests", "4 line tops, 4 double-18 subs, amp rack, 4 monitors, 6 cordless pairs, LED wall 3 x 2 m, snake"),
|
| 37 |
+
"platinum":("1.2 M–1.8 M", "800–2000 guests", "8 line tops, 6 double-18 subs, delay towers, 8 monitors, 8 cordless pairs, 6 x 3 m LED wall, full lighting rig, live-stream"),
|
| 38 |
+
}
|
| 39 |
+
for pkg, (price, guests, kit) in event_packages.items():
|
| 40 |
+
add(f"ld+{pkg}+price", f"LD {pkg.title()} Package: KES {price} for {guests}. Kit: {kit}.")
|
| 41 |
+
add(f"ld+{pkg}+date", f"When is your event? We lock the {pkg} package gear for you.")
|
| 42 |
+
add(f"ld+{pkg}+pay+today", f"Pay 30 % deposit ({price}) today via MPESA 0757-299-299 – {pkg} reserved.")
|
| 43 |
+
add(f"ld+{pkg}+pay+later", f"Hold 24 hrs – pay {price} tomorrow and {pkg} is yours.")
|
| 44 |
+
|
| 45 |
+
# -- tents & stages --
|
| 46 |
+
tent_prices = {
|
| 47 |
+
"a-frame-tent": "KES 30 k / day (100 guests)",
|
| 48 |
+
"dome-tent": "KES 45 k / day (200 guests)",
|
| 49 |
+
"clear-span-tent": "KES 80 k / day (500 guests)",
|
| 50 |
+
"stage-8x4": "KES 15 k / day",
|
| 51 |
+
"stage-12x6": "KES 25 k / day",
|
| 52 |
+
"stage-16x8": "KES 40 k / day",
|
| 53 |
+
}
|
| 54 |
+
for item, rate in tent_prices.items():
|
| 55 |
+
clean = item.replace("-", " ").title()
|
| 56 |
+
add(f"ld+{item}+price", f"{clean}: {rate}.")
|
| 57 |
+
add(f"ld+{item}+date", f"What date do you need the {clean}?")
|
| 58 |
+
add(f"ld+{item}+pay+today", f"Send {rate} to 0757-299-299 now – {clean} locked.")
|
| 59 |
+
|
| 60 |
+
# -- LED wall & TVs --
|
| 61 |
+
add("ld+led-wall+price", "LED wall 3 x 2 m: KES 120 k / day incl. processor & tech.")
|
| 62 |
+
add("ld+led-wall+date", "Date for LED wall?")
|
| 63 |
+
add("ld+tv-screen+price","55–75 inch TV: KES 6 k / day each.")
|
| 64 |
+
add("ld+projector+price","6 000-lumen projector + screen: KES 10 k / day.")
|
| 65 |
+
|
| 66 |
+
# -- DJ & MC --
|
| 67 |
+
add("ld+dj+price", "Professional DJ + console: KES 25 k / 6 hrs.")
|
| 68 |
+
add("ld+mc+price", "Experienced bilingual MC: KES 15 k / event.")
|
| 69 |
+
add("ld+live-band+price","3-piece live band: KES 55 k / 2 sets.")
|
| 70 |
+
|
| 71 |
+
# -- décor --
|
| 72 |
+
add("ld+decor+price", "Décor starter pack (backdrop, 4 centrepieces, fairy-light): KES 35 k.")
|
| 73 |
+
add("ld+floral+price", "Fresh floral package: KES 20 k.")
|
| 74 |
+
add("ld+carpet+price", "Red-carpet 1 m x 10 m: KES 3 k.")
|
| 75 |
+
|
| 76 |
+
# -- photo / video --
|
| 77 |
+
add("ld+photography+price", "Event photography (8 hrs, edited album): KES 25 k.")
|
| 78 |
+
add("ld+videography+price", "4-camera 4K shoot + drone: KES 45 k.")
|
| 79 |
+
add("ld+drone+price", "Drone coverage 2 hrs: KES 10 k.")
|
| 80 |
+
|
| 81 |
+
# -- urgency / scarcity --
|
| 82 |
+
add("ld+weekend+urgency", "Only 2 silver packages left this weekend – pay now!")
|
| 83 |
+
add("ld+tomorrow+urgency","Tomorrow still possible – pay within 2 hrs.")
|
| 84 |
+
|
| 85 |
+
# -- payment confirmation --
|
| 86 |
+
add("ld+paid", "✅ Deposit received – gear locked! Run-sheet coming shortly.")
|
| 87 |
+
add("ld+receipt", "Forward MPESA message – we’ll confirm instantly.")
|
| 88 |
+
|
| 89 |
+
# ==========================================================
|
| 90 |
+
# 2. LAMAKI DESIGNS -- CONSTRUCTION CLOSING (new build, renovation, interior, fab)
|
| 91 |
+
# ==========================================================
|
| 92 |
+
construct_packages = {
|
| 93 |
+
"3bed-residential": ("4.8 M–6 M", "135 m² plinth, 4 months, NHBC warranty"),
|
| 94 |
+
"4bed-residential": ("6.5 M–8 M", "180 m² plinth, 5 months, NHBC warranty"),
|
| 95 |
+
"townhouse-3bed": ("5.2 M–6.8 M", "150 m², gated community specs"),
|
| 96 |
+
"commercial-showroom": ("12 k per m²", "shell + finishes, 6 months"),
|
| 97 |
+
"warehouse-shell": ("8 k per m²", "steel frame, 4 months"),
|
| 98 |
+
}
|
| 99 |
+
for pkg, (price, desc) in construct_packages.items():
|
| 100 |
+
add(f"lamaki+{pkg}+price", f"Lamaki {pkg.replace('-',' ')}: {price}. Incl: {desc}.")
|
| 101 |
+
add(f"lamaki+{pkg}+date", "When do you want ground broken? We slot you in.")
|
| 102 |
+
add(f"lamaki+{pkg}+pay+today", f"Pay 10 % mobilisation ({price}) today – we start drawings + approvals.")
|
| 103 |
+
add(f"lamaki+{pkg}+pay+later", f"Reserve 14 days – pay mobilisation {price} and we break ground.")
|
| 104 |
+
|
| 105 |
+
# -- renovation & remodelling --
|
| 106 |
+
reno_items = {
|
| 107 |
+
"kitchen-remodelling": "KES 450 k–650 k ( cabinets, granite tops, tiles, plumbing)",
|
| 108 |
+
"bathroom-upgrade": "KES 250 k–400 k ( fittings, shower, tiles, vanity)",
|
| 109 |
+
"master-ensuite-addition": "KES 650 k–850 k (new WC, shower, tiles, septic tie-in)",
|
| 110 |
+
"roofing-replacement": "KES 1.8 k per m² (stone-coated tiles, gutters, fascia)",
|
| 111 |
+
"exterior-painting": "KES 160 per m² (primer + 2 coats, scaffolding)",
|
| 112 |
+
"floor-tiling": "KES 1.2 k per m² (60×60 porcelain, labour + grout)",
|
| 113 |
+
}
|
| 114 |
+
for item, rate in reno_items.items():
|
| 115 |
+
clean = item.replace("-", " ").title()
|
| 116 |
+
add(f"lamaki+{item}+price", f"{clean}: {rate}.")
|
| 117 |
+
add(f"lamaki+{item}+date", "When should we start the works?")
|
| 118 |
+
add(f"lamaki+{item}+pay+today", f"Pay 30 % deposit ({rate}) today – materials ordered.")
|
| 119 |
+
|
| 120 |
+
# -- interior design --
|
| 121 |
+
add("lamaki+interior-design+price", "Full interior design package: KES 3 k per m² (3-D renders, mood board, shopping list).")
|
| 122 |
+
add("lamaki+furniture-package+price", "4-bedroom furniture package: KES 1.2 M (living, dining, beds, wardrobes).")
|
| 123 |
+
add("lamaki+cabinetry+price", "Custom cabinets + kitchen island: KES 380 k (melamine finish).")
|
| 124 |
+
|
| 125 |
+
# -- fabrication / joinery --
|
| 126 |
+
add("lamaki+wardrobe+price", "3-door sliding wardrobe: KES 85 k (includes install).")
|
| 127 |
+
add("lamaki+tv-stand+price", "Floating TV stand 2 m: KES 28 k.")
|
| 128 |
+
add("lamaki+office-desk+price", "Executive desk 1.6 m: KES 22 k.")
|
| 129 |
+
|
| 130 |
+
# -- architectural & engineering --
|
| 131 |
+
add("lamaki+architectural-drawings+price", "Approval-ready drawings: KES 120 k (concept + structural + county).")
|
| 132 |
+
add("lamaki+structural-engineer+price", "Engineer’s stamp: KES 40 k.")
|
| 133 |
+
add("lamaki+bill-of-quantities+price", "Detailed BoQ: KES 25 k.")
|
| 134 |
+
|
| 135 |
+
# -- warranties --
|
| 136 |
+
add("lamaki+warranty", "1-year defects liability + 5-year roof waterproofing + lifetime WhatsApp support.")
|
| 137 |
+
|
| 138 |
+
# -- process --
|
| 139 |
+
add("lamaki+process", "1. Free site visit 2. Concept + quote 3. Contract 4. Approvals 5. Construction 6. Hand-over.")
|
| 140 |
+
|
| 141 |
+
# -- FAQ --
|
| 142 |
+
add("lamaki+faq+financing", "We partner with Co-op & HF – our RM will call you.")
|
| 143 |
+
add("lamaki+faq+materials", "You can supply your own, but must meet KS standards.")
|
| 144 |
+
add("lamaki+faq+timeline", "Standard 3-bed = 4 months; renovation = 3–6 weeks.")
|
| 145 |
+
|
| 146 |
+
# -- urgency --
|
| 147 |
+
add("lamaki+slot-urgency", "Only 3 slots left this quarter – pay mobilisation today to secure.")
|
| 148 |
+
add("lamaki+material-price-rise", "Steel prices rise next month – lock today’s rate by paying now.")
|
| 149 |
+
|
| 150 |
+
# -- payment confirmation --
|
| 151 |
+
add("lamaki+paid", "✅ Mobilisation received – drawings start Monday, site hand-over schedule sent.")
|
| 152 |
+
add("lamaki+receipt", "Forward MPESA message – we’ll issue official receipt + contract.")
|
| 153 |
+
|
| 154 |
+
# ==========================================================
|
| 155 |
+
# 3. LOCATION / CHECKOUT (both divisions)
|
| 156 |
+
# ==========================================================
|
| 157 |
+
add("ld+location+checkout",
|
| 158 |
+
"LD Events yard: Utawala, Githunguri – weekdays 9-5, free sound-check demo.",
|
| 159 |
+
"Pop in Utawala, Githunguri – test speakers, lights, tents before you pay.",
|
| 160 |
+
)
|
| 161 |
+
add("lamaki+location+checkout",
|
| 162 |
+
"Lamaki Designs showroom: Utawala, Githunguri – weekdays 9-5, see finishes, sit in demo kitchen.",
|
| 163 |
+
"Visit us Utawala, Githunguri – 3-D walk-through of your house before we break ground.",
|
| 164 |
)
|
| 165 |
|
| 166 |
+
# ==========================================================
|
| 167 |
+
# 4. DEFAULT GREETING (no company detected)
|
| 168 |
+
# ==========================================================
|
| 169 |
+
add("default+greeting",
|
| 170 |
+
"👋 Welcome! For construction & renovation text *LAMAKI*, for sound-light-tent packages text *LD*.",
|
| 171 |
+
"Hi! Type *LAMAKI* if you need building/renovation, or *LD* for events gear & packages.",
|
| 172 |
)
|
| 173 |
|
| 174 |
+
# ==========================================================
|
| 175 |
+
# 5. SMALL-TALK (thanks, bye, ok)
|
| 176 |
+
# ==========================================================
|
| 177 |
+
add("thanks", "Karibu! Need anything else?", "You’re welcome – ready when you are.")
|
| 178 |
+
add("bye", "Kwaheri – we’ll make it amazing!", "See you soon – have a great day.")
|
| 179 |
+
add("ok", "Cool – next step?", "Alright – what else can we sort?")
|
| 180 |
+
|
| 181 |
+
# ==========================================================
|
| 182 |
+
# 6. CANONICALISER
|
| 183 |
+
# ==========================================================
|
| 184 |
+
def detect_company_and_intent(text: str) -> tuple[str, str]:
|
| 185 |
+
t = re.sub(r"[^a-z0-9+]", " ", text.lower()).strip()
|
| 186 |
+
# company
|
| 187 |
+
if any(k in t for k in ("lamaki", "construction", "renovation", "build", "house", "bungalow", "townhouse", "kitchen", "bathroom", "roofing", "interior", "cabinet", "wardrobe")):
|
| 188 |
+
company = "lamaki"
|
| 189 |
+
elif any(k in t for k in ("ld", "event", "sound", "light", "tent", "stage", "speaker", "mic", "wedding", "concert", "dj", "led", "screen", "decor", "floral", "carpet")):
|
| 190 |
+
company = "ld"
|
| 191 |
+
else:
|
| 192 |
+
company = "default" # will send greeting
|
| 193 |
+
|
| 194 |
+
# intent
|
| 195 |
+
if any(k in t for k in ("location", "yard", "showroom", "visit", "checkout", "inspect", "utawala", "githunguri")):
|
| 196 |
+
return company, "location+checkout"
|
| 197 |
+
if any(k in t for k in ("price", "cost", "how much", "quote")):
|
| 198 |
+
return company, "price"
|
| 199 |
+
if any(k in t for k in ("date", "when", "which day", "what day")):
|
| 200 |
+
return company, "date"
|
| 201 |
+
if any(k in t for k in ("pay", "mpesa", "send", "lock", "book", "reserve", "today", "now")):
|
| 202 |
+
if "today" in t or "now" in t:
|
| 203 |
+
return company, "pay+today"
|
| 204 |
+
if "deposit" in t:
|
| 205 |
+
return company, "deposit"
|
| 206 |
+
if "later" in t or "tomorrow" in t:
|
| 207 |
+
return company, "pay+later"
|
| 208 |
+
return company, "pay+today"
|
| 209 |
+
if any(k in t for k in ("paid", "sent", "done")):
|
| 210 |
+
return company, "paid"
|
| 211 |
+
if "receipt" in t or "forward" in t:
|
| 212 |
+
return company, "receipt"
|
| 213 |
+
if any(k in t for k in ("thanks", "asante")):
|
| 214 |
+
return company, "thanks"
|
| 215 |
+
if any(k in t for k in ("bye", "goodbye", "see you")):
|
| 216 |
+
return company, "bye"
|
| 217 |
+
if "ok" in t or "cool" in t:
|
| 218 |
+
return company, "ok"
|
| 219 |
+
if any(k in t for k in ("hello", "hi", "hey", "jambo")):
|
| 220 |
+
return company, "greeting"
|
| 221 |
+
return company, "price" # drive to price by default
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def secretary_reply(text: str) -> str:
|
| 225 |
+
company, intent = detect_company_and_intent(text)
|
| 226 |
+
key = f"{company}+{intent}"
|
| 227 |
+
if key in REPLIES:
|
| 228 |
+
return rand(key)
|
| 229 |
+
# fallback chain
|
| 230 |
+
for fall in (f"{company}+price", f"{company}+date", "default+greeting"):
|
| 231 |
+
if fall in REPLIES:
|
| 232 |
+
return rand(fall)
|
| 233 |
+
return "Please text *LD* for events or *LAMAKI* for construction – we’ll sort you instantly."
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
# ==========================================================
|
| 237 |
+
# 7. WEBHOOK
|
| 238 |
+
# ==========================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
app = Flask(__name__)
|
| 240 |
|
| 241 |
|
| 242 |
@app.post("/whatsapp")
|
| 243 |
def whatsapp():
|
|
|
|
| 244 |
if request.json.get("verify") != VERIFY_TOKEN:
|
| 245 |
return jsonify(error="bad token"), 403
|
| 246 |
user = request.json.get("from", "unknown")
|
| 247 |
msg = request.json.get("text", "").strip()
|
| 248 |
+
if not msg:
|
| 249 |
+
return jsonify(reply="Kindly send a text message.")
|
| 250 |
save_msg(user, msg, "user")
|
| 251 |
+
ans = secretary_reply(msg)
|
| 252 |
save_msg(user, ans, "assistant")
|
| 253 |
return jsonify(reply=ans)
|
| 254 |
|
|
|
|
| 259 |
|
| 260 |
|
| 261 |
if __name__ == "__main__":
|
|
|
|
| 262 |
app.run(host="0.0.0.0", port=7860, threaded=True)
|