|
|
const express = require("express"); |
|
|
const axios = require("axios"); |
|
|
const OpenAI = require("openai"); |
|
|
const dotenv = require("dotenv"); |
|
|
dotenv.config(); |
|
|
|
|
|
const router = express.Router(); |
|
|
|
|
|
|
|
|
const { |
|
|
META_ACCESS_TOKEN, |
|
|
META_PHONE_NUMBER_ID, |
|
|
META_VERIFY_TOKEN, |
|
|
DEEPSEEK_API_KEY, |
|
|
PYTHON_API_BASE_URL, |
|
|
NVIDIA_API_KEY, |
|
|
} = 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); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const openaiLLM = new OpenAI({ |
|
|
apiKey: NVIDIA_API_KEY, |
|
|
baseURL: "https://integrate.api.nvidia.com/v1", |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const chatHistories = {}; |
|
|
function logChat(userId, direction, message) { |
|
|
if (!chatHistories[userId]) { |
|
|
chatHistories[userId] = []; |
|
|
} |
|
|
chatHistories[userId].push({ |
|
|
timestamp: new Date().toISOString(), |
|
|
direction, |
|
|
message, |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
const botResponse = await generateResponse(userMessage, from); |
|
|
console.log("Bot response:", botResponse); |
|
|
logChat(from, "outbound", botResponse); |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function generateResponse(message, from) { |
|
|
const lowerMessage = message.toLowerCase(); |
|
|
|
|
|
|
|
|
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.`; |
|
|
} |
|
|
|
|
|
|
|
|
if (lowerMessage === "1" || (lowerMessage.includes("menu") && !lowerMessage.includes("order"))) { |
|
|
return await getPythonChatResponse(from, "menu"); |
|
|
} |
|
|
|
|
|
|
|
|
if ( |
|
|
lowerMessage === "2" || |
|
|
lowerMessage.includes("order") || |
|
|
lowerMessage.includes("buy") || |
|
|
lowerMessage.includes("food") |
|
|
) { |
|
|
|
|
|
let selectedDish = null; |
|
|
for (let item of menu_items) { |
|
|
if (lowerMessage.includes(item.name.toLowerCase())) { |
|
|
selectedDish = item.name; |
|
|
break; |
|
|
} |
|
|
} |
|
|
if (selectedDish) { |
|
|
|
|
|
return await getPythonChatResponse(from, `order: ${selectedDish}`); |
|
|
} |
|
|
return await getPythonChatResponse(from, "order"); |
|
|
} |
|
|
|
|
|
|
|
|
if (lowerMessage === "3" || lowerMessage.includes("payment")) { |
|
|
return await getPythonChatResponse(from, "payment"); |
|
|
} |
|
|
|
|
|
|
|
|
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."; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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."; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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.`; |
|
|
} |
|
|
|
|
|
|
|
|
if (lowerMessage.includes("recommendations")) { |
|
|
return await getPythonChatResponse(from, "recommendations"); |
|
|
} |
|
|
|
|
|
|
|
|
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."; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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."; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|