ld_lamaki_bot / app.py
NimrodDev's picture
cbfgh
b3fba9f
#!/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)