Spaces:
Sleeping
Sleeping
File size: 18,507 Bytes
b905edf cf4aad5 537a3fc cf4aad5 537a3fc 1109321 b905edf 537a3fc cf4aad5 b905edf 297c727 a7825fa 1109321 cf4aad5 a7825fa cf4aad5 a6ff47d cf4aad5 1109321 cf4aad5 1109321 cf4aad5 13c99d8 913b3c4 cf4aad5 13c99d8 1109321 cf4aad5 d289770 cf4aad5 f393632 cf4aad5 f393632 cf4aad5 f393632 cf4aad5 f393632 cf4aad5 b3fba9f 13c99d8 cf4aad5 1109321 913b3c4 c2e6f03 cf4aad5 c2e6f03 913b3c4 cf4aad5 913b3c4 cf4aad5 a76c198 cf4aad5 913b3c4 13c99d8 cf4aad5 13c99d8 b3fba9f cf4aad5 13c99d8 cf4aad5 13c99d8 cf4aad5 13c99d8 cf4aad5 13c99d8 cf4aad5 13c99d8 cf4aad5 13c99d8 cf4aad5 13c99d8 cf4aad5 b3fba9f f393632 b3fba9f cf4aad5 b3fba9f cf4aad5 b3fba9f cf4aad5 b3fba9f cf4aad5 b3fba9f cf4aad5 13c99d8 cf4aad5 13c99d8 cf4aad5 913b3c4 cf4aad5 b3fba9f cf4aad5 297c727 b905edf cf4aad5 b905edf 1109321 cf4aad5 297c727 cf4aad5 1109321 cf4aad5 b905edf cf4aad5 1109321 cf4aad5 b905edf cf4aad5 d7253e7 c2e6f03 913b3c4 ed47d18 d289770 3b47ed4 f393632 a76c198 a995464 282f330 | 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 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 | #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
LD Events + Lamaki Designs — Intelligent Secretary Bot
Fully merged catalogue + About Us + booking flow + in-memory persistence.
Parts: 1/4
"""
from __future__ import annotations
import os
import re
import uuid
import datetime
import logging
from typing import Dict, Optional, List, Any
from flask import Flask, request, jsonify
# ---------- CONFIG ----------
VERIFY_TOKEN = os.getenv("WEBHOOK_VERIFY", "ldlamaki2025")
PORT = int(os.getenv("PORT", 7860))
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
log = logging.getLogger("ldbot")
# ---------- IN-MEMORY STORES ----------
CHAT_HISTORY: Dict[str, List[Dict[str, str]]] = {}
DRAFT_BOOKINGS: Dict[str, Dict[str, Any]] = {}
CONFIRMED_BOOKINGS: Dict[str, Dict[str, Any]] = {}
CALL_REQUESTS: Dict[str, bool] = {}
LAST_ITEM: Dict[str, str] = {}
def save_msg(user: str, text: str, role: str = "assistant"):
"""Save last messages per user in-memory for lightweight conversation continuity."""
hist = CHAT_HISTORY.setdefault(user, [])
hist.append({"role": role, "text": text, "time": datetime.datetime.utcnow().isoformat()})
if len(hist) > 60:
hist.pop(0)
# ---------- Normalization & small helpers ----------
_rx_clean = re.compile(r"[^a-z0-9\s\-\:\/\+]")
def normalize(text: str) -> str:
if text is None:
return ""
t = text.lower()
return _rx_clean.sub("", t).strip()
def gen_booking_code() -> str:
return uuid.uuid4().hex[:8].upper()
def parse_int_from_text(text: str) -> Optional[int]:
if not text:
return None
m = re.search(r"(\d{1,5})", text)
if m:
try:
return int(m.group(1))
except Exception:
return None
return None
def find_date_in_text(text: str) -> Optional[str]:
if not text:
return None
# ISO-ish or common formats
m = re.search(r"(20\d{2}[-/]\d{1,2}[-/]\d{1,2})", text)
if m:
return m.group(1)
m = re.search(r"(\d{1,2}[-/]\d{1,2}[-/](?:20)?\d{2})", text)
if m:
return m.group(1)
return None
def friendly(msg: str) -> str:
return msg.strip()
# ---------- Small utility for safe dict get -->
def get_last_user_messages(user: str, n: int = 5) -> List[str]:
hist = CHAT_HISTORY.get(user, [])
return [h["text"] for h in hist[-n:]]
# PART 2/4
"""
Knowledge base: catalogue items, packages, combos, About Us, testimonials, contact info.
"""
# ---------- Knowledge base mapping ----------
REPLY: Dict[str, str] = {}
def add(k: str, v: str):
REPLY[normalize(k)] = v
# --- AUDIO / SOUND ITEMS & PRICES (merged from images + PDF) ---
add("double 18", "Double 18\" sub – KES 6,000 per unit (day).")
add("single 18", "Single 18\" sub – KES 3,000 per unit (day).")
add("line array", "Line-array top – KES 4,000 per unit (day).")
add("monitors", "Monitor speaker – KES 2,000 per unit (day).")
add("amplifier 4ch", "Amplifier 4ch – KES 4,000 per unit (day).")
add("mixer wing", "Mixer Wing + stage box – KES 20,000 (event).")
add("mixer skytone", "Mixer (Skytone) – KES 8,000 (event).")
add("snake cable", "Snake cable – KES 5,000 (event).")
add("cordless mic", "Cordless mic – KES 2,000 (per mic).")
add("corded mic", "Corded mic – KES 500 (per mic).")
add("drone 2h", "Drone coverage (2 hours) – KES 10,000. (Included in Platinum package)")
# --- LIGHTS & VISUALS ---
add("parcan lights", "Parcan lights – KES 1,500 per unit/day.")
add("moving head", "Moving head – KES 3,000 per unit/day.")
add("bee eye", "BEE eye – KES 2,000 per unit/day.")
add("wall wash", "Wall wash – KES 2,000 per unit/day.")
add("led screen panel", "LED screen panel – KES 2,200 per panel/day.")
add("tv screen", "TV screen – KES 6,000 (event).")
# --- TENTS & FURNITURE ---
add("a frame tent", "A-frame tent – KES 30,000 (fits ~100 guests).")
add("dome tent", "Dome tent – KES 45,000 (fits ~200 guests).")
add("clear span tent", "Clear-span tent – KES 80,000 (large events up to ~500 guests).")
add("chair", "Chair – KES 20 each.")
add("tables", "Table – KES 500 each.")
# --- PACKAGES (merged) ---
add("bronze package", (
"Bronze Package (KES ~150k–200k): 4 speakers with stands, mixer + amp, 1 cordless mic, basic lights, A-frame tent or simple setup — best for smaller ceremonies."
))
add("silver package", (
"Silver Package (KES ~350k–450k): 3 line-array tops, 2 single 18\" subs, digital mixer, 2 amplifiers, monitor speakers, dome tent recommended — great for medium events."
))
add("gold package", (
"Gold Package (KES ~650k–850k): 4 line-array tops, 4 double 18\" subs, LED wall or panels, amplifier rack, monitor speakers, full rig for large weddings & graduations."
))
add("platinum package", (
"Platinum Package (KES 1.2M–1.8M): Full concert rig + live streaming + drone + large clear-span tent. For big concerts & stadium events."
))
# --- COMBOS ---
add("4 tops 2 subs combo", "4 line arrays + 2 double-18 subs + mixer + monitors + mics – KES 22,000 per day.")
add("6 tops 4 subs combo", "6 line arrays + 4 double-18 subs + amp rack + monitors + mics – KES 36,000 per day.")
add("wedding combo", "Wedding combo – KES 28,000 per day (line arrays + subs + monitors + mics + DJ console).")
add("corporate combo", "Corporate combo – KES 18,000 per day (line arrays + subs + monitors + mics + projector).")
# --- SERVICES & CONTACT ---
add("contact", "Call/WhatsApp: +254 757 299 299. Office: +254 113 710584.")
add("payment mpesa", "To book, send 50% deposit via MPESA to 0757 299 299. Balance on delivery.")
# --- ABOUT US (long multi-paragraph)
ABOUT_US = (
"About Us\n"
"At LD Events Solution Company, we specialize in delivering high-quality sound & lighting solutions for events of all sizes. "
"From concerts and corporate functions to private celebrations, our experienced team is dedicated to creating unforgettable experiences "
"through cutting-edge technology, creative designs, and reliable services. We pride ourselves on professionalism, attention to detail, "
"and a passion for making every event shine—literally and figuratively.\n\n"
"At LD Sounds & Lighting Company, we use only top-quality equipment to deliver clear sound, stunning lighting, and crisp visuals. Our gear "
"is reliable, our team is experienced, and we’re committed to making every event look and sound its best. We’re results-oriented—focused on "
"delivering impactful experiences through top-quality sound, lighting, and visuals. With reliable equipment and a skilled team, we ensure every "
"event runs smoothly from setup to showtime.\n\n"
"Why Choose LD Events Solutions?\n"
"• Quality Equipment\n• Results-Oriented\n• Competitive Rental Prices\n• Professional & Experienced Staff\n• Timeliness & Reliability\n\n"
"Highlights\n"
"• Professional-grade sound & lighting rigs\n• Wide range of tents & staging\n• In-house photography & videography options\n• Fast site visits & proposals\n\n"
"Testimonials\n"
"“LD Sounds & Lighting made our wedding unforgettable. The lighting was magical, the sound quality outstanding.” — Bishop Sam\n\n"
"“Working with LD Sounds & Lighting Company was seamless from start to finish. Their team delivered high-quality sound and visuals on time.” — Mr & Mrs Hassan\n\n"
"Contact Us\n"
"Phone: 0712074366 | 0113710584\n"
"Address: Ruai, Nairobi\n"
"LD Sounds & Lighting — Your Ultimate Event Partner."
)
# add ABOUT_US to REPLY as a key
add("about us", ABOUT_US)
# ---------- ITEM ALIASES ----------
ITEM_ALIASES = {
"drone": "drone 2h",
"drone coverage": "drone 2h",
"parcan": "parcan lights",
"parcan light": "parcan lights",
"moving head": "moving head",
"line array": "line array",
"led panel": "led screen panel",
"led screen": "led screen panel",
"dj": "dj console",
"mc": "mc service",
"stage": "stage 8x4",
"a-frame tent": "a frame tent",
"dome tent": "dome tent",
"clear span tent": "clear span tent",
"tent": "a frame tent",
"photography": "photography 8h",
"videography": "videography 4k",
"single 18 sub": "single 18",
"double 18 sub": "double 18",
"monitor": "monitors",
"cordless mic": "cordless mic",
}
# PART 3/4
"""
Reply engine, booking flow, tent logic, price handling, and conversational polishing.
"""
# ---------- Quick lists for detection ----------
GREETINGS = ("hello", "hi", "hey", "good morning", "good evening", "greetings")
GOODBYES = ("bye", "goodbye", "see you", "talk later", "thanks", "thank you")
SERVICE_ASK = ("services", "what do you offer", "offer", "what can you do", "do you offer")
GEAR_WORDS = ("sound", "light", "tent", "stage", "speaker", "sub", "array", "dj", "mic", "wedding", "concert", "event")
LAMAKI_WORDS = ("lamaki", "construction", "build", "house", "renovation", "kitchen", "bathroom", "architect", "interior")
# ---------- Track user booking drafts & call requests handled in PART1 ----------
# DRAFT_BOOKINGS, CONFIRMED_BOOKINGS, CALL_REQUESTS defined in Part 1
# ---------- Booking helpers ----------
def start_booking(user: str, intent: str) -> str:
draft = DRAFT_BOOKINGS.setdefault(user, {})
draft.setdefault("intent", intent)
draft.setdefault("created_at", datetime.datetime.utcnow().isoformat())
return "Sure — I can help book that. When would you like the booking? (send a date like 2025-12-31 or '25/12/2025')"
def add_booking_detail(user: str, key: str, value: Any) -> None:
draft = DRAFT_BOOKINGS.setdefault(user, {})
draft[key] = value
draft["updated_at"] = datetime.datetime.utcnow().isoformat()
def finalize_booking(user: str) -> str:
draft = DRAFT_BOOKINGS.get(user)
if not draft:
return "I don't have any booking details yet. Tell me what you'd like to book."
# minimal required fields
required = ["date", "guests", "location"]
missing = [k for k in required if k not in draft]
if missing:
return f"I need the following to finalize your booking: {', '.join(missing)}. Please provide them."
code = gen_booking_code()
CONFIRMED_BOOKINGS[code] = draft.copy()
CONFIRMED_BOOKINGS[code]["confirmed_at"] = datetime.datetime.utcnow().isoformat()
DRAFT_BOOKINGS.pop(user, None)
return f"All set — your booking is confirmed with code {code}. We’ll call you shortly to confirm details."
# ---------- Core reply engine ----------
# ---------- Core reply engine (improved) ----------
from difflib import get_close_matches
def secretary_reply(text: str, user: str = "unknown") -> str:
raw = text or ""
t = normalize(raw)
# --- Greetings ---
if any(g in t for g in GREETINGS):
return friendly("Hello! I'm here to help — tell me what you need for your event or construction project.")
# --- Goodbyes ---
if any(b in t for b in GOODBYES):
return friendly("Thanks for reaching out — have a great day! If you need anything else, message me anytime.")
# --- Call request detection ---
if re.search(r"\b(call|contact|phone|call me|ring me)\b", t):
CALL_REQUESTS[user] = True
return friendly("Okay — I’ve requested a call. Our team will ring you shortly during business hours.")
# --- About us / company inquiries ---
if "about" in t or "about us" in t or "who are you" in t:
return friendly(REPLY.get("about us", ABOUT_US))
# --- Services inquiry ---
if any(q in t for q in SERVICE_ASK):
if any(w in t for w in LAMAKI_WORDS):
return friendly(LAMAKI_SERVICES)
return friendly(LD_EVENTS_SERVICES)
# --- Tent guest detection ---
m = re.search(r"(tent|tents?).{0,20}?(\d{1,4})", raw, flags=re.I)
if m:
guests = int(m.group(2))
if guests <= 100:
LAST_ITEM[user] = "a frame tent"
return friendly(f"For {guests} guests I recommend our A-frame tent. {REPLY.get(normalize('a frame tent'))}")
elif guests <= 200:
LAST_ITEM[user] = "dome tent"
return friendly(f"For {guests} guests I recommend our Dome tent. {REPLY.get(normalize('dome tent'))}")
elif guests <= 500:
LAST_ITEM[user] = "clear span tent"
return friendly(f"For {guests} guests I recommend our Clear-span tent. {REPLY.get(normalize('clear span tent'))}")
else:
return friendly("We provide custom tent solutions for more than 500 guests — please share the guest count and venue location.")
# --- Generic tent request ---
if "tent" in t and "book" not in t:
return friendly(
"We offer A-frame (100 guests), Dome (200 guests), and Clear-span (500 guests) tents. "
"Tell me the number of guests and I’ll pick the best option."
)
# --- Price / Cost enquiries ---
if any(w in t for w in ("price", "cost", "how much", "rates", "fee", "charge")):
last = LAST_ITEM.get(user)
if last and normalize(last) in REPLY:
return friendly(REPLY[normalize(last)])
# try to find item words in text
for key in REPLY.keys():
if key in t:
LAST_ITEM[user] = key
return friendly(REPLY[key])
return friendly("Please tell me which item or package you'd like the price for (e.g., 'gold package' or 'double 18').")
# --- Generic gear suggestions ---
if "mixer" in t:
options = ["mixer wing", "mixer skytone"]
suggestions = "\n".join(f"- {opt}: {REPLY[normalize(opt)]}" for opt in options)
return friendly(f"We have the following mixers:\n{suggestions}")
if "line array" in t or "line arrays" in t:
return friendly(REPLY.get("line array", "Line arrays are available — please specify how many or which combo you want."))
if "sub" in t or "18" in t:
subs = ["single 18", "double 18"]
suggestions = "\n".join(f"- {opt}: {REPLY[normalize(opt)]}" for opt in subs)
return friendly(f"We have the following subwoofers:\n{suggestions}")
# --- Alias matching (safe) ---
for alias, key in ITEM_ALIASES.items():
if alias in t:
LAST_ITEM[user] = key
reply_text = REPLY.get(normalize(key))
if not reply_text:
reply_text = f"I found {key}, but price/description not available."
return friendly(reply_text)
# --- Exact REPLY keys match ---
for k in list(REPLY.keys()):
if k in t:
LAST_ITEM[user] = k
base = REPLY[k]
if "package" in k or "combo" in k:
base += " If you'd like to book this, tell me the date, approximate guest count, and venue."
return friendly(base)
# --- Fuzzy matching ---
def fuzzy_lookup(text):
keys = list(REPLY.keys()) + list(ITEM_ALIASES.keys())
matches = get_close_matches(text, keys, n=1, cutoff=0.5)
if matches:
return ITEM_ALIASES.get(matches[0], matches[0])
return None
fuzzy_key = fuzzy_lookup(t)
if fuzzy_key:
LAST_ITEM[user] = fuzzy_key
reply_text = REPLY.get(normalize(fuzzy_key), f"I found {fuzzy_key}, but price/description not available.")
return friendly(reply_text)
# --- Booking flow ---
if re.search(r"\b(book|reserve|schedule|site visit|site-visit|sitevisit)\b", t):
if "site" in t:
return friendly(start_booking(user, "site_visit"))
if "consult" in t:
return friendly(start_booking(user, "consultation"))
return friendly(start_booking(user, "event_booking"))
dt = find_date_in_text(raw)
if dt:
add_booking_detail(user, "date", dt)
return friendly(f"Got the date: {dt}. Where will the event be (venue)?")
guests_m = re.search(r"(\d{2,4})\s*(guests|people|pax|attendees)", t)
if guests_m:
g = int(guests_m.group(1))
add_booking_detail(user, "guests", g)
return friendly(f"Great — noted {g} guests. What’s the venue/location?")
loc_m = re.search(r"\b(at|venue|in)\s+([a-z0-9\s]+(?:hall|center|grounds|arena|stadium|venue)?)", raw, flags=re.I)
if loc_m:
loc = loc_m.group(2).strip()
add_booking_detail(user, "location", loc)
return friendly(f"Noted venue: {loc}. Any special requests or additional gear?")
if any(pat in t for pat in ("confirm booking", "finalize booking", "complete booking", "confirm reservation", "confirm my booking")):
res = finalize_booking(user)
return friendly(res)
mcode = re.search(r"\b([A-F0-9]{8})\b", raw, flags=re.I)
if mcode:
code = mcode.group(1).upper()
if code in CONFIRMED_BOOKINGS:
info = CONFIRMED_BOOKINGS[code]
return friendly(f"Booking {code} was created on {info.get('confirmed_at')}. Details: {info}")
return friendly("Sorry, I can't find that booking code. Please check and send it again.")
if any(w in t for w in LAMAKI_WORDS):
return friendly(LAMAKI_SERVICES)
if len(t.split()) <= 2:
return friendly("Could you provide a few more details? For example: 'price for silver package' or 'book site visit 2025-12-05 200 guests at ABC Hall'.")
return friendly("I’m here to help — tell me the item, package, or service you want and I’ll assist from there.")
"""
Flask endpoints, health check, and run instructions.
"""
app = Flask(__name__)
@app.post("/whatsapp")
def whatsapp():
j = request.json or {}
if j.get("verify") != VERIFY_TOKEN:
return jsonify(error="bad token"), 403
user = j.get("from", "unknown")
msg = (j.get("text") or "").strip()
if not msg:
return jsonify(reply="Please send a short message describing what you need (e.g., 'silver package price' or 'book site visit on 2025-12-10 for 200 people').")
# save incoming
save_msg(user, msg, "user")
try:
ans = secretary_reply(msg, user)
except Exception as e:
log.exception("Reply generation failed: %s", e)
ans = "Sorry — something went wrong while I prepared the response. Try again or call +254 757 299 299."
save_msg(user, ans, "assistant")
return jsonify(reply=ans)
@app.get("/")
def health():
return "ok\n"
if __name__ == "__main__":
log.info("Starting LD Secretary Bot on port %s", PORT)
app.run(host="0.0.0.0", port=PORT, threaded=True)
|