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)