Angel-Bot / webhook.js
Fred808's picture
Update webhook.js
05a1ecf verified
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;