Got you – here’s a single, consolidated **“mega URE”** prompt you can paste straight into Replit’s AI builder. Just copy **everything inside the code block**, including headings and examples 👇 --- ````text You are helping me build the “Universal Reply Engine” (URE) for our WhatsApp loyalty agent “Lia” in a project called Dealer Pulse. ==================================== PROJECT & CONTEXT ==================================== - Brand: Lincoln Audi (UK Audi main dealer). - Agent: “Lia” – an AI loyalty assistant messaging customers on WhatsApp about PCP finance renewals. - Channel: WhatsApp, via Twilio webhook → FastAPI endpoint. - Tech stack: - Backend: FastAPI (Python). - State: Supabase (PostgreSQL). - Current situation: - Outbound messages are **campaign-specific**: - 18 / 12 / 9 / 6 / 3 months to end of term. - These messages are already written and working. - We already have a simple text classifier: - `classify_inbound_tier(body_lower)` → returns: - T1_GENERAL - T2_SOFT_OBJECTION - T3_HARD_STOP - T4_SCHEDULED - T5_MONEY - T6_SUPPORT - Problem: Reply templates are: - Too long for WhatsApp. - Repeating the outbound EOT intros. - Ignoring simple questions like “when are you open?”. - Not handling small talk. - Pushing for appointments too robotically. We want to replace the current reply layer with a **Universal Reply Engine** (URE) that is: - Campaign-agnostic for replies (12/9/6/3 only matter for flavour, not structure). - Focused on moving naturally towards an **appointment**. - Able to handle: - small talk (hi/thanks), - direct questions (when are you open, how long, etc.), - money concerns, - soft objections (with a “two no” rule), - hard stops / opt-outs, - service/MOT queries. - Short, human, WhatsApp-friendly. - UK English, Audi-quality, non-cheesy. ==================================== STATE MODEL ==================================== Assume we store a per-customer state object in the database (Supabase), with at least: - `customer_id: str` - `customer_name: str | None` - `stage: str | None` # "12", "9", "6", "3" (campaign band), may be None - `soft_no_count: int` # default 0 - `appointment_status: str` # "NONE", "INVITED", "BOOKED" - `opted_out: bool` # default False - `last_message: str | None` # last inbound WhatsApp text - `last_reply_type: str | None` # optional tag for analytics ("T1", "T2_FIRST", etc.) You do NOT need to implement database code; just work with a `dict` representing this state. ==================================== TASK ==================================== Create a Python module called: `lia_ure.py` In that module, implement: ```python from typing import Tuple, Dict, Any def generate_lia_reply(customer_state: Dict[str, Any], inbound_text: str) -> Tuple[str, Dict[str, Any]]: """ Given the current customer_state and the inbound WhatsApp text, return (reply_text, updated_customer_state). - reply_text: the WhatsApp message Lia should send back (string, may be empty). - updated_customer_state: the original state with any fields updated. """ ```` I will call this function from my FastAPI webhook, then store the updated state in Supabase. ==================================== TONE & STYLE RULES (VERY IMPORTANT) =================================== Global rules for Lia’s replies: 1. UK English * Use British spelling: “favour”, “centre”, “programme”, “organise”, etc. 2. Tone * Warm, calm, professional. * Sound like a real Audi advisor: friendly, clear, confident. * No slang. Avoid being overly casual. * Emojis: allowed sparingly (e.g. 🙂, 👍, 🙌) but not in serious/money/complaint contexts. 3. Length & structure * WhatsApp-first formatting. * 2–4 short paragraphs max. * Avoid long blocks of text. * 1 main idea per message. 4. Language/phrasing * Avoid jargon like “loyalty support” in isolation. * Use phrases such as: * “extra savings reserved for you as an existing Audi customer” * “unclaimed loyalty savings attached to your profile” * Always end with **one clear, simple question**, unless: * it’s a hard stop (T3), * OR it’s the second soft objection reply (where we “park it”). 5. Objective * Natural conversation that **tends towards a showroom appointment**. * Do not push appointment on every single message. * Respect the “two soft no” rule: * After two soft objections, back off and stop chasing on this thread (but still respond if the customer re-engages). ==================================== OVERALL LOGIC ORDER =================== When `generate_lia_reply(customer_state, inbound_text)` is called: 1. Extract & normalise: * `body = inbound_text.strip()` * `body_lower = body.lower()` 2. Early exit for opt-out: * If `customer_state.get("opted_out")` is True → return `("", customer_state)` (no more replies). 3. Hard stop (T3) detection: * If message clearly means “stop / not interested / remove me”, handle T3: * Send opt-out confirmation. * Set `opted_out = True`. * Do NOT ask a follow-up question. 4. Direct question handler: * If the message is a direct practical question (e.g. opening times, “when can I come”, “how long?”): * Answer the question. * Gently invite them to choose a day. * Set `appointment_status` to "INVITED". 5. Appointment confirmation detection: * If the message appears to confirm a specific date/time (e.g. “Monday morning”, “Saturday at 10”, “tomorrow afternoon”): * Set `appointment_status` to "BOOKED". * Send a short confirmation. * No need to ask another question in that message. 6. Small talk / greeting handler: * If message is short and mostly a greeting/thanks: * Acknowledge. * Give a brief value/FOMO line. * Ask a simple question leaning towards an appointment. 7. Otherwise: * Use or stub `classify_inbound_tier(body_lower)` to get one of: * T1_GENERAL * T2_SOFT_OBJECTION * T3_HARD_STOP * T4_SCHEDULED * T5_MONEY * T6_SUPPORT * Apply tier-specific behaviour: * T1 → general interest → value + FOMO + appointment invite. * T2 → soft objection, using `soft_no_count` and two-no rule. * T3 → hard stop (as above). * T4 → scheduled later; confirm and don’t push hard. * T5 → money concerns with reassurance + appointment or call. * T6 → service/MOT support; route appropriately, lightly keep upgrade door open. 8. Always personalise: * Use `customer_name = customer_state.get("customer_name")`. * If missing, use “Hi there” instead of name. 9. Update: * `customer_state["last_message"] = inbound_text` * `customer_state["last_reply_type"] = ""` e.g. "T1", "SMALL_TALK", "DIRECT_Q", "T2_FIRST", etc. * Update: * `soft_no_count` * `appointment_status` * `opted_out` 10. Return `(reply_text, updated_state)`. ==================================== HELPER FUNCTIONS TO IMPLEMENT ============================= Implement these inside `lia_ure.py`: ```python def normalise_text(text: str) -> str: ... def is_small_talk(body_lower: str) -> bool: ... def is_direct_question(body_lower: str) -> bool: ... def detect_appointment_confirmation(body_lower: str) -> bool: ... def classify_inbound_tier(body_lower: str) -> str: """ For now you can implement a simple keyword-based stub, but write it so I can later override this with my own classifier. """ ``` ### Small talk detection Trigger if the message is short and contains any of: ```python SMALL_TALK_KEYWORDS = [ "hi lia", "hello lia", "hi, lia", "hi it's", "hi its", "morning", "good morning", "afternoon", "good afternoon", "evening", "good evening", "thanks", "thank you", "cheers", ] ``` Basic heuristic: * `len(body) < 80` and * contains at least one small talk keyword and * does not contain strong question words like “when”, “how much”, etc. **Small talk reply template:** ```text "Hi [customer_name], it’s Lia at Lincoln Audi.\n\n" "Thanks for coming back to me. Right now is a good time to review things because " "your Audi holds strong value and you’ve still got your existing-customer savings available.\n\n" "The simplest next step is a quick visit so we can show you your options clearly.\n" "What day normally works best for you to pop in?" ``` ### Direct question detection Detect if `body_lower` contains any of: ```python DIRECT_QUESTION_KEYWORDS = [ "when are you open", "what time are you open", "opening times", "opening hours", "what are your hours", "are you open today", "when can i come in", "when can i come", "when can i pop in", "what time do you close", "how long will it take", "how long does it take", ] ``` **Direct question reply template:** ```text "No problem, [customer_name]. We’re open:\n" "• Monday–Friday: 8:30am–6:00pm\n" "• Saturday: 9:00am–5:00pm\n" "• Sunday: closed\n\n" "A quick visit is usually 10–15 minutes, depending on what you’d like to do.\n\n" "What day would suit you best to pop in?" ``` After sending this reply: * set `appointment_status = "INVITED"`. ### Appointment confirmation detection Implement a simple heuristic to detect likely confirmations, such as messages containing: * Day words: “monday”, “tuesday”, “wednesday”, “thursday”, “friday”, “saturday”, “sunday”, “tomorrow”, “this weekend”. * Time indicators: “am”, “pm”. * Time patterns: `\d{1,2}:\d{2}` or `\d{1,2}am` / `\d{1,2}pm`. If `detect_appointment_confirmation(body_lower)` returns True and `appointment_status` is not "BOOKED": * Set `appointment_status = "BOOKED"`. * Send: ```text "Perfect, [customer_name] – I’ve made a note of that.\n\n" "If anything changes before then, just drop me a quick message here." ``` ==================================== TIER-SPECIFIC BEHAVIOUR & TEMPLATES =================================== Use or stub `classify_inbound_tier(body_lower)` so that it can return: * "T1_GENERAL" * "T2_SOFT_OBJECTION" * "T3_HARD_STOP" * "T4_SCHEDULED" * "T5_MONEY" * "T6_SUPPORT" Then use these templates/behaviours: --- ## T1_GENERAL – general interest Examples this should cover: * “Yes I’m interested” * “Tell me more” * “What are my options?” * General positive / neutral engagement. Reply template: ```text "Great to hear from you, [customer_name].\n\n" "You’re in a strong position at the moment – your Audi’s value is good and you’ve got " "unclaimed loyalty savings reserved as an existing Audi customer.\n\n" "The easiest way to see what this actually looks like is a quick visit to Lincoln Audi so we can " "check your figures and show you the best options while everything is still available.\n\n" "What day is usually easiest for you to pop in?" ``` Set: * `appointment_status = "INVITED"` if not already "BOOKED". --- ## T5_MONEY – money / affordability concerns Examples: * “too expensive” * “don’t want my payments going up” * “budget is tight” * “can’t afford it” Standard reply: ```text "I completely understand, [customer_name] – the monthly payment has to feel comfortable or it’s not the right move.\n\n" "What often helps is looking at your Audi’s actual value and the extra savings you get as an existing customer. " "Many people find the figures look better once we’ve checked everything properly, rather than guessing over WhatsApp.\n\n" "Could you come into the showroom one day next week so we can run through it clearly for you?" ``` If the inbound clearly mentions difficulty coming in (e.g. “can’t get there”, “live too far away”, “no time to come in”), instead use a phone fallback: ```text "I completely understand, [customer_name]. In that case, we can go through the options on a short call instead so you’ve got a clear picture.\n\n" "When is usually a good time of day for you to take a quick call?" ``` --- ## T2_SOFT_OBJECTION – “two soft no” rule Soft objections include: * “not right now” * “maybe later” * “bit busy at the moment” * “leave it for now” * “not urgent” Use `soft_no_count` from `customer_state`: * If `soft_no_count <= 0`: * Use FIRST reply. * Then set `soft_no_count = 1`. * Else (`soft_no_count >= 1`): * Use SECOND reply. * Set `soft_no_count = 2`. * Do NOT ask a further question in that message. First reply: ```text "No problem at all, [customer_name] – I understand it might not feel urgent right now.\n\n" "Just so you know, you’re in a good position at the moment because your Audi’s value is strong and you’ve still got " "your existing-customer savings available.\n\n" "Would a weekday or a Saturday generally work better for you if you did decide to pop in?" ``` Second reply (park it, no more chasing in this thread): ```text "That’s absolutely fine, [customer_name] – thanks for letting me know.\n\n" "I’ll leave things with you for now. If anything changes or you’d like to look at your options later on, " "you’re always welcome to message me here." ``` After the second reply: * Keep `soft_no_count` at 2. * Do not push another appointment unless they later send something clearly interested (which will be handled by T1 / direct question, etc.). --- ## T4_SCHEDULED – “contact me later” Examples: * “Try me next month” * “Closer to the time” * “In a few weeks” Reply: ```text "No problem, [customer_name] – I’ll make a note to get in touch nearer the time you’ve mentioned.\n\n" "If you’d like to look at anything sooner, just send me a quick message here." ``` No need to update `appointment_status` here, just confirm. --- ## T3_HARD_STOP – opt-out Examples: * “stop” * “do not contact me” * “remove my details” * “not interested, don’t contact me again” Reply: ```text "Of course, [customer_name] – I’ll update your preferences now and won’t contact you again about this.\n\n" "If you ever need anything from Lincoln Audi in the future, you can still message us here." ``` After this: * Set `opted_out = True`. Future calls should return an empty string. --- ## T6_SUPPORT – service / MOT / issue Examples: * “my car needs a service” * “MOT due” * “warning light on” * “car broken down” * “I need an update on my car in the workshop” Standard reply: ```text "Thanks for letting me know, [customer_name]. I’ll pass this straight to our service team so they can help.\n\n" "While we’re looking into it, if you’d also like to review your agreement or future options, I can help with that too." ``` If the text sounds urgent (e.g. “broken down”, “urgent”, “ASAP”, “warning light just came on”), add: ```text "If it’s urgent, you can also call us on 01522 xxxxxx and the team will pick it up straight away." ``` ==================================== MAIN FUNCTION BEHAVIOUR SUMMARY =============================== `generate_lia_reply(customer_state, inbound_text)` should: 1. Read and normalise text. 2. If `opted_out` → return empty reply. 3. If hard stop → T3 reply, set `opted_out = True`. 4. Else if direct question → DIRECT reply, set `appointment_status = "INVITED"`. 5. Else if appointment confirmation detected → APPOINTMENT_CONFIRM reply, set `"BOOKED"`. 6. Else if small talk → SMALL_TALK reply, set `"INVITED"` if not booked. 7. Else: * Classify tier (T1, T2, T4, T5, T6). * Use the relevant templates and rules above. 8. Always personalise `[customer_name]` token. 9. Update: * `last_message` * `last_reply_type` * `soft_no_count` * `appointment_status` * `opted_out` 10. Return `(reply_text, updated_state)`. ==================================== IMPLEMENTATION NOTES ==================== * Use clear constants at the top for templates (e.g. `SMALL_TALK_REPLY`, `T1_REPLY`, etc.). * Use type hints and docstrings. * At the bottom of `lia_ure.py`, add a small `if __name__ == "__main__":` block with a few hard-coded example states and inbound texts, print their outputs so I can test interactively in Replit. * Keep everything in a single module file `lia_ure.py`. This Universal Reply Engine must feel human, Audi-consistent, and always gently working towards an appointment, while respecting customer choice and the two soft “no” rule. ``` --- If you want, next step I can do is draft the **actual `lia_ure.py` code** following this spec so you can just drop it into Replit and wire it up to your webhook. ```