#!/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)