Fred808 commited on
Commit
99b5f6e
Β·
verified Β·
1 Parent(s): c9768fc

Create webhook.js

Browse files
Files changed (1) hide show
  1. webhook.js +386 -0
webhook.js ADDED
@@ -0,0 +1,386 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // restaurantBot.js
2
+
3
+ const express = require("express");
4
+ const axios = require("axios");
5
+ const { Configuration, OpenAIApi } = require("openai");
6
+ require("dotenv").config();
7
+
8
+ const router = express.Router();
9
+
10
+ // Environment Variables
11
+ const {
12
+ META_ACCESS_TOKEN,
13
+ META_PHONE_NUMBER_ID,
14
+ META_VERIFY_TOKEN,
15
+ DEEPSEEK_API_KEY,
16
+ PYTHON_API_BASE_URL, // e.g. http://localhost:8000
17
+ NVIDIA_API_KEY, // Your NVIDIA secret key for LLM fallback
18
+ } = process.env;
19
+
20
+ // Validate Environment Variables
21
+ if (
22
+ !META_ACCESS_TOKEN ||
23
+ !META_PHONE_NUMBER_ID ||
24
+ !META_VERIFY_TOKEN ||
25
+ !DEEPSEEK_API_KEY ||
26
+ !PYTHON_API_BASE_URL ||
27
+ !NVIDIA_API_KEY
28
+ ) {
29
+ console.error("Missing environment variables. Please check your .env file.");
30
+ process.exit(1);
31
+ }
32
+
33
+ // -----------------------------------------------------------------------------
34
+ // Set up the OpenAI client for NVIDIA LLM fallback (if needed)
35
+ // -----------------------------------------------------------------------------
36
+ const llmConfig = new Configuration({
37
+ apiKey: NVIDIA_API_KEY,
38
+ // Use NVIDIA's integration endpoint:
39
+ basePath: "https://integrate.api.nvidia.com/v1",
40
+ });
41
+ const llmOpenai = new OpenAIApi(llmConfig);
42
+
43
+ /**
44
+ * Call the LLM fallback directly using the OpenAI API configured for NVIDIA.
45
+ * This function returns a Promise that resolves to the complete streamed response.
46
+ */
47
+ async function callLLMFallback(message) {
48
+ return new Promise(async (resolve, reject) => {
49
+ try {
50
+ const response = await llmOpenai.createChatCompletion(
51
+ {
52
+ model: "meta/llama-3.1-405b-instruct",
53
+ messages: [{ role: "user", content: message }],
54
+ temperature: 0.2,
55
+ top_p: 0.7,
56
+ max_tokens: 1024,
57
+ stream: true,
58
+ },
59
+ { responseType: "stream" }
60
+ );
61
+
62
+ let fullText = "";
63
+ response.data.on("data", (chunk) => {
64
+ fullText += chunk.toString();
65
+ });
66
+ response.data.on("end", () => {
67
+ resolve(fullText);
68
+ });
69
+ response.data.on("error", (err) => {
70
+ console.error("Error in LLM fallback stream:", err);
71
+ reject(err);
72
+ });
73
+ } catch (error) {
74
+ console.error("Error calling LLM fallback:", error);
75
+ reject(error);
76
+ }
77
+ });
78
+ }
79
+
80
+ // -----------------------------------------------------------------------------
81
+ // Helper Functions for calling Python API endpoints
82
+ // -----------------------------------------------------------------------------
83
+
84
+ // Forward a generic chat request to the Python chatbot endpoint and format response.
85
+ async function getPythonChatResponse(userId, message) {
86
+ try {
87
+ console.log("Forwarding chat request to Python API...");
88
+ const response = await axios.post(`${PYTHON_API_BASE_URL}/chatbot`, {
89
+ user_id: userId,
90
+ message: message,
91
+ });
92
+
93
+ // If the response data is an object, format it into a friendly string.
94
+ if (typeof response.data === "object" && response.data !== null) {
95
+ let result = response.data.response || "";
96
+
97
+ // If there's a menu array, format it nicely.
98
+ if (Array.isArray(response.data.menu)) {
99
+ result += "\n\n*Menu:*\n";
100
+ response.data.menu.forEach((item) => {
101
+ result += `β€’ *${item.name}* - ${item.description} - ₦${item.price}\n`;
102
+ });
103
+ }
104
+
105
+ // Append any follow-up text.
106
+ if (response.data.follow_up) {
107
+ result += "\n" + response.data.follow_up;
108
+ }
109
+ return result;
110
+ }
111
+ return response.data.response || "I'm sorry, I didn't get a response.";
112
+ } catch (error) {
113
+ console.error("Error from Python API (chatbot):", error.message);
114
+ return "I'm experiencing technical difficulties. Please try again later.";
115
+ }
116
+ }
117
+
118
+ // Retrieve the chat history for the given user from the Python API.
119
+ async function getPythonChatHistory(userId) {
120
+ try {
121
+ console.log("Requesting chat history from Python API...");
122
+ const response = await axios.get(`${PYTHON_API_BASE_URL}/chat_history/${userId}`);
123
+ return response.data;
124
+ } catch (error) {
125
+ console.error("Error from Python API (chat_history):", error.message);
126
+ return null;
127
+ }
128
+ }
129
+
130
+ // Retrieve order details from the Python API.
131
+ async function getPythonOrderDetails(orderId) {
132
+ try {
133
+ console.log("Requesting order details from Python API...");
134
+ const response = await axios.get(`${PYTHON_API_BASE_URL}/order/${orderId}`);
135
+ return response.data;
136
+ } catch (error) {
137
+ console.error("Error from Python API (order details):", error.message);
138
+ return null;
139
+ }
140
+ }
141
+
142
+ // Retrieve user profile from the Python API.
143
+ async function getPythonUserProfile(userId) {
144
+ try {
145
+ console.log("Requesting user profile from Python API...");
146
+ const response = await axios.get(`${PYTHON_API_BASE_URL}/user_profile/${userId}`);
147
+ return response.data;
148
+ } catch (error) {
149
+ console.error("Error from Python API (user profile):", error.message);
150
+ return null;
151
+ }
152
+ }
153
+
154
+ // Retrieve analytics from the Python API.
155
+ async function getPythonAnalytics() {
156
+ try {
157
+ console.log("Requesting analytics from Python API...");
158
+ const response = await axios.get(`${PYTHON_API_BASE_URL}/analytics`);
159
+ return response.data;
160
+ } catch (error) {
161
+ console.error("Error from Python API (analytics):", error.message);
162
+ return null;
163
+ }
164
+ }
165
+
166
+ // -----------------------------------------------------------------------------
167
+ // Utility: Log chat history locally (for debugging purposes)
168
+ // -----------------------------------------------------------------------------
169
+ const chatHistories = {}; // For debugging: { userId: [ {timestamp, direction, message}, ... ] }
170
+ function logChat(userId, direction, message) {
171
+ if (!chatHistories[userId]) {
172
+ chatHistories[userId] = [];
173
+ }
174
+ chatHistories[userId].push({
175
+ timestamp: new Date().toISOString(),
176
+ direction,
177
+ message,
178
+ });
179
+ }
180
+
181
+ // -----------------------------------------------------------------------------
182
+ // Webhook Verification (GET)
183
+ // -----------------------------------------------------------------------------
184
+ router.get("/", (req, res) => {
185
+ const {
186
+ "hub.mode": mode,
187
+ "hub.verify_token": token,
188
+ "hub.challenge": challenge,
189
+ } = req.query;
190
+ if (mode === "subscribe" && token === META_VERIFY_TOKEN) {
191
+ console.log("Webhook verified successfully.");
192
+ return res.status(200).send(challenge);
193
+ }
194
+ console.error("Webhook verification failed.");
195
+ res.sendStatus(403);
196
+ });
197
+
198
+ // -----------------------------------------------------------------------------
199
+ // WhatsApp Webhook Handler (POST)
200
+ // -----------------------------------------------------------------------------
201
+ router.post("/", async (req, res) => {
202
+ try {
203
+ const body = req.body;
204
+ console.log("Webhook received:", JSON.stringify(body, null, 2));
205
+
206
+ if (body.object === "whatsapp_business_account") {
207
+ const changes = body.entry[0]?.changes[0]?.value;
208
+
209
+ // Handle incoming messages
210
+ if (changes?.messages) {
211
+ const messageObj = changes.messages[0];
212
+ const { from, text } = messageObj;
213
+ const userMessage = text?.body;
214
+ console.log(`Message from ${from}: ${userMessage}`);
215
+
216
+ // Log the incoming message locally for debugging
217
+ logChat(from, "inbound", userMessage);
218
+
219
+ // Generate a response based on the user's message
220
+ const botResponse = await generateResponse(userMessage, from);
221
+ console.log("Bot response:", botResponse);
222
+ logChat(from, "outbound", botResponse);
223
+
224
+ // Send the response back to the user via WhatsApp API.
225
+ await sendMessage(from, botResponse);
226
+ return res.status(200).send("EVENT_RECEIVED");
227
+ }
228
+
229
+ // Handle status updates (e.g., delivery receipts)
230
+ if (changes?.statuses) {
231
+ console.log("Status update received:", JSON.stringify(changes.statuses, null, 2));
232
+ return res.status(200).send("EVENT_RECEIVED");
233
+ }
234
+ }
235
+
236
+ console.log("No valid content found in webhook.");
237
+ res.sendStatus(404);
238
+ } catch (error) {
239
+ console.error("Error handling webhook:", error.message);
240
+ res.sendStatus(500);
241
+ }
242
+ });
243
+
244
+ // -----------------------------------------------------------------------------
245
+ // Enhanced Response Generator
246
+ // -----------------------------------------------------------------------------
247
+ async function generateResponse(message, from) {
248
+ const lowerMessage = message.toLowerCase();
249
+
250
+ // 1. Welcome and Main Menu
251
+ if (lowerMessage === "hi" || lowerMessage === "hello" || lowerMessage.includes("welcome")) {
252
+ return `πŸ‘‹ Hi there! Welcome to *Angelo Foods*! πŸ”πŸ•\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.`;
253
+ }
254
+
255
+ // 2. Menu Display
256
+ if (lowerMessage === "1" || (lowerMessage.includes("menu") && !lowerMessage.includes("order"))) {
257
+ // Display the menu (with images and details)
258
+ const menuResponse = await getPythonChatResponse(from, "menu");
259
+ return menuResponse;
260
+ }
261
+
262
+ // 3. Order Flow – Triggered by multiple keywords
263
+ if (
264
+ lowerMessage === "2" ||
265
+ lowerMessage.includes("order") ||
266
+ lowerMessage.includes("buy") ||
267
+ lowerMessage.includes("food")
268
+ ) {
269
+ // Check if the message exactly matches one of the dish names from the menu.
270
+ let selectedDish = null;
271
+ for (let item of menu_items) {
272
+ if (lowerMessage.includes(item.name.toLowerCase())) {
273
+ selectedDish = item.name;
274
+ break;
275
+ }
276
+ }
277
+ // If a dish is selected, pass it along as part of the order command.
278
+ if (selectedDish) {
279
+ return await getPythonChatResponse(from, `order: ${selectedDish}`);
280
+ }
281
+ // Otherwise, if user simply says "order", "buy", or "food", delegate to the order flow in Python API.
282
+ return await getPythonChatResponse(from, "order");
283
+ }
284
+
285
+ // 4. Payment Status
286
+ if (lowerMessage === "3" || lowerMessage.includes("payment")) {
287
+ return await getPythonChatResponse(from, "payment");
288
+ }
289
+
290
+ // 5. User Profile
291
+ if (lowerMessage === "4" || lowerMessage.includes("profile")) {
292
+ const profile = await getPythonUserProfile(from);
293
+ if (profile) {
294
+ return `πŸ‘€ *Your Profile*\nName: ${profile.name}\nPhone: ${profile.phone_number}\nEmail: ${profile.email}\nPreferences: ${profile.preferences}\nLast Interaction: ${profile.last_interaction}`;
295
+ } else {
296
+ return "No profile information found.";
297
+ }
298
+ }
299
+
300
+ // 6. Chat History
301
+ if (lowerMessage === "5" || lowerMessage.includes("chat history")) {
302
+ const history = await getPythonChatHistory(from);
303
+ if (history && history.length > 0) {
304
+ let formattedHistory = "*Your Chat History:*\n";
305
+ history.forEach((entry) => {
306
+ formattedHistory += `[${entry.timestamp}] ${entry.direction}: ${entry.message}\n`;
307
+ });
308
+ return formattedHistory;
309
+ } else {
310
+ return "No chat history found for your account.";
311
+ }
312
+ }
313
+
314
+ // 7. Help & Support
315
+ if (lowerMessage === "6" || lowerMessage.includes("help") || lowerMessage.includes("support")) {
316
+ 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.`;
317
+ }
318
+
319
+ // 8. Recommendations (Advanced Feature)
320
+ if (lowerMessage.includes("recommendations")) {
321
+ return await getPythonChatResponse(from, "recommendations");
322
+ }
323
+
324
+ // 9. Analytics (for admin use)
325
+ if (lowerMessage.includes("analytics")) {
326
+ const analytics = await getPythonAnalytics();
327
+ if (analytics) {
328
+ return `πŸ“Š *Analytics*\nTotal Messages: ${analytics.total_messages}\nTotal Orders: ${analytics.total_orders}\nAverage Sentiment: ${analytics.average_sentiment}`;
329
+ } else {
330
+ return "Analytics data is not available.";
331
+ }
332
+ }
333
+
334
+ // 10. Fallback: Use NVIDIA LLM fallback
335
+ try {
336
+ const llmResponse = await callLLMFallback(message);
337
+ return llmResponse;
338
+ } catch (error) {
339
+ console.error("LLM fallback error:", error);
340
+ return "I'm sorry, I didn't get a response.";
341
+ }
342
+ }
343
+
344
+ // -----------------------------------------------------------------------------
345
+ // Send Message to User via WhatsApp API
346
+ // -----------------------------------------------------------------------------
347
+ async function sendMessage(to, message) {
348
+ try {
349
+ const url = `https://graph.facebook.com/v16.0/${META_PHONE_NUMBER_ID}/messages`;
350
+ const response = await axios.post(
351
+ url,
352
+ {
353
+ messaging_product: "whatsapp",
354
+ to,
355
+ text: { body: message },
356
+ },
357
+ {
358
+ headers: { Authorization: `Bearer ${META_ACCESS_TOKEN}` },
359
+ }
360
+ );
361
+ console.log("Message sent successfully:", response.data);
362
+ } catch (error) {
363
+ if (error.response) {
364
+ const { status } = error.response;
365
+ console.error(`Error sending message: Status ${status}`);
366
+ if (status === 401 || status === 403) {
367
+ console.error(
368
+ "[Token Issue Detected] The META_ACCESS_TOKEN may have expired or is invalid. Please refresh the token."
369
+ );
370
+ }
371
+ } else {
372
+ console.error("Error sending message:", error.message);
373
+ }
374
+ }
375
+ }
376
+
377
+ // -----------------------------------------------------------------------------
378
+ // Proactive Engagement: Send a greeting to inactive users
379
+ // -----------------------------------------------------------------------------
380
+ async function sendProactiveGreeting(userId) {
381
+ const greeting = "πŸ‘‹ Hi again! We miss you. 😊\nWould you like to see our new menu items or get personalized recommendations? Type *'Menu'* or *'Recommendations'*.";
382
+ await sendMessage(userId, greeting);
383
+ return greeting;
384
+ }
385
+
386
+ module.exports = router;