const express = require("express"); const axios = require("axios"); const OpenAI = require("openai"); const dotenv = require("dotenv"); dotenv.config(); const router = express.Router(); // Environment Variables const { META_ACCESS_TOKEN, META_PHONE_NUMBER_ID, META_VERIFY_TOKEN, DEEPSEEK_API_KEY, PYTHON_API_BASE_URL, // e.g. http://localhost:8000 NVIDIA_API_KEY, // Your NVIDIA secret key for LLM fallback } = process.env; if ( !META_ACCESS_TOKEN || !META_PHONE_NUMBER_ID || !META_VERIFY_TOKEN || !DEEPSEEK_API_KEY || !PYTHON_API_BASE_URL || !NVIDIA_API_KEY ) { console.error("Missing environment variables. Please check your .env file."); process.exit(1); } // ----------------------------------------------------------------------------- // Set up the OpenAI client for NVIDIA LLM fallback using the new format // ----------------------------------------------------------------------------- const openaiLLM = new OpenAI({ apiKey: NVIDIA_API_KEY, baseURL: "https://integrate.api.nvidia.com/v1", }); /** * Call the LLM fallback using the new syntax. * This function streams the response and returns the full text. */ async function callLLMFallback(message) { try { const completion = await openaiLLM.chat.completions.create({ model: "meta/llama-3.1-405b-instruct", messages: [{ role: "user", content: message }], temperature: 0.2, top_p: 0.7, max_tokens: 1024, stream: true, }); let fullText = ""; for await (const chunk of completion) { fullText += chunk.choices[0]?.delta?.content || ""; } return fullText; } catch (err) { console.error("LLM fallback error:", err); throw err; } } // ----------------------------------------------------------------------------- // Helper Functions for calling Python API endpoints // ----------------------------------------------------------------------------- async function getPythonChatResponse(userId, message) { try { console.log("Forwarding chat request to Python API..."); const response = await axios.post(`${PYTHON_API_BASE_URL}/chatbot`, { user_id: userId, message: message, }); if (typeof response.data === "object" && response.data !== null) { let result = response.data.response || ""; if (Array.isArray(response.data.menu)) { result += "\n\n*Menu:*\n"; response.data.menu.forEach((item) => { result += `• *${item.name}* - ${item.description} - ₦${item.price}\n`; }); } if (response.data.follow_up) { result += "\n" + response.data.follow_up; } return result; } return response.data.response || "I'm sorry, I didn't get a response."; } catch (error) { console.error("Error from Python API (chatbot):", error.message); return "I'm experiencing technical difficulties. Please try again later."; } } async function getPythonChatHistory(userId) { try { console.log("Requesting chat history from Python API..."); const response = await axios.get(`${PYTHON_API_BASE_URL}/chat_history/${userId}`); return response.data; } catch (error) { console.error("Error from Python API (chat_history):", error.message); return null; } } async function getPythonOrderDetails(orderId) { try { console.log("Requesting order details from Python API..."); const response = await axios.get(`${PYTHON_API_BASE_URL}/order/${orderId}`); return response.data; } catch (error) { console.error("Error from Python API (order details):", error.message); return null; } } async function getPythonUserProfile(userId) { try { console.log("Requesting user profile from Python API..."); const response = await axios.get(`${PYTHON_API_BASE_URL}/user_profile/${userId}`); return response.data; } catch (error) { console.error("Error from Python API (user profile):", error.message); return null; } } async function getPythonAnalytics() { try { console.log("Requesting analytics from Python API..."); const response = await axios.get(`${PYTHON_API_BASE_URL}/analytics`); return response.data; } catch (error) { console.error("Error from Python API (analytics):", error.message); return null; } } // ----------------------------------------------------------------------------- // Utility: Log chat history locally (for debugging purposes) // ----------------------------------------------------------------------------- const chatHistories = {}; function logChat(userId, direction, message) { if (!chatHistories[userId]) { chatHistories[userId] = []; } chatHistories[userId].push({ timestamp: new Date().toISOString(), direction, message, }); } // ----------------------------------------------------------------------------- // Webhook Verification (GET) // ----------------------------------------------------------------------------- router.get("/", (req, res) => { const { "hub.mode": mode, "hub.verify_token": token, "hub.challenge": challenge, } = req.query; if (mode === "subscribe" && token === META_VERIFY_TOKEN) { console.log("Webhook verified successfully."); return res.status(200).send(challenge); } console.error("Webhook verification failed."); res.sendStatus(403); }); // ----------------------------------------------------------------------------- // WhatsApp Webhook Handler (POST) // ----------------------------------------------------------------------------- router.post("/", async (req, res) => { try { const body = req.body; console.log("Webhook received:", JSON.stringify(body, null, 2)); if (body.object === "whatsapp_business_account") { const changes = body.entry[0]?.changes[0]?.value; if (changes?.messages) { const messageObj = changes.messages[0]; const { from, text } = messageObj; const userMessage = text?.body; console.log(`Message from ${from}: ${userMessage}`); logChat(from, "inbound", userMessage); // Generate response const botResponse = await generateResponse(userMessage, from); console.log("Bot response:", botResponse); logChat(from, "outbound", botResponse); // Send the response via WhatsApp API. await sendMessage(from, botResponse); return res.status(200).send("EVENT_RECEIVED"); } if (changes?.statuses) { console.log("Status update received:", JSON.stringify(changes.statuses, null, 2)); return res.status(200).send("EVENT_RECEIVED"); } } console.log("No valid content found in webhook."); res.sendStatus(404); } catch (error) { console.error("Error handling webhook:", error.message); res.sendStatus(500); } }); // ----------------------------------------------------------------------------- // Order Flow is delegated to the Python API entirely, so no local order flow // ----------------------------------------------------------------------------- // ----------------------------------------------------------------------------- // Enhanced Response Generator // ----------------------------------------------------------------------------- async function generateResponse(message, from) { const lowerMessage = message.toLowerCase(); // 1. Welcome/Main Menu if (lowerMessage === "hi" || lowerMessage === "hello" || lowerMessage.includes("welcome")) { return `👋 Hi there! Welcome to *FoodieBot*! 🍔🍕\nPlease choose an option:\n1️⃣ *View Menu*\n2️⃣ *Place an Order*\n3️⃣ *Payment Status*\n4️⃣ *My Profile*\n5️⃣ *Chat History*\n6️⃣ *Help & Support*\nSimply type the number or the option name.`; } // 2. Menu Display – Option "1" or any message that includes "menu" (but not "order") if (lowerMessage === "1" || (lowerMessage.includes("menu") && !lowerMessage.includes("order"))) { return await getPythonChatResponse(from, "menu"); } // 3. Order Flow – Trigger on multiple keywords: "2", "order", "buy", "food" if ( lowerMessage === "2" || lowerMessage.includes("order") || lowerMessage.includes("buy") || lowerMessage.includes("food") ) { // Check if the user's message includes a dish name from the menu. let selectedDish = null; for (let item of menu_items) { // Assumes menu_items is defined as an array of dish objects. if (lowerMessage.includes(item.name.toLowerCase())) { selectedDish = item.name; break; } } if (selectedDish) { // Prepend the dish name to the order command so the Python API can handle it. return await getPythonChatResponse(from, `order: ${selectedDish}`); } return await getPythonChatResponse(from, "order"); } // 4. Payment Status – Option "3" or keyword "payment" if (lowerMessage === "3" || lowerMessage.includes("payment")) { return await getPythonChatResponse(from, "payment"); } // 5. User Profile – Option "4" or keyword "profile" if (lowerMessage === "4" || lowerMessage.includes("profile")) { const profile = await getPythonUserProfile(from); if (profile) { return `👤 *Your Profile*\nName: ${profile.name}\nPhone: ${profile.phone_number}\nEmail: ${profile.email}\nPreferences: ${profile.preferences}\nLast Interaction: ${profile.last_interaction}`; } else { return "No profile information found."; } } // 6. Chat History – Option "5" or keyword "chat history" if (lowerMessage === "5" || lowerMessage.includes("chat history")) { const history = await getPythonChatHistory(from); if (history && history.length > 0) { let formattedHistory = "*Your Chat History:*\n"; history.forEach((entry) => { formattedHistory += `[${entry.timestamp}] ${entry.direction}: ${entry.message}\n`; }); return formattedHistory; } else { return "No chat history found for your account."; } } // 7. Help & Support – Option "6" or keyword "help" / "support" if (lowerMessage === "6" || lowerMessage.includes("help") || lowerMessage.includes("support")) { return `❓ *How can I help you?*\nType one of the following:\n- *Order Status* (e.g., "order tracking ORD-123456789")\n- *Contact Support*\n- *Main Menu* to see all options again.`; } // 8. Recommendations (Advanced Feature) if (lowerMessage.includes("recommendations")) { return await getPythonChatResponse(from, "recommendations"); } // 9. Analytics (for admin use) if (lowerMessage.includes("analytics")) { const analytics = await getPythonAnalytics(); if (analytics) { return `📊 *Analytics*\nTotal Messages: ${analytics.total_messages}\nTotal Orders: ${analytics.total_orders}\nAverage Sentiment: ${analytics.average_sentiment}`; } else { return "Analytics data is not available."; } } // 10. Fallback: Use NVIDIA LLM fallback try { const llmResponse = await callLLMFallback(message); return llmResponse; } catch (error) { console.error("LLM fallback error:", error); return "I'm sorry, I didn't get a response."; } } // ----------------------------------------------------------------------------- // Send Message to User via WhatsApp API // ----------------------------------------------------------------------------- async function sendMessage(to, message) { try { const url = `https://graph.facebook.com/v16.0/${META_PHONE_NUMBER_ID}/messages`; const response = await axios.post( url, { messaging_product: "whatsapp", to, text: { body: message }, }, { headers: { Authorization: `Bearer ${META_ACCESS_TOKEN}` }, } ); console.log("Message sent successfully:", response.data); } catch (error) { if (error.response) { const { status } = error.response; console.error(`Error sending message: Status ${status}`); if (status === 401 || status === 403) { console.error( "[Token Issue Detected] The META_ACCESS_TOKEN may have expired or is invalid. Please refresh the token." ); } } else { console.error("Error sending message:", error.message); } } } // ----------------------------------------------------------------------------- // Proactive Engagement: Send a greeting to inactive users // ----------------------------------------------------------------------------- async function sendProactiveGreeting(userId) { const greeting = "👋 Hi again! We miss you. 😊\nWould you like to see our new menu items or get personalized recommendations? Type *'Menu'* or *'Recommendations'*."; await sendMessage(userId, greeting); return greeting; } module.exports = router;