Download webhook.js from Fred808/Angel-Bot: direct link, hf CLI and curl.
- Browser
- Download file 12.8 kB
-
https://huggingface.co/spaces/Fred808/Angel-Bot/resolve/main/webhook.js
- Command line
-
hf download hf://spaces/Fred808/Angel-Bot/webhook.js
-
curl -L -o webhook.js https://huggingface.co/spaces/Fred808/Angel-Bot/resolve/main/webhook.js
12.8 kB
| 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; | |