// ========================================== // 1. CONFIGURATION & CREDENTIALS // ========================================== const WHATSAPP_TOKEN = 'YOUR_META_TOKEN'; const WHATSAPP_PHONE_ID = '1013222271877801'; const VERIFY_TOKEN = 'eye_center_verify_token_123'; const GROQ_API_KEY = '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'; const PHP_DASHBOARD_URL = 'https://yourdomain.com/api/whatsapp_lead.php'; const PHP_API_SECRET = 'wa_api_key_eyecenter_2026_123456789'; // ========================================== // 2. SYSTEM PROMPT (The Intelligent Brain) // ========================================== const SYSTEM_PROMPT = `# Role & Persona You are Sarah, the virtual WhatsApp receptionist for "The Eye Center" in Clifton, Karachi. Your tone is warm, professional, and concise. Mirror the user's language (English or Roman Urdu). # DOCTOR SCHEDULES & KNOWLEDGE BASE Use this data to answer inquiries if a patient explicitly asks "What time does Doctor X sit?". - Dr. Mahnaz Naveed Shah: Tue, Wed, Fri (09:00 AM to 12:00 PM) - Dr. Ali Zia: Mon, Wed, Thu, Sat (10:00 AM to 12:00 PM) | Tue, Fri (03:30 PM to 05:00 PM) - Dr. Shahyan Shadmani: Mon (12:00 PM to 02:00 PM) - Dr. Syeda Aisha Bokhari: Mon, Tue, Wed, Thu, Fri (01:30 PM to 04:00 PM) - Dr. M. Saeed Iqbal: Tue, Thu (05:00 PM to 06:30 PM) | Sat (01:00 PM to 03:00 PM) - Dr. Shahab Ul Hasan Siddique: Mon, Wed, Sat (03:30 PM to 05:30 PM) - Dr. Uzma Naz: Tue, Fri (09:00 AM to 05:00 PM) | Thu (02:00 PM to 04:00 PM) | Sat (09:00 PM to 12:00 PM) - Dr. Saliha Naz: Tue, Thu (04:30 PM to 06:00 PM) - Dr. M. Omer Hassan: Tue, Thu, Sat (02:30 PM to 04:30 PM) - Dr. Zeeshan Kamil: Thu (05:00 PM to 06:30 PM) | Sat (06:00 PM to 07:30 PM) - Dr. Hina Nawaz: Mon, Wed, Fri (03:30 PM to 05:30 PM) Hours: Mon-Sat, 9:00 AM to 6:00 PM. Sunday is CLOSED. Address: First Floor, GPC 13 Rojhan Street, Block 5, Clifton, Karachi. # INTERACTIVE UI RULES (STRICT) To show options, you MUST use one of these two formats at the very end of your message: 1. For 1 to 3 short options: Use [BUTTONS: Option 1 | Option 2 | Option 3] 2. For 4 to 10 options: Use [LIST: Menu Title | Item 1 | Item 2 | Item 3 | Item 4] Never exceed 20 characters for a button/list item. # SCENARIO 1: BOOK TENTATIVE APPOINTMENT Guide them step-by-step. Ask ONE question at a time. No buttons for data entry. 1. "Please type your First and Last name." 2. "Please type your cell phone number." 3. "Which doctor would you like to see? [LIST: View Doctors | Dr. Mahnaz | Dr. Ali Zia | Not Sure]" (If 'Not Sure', ask their symptoms). 4. SUGGEST DAYS (CRITICAL RULE): Look at the chosen doctor's schedule. Calculate the NEXT available sitting day based on the current date provided by the system. Say: "Dr. [Name] is available on [Valid Days]. Would you like to request [Next Available Day] or another valid day?" --> STRICT FORBIDDEN ACTION: DO NOT mention or offer specific times during this step. Only offer the DAY. 5. SHIFT PREFERENCE: Once a valid day is chosen, DO NOT ASK FOR A SPECIFIC TIME. Ask: "Do you prefer a Morning or Evening slot on that day? Our staff will call you to confirm the exact time." Once all details are collected, read them back cleanly: "Please confirm your tentative booking request: 👤 Name: [Name] 📱 Phone: [Phone] 👨‍⚕️ Doctor: [Doctor] 📅 Requested Day & Shift: [Day, Morning/Evening] [BUTTONS: Yes Confirm | Edit Details]" If they click 'Yes Confirm', trigger the 'submit_patient_lead' tool. # SCENARIO 2: TALK TO HUMAN If asked for a real person, reply: "I have notified our team. A representative will reply to you here shortly. 😊" (Stop asking questions). # STRICT RULES - NEVER confirm an exact time (e.g., 3:00 PM) during booking. Only record Morning/Evening preferences. - NEVER give medical advice. - KEEP MESSAGES SHORT.`; // ========================================== // 3. WEBHOOK VERIFICATION (GET) // ========================================== function doGet(e) { if (e.parameter['hub.mode'] === 'subscribe' && e.parameter['hub.verify_token'] === VERIFY_TOKEN) { return ContentService.createTextOutput(e.parameter['hub.challenge']); } return ContentService.createTextOutput("Verification failed"); } // ========================================== // 4. MAIN PROCESSOR (POST) // ========================================== function doPost(e) { try { const data = JSON.parse(e.postData.contents); if (!data.entry || !data.entry[0].changes[0].value.messages) return ContentService.createTextOutput("OK"); const msgData = data.entry[0].changes[0].value.messages[0]; const senderPhone = msgData.from; const messageId = msgData.id; // DEDUPLICATION: Prevent Meta Timeout Death Loops const cache = CacheService.getScriptCache(); if (cache.get(messageId)) return ContentService.createTextOutput("OK - Duplicate"); cache.put(messageId, 'true', 300); // Lock for 5 mins let userMessage = ""; // -- MESSAGE PARSING & MEDIA HANDLING -- if (msgData.type === 'text') { userMessage = msgData.text.body; } else if (msgData.type === 'interactive') { userMessage = msgData.interactive.type === 'button_reply' ? msgData.interactive.button_reply.title : msgData.interactive.list_reply.title; } else if (msgData.type === 'audio') { userMessage = transcribeAudio(msgData.audio.id); } else if (msgData.type === 'image' || msgData.type === 'document') { userMessage = "[SYSTEM NOTE: The patient sent an image/document. Tell them you cannot view images yet and ask them to type their issue or send a voice note.]"; } else { return ContentService.createTextOutput("OK"); } // -- RESET SWITCH -- if (userMessage.toLowerCase().trim() === 'reset' || userMessage.toLowerCase().trim() === 'restart') { cache.remove(senderPhone + '_history'); sendWhatsAppMessage(senderPhone, "Welcome to The Eye Center! How can I help you? [BUTTONS: Book Appointment | General Inquiry | Talk to Human]"); return ContentService.createTextOutput("SUCCESS"); } // -- CHAT HISTORY & GROQ LLM -- let history = getChatHistory(senderPhone); history.push({ "role": "user", "content": userMessage }); let groqResponse = callGroq(history); let replyText = ""; // -- TOOL EXECUTION (DASHBOARD ROUTING) -- if (groqResponse.tool_calls) { const toolCall = groqResponse.tool_calls[0]; const args = JSON.parse(toolCall.function.arguments); // Send to Dashboard sendToDashboard(args, history); history.push(groqResponse); history.push({ "role": "tool", "tool_call_id": toolCall.id, "content": "Dashboard updated successfully." }); replyText = callGroq(history).content; } else { replyText = groqResponse.content; } // Update Memory & Reply history.push({ "role": "assistant", "content": replyText }); saveChatHistory(senderPhone, history); sendWhatsAppMessage(senderPhone, replyText); return ContentService.createTextOutput("SUCCESS"); } catch (error) { console.error(error); return ContentService.createTextOutput("ERROR"); } } // ========================================== // 5. WHATSAPP UI GENERATOR (BUTTONS VS LISTS) // ========================================== function sendWhatsAppMessage(to, text) { const url = `https://graph.facebook.com/v19.0/${WHATSAPP_PHONE_ID}/messages`; let payload = { "messaging_product": "whatsapp", "to": to }; const buttonMatch = text.match(/\[BUTTONS:\s*(.+?)\]/); const listMatch = text.match(/\[LIST:\s*(.+?)\]/); if (listMatch) { // GENERATE LIST (For 4-10 options) const items = listMatch[1].split('|').map(b => b.trim()); const menuTitle = items.shift().substring(0, 20); const cleanText = text.replace(listMatch[0], '').trim(); const rows = items.slice(0, 10).map((label, index) => ({ "id": `list_${index}`, "title": label.substring(0, 24) })); payload.type = "interactive"; payload.interactive = { "type": "list", "body": { "text": cleanText || "Please select an option:" }, "action": { "button": menuTitle, "sections": [{ "title": "Available Options", "rows": rows }] } }; } else if (buttonMatch) { // GENERATE BUTTONS (Max 3) const labels = buttonMatch[1].split('|').map(b => b.trim()).slice(0, 3); const cleanText = text.replace(buttonMatch[0], '').trim(); const buttons = labels.map((label, index) => ({ "type": "reply", "reply": { "id": `btn_${index}`, "title": label.substring(0, 20) } })); payload.type = "interactive"; payload.interactive = { "type": "button", "body": { "text": cleanText || "Choose an option:" }, "action": { "buttons": buttons } }; } else { // NORMAL TEXT payload.type = "text"; payload.text = { "body": text }; } UrlFetchApp.fetch(url, { "method": "post", "headers": { "Authorization": "Bearer " + WHATSAPP_TOKEN, "Content-Type": "application/json" }, "payload": JSON.stringify(payload), "muteHttpExceptions": true }); } // ========================================== // 6. AUDIO PARSING & LLM FUNCTIONS // ========================================== function transcribeAudio(mediaId) { const mediaMetaUrl = `https://graph.facebook.com/v19.0/${mediaId}`; const metaRes = UrlFetchApp.fetch(mediaMetaUrl, { headers: { "Authorization": "Bearer " + WHATSAPP_TOKEN }}); const mediaUrl = JSON.parse(metaRes.getContentText()).url; const audioBlob = UrlFetchApp.fetch(mediaUrl, { headers: { "Authorization": "Bearer " + WHATSAPP_TOKEN }}).getBlob(); const formData = { "file": audioBlob, "model": "whisper-large-v3" }; const options = { "method": "post", "headers": { "Authorization": "Bearer " + GROQ_API_KEY }, "payload": formData }; const whisperRes = UrlFetchApp.fetch("https://api.groq.com/openai/v1/audio/transcriptions", options); return JSON.parse(whisperRes.getContentText()).text; } function callGroq(messages) { // Inject real-time Karachi date awareness into the System Prompt const today = new Date().toLocaleString("en-US", { timeZone: "Asia/Karachi", weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); if (messages[0].role === "system") { // Ensure we don't append the date multiple times if the prompt is re-used let basePrompt = messages[0].content.split("# CURRENT SYSTEM CONTEXT")[0].trim(); messages[0].content = basePrompt + `\n\n# CURRENT SYSTEM CONTEXT\nToday's Date in Karachi is: ${today}. Always use this to calculate "tomorrow", "next week", etc.`; } const payload = { "model": "llama-3.3-70b-versatile", "temperature": 0.2, "messages": messages, "tools": [{ "type": "function", "function": { "name": "submit_patient_lead", "description": "Call this ONLY when the user clicks 'Yes Confirm' to finalize their booking request.", "parameters": { "type": "object", "properties": { "first_name": { "type": "string" }, "last_name": { "type": "string" }, "phone_number": { "type": "string" }, "doctor": { "type": "string" }, "requested_day_and_shift": { "type": "string", "description": "The confirmed day and shift (e.g., Monday Morning)" } }, "required": ["first_name", "last_name", "phone_number", "doctor", "requested_day_and_shift"] } } }] }; const options = { "method": "post", "headers": { "Authorization": "Bearer " + GROQ_API_KEY, "Content-Type": "application/json" }, "payload": JSON.stringify(payload) }; const response = UrlFetchApp.fetch("https://api.groq.com/openai/v1/chat/completions", options); return JSON.parse(response.getContentText()).choices[0].message; } // ========================================== // 7. DASHBOARD ROUTING & MEMORY // ========================================== function sendToDashboard(args, chatHistory) { const payload = { source: "whatsapp", patient: { first_name: args.first_name, last_name: args.last_name, phone: args.phone_number }, booking: { doctor: args.doctor, date_time: args.requested_day_and_shift }, full_transcript: chatHistory }; UrlFetchApp.fetch(PHP_DASHBOARD_URL, { "method": "post", "headers": { "X-API-KEY": PHP_API_SECRET, "Content-Type": "application/json" }, "payload": JSON.stringify(payload), "muteHttpExceptions": true }); } function getChatHistory(phone) { const cache = CacheService.getScriptCache(); const cached = cache.get(phone + '_history'); if (cached) return JSON.parse(cached); return [{ "role": "system", "content": SYSTEM_PROMPT }]; } function saveChatHistory(phone, history) { const cache = CacheService.getScriptCache(); if (history.length > 15) history = [history[0]].concat(history.slice(-14)); cache.put(phone + '_history', JSON.stringify(history), 21600); // 6 hours }