├── .gitignore ├── images ├── a1.jpg ├── a3.jpg ├── a4.jpg ├── a5.jpg ├── a6.jpg ├── a7.jpg ├── b1.jpg ├── b2.jpg ├── b3.jpg ├── 4_1.jpg ├── 4_10.jpg ├── 4_11.jpg ├── 4_2.jpg ├── 4_3.jpg ├── 4_4.jpg ├── 4_5.jpg ├── 4_6.jpg ├── 4_7.jpg ├── 4_8.jpg ├── 4_9.jpg ├── db_1.jpg ├── db_2.jpg ├── db_3.jpg ├── db_4.jpg ├── receipt.png ├── cart_add.png ├── checkout.png ├── create_page.jpg ├── hello_message.jpg ├── nlp_add_token.jpg ├── quick_replies.png ├── nlp_select_page.jpg ├── recommendations.png ├── send_same_message.png ├── confirmation_order.png ├── order_confirmation.jpg ├── rectified_utterance.jpg ├── send_message_button.jpg ├── architecture_diagram.png ├── check_webhook_fields.jpg ├── chocolate_chip_cookies.jpg ├── handling_entity_values.jpg ├── pretrained_greetings.png ├── link_webhook_to_facebook.jpg ├── misclassified_utterance.jpg ├── rectified_message_handling.jpg ├── rectify_misclassified_utterance.jpg ├── earl_grey_sunflower_seeds_cookies.jpg └── misclassified_message_and_response.jpg ├── package.json ├── models ├── User.js ├── Order.js ├── Product.js └── Cart.js ├── LICENSE ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | npm-debug.log 3 | .DS_Store 4 | /*.env -------------------------------------------------------------------------------- /images/a1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/a1.jpg -------------------------------------------------------------------------------- /images/a3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/a3.jpg -------------------------------------------------------------------------------- /images/a4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/a4.jpg -------------------------------------------------------------------------------- /images/a5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/a5.jpg -------------------------------------------------------------------------------- /images/a6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/a6.jpg -------------------------------------------------------------------------------- /images/a7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/a7.jpg -------------------------------------------------------------------------------- /images/b1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/b1.jpg -------------------------------------------------------------------------------- /images/b2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/b2.jpg -------------------------------------------------------------------------------- /images/b3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/b3.jpg -------------------------------------------------------------------------------- /images/4_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/4_1.jpg -------------------------------------------------------------------------------- /images/4_10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/4_10.jpg -------------------------------------------------------------------------------- /images/4_11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/4_11.jpg -------------------------------------------------------------------------------- /images/4_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/4_2.jpg -------------------------------------------------------------------------------- /images/4_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/4_3.jpg -------------------------------------------------------------------------------- /images/4_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/4_4.jpg -------------------------------------------------------------------------------- /images/4_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/4_5.jpg -------------------------------------------------------------------------------- /images/4_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/4_6.jpg -------------------------------------------------------------------------------- /images/4_7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/4_7.jpg -------------------------------------------------------------------------------- /images/4_8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/4_8.jpg -------------------------------------------------------------------------------- /images/4_9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/4_9.jpg -------------------------------------------------------------------------------- /images/db_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/db_1.jpg -------------------------------------------------------------------------------- /images/db_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/db_2.jpg -------------------------------------------------------------------------------- /images/db_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/db_3.jpg -------------------------------------------------------------------------------- /images/db_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/db_4.jpg -------------------------------------------------------------------------------- /images/receipt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/receipt.png -------------------------------------------------------------------------------- /images/cart_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/cart_add.png -------------------------------------------------------------------------------- /images/checkout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/checkout.png -------------------------------------------------------------------------------- /images/create_page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/create_page.jpg -------------------------------------------------------------------------------- /images/hello_message.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/hello_message.jpg -------------------------------------------------------------------------------- /images/nlp_add_token.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/nlp_add_token.jpg -------------------------------------------------------------------------------- /images/quick_replies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/quick_replies.png -------------------------------------------------------------------------------- /images/nlp_select_page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/nlp_select_page.jpg -------------------------------------------------------------------------------- /images/recommendations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/recommendations.png -------------------------------------------------------------------------------- /images/send_same_message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/send_same_message.png -------------------------------------------------------------------------------- /images/confirmation_order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/confirmation_order.png -------------------------------------------------------------------------------- /images/order_confirmation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/order_confirmation.jpg -------------------------------------------------------------------------------- /images/rectified_utterance.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/rectified_utterance.jpg -------------------------------------------------------------------------------- /images/send_message_button.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/send_message_button.jpg -------------------------------------------------------------------------------- /images/architecture_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/architecture_diagram.png -------------------------------------------------------------------------------- /images/check_webhook_fields.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/check_webhook_fields.jpg -------------------------------------------------------------------------------- /images/chocolate_chip_cookies.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/chocolate_chip_cookies.jpg -------------------------------------------------------------------------------- /images/handling_entity_values.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/handling_entity_values.jpg -------------------------------------------------------------------------------- /images/pretrained_greetings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/pretrained_greetings.png -------------------------------------------------------------------------------- /images/link_webhook_to_facebook.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/link_webhook_to_facebook.jpg -------------------------------------------------------------------------------- /images/misclassified_utterance.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/misclassified_utterance.jpg -------------------------------------------------------------------------------- /images/rectified_message_handling.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/rectified_message_handling.jpg -------------------------------------------------------------------------------- /images/rectify_misclassified_utterance.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/rectify_misclassified_utterance.jpg -------------------------------------------------------------------------------- /images/earl_grey_sunflower_seeds_cookies.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/earl_grey_sunflower_seeds_cookies.jpg -------------------------------------------------------------------------------- /images/misclassified_message_and_response.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/HEAD/images/misclassified_message_and_response.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "messenger-webhook", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node index.js" 9 | }, 10 | "keywords": [ 11 | "example", 12 | "heroku" 13 | ], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "body-parser": "^1.19.0", 18 | "dotenv": "^8.2.0", 19 | "express": "^4.17.1", 20 | "node-fetch": "^2.6.1", 21 | "request": "^2.88.2" 22 | }, 23 | "engines": { 24 | "node": "v12.x" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | var uniqueValidator = require('mongoose-unique-validator'); 3 | 4 | const userSchema = new mongoose.Schema({ 5 | id: { type: String, required: true, unique: true }, 6 | name: String, 7 | cartId: { type: String } 8 | }); 9 | 10 | const User = mongoose.model('User',userSchema); 11 | 12 | // Create user if user not found in database 13 | function createUser(fbid,name){ 14 | var newUser = new User({ 15 | id:fbid, 16 | name:name 17 | }); 18 | return newUser.save() 19 | .then(doc => doc) 20 | .catch(err => null); 21 | } 22 | 23 | function checkUser(fbid){ 24 | return User.findOne({'id':fbid}) 25 | .then(function (user) { return user; }) 26 | .catch(function (err) { return null }); 27 | } 28 | 29 | export { checkUser, createUser }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Aaron Teo, Douglas Sim, Eugene Yuen, Jelissa Ong, Randy Lai, Rui Qin Ng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /models/Order.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | var uniqueValidator = require('mongoose-unique-validator'); 3 | var Float = require('mongoose-float').loadType(mongoose); 4 | var User = mongoose.model('User'); 5 | var Cart = mongoose.model('Cart'); 6 | var Product = mongoose.model('Product'); 7 | 8 | import { v4 as uuidv4 } from 'uuid'; 9 | import {getProductByID} from './Product'; 10 | 11 | const orderSchema = new mongoose.Schema({ 12 | customer: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, 13 | trackingNumber: { type: String, unique: true }, 14 | orderStatus: { 15 | type: String, 16 | enum : ['Order Received','Packing','Out For Delivery','Delivered','Refund'], 17 | default: 'Order Received' 18 | }, 19 | products : [{ 20 | title:String, 21 | price:Float, 22 | image_link:String, 23 | pattern:String, 24 | quantity:Float 25 | }] 26 | }, { timestamps: true }); 27 | 28 | orderSchema.plugin(uniqueValidator, { message: 'is already exist.' }); 29 | 30 | const Order = mongoose.model('Order', orderSchema); 31 | 32 | // Pass in user and cart object 33 | async function createOrder(user){ 34 | const trackingNum = uuidv4(); 35 | let cart = await Cart.findOne({uid:user.cartId}).then((cart) => cart).catch((err) => null); 36 | 37 | if (cart==null){ 38 | return null; 39 | } 40 | 41 | let productItems = []; 42 | for (var i = 0; i < cart.items.length; i++){ 43 | let pId = cart.items[i].pid; 44 | let product = await getProductByID(pId); 45 | productItems.push({ 46 | title:product.title, 47 | price:product.price, 48 | image_link:product.image_link, 49 | pattern:product.pattern, 50 | quantity:cart.items[i].quantity 51 | }); 52 | } 53 | 54 | var newOrder = new Order({ 55 | customer: user, 56 | trackingNumber: trackingNum, 57 | products: productItems 58 | }) 59 | 60 | let setOrder = await newOrder.save().then((doc) => doc).catch((err) => {console.log(err); null}); 61 | 62 | return setOrder; 63 | } 64 | 65 | async function getAllOrders(user){ 66 | 67 | let getOrders = await Order.find({customer: user}).then((order) => order).catch((err) => null); 68 | 69 | return getOrders; 70 | 71 | } 72 | 73 | async function getOrder(user,trackingNumber){ 74 | let getOrder = await Order.findOne({customer: user, trackingNumber: trackingNumber}).then((order) => order).catch((err) => null); 75 | 76 | return getOrder; 77 | } 78 | 79 | export{ createOrder, getAllOrders, getOrder}; -------------------------------------------------------------------------------- /models/Product.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | var Float = require('mongoose-float').loadType(mongoose); 3 | 4 | const productsSchema = new mongoose.Schema({ 5 | pid: String, 6 | pid: String, 7 | title: String, 8 | description: String, 9 | availability: String, 10 | inventory: String, 11 | condition: String, 12 | price: Float, 13 | link: String, 14 | image_link: String, 15 | brand: String, 16 | item_group_id: String, 17 | color: String, 18 | pattern: String, 19 | product_type: String, 20 | allergens: [String], 21 | ingredients: [String], 22 | weight: String, 23 | flavor: String, 24 | color: String, 25 | quantity: Float 26 | }); 27 | 28 | const Product = mongoose.model('Product',productsSchema); 29 | 30 | 31 | //Get all products 32 | function getAllProducts(){ 33 | return Product.find({}).then(function(products){ 34 | return products; 35 | }).catch(function(err){ 36 | console.log(err); 37 | return []; 38 | }); 39 | } 40 | 41 | //Get product by type 42 | function getProductsByType(typeValue){ 43 | return Product.find({'product_type':typeValue}).then(function(products){ 44 | return products; 45 | }).catch(function(err){ 46 | console.log(err); 47 | return []; 48 | }); 49 | } 50 | 51 | //Get product by ID 52 | function getProductByID(pid){ 53 | return Product.findOne({'pid':pid}).then(function(prod){ 54 | return prod; 55 | }).catch(function(err){ 56 | console.log(err); 57 | return null; 58 | }); 59 | } 60 | 61 | // Get product Price by ID 62 | function getProductPrice(pid){ 63 | return Product.findOne({'pid':pid}).then(function(prod){ 64 | return prod.price; 65 | }).catch(function(err){ 66 | console.log(err); 67 | return null; 68 | }); 69 | } 70 | 71 | // get product Desc by ID 72 | function getProductDesc(pid){ 73 | return Product.findOne({'pid':pid}).then(function(prod){ 74 | return prod.description; 75 | }).catch(function(err){ 76 | console.log(err); 77 | return null; 78 | }); 79 | } 80 | 81 | // get products with all the differnt variations by Name 82 | function getProductsByName(name){ 83 | return Product.find({'title':name}).then(function(prod){ 84 | return prod; 85 | }).catch(function(err){ 86 | console.log(err); 87 | return null; 88 | }); 89 | } 90 | 91 | // get product by name and variation 92 | function getProductByNameVar(name,variation){ 93 | return Product.findOne({'title':name,'pattern':variation}).then(function(prod){ 94 | return prod; 95 | }).catch(function(err){ 96 | console.log(err); 97 | return null; 98 | }); 99 | } 100 | 101 | //get products by allergens 102 | function getProductsByAllergens(allergen){ 103 | return Product.find({allergens:allergen}).then(function(products){ 104 | return products; 105 | }).catch(function(err){ 106 | console.log(err); 107 | return []; 108 | }); 109 | } 110 | 111 | //get products by _id 112 | function getProductsByDefaultId(defId){ 113 | return Product.findOne({_id:defId}).then(function(product){ 114 | return product; 115 | }).catch(function(err){ 116 | console.log(err); 117 | return []; 118 | }); 119 | } 120 | 121 | export { getAllProducts, getProductsByType, getProductByID, getProductPrice, getProductDesc, getProductsByName, getProductByNameVar, getProductsByAllergens, getProductsByDefaultId }; -------------------------------------------------------------------------------- /models/Cart.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | var uniqueValidator = require('mongoose-unique-validator'); 3 | var Float = require('mongoose-float').loadType(mongoose); 4 | var User = mongoose.model('User'); 5 | import { v4 as uuidv4 } from 'uuid'; 6 | 7 | const cartSchema = new mongoose.Schema({ 8 | uid: { type: String, required: true, unique: true }, 9 | // items: [{ 10 | // pid: String, 11 | // title: String, 12 | // description: String, 13 | // condition: String, 14 | // price: Float, 15 | // quantity: Number, 16 | // brand: String, 17 | // item_group_id: String, 18 | // color: String, 19 | // pattern: String, 20 | // product_type: String, 21 | // allergens: String 22 | // }], 23 | items: [{ 24 | pid: String, 25 | quantity: Float 26 | }] 27 | // totalPrice: { type: Float, required: true, default: 0.00 }, 28 | }) 29 | 30 | cartSchema.plugin(uniqueValidator, { message: 'is already taken.' }); 31 | 32 | const Cart = mongoose.model('Cart', cartSchema); 33 | 34 | // Initial creation of a fresh cart with an item(s) 35 | async function createCart(userId, pid, quantity) { 36 | // const totalPrice = price * quantity; 37 | const cartId = uuidv4(); 38 | var newCart = new Cart({ 39 | uid: cartId, 40 | items: [{ 41 | pid: pid, 42 | quantity: quantity 43 | }] 44 | // totalPrice: totalPrice 45 | }); 46 | 47 | // Add cart id to user document 48 | await User.findOne({id: userId}) 49 | .then((user) => { 50 | if (!user) { 51 | console.log("User not found when creating cart."); 52 | } 53 | 54 | const query_param_userId = user._id; 55 | const user_cartId = newCart.uid; 56 | 57 | User.findByIdAndUpdate(query_param_userId, { "cartId": user_cartId },{ new: true }) 58 | .then((result) => console.log("User document updated with cart id.")) 59 | .catch((err) => console.log("User document failed to update cart id.")) 60 | }) 61 | .catch(err => console.log("Error in finding user during cart creation.")); 62 | 63 | // Save cart document 64 | let setCart = await newCart.save() 65 | .then(doc => doc) 66 | .catch(err => { 67 | console.log("Error in saving cart"); 68 | return null; 69 | }); 70 | 71 | return setCart; 72 | } 73 | 74 | //Check for user active cart 75 | async function checkCart(userId) { 76 | 77 | let getCartId = await User.findOne({id: userId}) 78 | .then((user) => { 79 | if (!user) console.log('User doesnt exist!'); 80 | 81 | const cartId = user.cartId; 82 | return cartId; 83 | }); 84 | 85 | // Early return if cart id is not found 86 | if (!getCartId) return null; 87 | 88 | let getCart = await Cart.findOne({ uid: getCartId }) 89 | .then((result) => result) 90 | .catch((err) => null); 91 | 92 | return getCart 93 | } 94 | 95 | async function addItemToCart(cartId, pid, quantity) { 96 | let itemData = { 97 | pid: pid, 98 | quantity: quantity 99 | }; 100 | 101 | let userCart = await Cart.findOne({uid:cartId,'items.pid':pid}).then((cart) => cart).catch((err) => null) 102 | 103 | if (userCart==null){ 104 | let addItem = await Cart.findOneAndUpdate( 105 | { uid: cartId }, 106 | { $push: { items: itemData } }, 107 | { new: true }) 108 | .then((result) => result) 109 | .catch((err) => { 110 | console.error(err); 111 | return null; 112 | }); 113 | 114 | return addItem 115 | } 116 | var cartItems = []; 117 | for (var i = 0; i < userCart.items.length; i++){ 118 | let cartItem = userCart.items[i]; 119 | if (cartItem.pid===pid){ 120 | cartItem.quantity += quantity; 121 | } 122 | cartItems.push(cartItem); 123 | } 124 | userCart.items = cartItems; 125 | 126 | // Save cart document 127 | let setCart = await userCart.save() 128 | .then(doc => doc) 129 | .catch(err => { 130 | console.log("Error in saving cart"); 131 | return null; 132 | }); 133 | 134 | return setCart; 135 | 136 | } 137 | 138 | async function removeItemFromCart(cartId,pid){ 139 | let userCart = await Cart.findOne({uid:cartId,'items.pid':pid}).then((cart) => cart).catch((err) => null) 140 | 141 | if (userCart == null){ 142 | return userCart; 143 | } 144 | 145 | var cartItems = []; 146 | for (var i = 0; i < userCart.items.length; i++){ 147 | let cartItem = userCart.items[i]; 148 | if (cartItem.pid!==pid){ 149 | cartItems.push(cartItem); 150 | } 151 | } 152 | userCart.items = cartItems; 153 | 154 | // Save cart document 155 | let setCart = await userCart.save() 156 | .then(doc => doc) 157 | .catch(err => { 158 | console.log("Error in saving cart"); 159 | return null; 160 | }); 161 | 162 | return setCart; 163 | } 164 | 165 | async function removeAllItemsFromCart(cartId){ 166 | let deleteItemsFromCart = await Cart.findOneAndUpdate( 167 | { uid: cartId }, 168 | { items: [] }, 169 | { new: true } 170 | ).then((result) => { console.log(result); return result }).catch((err) => {console.error(err); return null;}) 171 | 172 | return deleteItemsFromCart; 173 | } 174 | 175 | async function deleteCart(userId, cartId) { 176 | 177 | await User.findOne({id: userId}).then((user) => { 178 | if (!user) { return res.status(401).send('User doesnt exist!'); } 179 | 180 | const query_param_userId = user._id; 181 | const user_cartId = ""; 182 | 183 | User.findByIdAndUpdate(query_param_userId, { "cartId": user_cartId },{ new: true }) 184 | .then((result) => { console.log(result); return result }) 185 | .catch((err) => {console.error(err); return null;}) 186 | }); 187 | 188 | await Cart.findOneAndDelete({ uid: cartId }) 189 | .then((result) => { console.log(result); return result }) 190 | .catch((err) => {console.error(err); return null;}); 191 | } 192 | 193 | export { createCart, checkCart, addItemToCart, removeItemFromCart, removeAllItemsFromCart, deleteCart }; 194 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Imports dependencies and set up http server 4 | const 5 | express = require('express'), 6 | bodyParser = require('body-parser'), 7 | app = express().use(bodyParser.json()), // creates express http server 8 | dotenv = require('dotenv'), 9 | request = require('request'); 10 | global.fetch = require("node-fetch"); 11 | 12 | dotenv.config(); 13 | const PAGE_ACCESS_TOKEN = process.env.PAGE_ACCESS_TOKEN; 14 | 15 | // Sets server port and logs message on success 16 | const listener = app.listen(process.env.PORT || 1337, () => console.log('webhook is listening on port ' + listener.address().port)); 17 | 18 | /* 19 | Mock data in database (remove if database is implemented) 20 | */ 21 | let products = [ 22 | { 23 | pid: 123, 24 | title: 'Chocolate Chip Cookies', 25 | pattern: 'Box of 6', 26 | price: 15.5, 27 | image_link: 'https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/main/images/chocolate_chip_cookies.jpg' 28 | }, 29 | { 30 | pid: 456, 31 | title: 'Earl Grey Sunflower Seeds Cookies', 32 | pattern: 'Box of 9', 33 | price: 18.5, 34 | image_link: 'https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/main/images/earl_grey_sunflower_seeds_cookies.jpg' 35 | } 36 | ] 37 | 38 | let cart = [ 39 | { 40 | pid: 123, 41 | title: 'Chocolate Chip Cookies', 42 | pattern: 'Box of 6', 43 | price: 15.5, 44 | quantity: 1, 45 | image_link: 'https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/main/images/chocolate_chip_cookies.jpg' 46 | }, 47 | { 48 | pid: 456, 49 | title: 'Earl Grey Sunflower Seeds Cookies', 50 | pattern: 'Box of 9', 51 | price: 18.5, 52 | quantity: 2, 53 | image_link: 'https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/main/images/earl_grey_sunflower_seeds_cookies.jpg' 54 | } 55 | ] 56 | 57 | let order = [ 58 | { 59 | pid: 123, 60 | title: 'Chocolate Chip Cookies', 61 | pattern: 'Box of 6', 62 | price: 15.5, 63 | quantity: 1, 64 | image_link: 'https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/main/images/chocolate_chip_cookies.jpg' 65 | }, 66 | { 67 | pid: 456, 68 | title: 'Earl Grey Sunflower Seeds Cookies', 69 | pattern: 'Box of 9', 70 | price: 18.5, 71 | quantity: 2, 72 | image_link: 'https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/main/images/earl_grey_sunflower_seeds_cookies.jpg' 73 | } 74 | ] 75 | /* 76 | End of mock data in database (remove if database is implemented) 77 | */ 78 | 79 | app.get('/', (req, res) => { 80 | res.status(200).send('You are connected to the chatbot application.'); 81 | }) 82 | 83 | // Adds support for GET requests to our webhook 84 | app.get('/webhook', (req, res) => { 85 | 86 | // Your verify token. Should be a random string. 87 | let VERIFY_TOKEN = process.env.VERIFY_TOKEN; 88 | 89 | // Parse the query params 90 | let mode = req.query['hub.mode']; 91 | let token = req.query['hub.verify_token']; 92 | let challenge = req.query['hub.challenge']; 93 | 94 | // Checks if a token and mode is in the query string of the request 95 | if (mode && token) { 96 | 97 | // Checks the mode and token sent is correct 98 | if (mode === 'subscribe' && token === VERIFY_TOKEN) { 99 | 100 | // Responds with the challenge token from the request 101 | console.log('WEBHOOK_VERIFIED'); 102 | res.status(200).send(challenge); 103 | 104 | } else { 105 | // Responds with '403 Forbidden' if verify tokens do not match 106 | res.sendStatus(403); 107 | } 108 | } 109 | }); 110 | 111 | // Creates the endpoint for our webhook 112 | app.post('/webhook', (req, res) => { 113 | 114 | let body = req.body; 115 | 116 | // Checks this is an event from a page subscription 117 | if (body.object === 'page') { 118 | 119 | // Iterates over each entry - there may be multiple if batched 120 | body.entry.forEach(async function(entry) { 121 | 122 | // Gets the message. entry.messaging is an array, but 123 | // will only ever contain one message, so we get index 0 124 | let webhook_event = entry.messaging[0]; 125 | console.log(webhook_event); 126 | 127 | let sender_psid = webhook_event['sender']['id']; 128 | 129 | let response = getDefaultResponse(); 130 | if (webhook_event['message']) { 131 | let message = webhook_event['message']['text']; 132 | console.log('Message received from sender ' + sender_psid + ' : ' + message); 133 | 134 | if (webhook_event['message']['quick_reply']) { 135 | let payload = webhook_event['message']['quick_reply']['payload']; 136 | response = await processPayload(sender_psid, payload); 137 | } else { 138 | let nlp = webhook_event['message']['nlp']; 139 | response = await processMessage(sender_psid, message, nlp); 140 | } 141 | } else if (webhook_event['postback']) { 142 | let payload = webhook_event['postback']['payload']; 143 | response = await processPayload(sender_psid, payload); 144 | } 145 | 146 | callSendAPI(sender_psid, response); 147 | }); 148 | 149 | // Returns a '200 OK' response to all requests 150 | res.status(200).send('EVENT_RECEIVED'); 151 | } else { 152 | // Returns a '404 Not Found' if event is not from a page subscription 153 | res.sendStatus(404); 154 | } 155 | 156 | }); 157 | 158 | // Sends response messages via the Send API 159 | function callSendAPI(sender_psid, response) { 160 | 161 | // Construct the message body 162 | let request_body = { 163 | messaging_type: 'RESPONSE', 164 | recipient: { 165 | id: sender_psid 166 | }, 167 | message: response 168 | }; 169 | 170 | // console.log(request_body); 171 | 172 | // Send the HTTP request to the Messenger Platform 173 | request( 174 | { 175 | uri: "https://graph.facebook.com/v8.0/me/messages", 176 | qs: { access_token: process.env.PAGE_ACCESS_TOKEN }, 177 | method: "POST", 178 | json: request_body 179 | }, 180 | (err, res, body) => { 181 | if (!err) { 182 | console.log("Message sent!"); 183 | } else { 184 | console.error("Unable to send message: " + err); 185 | } 186 | } 187 | ); 188 | } 189 | 190 | // Wrapper method to convert text message string to response object, and sends the message 191 | function getResponseFromMessage(message) { 192 | const response = { 193 | text: message 194 | }; 195 | 196 | return response; 197 | } 198 | 199 | function getDefaultResponse() { 200 | return getResponseFromMessage("We could not understand your message. Kindly rephrase your message and send us again."); 201 | } 202 | 203 | // Processes and sends text message 204 | async function processMessage(sender_psid, message, nlp) { 205 | 206 | if (nlp['intents'].length === 0) { 207 | 208 | // Check if greeting 209 | let traits = nlp['traits']; 210 | 211 | if (traits['wit$greetings'] && traits['wit$greetings'][0]['value'] === 'true') { 212 | console.log('Is greeting'); 213 | // Add the getName function call here 214 | let name = await getName(PAGE_ACCESS_TOKEN,sender_psid); 215 | return getResponseFromMessage('Hi ' + name + '! Welcome to Bright. How can I help you?'); 216 | } 217 | 218 | console.log('Returning default response'); 219 | return getDefaultResponse(); 220 | } 221 | 222 | console.log('Intents inferred from NLP model: ') 223 | console.table(nlp['intents']); 224 | 225 | // Get the intent with the highest confidence 226 | let intent = nlp['intents'][0]['name'] 227 | let confidence = nlp['intents'][0]['confidence'] 228 | 229 | // If confidence of intent is less than threshold, do not process 230 | if (confidence < 0.7) return getDefaultResponse(); 231 | 232 | let entities = nlp['entities']; 233 | let highest_confidence = 0; 234 | 235 | switch (intent) { 236 | case 'enquiry_general': 237 | // Get entity with highest confidence 238 | let entity = null; 239 | for (const e in entities) { 240 | let confidence = entities[e][0]['confidence']; 241 | if (confidence > highest_confidence) { 242 | highest_confidence = confidence; 243 | entity = entities[e][0]['name']; 244 | } 245 | } 246 | 247 | console.log('Entity with highest confidence: ' + entity); 248 | 249 | return handleGeneralEnquiry(entity); 250 | case 'enquiry_delivery': 251 | // Get value with highest confidence 252 | let value = null; 253 | if ('wit$datetime:datetime' in entities) { 254 | for (const e of entities['wit$datetime:datetime']) { 255 | console.log(e); 256 | let confidence = e['confidence']; 257 | if (confidence > highest_confidence) { 258 | highest_confidence = confidence; 259 | // Type is either "value" or "interval" 260 | value = (e['type'] === 'value') ? e['value'] : e['to']['value']; 261 | } 262 | } 263 | console.log('Value with highest confidence: ' + value); 264 | } 265 | 266 | return handleDeliveryEnquiry(value); 267 | case 'recommendation': 268 | // For Option 1 only: Get products from database and assign it to a variable named "products" 269 | 270 | return generateCarouselOfProductsResponse(products); 271 | 272 | default: 273 | return getDefaultResponse(); 274 | } 275 | 276 | } 277 | 278 | function handleGeneralEnquiry(entity) { 279 | 280 | if (entity == null) return getDefaultResponse(); 281 | 282 | let responses = { 283 | organisation: "Bright is a social enterprise where we provide vocational training to adults with intellectual disabilities.\n\n" + 284 | "We started a range of social enterprise projects to provide alternative work engagement opportunities for our adult trainees. " + 285 | "Some of the projects began as therapy programmes which encourage the development of fine motor skills; others provide a realistic vocational training environment.\n\n" + 286 | "All net revenue earned from the sale of our products and services go towards paying a monthly allowance for our clients' work, as well as their lunch expenses while undergoing training.", 287 | profit: 288 | "All net revenue earned from the sale of our products and services go towards paying a monthly allowance for our clients' work, as well as their lunch expenses while undergoing training.", 289 | manufacturer: 290 | "We support adults with intellectual disabilities. We started a range of social enterprise projects to provide alternative work engagement for our adult trainees.", 291 | products: "We sell craft and baker goods.\nLike our Facebook page http://fb.me/brightsocialsg to stay updated!", 292 | safety: "Our cookies are made by our clients in a clean and sanitised environment. The cookies are safe to consume before the expiry date that is printed on the packaging." 293 | }; 294 | 295 | return getResponseFromMessage(responses[entity]); 296 | } 297 | 298 | function handleDeliveryEnquiry(value) { 299 | if (value == null) return getResponseFromMessage('We deliver islandwide. The average delivery time takes 5-7 days.'); 300 | 301 | // Get days between now and date time value and round up 302 | let days = ((new Date(value)) - Date.now()) / (1000 * 60 * 60 * 24); 303 | days = Math.ceil(days); 304 | 305 | let message = 'The average delivery time takes 5-7 days.'; 306 | if (days < 5) { 307 | message = 'It is unlikely to arrive in ' + days + ' days. ' + message; 308 | } else if (days < 7) { 309 | message = 'It may arrive in ' + days + ' days. ' + message; 310 | } else { 311 | message = 'It is likely to arrive in ' + days + ' days. ' + message; 312 | } 313 | 314 | return getResponseFromMessage(message); 315 | } 316 | 317 | function generateCarouselOfProductsResponse(products) { 318 | 319 | return { 320 | "attachment": { 321 | "type": "template", 322 | "payload": { 323 | "template_type": "generic", 324 | "elements": 325 | products.map(p => { 326 | let subtitle = '$' + p['price'].toFixed(2); 327 | if (p['pattern']) { 328 | subtitle = '(' + p['pattern'] + ') ' + subtitle; 329 | } 330 | 331 | return { 332 | title: p['title'], 333 | subtitle: subtitle, 334 | image_url: p['image_link'], 335 | buttons: [ 336 | { 337 | type: "postback", 338 | title: "Learn more", 339 | payload: `enquiry_product ${p['title']}` 340 | }, 341 | { 342 | type: "postback", 343 | title: `Add to cart`, 344 | payload: `cart_add 1 ${p['pid']} ${p['title']}` 345 | } 346 | ] 347 | } 348 | }) 349 | } 350 | } 351 | } 352 | 353 | } 354 | 355 | async function processPayload(sender_psid, payload) { 356 | let payload_parts = payload.split(' '); 357 | // console.log(payload_parts); 358 | let intent = payload_parts.shift(); 359 | 360 | switch(intent) { 361 | case 'cart_add': 362 | // Example of payload: cart_add 1 123 Earl Grey Sunflower Seeds Cookies 363 | let quantity = payload_parts.shift(); 364 | let pid = payload_parts.shift(); 365 | let product_name = payload_parts.join(' '); 366 | 367 | // For Option 1 only: Update cart in database 368 | 369 | return generateAddCartQuickRepliesResponse('Added ' + quantity + ' ' + product_name + ' to cart.'); 370 | case 'recommendation': 371 | // For Option 1 only: Get products from database and assign it to a variable named "products" 372 | 373 | return generateCarouselOfProductsResponse(products); 374 | case 'cart_view': 375 | // For Option 1 only: Get cart from database and assign it to a variable named "cart" 376 | 377 | return generateCartResponse(cart); 378 | case 'checkout': 379 | // For Option 1 only: Get order from database and assign it to a variable named "order" 380 | 381 | // For Option 1 only: Add to order and delete cart from database 382 | 383 | return generateCheckoutResponse(order); 384 | case 'paid': 385 | // For Option 1 only: Get latest order from database and assign it to a variable named "order" 386 | 387 | return await generateReceiptResponse(sender_psid, order); 388 | default: 389 | return getDefaultResponse(); 390 | } 391 | 392 | } 393 | 394 | function generateAddCartQuickRepliesResponse(message) { 395 | return { 396 | "text": message, 397 | "quick_replies": [ 398 | { 399 | "content_type": "text", 400 | "title": "Checkout", 401 | "payload": "checkout" 402 | }, 403 | { 404 | "content_type": "text", 405 | "title": "View more products", 406 | "payload": "recommendation" 407 | }, { 408 | "content_type": "text", 409 | "title": 'View cart', 410 | "payload": "cart_view" 411 | } 412 | ] 413 | } 414 | 415 | } 416 | 417 | function generateCartResponse(cart) { 418 | 419 | return { 420 | "attachment": { 421 | "type": "template", 422 | "payload": { 423 | "template_type": "generic", 424 | "elements": 425 | cart.map(p => { 426 | let subtitle = 'Qty: ' + p['quantity'] + ' ($' + p['price'].toFixed(2) + ' each)'; 427 | if (p['pattern']) { 428 | subtitle = p['pattern'] + ', ' + subtitle; 429 | } 430 | 431 | return { 432 | title: p['title'], 433 | subtitle: subtitle, 434 | image_url: p['image_link'], 435 | buttons: [ 436 | { 437 | type: "postback", 438 | title: "Add 1 to Cart", 439 | payload: `cart_add 1 ${p['pid']} ${p['title']}` 440 | }, 441 | { 442 | type: "postback", 443 | title: `Remove All`, 444 | payload: `cart_remove_all ${p['pid']} ${p['title']}` 445 | } 446 | ] 447 | } 448 | }) 449 | } 450 | } 451 | } 452 | 453 | } 454 | 455 | function generateCheckoutResponse(order) { 456 | let total_price = order.reduce((acc, p) => acc + p['price'] * p['quantity'], 0); 457 | 458 | return { 459 | attachment: { 460 | type: "template", 461 | payload: { 462 | template_type: "button", 463 | text: `Your order (including shipping) will be $${(total_price + 5).toFixed(2)}.\n\nYou will be contributing to ${Math.ceil(total_price / 5)} meals for our beneficiaries.`, 464 | buttons: [ 465 | { 466 | type: "postback", 467 | title: "Proceed to Pay", 468 | payload: "paid" 469 | } 470 | ] 471 | } 472 | } 473 | } 474 | 475 | } 476 | 477 | async function generateReceiptResponse(sender_psid, order) { 478 | let total_price = order.reduce((acc, p) => acc + p['price'] * p['quantity'], 0); 479 | let name = await getName(PAGE_ACCESS_TOKEN,sender_psid); 480 | 481 | return { 482 | attachment: { 483 | type: "template", 484 | payload: { 485 | template_type: "receipt", 486 | recipient_name: name, 487 | order_number: "bf23ad46d123", 488 | currency: "SGD", 489 | payment_method: "PayPal", 490 | order_url: "", 491 | address: { 492 | street_1: "9 Straits View", 493 | city: "Singapore", 494 | postal_code: "018937", 495 | state: "SG", 496 | country: "SG" 497 | }, 498 | summary: { 499 | subtotal: total_price.toFixed(2), 500 | shipping_cost: 5, 501 | total_tax: ((total_price+5)*0.07).toFixed(2), 502 | total_cost: (total_price+5).toFixed(2) 503 | }, 504 | elements: order.map(product => { 505 | return { 506 | title: `${product["name"]}`, 507 | title: product["title"], 508 | subtitle: product["pattern"], 509 | quantity: product["quantity"], 510 | price: product["price"]*product["quantity"], 511 | currency: "SGD", 512 | image_url: product["image_link"] 513 | }; 514 | }) 515 | } 516 | } 517 | }; 518 | } 519 | 520 | async function getName(PAGE_ACCESS_TOKEN, sender_psid) { 521 | let uri = "https://graph.facebook.com/v8.0/" 522 | let response = await fetch(uri + sender_psid + "?fields=first_name&access_token=" + PAGE_ACCESS_TOKEN); 523 | if (response.ok) { 524 | let body = await response.json(); 525 | return body.first_name; 526 | } 527 | 528 | // Returns default name if name is not able to be retrieved 529 | return "John Doe"; 530 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Create Smarter Messenger Experiences on Facebook with Bright Commerce Chatbot 2 | 3 | **Table of Contents** 4 | - [Create Smarter Messenger Experiences on Facebook with Bright Commerce Chatbot](#create-smarter-messenger-experiences-on-facebook-with-bright-commerce-chatbot) 5 | - [Introduction](#introduction) 6 | - [Pre-Requisites](#pre-requisites) 7 | - [JavaScript Knowledge](#javascript-knowledge) 8 | - [Git Knowledge](#git-knowledge) 9 | - [Facebook Page](#facebook-page) 10 | - [Optional: Add a “Send Message” Button](#optional-add-a-send-message-button) 11 | - [Optional: Create a Greeting](#optional-create-a-greeting) 12 | - [Optional: Create Frequently Asked Questions and Responses](#optional-create-frequently-asked-questions-and-responses) 13 | - [Connect your Webhook onto your Facebook for Developers App](#connect-your-webhook-onto-your-facebook-for-developers-app) 14 | - [Create an App on Facebook for Developers](#create-an-app-on-facebook-for-developers) 15 | - [Create the Webhook](#create-the-webhook) 16 | - [Hosting on Heroku](#hosting-on-heroku) 17 | - [Link the Webhook with your Facebook for Developers App](#link-the-webhook-with-your-facebook-for-developers-app) 18 | - [Sending and Receiving Messages](#sending-and-receiving-messages) 19 | - [Receiving and Sending Back the Same Message](#receiving-and-sending-back-the-same-message) 20 | - [Sending Standard Responses](#sending-standard-responses) 21 | - [Introduction to `Wit.ai` Natural Language Processing (NLP)](#introduction-to-witai-natural-language-processing-nlp) 22 | - [Creating a Customised `Wit.ai` Application](#creating-a-customised-witai-application) 23 | - [Connecting the Custom `Wit.ai` NLP Model to your Facebook for Developers App](#connecting-the-custom-witai-nlp-model-to-your-facebook-for-developers-app) 24 | - [Example of a Built-in Trait](#example-of-a-built-in-trait) 25 | - [Training the Model to Understand the Intent of a Message](#training-the-model-to-understand-the-intent-of-a-message) 26 | - [Training the Model to Understand the Entity in a Message](#training-the-model-to-understand-the-entity-in-a-message) 27 | - [Adding more Training Data](#adding-more-training-data) 28 | - [Making Changes in the Management Tab](#making-changes-in-the-management-tab) 29 | - [Evaluating Performance and Additional Training](#evaluating-performance-and-additional-training) 30 | - [Reviewing Confidence Levels](#reviewing-confidence-levels) 31 | - [Retraining the Model to Detect New Terms](#retraining-the-model-to-detect-new-terms) 32 | - [Training with Inputs from Users](#training-with-inputs-from-users) 33 | - [More on `Wit.ai` Integration with Facebook Messenger](#more-on-witai-integration-with-facebook-messenger) 34 | - [Processing Messages with NLP](#processing-messages-with-nlp) 35 | - [Handling the Greeting Trait](#handling-the-greeting-trait) 36 | - [Handling the General Enquiry Intent](#handling-the-general-enquiry-intent) 37 | - [Handling the Delivery Enquiry Intent](#handling-the-delivery-enquiry-intent) 38 | - [Working with `Wit.ai` and Messenger](#working-with-witai-and-messenger) 39 | - [Personalising Responses with the Value of `wit/datetime` Built-In Entity](#personalising-responses-with-the-value-of-witdatetime-built-in-entity) 40 | - [Option 1: Creating a Business Database](#option-1-creating-a-business-database) 41 | - [Option 2: Adding Mock Commerce Data in `index.js`](#option-2-adding-mock-commerce-data-in-indexjs) 42 | - [Solution Overview](#solution-overview) 43 | - [Listing Products](#listing-products) 44 | - [Introduction to Templates](#introduction-to-templates) 45 | - [Updating `processMessage`](#updating-processmessage) 46 | - [Adding to Cart](#adding-to-cart) 47 | - [Deciphering Postbacks and Accessing Payload](#deciphering-postbacks-and-accessing-payload) 48 | - [Processing the Postback Payload](#processing-the-postback-payload) 49 | - [Following Up with Quick Replies](#following-up-with-quick-replies) 50 | - [Checkout](#checkout) 51 | - [Mocking Payment with Button Template](#mocking-payment-with-button-template) 52 | - [Issuing a Receipt with the Receipt Template](#issuing-a-receipt-with-the-receipt-template) 53 | - [Personalising Replies with User's First Name using Facebook Graph API](#personalising-replies-with-users-first-name-using-facebook-graph-api) 54 | - [Challenge 1: Order Enquiry](#challenge-1-order-enquiry) 55 | - [Challenge 2: Implement Product Enquiries](#challenge-2-implement-product-enquiries) 56 | - [Wrapping Up](#wrapping-up) 57 | - [Acknowledgements](#acknowledgements) 58 | - [References](#references) 59 | - [Tutorial GitHub Link](#tutorial-github-link) 60 | - [Devpost Submission](#devpost-submission) 61 | 62 | ## Introduction 63 | 64 | Chatbots are the face of customer service across many businesses as it is a form of automation that enables them to free up manpower for other operations. Businesses can greatly benefit from these efficiency gains and alleviate their lack of manpower through automating their chat services. 65 | 66 | Consumers are increasingly using ecommerce sites to replace their everyday shopping needs. Some users prefer human-like interactions in their shopping experiences and the technologies used to build chatbots have become intelligent enough to create immersive chat experiences without human intervention. 67 | 68 | We felt that this business case would be a good example to showcase the functionalities of a Messenger chatbot and introduce budding chatbot developers to the tools needed to get started on their own messaging commerce platforms using their Facebook Pages and `Wit.ai`. 69 | 70 | ## Pre-Requisites 71 | 72 | The following subsections list the background knowledge and pre-requisites to create a chatbot. 73 | 74 | ### JavaScript Knowledge 75 | 76 | Basic Javascript knowledge on objects, conditional statements, and functions will be helpful in the implementation of Bright chatbot. Specific knowledge on Node.js is also used in this tutorial, which acts as the server to handle communications between Bright and Facebook. 77 | 78 | Browse the following resources below to learn more about the technologies used: 79 | - JavaScript: https://javascript.info/ 80 | - Basic Node.js: https://www.w3schools.com/nodejs/ 81 | 82 | ### Git Knowledge 83 | 84 | Git is a version control system that will be mainly used to update code used to build the chatbot. More information on Git can be found on https://git-scm.com/ 85 | 86 | We recommend versioning your code on a GitHub repository. 87 | 88 | ### Facebook Page 89 | Creating a Facebook Page is a key step in your journey to creating a Messenger experience. Here are a few simple steps to help you get started: 90 | 91 | 1. Go to https://www.facebook.com/pages/create - it should lead you to a page shown below 92 | 93 | ![](images/create_page.jpg) 94 | 95 | 2. Fill in your page information 96 | 3. Add images - adding a profile and cover image is important to allow others to identify your business 97 | 98 | Congratulations! You have completed the setup of your Facebook Business Page! Next up, is on how to create a Messenger Bot for your page! 99 | 100 | ### Optional: Add a “Send Message” Button 101 | After creating your very own Facebook Page for your business, you will want a way to communicate with your customers. Before even going into the details on how to create a chatbot, what better way is there to direct users to send messages to you when they visit your page than having a 'Send Message' button? 102 | 103 | This button should be found at the top of your page. Click on **Add a Button** and select the **Send Message** option. 104 | 105 | ![](images/send_message_button.jpg) 106 | 107 | ### Optional: Create a Greeting 108 | 109 | Customize your personal message to your customers when they first start communicating with your page through Messenger. So how do we go about doing this? 110 | 111 | 1. Go to your page’s General Settings and select **Messaging** 112 | 2. Scroll down and turn on **Show a greeting** 113 | 3. Customise your greeting and click **Change** 114 | 115 | ### Optional: Create Frequently Asked Questions and Responses 116 | 117 | Create a set of creative responses for frequently asked questions regarding your business. This is highly recommended before creating your chatbot as it helps you brainstorm on what customers may ask and lets you implement various prompts for users to interact with your page on Messenger. 118 | 119 | Here is how you can do it: 120 | 121 | 1. Under Manage Page, select **Inbox** 122 | 2. In the left menu sidebar, select **Automated Responses** 123 | 3. Enable **Frequently Asked Questions** 124 | 4. Click **Edit** 125 | 5. Add questions and responses (remember to save your responses with the **Save** button on the top right!) 126 | 127 | ### Connect your Webhook onto your Facebook for Developers App 128 | 129 | These are the important few steps to getting started before you can develop your chatbot to serve your users using code. 130 | 131 | #### Create an App on Facebook for Developers 132 | 133 | 1. Head to https://developers.facebook.com, click on **My Apps** 134 | 2. Click on **Create App**, located on the top right of the screen 135 | 3. Select **Manage Business Integration** 136 | 4. Enter your **App Display Name** and **App Purpose** 137 | 5. After creating the app, on the side bar, click on add products, leading you to the **Add Products** section on the main page. Find the **Messenger** product and click **Set Up** 138 | 139 | Next, the following steps will be detailed in the next two sections. To summarise: 140 | 141 | 6. You will have to generate a page access token by adding a new page to the Messenger product, after clicking on the **Add or Remove Pages** button, select your Facebook page that you would like to add. After adding the page, you will be able to see a **Generate Token** button, which will give you a page access token that will be used on your Node.js for communication with Facebook via the Send API. 142 | 7. Set up a callback URL by entering your callback URL (location where your Node.js server is hosted) and also enter the verify token (which is a random string) that is determined by you in the Node.js server during the Node.js server setup. Lastly, click on **Verify and Save** to confirm your callback URL. 143 | 8. Now you’re ready to test the app, go to Messenger, and type a message to your Page. If your Node.js server (with the callback URL) receives a webhook event, you’ve successfully set up your app to receive messages! 144 | 145 | #### Create the Webhook 146 | 147 | Webhooks are API endpoints which you expose, to allow client(s) to connect to. This allows the users to interact to either send data or collect data from your servers. In this case, we are using this endpoint as an interaction between your webhook and your Facebook for Developers App, to send and receive data from our chatbot on your Facebook business page. 148 | 149 | 1. Create a directory (folder), and navigate into it via the command line 150 | 2. Next, create an empty, `index.js` file in your project folder 151 | 3. Issue the command, `npm init` on the command line, accept default for all questions, and this will create `package.json` in your project folder automatically. 152 | 4. Issue the command, `npm install express body-parser --save`, this installs the Express.js http server framework module, and add them as dependencies in your `package.json` file 153 | 5. Create a server that listens for communication at the default port by adding the following code below into your `index.js` file 154 | 155 | ```js 156 | 'use strict'; 157 | 158 | // Imports dependencies and set up http server 159 | const 160 | express = require('express'), 161 | bodyParser = require('body-parser'), 162 | app = express().use(bodyParser.json()), // creates express http server 163 | dotenv = require('dotenv'); 164 | 165 | dotenv.config(); 166 | 167 | // Sets server port and logs message on success 168 | const listener = app.listen(process.env.PORT || 1337, () => console.log('webhook is listening on port ' + listener.address().port)); 169 | 170 | app.get('/', (req, res) => { 171 | res.status(200).send('You are connected to the chatbot application.'); 172 | }) 173 | ``` 174 | 175 | 6. Let us now create a `GET` request `/webhook` endpoint for your Facebook for Developers App to verify the webhook. We suggest adding your verification token in a `.env` file. 176 | 177 | ```js 178 | // Adds support for GET requests to our webhook 179 | app.get('/webhook', (req, res) => { 180 | 181 | // Your verify token. Should be a string you set. 182 | let VERIFY_TOKEN = process.env.VERIFY_TOKEN; 183 | 184 | // Parse the query params 185 | let mode = req.query['hub.mode']; 186 | let token = req.query['hub.verify_token']; 187 | let challenge = req.query['hub.challenge']; 188 | 189 | // Checks if a token and mode is in the query string of the request 190 | if (mode && token) { 191 | 192 | // Checks the mode and token sent is correct 193 | if (mode === 'subscribe' && token === VERIFY_TOKEN) { 194 | 195 | // Responds with the challenge token from the request 196 | console.log('WEBHOOK_VERIFIED'); 197 | res.status(200).send(challenge); 198 | 199 | } else { 200 | // Responds with '403 Forbidden' if verify tokens do not match 201 | res.sendStatus(403); 202 | } 203 | } 204 | }); 205 | ``` 206 | 207 | 7. Finally, you can now test your webhook by issuing the curl command (replace the `` with your own token that you had specified in the previous step: 208 | 209 | ```sh 210 | curl -X GET "localhost:1337/webhook?hub.verify_token=&hub.challenge=CHALLENGE_ACCEPTED&hub.mode=subscribe" 211 | ``` 212 | 213 | #### Hosting on Heroku 214 | 215 | As the chatbot's callback URL requires a HTTPS endpoint, we recommend hosting the chatbot on Heroku. The guide on how to deploy can be found at https://devcenter.heroku.com/articles/getting-started-with-nodejs?singlepage=true 216 | 217 | Next, to connect to a Git repository, you can utilise the GitHub integration in Heroku, which allows for automatic deployment whenever your `main` branch is updated https://devcenter.heroku.com/articles/github-integration 218 | 219 | #### Link the Webhook with your Facebook for Developers App 220 | 221 | So how can you connect with Facebook? 222 | 223 | 1. Go to your Application that you have created earlier in https://developers.facebook.com 224 | 2. Select **Messenger** and click **Settings** 225 | 3. Insert your server’s **Callback URL**, followed by `/webhook`. In this tutorial the webhook endpoint is `https:///webhook`. Note here that the URL has to be served with HTTPS, or else you would not be able to connect to Facebook 226 | 227 | Note that the verify token is like a shared secret that Facebook has between you and your application for them to authenticate the connection to the callback URL. 228 | 229 | This is how it looks like if you have successfully added the webhook callback URL: 230 | 231 | ![](images/link_webhook_to_facebook.jpg) 232 | 233 | There you have it, you have successfully connected your chatbot application with Facebook! 234 | 235 | ## Sending and Receiving Messages 236 | 237 | Sending messages require the use of Send API via a POST request coupled with your page access token included in your URL query string. 238 | 239 | Below is a wrapper function that takes in the sender’s identifier (called Page-scoped ID, or PSID in short) and the response object. This function converts parameters into the standardised data structure and proceeds to send the response. 240 | 241 | ```js 242 | // Sends response messages via the Send API 243 | function callSendAPI(sender_psid, response) { 244 | 245 | // Construct the message body 246 | let request_body = { 247 | messaging_type: 'RESPONSE', 248 | recipient: { 249 | id: sender_psid 250 | }, 251 | message: response 252 | }; 253 | 254 | // Send the HTTP request to the Messenger Platform 255 | request( 256 | { 257 | uri: "https://graph.facebook.com/v8.0/me/messages", 258 | qs: { access_token: process.env.PAGE_ACCESS_TOKEN }, 259 | method: "POST", 260 | json: request_body 261 | }, 262 | (err, res, body) => { 263 | if (!err) { 264 | console.log("Message sent!"); 265 | } else { 266 | console.error("Unable to send message: " + err); 267 | } 268 | } 269 | ); 270 | } 271 | ``` 272 | 273 | In the `request_body`, the recipient’s ID is the sender of the messenger, which can be referenced by the PSID. This allows us to specify which user the message response is for, and for the Messenger Platform to route to the right user. The message attribute in the `request_body` simply refers to the response of the message that you will like to send back. 274 | 275 | In order to make a HTTP request to the Messenger Platform, we will use the request package. More information on that package can be found here: https://www.npmjs.com/package/request. 276 | 277 | The request object takes in 278 | - `uri`: The URI of the Send API 279 | - `qs`: Query string parameters that will be attached to the URL 280 | - `PAGE_ACCESS_TOKEN` 281 | - `json`: Request body JSON that you had created 282 | 283 | To get the `PAGE_ACCESS_TOKEN`, navigate to your App on Facebook for Developers. Under **Access Tokens**, click **Add or Remove Pages**. Add your page and click **Generate Token**. Read and check **I Understand**, then create a `.env` file and paste the token. 284 | 285 | Your `.env` file should now look like this: 286 | ``` 287 | PAGE_ACCESS_TOKEN= 288 | VERIFY_TOKEN= 289 | ``` 290 | 291 | Create a `.gitignore` and add `.env` inside (if you have not). Also, [add the variables to Heroku](https://devcenter.heroku.com/articles/config-vars). In our script, we the `dotenv` package is used to configure and retrieve the page access token. 292 | 293 | The start of your `index.js` should look like this (remember to add the new dependencies in `package.json` too): 294 | 295 | ```js 296 | // Imports dependencies and set up http server 297 | const 298 | express = require('express'), 299 | bodyParser = require('body-parser'), 300 | app = express().use(bodyParser.json()), // creates express http server 301 | dotenv = require('dotenv'), 302 | request = require('request'); 303 | 304 | dotenv.config(); 305 | ``` 306 | 307 | Once all these components are in place, you’re ready to execute standard responses back to the sender. Let us try automating some standard responses! 308 | 309 | We first define a wrapper function `sendMessage` that takes in `sender_psid` and `message` (of string type), converts the string to a response object, then sends the message. This function might seem unnecessary now, but is definitely useful when the scale of the chatbot gets larger. 310 | 311 | ```js 312 | // Wrapper method to convert text message string to response object, and sends the message 313 | function getResponseFromMessage(message) { 314 | const response = { 315 | text: message 316 | }; 317 | 318 | return response; 319 | } 320 | ``` 321 | 322 | ### Receiving and Sending Back the Same Message 323 | We will first start by responding to the same message as what the user sent. 324 | 325 | ```js 326 | // Creates the endpoint for our webhook 327 | app.post('/webhook', (req, res) => { 328 | 329 | let body = req.body; 330 | 331 | // Checks this is an event from a page subscription 332 | if (body.object === 'page') { 333 | 334 | // Iterates over each entry - there may be multiple if batched 335 | body.entry.forEach(function(entry) { 336 | 337 | // Gets the message. entry.messaging is an array, but 338 | // will only ever contain one message, so we get index 0 339 | let webhook_event = entry.messaging[0]; 340 | let sender_psid = webhook_event['sender']['id']; 341 | let message = webhook_event['message']['text']; 342 | console.log('Message received from sender ' + sender_psid + ' : ' + message); 343 | 344 | let response = getResponseFromMessage(message); 345 | callSendAPI(sender_psid, response); 346 | }); 347 | 348 | // Returns a '200 OK' response to all requests 349 | res.status(200).send('EVENT_RECEIVED'); 350 | } else { 351 | // Returns a '404 Not Found' if event is not from a page subscription 352 | res.sendStatus(404); 353 | } 354 | 355 | }); 356 | ``` 357 | 358 | Try it out! You should see the following behaviour: 359 | 360 | ![](images/send_same_message.png) 361 | 362 | ### Sending Standard Responses 363 | 364 | Now let us implement some standard reponses. First, replace `sendMessage` with a new `processMessage` function that will be implemented. 365 | 366 | The `processMessage` function encapsulates the implementation of the standard responses. 367 | 368 | ```js 369 | function processMessage(message) { 370 | 371 | message = message.toLowerCase(); 372 | 373 | let responses = { 374 | hi: "Hi there! Welcome to Bright. How can I help you?", 375 | bright: "Bright is a social enterprise where we provide vocational training to adults with intellectual disabilities.\n\n" + 376 | "We started a range of social enterprise projects to provide alternative work engagement opportunities for our adult trainees. " + 377 | "Some of the projects began as therapy programmes which encourage the development of fine motor skills; others provide a realistic vocational training environment.\n\n" + 378 | "All net revenue earned from the sale of our products and services go towards paying a monthly allowance for our clients' work, as well as their lunch expenses while undergoing training.", 379 | proceeds: 380 | "All net revenue earned from the sale of our products and services go towards paying a monthly allowance for our clients' work, as well as their lunch expenses while undergoing training.", 381 | support: 382 | "We support adults with intellectual disabilities. We started a range of social enterprise projects to provide alternative work engagement for our adult trainees.", 383 | sell: "We sell craft and baker goods.\nLike our Facebook page http://fb.me/brightsocialsg to stay updated!", 384 | safe: "Our cookies are made by our clients in a clean and sanitised environment. The cookies are safe to consume before the expiry date that is printed on the packaging." 385 | }; 386 | 387 | for (const key in responses) { 388 | if (message.includes(key)) { 389 | return getResponseFromMessage(responses[key]); 390 | break; 391 | } 392 | } 393 | 394 | // Message does not match any keyword, send default response 395 | return getResponseFromMessage("We could not understand your message. Kindly rephrase your message and send us again."); 396 | 397 | } 398 | ``` 399 | 400 | Now, whenever your message includes any of those keywords in the `responses` object, a standard response message will be sent. 401 | 402 | ## Introduction to `Wit.ai` Natural Language Processing (NLP) 403 | 404 | `Wit.ai` is a NLP service that provides a user-friendly interface to facilitate training your Facebook Messenger chatbot. Using its own NLP engine, `Wit.ai` is essentially used to build the “brain” of Bright chatbot. 405 | 406 | ### Creating a Customised `Wit.ai` Application 407 | 408 | Sign in to `Wit.ai` using Facebook and click on **New App**. 409 | 410 | ![](images/4_1.jpg) 411 | 412 | Let's name our application **BrightChatbot** and make its visibility **Private**. Click on **Create**. 413 | 414 | ![](images/4_2.jpg) 415 | 416 | Congratulations, you’ve now made your first `Wit.ai` application. 417 | 418 | ### Connecting the Custom `Wit.ai` NLP Model to your Facebook for Developers App 419 | 420 | We will now connect the NLP model to your Facebook for Developers application. This is done by going to your Facebook for Developers application’s Messenger Settings page and toggling the built-in NLP option for your page under the **Built-In NLP** section. 421 | 422 | ![](images/nlp_select_page.jpg) 423 | 424 | Although Messenger comes with a default NLP engine with `Wit.ai`, we are training our own custom NLP model on `Wit.ai`. 425 | 426 | To connect your `Wit.ai` model to your Facebook for Developers application, you will first need to generate a **Server Access Token**. Generating your Server Access Token is simple. In `Wit.ai`, click on the **Settings** on the left tab (under **Management**). 427 | 428 | ![](images/b2.jpg) 429 | 430 | Under **Server Access Token**, click **Generate New Token** and your access token will be generated. 431 | 432 | ![](images/b3.jpg) 433 | 434 | Copy the Access Token and head over to the Facebook for Developers application's **Messenger Settings** page. Then, under **Built-In NLP**, click on the dropdown for **Default Language Model** and select **Custom Model**. Paste the server access token into the textbox field. 435 | 436 | ![](images/nlp_add_token.jpg) 437 | 438 | Congratulations, your `Wit.ai` NLP model has been successfully connected to your Facebook chatbot application! 439 | 440 | We will now spending some time training our `Wit.ai` model to understand our custom intents and entities for our use case, to make our chatbot more capable of handling the messages it may face. 441 | 442 | ### Example of a Built-in Trait 443 | 444 | Back to the `Wit.ai` model, you will see three tabs titled **Understanding**, **Management** and **Insights** on the left. Let’s start by training our chatbot on the **Understanding** tab. 445 | 446 | ![](images/4_3.jpg) 447 | 448 | Click the **Understanding** tab. A sentence, such as a question to the chatbot, is known as an **utterance**. The intention behind this utterance is quite simply, the **intent**. 449 | 450 | `Wit.ai` has built-in entities that can be readily used for a number of common conversation topics. We often start off a conversation with a greeting and `Wit.ai` has a corresponding trait called `wit/greetings`. Traits are used to capture intents from the whole utterance. 451 | Our `Wit.ai` application is pre-trained to identify a number common greetings. Whether you type “hello”, “hi” or even “aloha” in the Utterance section, the trait will be identified as `wit/greetings`. 452 | 453 | ![](images/4_4.jpg) 454 | 455 | ### Training the Model to Understand the Intent of a Message 456 | 457 | Let’s train the chatbot to understand a generic question seeking a product recommendation. 458 | 459 | In the **Utterance** tab, type the utterance “I want to buy something”. Click the **Choose or add intent** drop-down list and create new intent **recommendation**. 460 | 461 | ![](images/4_5.jpg) 462 | 463 | Click **Train and Validate**. 464 | - You will see **Training scheduled** followed by **Training ongoing** on the top right-hand corner as `Wit.ai` trains to identify the recommendation intent from your sentence 465 | - **Training complete** is shown when the training is finished 466 | 467 | ![](images/4_6.jpg) 468 | 469 | During the training process, there is no need to wait for the previous utterance’s training to complete before training new utterances. You can keep training new utterances back-to-back as the training process for each utterance can run in parallel on `Wit.ai`. 470 | 471 | ### Training the Model to Understand the Entity in a Message 472 | 473 | What if instead of a general recommendation, the buyer wants to specifically buy a type of product such as cookies? We classify "cookies" as `baker` type products and would need the chatbot to recognise "cookies" as the `product_type` entity. 474 | 475 | Set utterance as “What types of cookies do you have?” 476 | The intent is `recommendation` by default as that’s the only intent we have trained so far. 477 | 478 | To identify the intent, highlight "cookies": 479 | 480 | ![](images/4_7.jpg) 481 | 482 | Create the entity `product_type`. The entity is now shown prior to training. For more complicated sentences with multiple entities, you can highlight and label them one by one. 483 | 484 | Change the **Resolved value** to `baker` as we want the cookies to be identified as being in the baker category. 485 | 486 | ![](images/4_8.jpg) 487 | 488 | Now **Train and Validate**. The chatbot’s learning is not limited to only the utterances shared through **Understand**. The model itself will be able to infer additional ways the same question or utterance can be asked based on the few examples provided by you. 489 | 490 | ### Adding more Training Data 491 | 492 | Let’s train the chatbot to understand more utterances and intents to help it better understand future queries. Train it with the following: 493 | 494 | | Utterance | Intent | 495 | | --- | --- | 496 | | How do I know if they are safe to consume? | `enquiry_general` | 497 | | I need help | `enquiry_general` | 498 | | When is delivery coming? | `enquiry_delivery` | 499 | | When is my order arriving? | `enquiry_delivery` | 500 | | What kind of cookies do you have? | `recommendation` | 501 | | Can you send me the list of products here to choose? | `recommendation` | 502 | 503 | We have added in a few more `recommendation` intents as well, to help the model further differentiate between the three unique entities. 504 | 505 | ### Making Changes in the Management Tab 506 | 507 | Oops, we forgot to add entities to the sentences in bold in the table we used for training earlier! Let’s add the entities using the **Management** tab. 508 | 509 | Click on **Management**, then **Utterances**. 510 | 511 | ![](images/4_9.jpg) 512 | 513 | Find the sentence “How do I know if they are safe to consume” that you have previously trained the chatbot with and click on the down arrow beside it. 514 | 515 | ![](images/4_10.jpg) 516 | 517 | You can now add entities to it! Highlight the word "safe" and add an entity called `safety`. 518 | 519 | ![](images/4_11.jpg) 520 | 521 | Now, train! That’s really all there is to training your chatbot and managing it on `Wit.ai`. 522 | 523 | Deletions of any intents, entities and utterances can be handled in the management tab and their respective sections. 524 | 525 | Before we conclude with a test of our training’s accuracy, update the entities for the following two sentences that we have previously trained: 526 | - When is my delivery coming? 527 | - When is my order arriving? 528 | 529 | Set their entities to `estimated_arrival` for the text "when". 530 | 531 | ### Evaluating Performance and Additional Training 532 | 533 | Test your chatbot and see whether it can correctly identify the intents for these questions. 534 | 535 | | Utterance | Actual Intent | 536 | | --- | --- | 537 | | What would you suggest? | `recommendation` | 538 | | When will my order be here? | `enquiry_delivery` | 539 | 540 | In this scenario, it should be a success! However, it is completely alright if the model does not recognise all of the intents correctly. In machine learning, the model will only work well with lots and lots of training. Therefore, do remember to train the model with these new data! 541 | 542 | #### Reviewing Confidence Levels 543 | 544 | We previously trained the chatbot to identify the word "cookies" as being under the entity `product_type`, with a resolved value of `baker`. This is meant to represent baked goods. 545 | 546 | Let's evaluate the chatbot with this sentence “Do you have many types of cookies?” 547 | 548 | ![](images/a1.jpg) 549 | 550 | You will see that while the model is able to correctly identify the intent and entity, it has a confidence of 79%. This means that the model is only 79% confident that it has correctly identified the entity within the sentence. By adding more training data (i.e. more utterances) to train the model, you can increase its confidence level on a wider array of inputs from consumers. 551 | 552 | Note that you can also view the confidence of intents by clicking on the intent dropdown. 553 | 554 | ![](images/b1.jpg) 555 | 556 | #### Retraining the Model to Detect New Terms 557 | 558 | What happens if the model is unable to identify the intent or entity (or both!) from an utterance? 559 | 560 | Evaluate the model with the sentence “What type of cookie is there?” 561 | The model will correctly identify this as being under the intent `recommendation` but it cannot match "cookie" to the entity `product_type`. 562 | 563 | ![](images/a3.jpg) 564 | 565 | To make your model understand these new terms, remember to continuously train it with a larger number of utterances using the steps shown previously in [Training the Model to Understand the Intent of a Message](#training-the-model-to-understand-the-intent-of-a-message) and [Training the Model to Understand the Entity in a Message](#training-the-model-to-understand-the-entity-in-a-message) sections. 566 | 567 | Now you must be thinking, where am I going to find the time to come up with so many different utterances that users could possibly ask? Fret not, as the `Wit.ai` platform records these messages as they are sent to the bot! 568 | 569 | ### Training with Inputs from Users 570 | 571 | > Messages sent to the Facebook Messenger chatbot will only be received by the `Wit.ai` model after the `Wit.ai` application has been connected to the Facebook Messenger application. We have shown you how to connect them in an earlier section of our tutorial: [Connecting the Custom Wit.ai NLP Model to your Facebook for Developers App](#connecting-the-custom-witai-nlp-model-to-your-facebook-for-developers-app)! 572 | 573 | What is the best way to gather more data to train the model? Our answer: Get the questions from your customers directly! The beauty of `Wit.ai` is that every message that gets sent to your chatbot gets recorded on the **Understanding** tab that we used to train our model. 574 | 575 | ![](images/a4.jpg) 576 | 577 | You can simply click the down arrow beside each string of text to open additional options to **Train and Validate** that utterance (as per what you are already familiar with from the previous sections). 578 | 579 | ![](images/a6.jpg) 580 | 581 | Alternatively, if the utterance is irrelevant, you can mark it as **Out of Scope** (by checking the checkbox) before clicking on **Train and Validate**. 582 | 583 | ![](images/a7.jpg) 584 | 585 | Only label an utterance as **Out of Scope** if you want to train the model to recognise it as one that is not meant to be handled in the future. 586 | 587 | > Note that upon clicking the intent/entity dropdown, only trained intents and/or entities will be recommended. Should you want to train a new intent and/or entity, you will have to create it from scratch. 588 | 589 | This sums up training your chatbot with `Wit.ai`’s easy-to-use NLP model! 590 | 591 | ### More on `Wit.ai` Integration with Facebook Messenger 592 | 593 | You have finished training your model and integrating it with your Facebook chatbot application, great job! Here is a brief description of how the `Wit.ai` NLP model integrates with Facebook Messenger for those of you who are more curious. 594 | 595 | Messages that are received by the chatbot are inputed to the model to identify intents, entities and traits. When the model detects any of them, it ties each of them to a confidence value. 596 | 597 | A confidence value falls between 0 and 1, showing how confident the NLP model is of what has been detected. The closer the confidence is to 1, the more confident the model is of its interpretation. The more you train your model, the better it gets at identifying these values and the more confident it will be in interpreting messages coming from users interacting with your chatbot. 598 | 599 | Within your Facebook Messenger webhook, you can handle received messages by setting your own criteria for handling these responses based on the identified intents, entities and/or traits, which are usually based on `Wit.ai` model. The next section, processing messages with NLP, will discuss this in detail. 600 | 601 | ## Processing Messages with NLP 602 | 603 | Now we are ready to use your `Wit.ai` model (NLP model) outputs to process messages! Let us first understand how this works. When a user interacts with Bright, the message will first be sent to the NLP model. The outputs of the model will be sent in the message webhook and can be retrieved in the `nlp` key in the `message` object. 604 | 605 | ### Handling the Greeting Trait 606 | 607 | Let us modify our POST request function to retrieve the NLP model output and pass it in as a new, second argument in `processMessage`. 608 | 609 | ```js 610 | console.log('Message received from sender ' + sender_psid + ' : ' + message); 611 | 612 | let nlp = webhook_event['message']['nlp']; 613 | 614 | let response = processMessage(message, nlp); 615 | callSendAPI(sender_psid, response); 616 | ``` 617 | 618 | Then, wrap the default message in a new `getDefaultResponse` function, which will be used in early returns and when there are neither intents nor traits found in the message. 619 | 620 | ```js 621 | function getDefaultResponse() { 622 | return getResponseFromMessage("We could not understand your message. Kindly rephrase your message and send us again."); 623 | } 624 | ``` 625 | 626 | Next, we modify the `processMessage` function. 627 | 628 | ```js 629 | // Processes and sends text message 630 | function processMessage(message, nlp) { 631 | 632 | if (nlp['intents'].length === 0) { 633 | 634 | // Check if greeting 635 | let traits = nlp['traits']; 636 | 637 | if (traits['wit$greetings'] && traits['wit$greetings'][0]['value'] === 'true') { 638 | console.log('Is greeting'); 639 | return getResponseFromMessage('Hi there! Welcome to Bright. How can I help you?'); 640 | } 641 | 642 | console.log('Returning default response'); 643 | return getDefaultResponse(); 644 | } 645 | 646 | } 647 | ``` 648 | 649 | By utilising `Wit.ai`'s built-in greetings trait `wit/greetings`, we are able to leverage on the detection based on pretrained data, which means that we do not have to train greetings like "Hi" ourselves! 650 | 651 | ![](images/pretrained_greetings.png) 652 | 653 | ### Handling the General Enquiry Intent 654 | 655 | We will now train the messages that we have handled with the [standard responses](#sending-standard-responses) in the NLP model. All of the messages are of the same intent `enquiry_general`, but the entities of the general enquiry are different, which allows us to differentiate between the queries the user has. 656 | 657 | | Utterance | Intent | Phrase -> Entity | 658 | | --- | --- | --- | 659 | | Who is Bright? | `enquiry_general` | Bright -> `organisation` | 660 | | Where does your proceeds go to? | `enquiry_general` | proceeds -> `profit` | 661 | | Who makes the products? | `enquiry_general` | Who makes -> `manufacturer` | 662 | | What do you sell? | `enquiry_general` | sell -> `products` | 663 | | How do I know if they are safe to consume? | `enquiry_general` | safe -> `safety` | 664 | 665 | To handle general enquiries, we first modify the `processMessage` function to retrieve the `enquiry_general` intent and its entities, then abstract the handling of general enquiries into another function `handleGeneralEnquiry`. 666 | 667 | ```js 668 | // Processes and sends text message 669 | function processMessage(message, nlp) { 670 | 671 | if (nlp['intents'].length === 0) { 672 | ... 673 | } 674 | 675 | console.log('Intents inferred from NLP model: ') 676 | console.table(nlp['intents']); 677 | 678 | // Get the intent with the highest confidence 679 | let intent = nlp['intents'][0]['name'] 680 | let confidence = nlp['intents'][0]['confidence'] 681 | 682 | // If confidence of intent is less than threshold, do not process 683 | if (confidence < 0.7) return getDefaultResponse(); 684 | 685 | let entities = nlp['entities']; 686 | let highest_confidence = 0; 687 | 688 | switch (intent) { 689 | case 'enquiry_general': 690 | // Get entity with highest confidence 691 | let entity = null; 692 | for (const e in entities) { 693 | let confidence = entities[e][0]['confidence']; 694 | if (confidence > highest_confidence) { 695 | highest_confidence = confidence; 696 | entity = entities[e][0]['name']; 697 | } 698 | } 699 | 700 | console.log('Entity with highest confidence: ' + entity); 701 | 702 | return handleGeneralEnquiry(entity); 703 | default: 704 | return getDefaultResponse(); 705 | } 706 | 707 | } 708 | ``` 709 | 710 | We had set a arbitrary confidence threshold of 0.7 in order to prevent handling of messages with low confidence scores. 711 | 712 | ```js 713 | function handleGeneralEnquiry(entity) { 714 | 715 | if (entity == null) return getDefaultResponse(); 716 | 717 | let responses = { 718 | organisation: "Bright is a social enterprise where we provide vocational training to adults with intellectual disabilities.\n\n" + 719 | "We started a range of social enterprise projects to provide alternative work engagement opportunities for our adult trainees. " + 720 | "Some of the projects began as therapy programmes which encourage the development of fine motor skills; others provide a realistic vocational training environment.\n\n" + 721 | "All net revenue earned from the sale of our products and services go towards paying a monthly allowance for our clients' work, as well as their lunch expenses while undergoing training.", 722 | profit: 723 | "All net revenue earned from the sale of our products and services go towards paying a monthly allowance for our clients' work, as well as their lunch expenses while undergoing training.", 724 | manufacturer: 725 | "We support adults with intellectual disabilities. We started a range of social enterprise projects to provide alternative work engagement for our adult trainees.", 726 | products: "We sell craft and baker goods.\nLike our Facebook page http://fb.me/brightsocialsg to stay updated!", 727 | safety: "Our cookies are made by our clients in a clean and sanitised environment. The cookies are safe to consume before the expiry date that is printed on the packaging." 728 | }; 729 | 730 | return getResponseFromMessage(responses[entity]); 731 | } 732 | ``` 733 | 734 | ### Handling the Delivery Enquiry Intent 735 | 736 | Let us now work with messages of the `enquiry_delivery` intent. To recap, the following utterances have been trained to be classified as delivery enquiries: 737 | 738 | | Utterance | Intent | 739 | | --- | --- | 740 | | When is delivery coming? | `enquiry_delivery` | 741 | | When is my order arriving? | `enquiry_delivery` | 742 | | When will my order be here? | `enquiry_delivery` | 743 | 744 | Again, we will modify the `processMessage` function: 745 | 746 | ```js 747 | // Processes and sends text message 748 | function processMessage(message, nlp) { 749 | ... 750 | switch (intent) { 751 | ... 752 | 753 | case 'enquiry_delivery': 754 | return handleDeliveryEnquiry(); 755 | 756 | default: 757 | return getDefaultResponse(); 758 | } 759 | 760 | } 761 | ``` 762 | 763 | This time, we create the function `handleDeliveryEnquiry` to handle delivery enquiries: 764 | 765 | ```js 766 | function handleDeliveryEnquiry() { 767 | return getResponseFromMessage('We deliver islandwide. The average delivery time takes 5-7 days.'); 768 | } 769 | ``` 770 | 771 | Let's see if the chatbot can handle the message "Can my order arrive in 2 days?". It works! 772 | 773 | What about "Can I receive the package in 2 days?". Unfortunately, it is unable to correctly classify the intent based on the response. We will need to investigate why. 774 | 775 | ![](images/misclassified_message_and_response.jpg) 776 | 777 | #### Working with `Wit.ai` and Messenger 778 | 779 | The message "Can I receive the package in 2 days?" is wrongly classified as a general enquiry with a confidence score of 0.572 (do note that it is completely normal if your confidence score is different). 780 | 781 | Since the message is classified as a general enquiry, did the chatbot handle the message as a general response and return the default response? No. The default response was returned from the `processMessage` function, as the confidence score is [below the threshold of 0.7 that we had set in the previous section](#handling-the-general-enquiry-intent). 782 | 783 | Even though the model classified the message as a general enquiry, the confidence score of only 0.572 shows that it is unsure of the classification. 784 | 785 | So how can we view the confidence score? Previously, we implemented the following logging statements in the `processMessage` function: 786 | 787 | ```js 788 | console.log('Intents inferred from NLP model: ') 789 | console.table(nlp['intents']); 790 | ``` 791 | 792 | We can view the score in the webhook's logs [in Heroku](https://devcenter.heroku.com/articles/logging#log-retrieval-via-the-web-dashboard): 793 | ``` 794 | Intents inferred from NLP model: 795 | ┌─────────┬────────────────────┬───────────────────┬────────────┐ 796 | │ (index) │ id │ name │ confidence │ 797 | ├─────────┼────────────────────┼───────────────────┼────────────┤ 798 | │ 0 │ '1694432440724955' │ 'enquiry_general' │ 0.572 │ 799 | └─────────┴────────────────────┴───────────────────┴────────────┘ 800 | ``` 801 | 802 | Similarly, we see that the message is misclassified in the `Wit.ai` model's **Utterance** tab: 803 | 804 | ![](images/misclassified_utterance.jpg) 805 | 806 | Let us rectify the misclassified message. Select the **Intent dropdown**. From the dropdown, we can verify the confidence (57%) in `Wit.ai`. Click on `enquiry_delivery`, then click **Train and Validate**. 807 | 808 | ![](images/rectify_misclassified_utterance.jpg) 809 | 810 | In the chatbot, let us send the same message "Can I receive the package in 2 days?". A high confidence score can be seen in both the logs and on `Wit.ai`. 811 | 812 | ``` 813 | Intents inferred from NLP model: 814 | ┌─────────┬────────────────────┬───────────────────┬────────────┐ 815 | │ (index) │ id │ name │ confidence │ 816 | ├─────────┼────────────────────┼───────────────────┼────────────┤ 817 | │ 0 │ '381501166565779' │ 'enquiry_general' │ 0.9959 │ 818 | └─────────┴────────────────────┴───────────────────┴────────────┘ 819 | ``` 820 | 821 | ![](images/rectified_utterance.jpg) 822 | 823 | The message will be correctly handled. 824 | 825 | ![](images/rectified_message_handling.jpg) 826 | 827 | You have now learnt how the NLP model on `Wit.ai` is linked to the chatbot and how to build with them! 828 | 829 | #### Personalising Responses with the Value of `wit/datetime` Built-In Entity 830 | 831 | Notice that `Wit.ai` has a suggested a date and time entity `wit/datetime` in the "Can I receive the package in 2 days?" utterance? Similar to the greetings trait `wit/greetings`, the date and time entity is built-in. The list of built-in entities and traits can be found at https://wit.ai/docs/built-in-entities/ 832 | 833 | When we modify the `enquiry_delivery` case in the switch statement of `processMessage` to log the message's entities, an empty object `{}` is printed. 834 | 835 | ```js 836 | // Processes and sends text message 837 | function processMessage(message, nlp) { 838 | ... 839 | switch (intent) { 840 | ... 841 | 842 | case 'enquiry_delivery': 843 | console.log(nlp['entities']); 844 | return handleDeliveryEnquiry(); 845 | 846 | default: 847 | return getDefaultResponse(); 848 | } 849 | 850 | } 851 | ``` 852 | 853 | In the **Utterance** tab, under the "Can I receive the package in 2 days?" utterance, click **Add** to add the entity. When `nlp['entities']` is logged, the date and time entity will be shown: 854 | 855 | ```js 856 | { 'wit$datetime:datetime': 857 | [ { id: '222986959252376', 858 | name: 'wit$datetime', 859 | role: 'datetime', 860 | start: 26, 861 | end: 35, 862 | body: 'in 2 days', 863 | confidence: 0.9672, 864 | entities: [], 865 | type: 'value', 866 | grain: 'hour', 867 | value: '2020-11-30T22:00:00.000+08:00', 868 | values: [Array] } 869 | ] 870 | } 871 | ``` 872 | 873 | As of the date and time of writing (2020-11-28, 10pm Singapore time), the date and time two days later is 2020-11-30, 10pm. To calculate the date and time, we will need to retrieve the future date in the `values` attribute. 874 | 875 | We modify the `processMessage` function to pass the `entities` object as an argument in the `handleDeliveryEnquiry` function: 876 | 877 | ```js 878 | // Processes and sends text message 879 | function processMessage(message, nlp) { 880 | ... 881 | switch (intent) { 882 | ... 883 | 884 | case 'enquiry_delivery': 885 | // Get value with highest confidence 886 | let value = null; 887 | if ('wit$datetime:datetime' in entities) { 888 | for (const e of entities['wit$datetime:datetime']) { 889 | console.log(e); 890 | let confidence = e['confidence']; 891 | if (confidence > highest_confidence) { 892 | highest_confidence = confidence; 893 | // Type is either "value" or "interval" 894 | value = (e['type'] === 'value') ? e['value'] : e['to']['value']; 895 | } 896 | } 897 | console.log('Value with highest confidence: ' + value); 898 | } 899 | 900 | return handleDeliveryEnquiry(value); 901 | 902 | default: 903 | return getDefaultResponse(); 904 | } 905 | 906 | } 907 | ``` 908 | 909 | Next, we modify the `handleDeliveryEnquiry` function: 910 | 911 | ```js 912 | function handleDeliveryEnquiry(value) { 913 | if (value == null) return getResponseFromMessage('We deliver islandwide. The average delivery time takes 5-7 days.'); 914 | 915 | // Get days between now and date time value and round up 916 | let days = ((new Date(value)) - Date.now()) / (1000 * 60 * 60 * 24); 917 | days = Math.ceil(days); 918 | 919 | let message = 'The average delivery time takes 5-7 days.'; 920 | if (days < 5) { 921 | message = 'It is unlikely to arrive in ' + days + ' days. ' + message; 922 | } else if (days < 7) { 923 | message = 'It may arrive in ' + days + ' days. ' + message; 924 | } else { 925 | message = 'It is likely to arrive in ' + days + ' days. ' + message; 926 | } 927 | 928 | return getResponseFromMessage(message); 929 | } 930 | ``` 931 | 932 | Give it a try! Notice how in the following example, the built-in date and time entity in `Wit.ai` is able to interpet the term "two weeks" without us training the model. 933 | 934 | ![](images/handling_entity_values.jpg) 935 | 936 | ## Option 1: Creating a Business Database 937 | 938 | > This section is one of the two options to insert data for Bright to consume. Alternatively, you can find the mock data to be inserted at the top of `index.js` for chatbot development in the next section. 939 | 940 | It is imperative that if you want to build a e-commerce platform, you would need a database to maintain certain details like processing of payments and tracking of delivery status of your products. Here, we are going to give a brief overview on how to create and connect your webhook application to **MongoDB**, a NoSQL Database. 941 | 942 | For the database, we will be using **mlabs**, a MongoDB Hosting, Database-as-a-service for MongoDB. After signing up with you email at https://mlab.com/, select your cluster and relevant service as seen in the picture below. 943 | 944 | ![](images/db_1.jpg) 945 | 946 | Next, click on **connect** when the database is created, add the ip address of where your server will be, or even your own local ip address for local development. Also, create a user for you to use to connect to your mlabs database in your application. 947 | 948 | ![](images/db_2.jpg) 949 | 950 | Select **Connect Your Application**, then copy and replace the respective fields (your username and password) in your code. 951 | 952 | ![](images/db_3.jpg) 953 | 954 | ![](images/db_4.jpg) 955 | 956 | Now onto the code! In your `index.js` file, include the following: 957 | 958 | ```js 959 | // Setup to connect to DB 960 | const DB_PASSWORD = process.env.DB_PASSWORD; 961 | mongoose.connect('mongodb+srv://mongoadmin:' + DB_PASSWORD + '@fb-hack-chatbot-cevnk.mongodb.net/fbmsg', { useNewUrlParser: true, useCreateIndex: true, useFindAndModify: false }).then(() => console.log("DB Connection successful")); 962 | mongoose.Promise = global.Promise; 963 | ``` 964 | 965 | This is to setup and allow you to connect to the database that is hosted on mlabs. Next, include the model files that you are going to use in your application. 966 | 967 | ```js 968 | //Import Schema 969 | require('./models/User'); 970 | require('./models/Product'); 971 | require('./models/Cart'); 972 | require('./models/Order'); 973 | ``` 974 | 975 | The model files are all provided in the `models` [folder on GitHub](https://github.com/ngrq123/bright-chatbot-tutorial/tree/main/models). Their corresponding schemas can be found in these files. Here is an example on how to the schema is written: 976 | 977 | ```js 978 | const orderSchema = new mongoose.Schema({ 979 | customer: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, 980 | trackingNumber: { type: String, unique: true }, 981 | orderStatus: { 982 | type: String, 983 | enum : ['Order Received','Packing','Out For Delivery','Delivered','Refund'], 984 | default: 'Order Received' 985 | }, 986 | products : [{ 987 | title:String, 988 | price:Float, 989 | image_link:String, 990 | pattern:String, 991 | quantity:Float 992 | }] 993 | }, { timestamps: true }); 994 | ``` 995 | 996 | The **key** in the schema object is the name of the column, and **value** is the database type that you want your column to be in the database. Here we are using **mongoose** as a library and you can see the [documentation](https://mongoosejs.com/docs/) to learn how to perform CRUD operations in Node.js. 997 | 998 | One advantage of using MongoDB is that you would not have to create the collections, as the database system will automatically create them when we insert the first set of data (called **document**) into the database. 999 | 1000 | Mock data to be inserted into the database can be found in the following section. 1001 | 1002 | ## Option 2: Adding Mock Commerce Data in `index.js` 1003 | 1004 | Insert the following mock data in `index.js`: 1005 | 1006 | ```js 1007 | // Sets server port and logs message on success 1008 | const listener = app.listen(process.env.PORT || 1337, () => console.log('webhook is listening on port ' + listener.address().port)); 1009 | 1010 | /* 1011 | Mock data in database (remove if database is implemented) 1012 | */ 1013 | let products = [ 1014 | { 1015 | pid: 123, 1016 | title: 'Chocolate Chip Cookies', 1017 | pattern: 'Box of 6', 1018 | price: 15.5, 1019 | image_link: 'https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/main/images/chocolate_chip_cookies.jpg' 1020 | }, 1021 | { 1022 | pid: 456, 1023 | title: 'Earl Grey Sunflower Seeds Cookies', 1024 | pattern: 'Box of 9', 1025 | price: 18.5, 1026 | image_link: 'https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/main/images/earl_grey_sunflower_seeds_cookies.jpg' 1027 | } 1028 | ] 1029 | 1030 | let cart = [ 1031 | { 1032 | pid: 123, 1033 | title: 'Chocolate Chip Cookies', 1034 | pattern: 'Box of 6', 1035 | price: 15.5, 1036 | quantity: 1, 1037 | image_link: 'https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/main/images/chocolate_chip_cookies.jpg' 1038 | }, 1039 | { 1040 | pid: 456, 1041 | title: 'Earl Grey Sunflower Seeds Cookies', 1042 | pattern: 'Box of 9', 1043 | price: 18.5, 1044 | quantity: 2, 1045 | image_link: 'https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/main/images/earl_grey_sunflower_seeds_cookies.jpg' 1046 | } 1047 | ] 1048 | 1049 | let order = [ 1050 | { 1051 | pid: 123, 1052 | title: 'Chocolate Chip Cookies', 1053 | pattern: 'Box of 6', 1054 | price: 15.5, 1055 | quantity: 1, 1056 | image_link: 'https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/main/images/chocolate_chip_cookies.jpg' 1057 | }, 1058 | { 1059 | pid: 456, 1060 | title: 'Earl Grey Sunflower Seeds Cookies', 1061 | pattern: 'Box of 9', 1062 | price: 18.5, 1063 | quantity: 2, 1064 | image_link: 'https://raw.githubusercontent.com/ngrq123/bright-chatbot-tutorial/main/images/earl_grey_sunflower_seeds_cookies.jpg' 1065 | } 1066 | ] 1067 | /* 1068 | End of mock data in database (remove if database is implemented) 1069 | */ 1070 | ``` 1071 | 1072 | ## Solution Overview 1073 | 1074 | The simplified technology stack as follows: 1075 | 1076 | ![](images/architecture_diagram.png) 1077 | 1078 | ## Listing Products 1079 | 1080 | ### Introduction to Templates 1081 | 1082 | [Templates](https://developers.facebook.com/docs/messenger-platform/send-messages/templates) offer a richer experience for users by adding more ways to interact with the chatbot. 1083 | 1084 | The template that we will use is the carousel of generic template, which consists of a list of elements. To do so, we create a new function `generateCarouselofProductsResponse` that takes in a list of products and converts to a response with a `template` attachment. 1085 | 1086 | ```js 1087 | function generateCarouselOfProductsResponse(products) { 1088 | 1089 | return { 1090 | "attachment": { 1091 | "type": "template", 1092 | "payload": { 1093 | "template_type": "generic", 1094 | "elements": 1095 | products.map(p => { 1096 | let subtitle = '$' + p['price'].toFixed(2); 1097 | if (p['pattern']) { 1098 | subtitle = '(' + p['pattern'] + ') ' + subtitle; 1099 | } 1100 | 1101 | return { 1102 | title: p['title'], 1103 | subtitle: subtitle, 1104 | image_url: p['image_link'], 1105 | buttons: [ 1106 | { 1107 | type: "postback", 1108 | title: "Learn more", 1109 | payload: `enquiry_product ${p['title']}` 1110 | }, 1111 | { 1112 | type: "postback", 1113 | title: `Add to cart`, 1114 | payload: `cart_add ${p['pid']}` 1115 | } 1116 | ] 1117 | } 1118 | }) 1119 | } 1120 | } 1121 | } 1122 | 1123 | } 1124 | ``` 1125 | 1126 | Notice that other than textual information, we have included **Learn More** and **Add to Cart** buttons. **Postbacks** are unlike normal messages, as they carry a `payload` property. More on postback will be discussed later. 1127 | 1128 | ### Updating `processMessage` 1129 | 1130 | Previously, we have trained the sentence "Can you send me the list of products here to choose?" with the `recommendation` intent. Now, we add the intent into the switch case. 1131 | 1132 | ```js 1133 | // Processes and sends text message 1134 | function processMessage(message, nlp) { 1135 | ... 1136 | switch (intent) { 1137 | ... 1138 | 1139 | case 'recommendation': 1140 | // For Option 1 only: Get products from database and assign it to a variable named "products" 1141 | 1142 | return generateCarouselOfProductsResponse(products); 1143 | 1144 | default: 1145 | return getDefaultResponse(); 1146 | } 1147 | 1148 | } 1149 | ``` 1150 | 1151 | To process the recommendation intent, we queried the database for all products and passed it into the new `generateCarouselOfProductsResponse` function. 1152 | 1153 | ![](images/recommendations.png) 1154 | 1155 | ## Adding to Cart 1156 | 1157 | ### Deciphering Postbacks and Accessing Payload 1158 | 1159 | Postbacks are buttons with a `payload`, and a **postback event** is triggered when users interact with postback buttons. 1160 | 1161 | > Postbacks are not utterances - they will not be able to be trained on, and will not be passed through the `Wit.ai` NLP model. 1162 | 1163 | In order to retrieve the content of a postback, we will access the `postback` attribute of the `webhook_event` object. 1164 | 1165 | In the `postback` object, there are two attributes: `title` and `payload`, which are both defined in the **postback button** object. 1166 | 1167 | Here is an example: 1168 | 1169 | ```js 1170 | { 1171 | title: 'Add to cart', 1172 | payload: 'cart_add 1 123 Earl Grey Sunflower Seeds Cookies' 1173 | } 1174 | ``` 1175 | 1176 | To process the postback, the `webhook_event` handling needs to be modified in the POST request to the callback URL. 1177 | 1178 | ```js 1179 | let response = getDefaultResponse(); 1180 | if (webhook_event['message']) { 1181 | let message = webhook_event['message']['text']; 1182 | console.log('Message received from sender ' + sender_psid + ' : ' + message); 1183 | 1184 | let nlp = webhook_event['message']['nlp']; 1185 | response = processMessage(message, nlp); 1186 | } else if (webhook_event['postback']) { 1187 | let payload = webhook_event['postback']['payload']; 1188 | response = processPayload(payload); 1189 | } 1190 | ``` 1191 | 1192 | ### Processing the Postback Payload 1193 | 1194 | Let us now define how the postback will be processed. We have formatted the add to cart postback string in this format: `cart_add ` 1195 | 1196 | ```js 1197 | function processPayload(payload) { 1198 | let payload_parts = payload.split(' '); 1199 | // console.log(payload_parts); 1200 | let intent = payload_parts.shift(); 1201 | 1202 | switch(intent) { 1203 | case 'cart_add': 1204 | // Example of payload: cart_add 1 123 Earl Grey Sunflower Seeds Cookies 1205 | let quantity = payload_parts.shift(); 1206 | let pid = payload_parts.shift(); 1207 | let product_name = payload_parts.join(' '); 1208 | 1209 | // For Option 1 only: Update cart in database 1210 | 1211 | return getResponseFromMessage('Added ' + quantity + ' ' + product_name + ' to cart.'); 1212 | 1213 | default: 1214 | return getDefaultResponse(); 1215 | } 1216 | 1217 | } 1218 | ``` 1219 | 1220 | ![](images/cart_add.png) 1221 | 1222 | > If you are unable to receive the postback, check your Facebook for Developers App to see if `messaging_postbacks` field is shown under **Webhooks** in the Messenger settings. 1223 | > ![](images/check_webhook_fields.jpg) 1224 | > If it is not shown, click **Edit** and check **messaging_postbacks** 1225 | 1226 | ### Following Up with Quick Replies 1227 | 1228 | One way to increase engagement is to utilise **quick replies**. Quick replies are buttons anchored right on top of the the textbox (composer) for users to type their message. 1229 | 1230 | Let's try implementing it. First, replace 1231 | 1232 | ```js 1233 | return getResponseFromMessage('Added ' + quantity + ' ' + product_name + ' to cart.'); 1234 | ``` 1235 | 1236 | with 1237 | 1238 | ```js 1239 | return generateAddCartQuickRepliesResponse('Added ' + quantity + ' ' + product_name + ' to cart.'); 1240 | ``` 1241 | 1242 | The new `generateAddCartQuickRepliesResponse` function will create three buttons: **Checkout**, **View more products** and **View cart**. Quick replies, like postbacks, have a `payload` property. However, they are sent as `message` events instead of a `postback` events. 1243 | 1244 | ```js 1245 | function generateAddCartQuickRepliesResponse(message) { 1246 | return { 1247 | "text": message, 1248 | "quick_replies": [ 1249 | { 1250 | "content_type": "text", 1251 | "title": "Checkout", 1252 | "payload": "checkout" 1253 | }, 1254 | { 1255 | "content_type": "text", 1256 | "title": "View more products", 1257 | "payload": "recommendation" 1258 | }, { 1259 | "content_type": "text", 1260 | "title": 'View cart', 1261 | "payload": "cart_view" 1262 | } 1263 | ] 1264 | } 1265 | 1266 | } 1267 | ``` 1268 | 1269 | Next, let us handle the quick reply payload in the POST endpoint of the callback URL. 1270 | 1271 | ```js 1272 | if (webhook_event['message']) { 1273 | let message = webhook_event['message']['text']; 1274 | console.log('Message received from sender ' + sender_psid + ' : ' + message); 1275 | 1276 | if (webhook_event['message']['quick_reply']) { 1277 | payload = webhook_event['message']['quick_reply']['payload']; 1278 | response = processPayload(payload); 1279 | } else { 1280 | let nlp = webhook_event['message']['nlp']; 1281 | response = processMessage(message, nlp); 1282 | } 1283 | } else if (webhook_event['postback']) { 1284 | ... 1285 | ``` 1286 | 1287 | Lastly, implement `recommendations` and `cart_view` intents in the `processPayload` function. 1288 | 1289 | ```js 1290 | case 'recommendation': 1291 | // For Option 1 only: Get products from database and assign it to a variable named "products" 1292 | 1293 | return generateCarouselOfProductsResponse(products); 1294 | case 'cart_view': 1295 | // For Option 1 only: Get cart from database and assign it to a variable named "cart" 1296 | 1297 | return generateCartResponse(cart); 1298 | ``` 1299 | 1300 | ```js 1301 | function generateCartResponse(cart) { 1302 | 1303 | return { 1304 | "attachment": { 1305 | "type": "template", 1306 | "payload": { 1307 | "template_type": "generic", 1308 | "elements": 1309 | cart.map(p => { 1310 | let subtitle = 'Qty: ' + p['quantity'] + ' ($' + p['price'].toFixed(2) + ' each)'; 1311 | if (p['pattern']) { 1312 | subtitle = p['pattern'] + ', ' + subtitle; 1313 | } 1314 | 1315 | return { 1316 | title: p['title'], 1317 | subtitle: subtitle, 1318 | image_url: p['image_link'], 1319 | buttons: [ 1320 | { 1321 | type: "postback", 1322 | title: "Add 1 to Cart", 1323 | payload: `cart_add 1 ${p['pid']} ${p['title']}` 1324 | }, 1325 | { 1326 | type: "postback", 1327 | title: `Remove All`, 1328 | payload: `cart_remove_all ${p['pid']} ${p['title']}` 1329 | } 1330 | ] 1331 | } 1332 | }) 1333 | } 1334 | } 1335 | } 1336 | 1337 | } 1338 | ``` 1339 | 1340 | > For practice, try modifying how the `recommendation` intent is handled in `processPayload` by removing products that are in the user's cart! 1341 | 1342 | ![](images/quick_replies.png) 1343 | 1344 | ## Checkout 1345 | 1346 | ### Mocking Payment with Button Template 1347 | 1348 | Continuing the ordering process, the user eventually checks out his cart. The `payload` in the previous section introduced the `checkout` intent, which is implemented as follows: 1349 | 1350 | ```js 1351 | case 'checkout': 1352 | // For Option 1 only: Get order from database and assign it to a variable named "order" 1353 | 1354 | // For Option 1 only: Add to order and delete cart from database 1355 | 1356 | return generateCheckoutResponse(order); 1357 | ``` 1358 | 1359 | We then create a new `generateCheckoutResponse` function to take in an `order` to calculate the total amount, and ask for the user to pay with a **button template**. 1360 | 1361 | ```js 1362 | function generateCheckoutResponse(order) { 1363 | let total_price = order.reduce((acc, p) => acc + p['price'] * p['quantity'], 0); 1364 | 1365 | return { 1366 | attachment: { 1367 | type: "template", 1368 | payload: { 1369 | template_type: "button", 1370 | text: `Your order (including shipping) will be $${(total_price + 5).toFixed(2)}.\n\nYou will be contributing to ${Math.ceil(total_price / 5)} meals for our beneficiaries.`, 1371 | buttons: [ 1372 | { 1373 | type: "postback", 1374 | title: "Proceed to pay", 1375 | payload: "paid" 1376 | } 1377 | ] 1378 | } 1379 | } 1380 | } 1381 | 1382 | } 1383 | ``` 1384 | 1385 | ![](images/checkout.png) 1386 | 1387 | ### Issuing a Receipt with the Receipt Template 1388 | 1389 | After the user clicks/taps on **Proceed to pay**, a receipt will be generated with the **receipt template**, implemented with the `generateReceiptTemplate` function. 1390 | 1391 | ```js 1392 | case 'paid': 1393 | // For Option 1 only: Get latest order from database and assign it to a variable named "order" 1394 | 1395 | return generateReceiptResponse(order); 1396 | ``` 1397 | 1398 | ```js 1399 | function generateReceiptResponse(order) { 1400 | let total_price = order.reduce((acc, p) => acc + p['price'] * p['quantity'], 0); 1401 | 1402 | return { 1403 | attachment: { 1404 | type: "template", 1405 | payload: { 1406 | template_type: "receipt", 1407 | recipient_name: "John Doe", 1408 | order_number: "bf23ad46d123", 1409 | currency: "SGD", 1410 | payment_method: "PayPal", 1411 | order_url: "", 1412 | address: { 1413 | street_1: "9 Straits View", 1414 | city: "Singapore", 1415 | postal_code: "018937", 1416 | state: "SG", 1417 | country: "SG" 1418 | }, 1419 | summary: { 1420 | subtotal: total_price.toFixed(2), 1421 | shipping_cost: 5, 1422 | total_tax: ((total_price+5)*0.07).toFixed(2), 1423 | total_cost: (total_price+5).toFixed(2) 1424 | }, 1425 | elements: order.map(product => { 1426 | return { 1427 | title: `${product["name"]}`, 1428 | title: product["title"], 1429 | subtitle: product["pattern"], 1430 | quantity: product["quantity"], 1431 | price: product["price"]*product["quantity"], 1432 | currency: "SGD", 1433 | image_url: product["image_link"] 1434 | }; 1435 | }) 1436 | } 1437 | } 1438 | }; 1439 | } 1440 | ``` 1441 | 1442 | ![](images/confirmation_order.png) 1443 | 1444 | When clicked, the receipt expands: 1445 | 1446 | ![](images/receipt.png) 1447 | 1448 | ## Personalising Replies with User's First Name using Facebook Graph API 1449 | 1450 | One way we can further personalise the chatbot's responses is by incorporating the user's first name. 1451 | 1452 | To do this, we will use the Facebook Graph API to retrieve this publicly available information of the user's Facebook account. 1453 | 1454 | Let's retrieve the user's first name with a `getName` function: 1455 | 1456 | ```js 1457 | async function getName(PAGE_ACCESS_TOKEN, sender_psid) { 1458 | let uri = "https://graph.facebook.com/v8.0/" 1459 | let response = await fetch(uri + sender_psid + "?fields=first_name&access_token=" + PAGE_ACCESS_TOKEN); 1460 | if (response.ok) { 1461 | let body = await response.json(); 1462 | return body.first_name; 1463 | } 1464 | 1465 | // Returns default name if name is not able to be retrieved 1466 | return "John Doe"; 1467 | } 1468 | ``` 1469 | 1470 | The `getName` function takes in two parameters: `PAGE_ACCESS_TOKEN` and `sender_psid`. These are required to retrieve the user's first name. 1471 | 1472 | To use `fetch`, we import the `node-fetch` package: 1473 | 1474 | ```js 1475 | // Imports dependencies and set up http server 1476 | const 1477 | express = require('express'), 1478 | bodyParser = require('body-parser'), 1479 | app = express().use(bodyParser.json()), // creates express http server 1480 | dotenv = require('dotenv'), 1481 | request = require('request'); 1482 | global.fetch = require("node-fetch"); 1483 | ``` 1484 | 1485 | > The `getName` function sends a request to Facebook and the result has to be processed before generating a response. To do so, you need to utilise async/await. You can only perform an await in an asynchronous function and whenever you call an asynchronous function, any function that calls it has to be asynchronous as well. 1486 | 1487 | Let us modify the following in our code: 1488 | - POST request endpoint 1489 | - `processMessage` function 1490 | - `processPayload` function 1491 | - `generateReceiptResponse` function 1492 | 1493 | First, update the POST request endpoint: 1494 | 1495 | ```js 1496 | // Creates the endpoint for our webhook 1497 | app.post('/webhook', (req, res) => { 1498 | 1499 | let body = req.body; 1500 | 1501 | // Checks this is an event from a page subscription 1502 | if (body.object === 'page') { 1503 | 1504 | // Iterates over each entry - there may be multiple if batched 1505 | body.entry.forEach(async function(entry) { 1506 | 1507 | // Gets the message. entry.messaging is an array, but 1508 | // will only ever contain one message, so we get index 0 1509 | let webhook_event = entry.messaging[0]; 1510 | console.log(webhook_event); 1511 | 1512 | let sender_psid = webhook_event['sender']['id']; 1513 | 1514 | let response = getDefaultResponse(); 1515 | if (webhook_event['message']) { 1516 | let message = webhook_event['message']['text']; 1517 | console.log('Message received from sender ' + sender_psid + ' : ' + message); 1518 | 1519 | if (webhook_event['message']['quick_reply']) { 1520 | let payload = webhook_event['message']['quick_reply']['payload']; 1521 | response = await processPayload(sender_psid, payload); 1522 | } else { 1523 | let nlp = webhook_event['message']['nlp']; 1524 | response = await processMessage(sender_psid, message, nlp); 1525 | } 1526 | } else if (webhook_event['postback']) { 1527 | let payload = webhook_event['postback']['payload']; 1528 | response = await processPayload(sender_psid, payload); 1529 | } 1530 | 1531 | callSendAPI(sender_psid, response); 1532 | }); 1533 | 1534 | // Returns a '200 OK' response to all requests 1535 | res.status(200).send('EVENT_RECEIVED'); 1536 | } else { 1537 | // Returns a '404 Not Found' if event is not from a page subscription 1538 | res.sendStatus(404); 1539 | } 1540 | 1541 | }); 1542 | ``` 1543 | 1544 | Second, we will incorporate the user's first name in the welcome response in `processMessage`. 1545 | 1546 | ```js 1547 | // Processes and sends text message 1548 | async function processMessage(sender_psid, message, nlp) { 1549 | 1550 | if (nlp['intents'].length === 0) { 1551 | 1552 | // Check if greeting 1553 | let traits = nlp['traits']; 1554 | 1555 | if (traits['wit$greetings'] && traits['wit$greetings'][0]['value'] === 'true') { 1556 | console.log('Is greeting'); 1557 | // Add the getName function call here 1558 | let name = await getName(PAGE_ACCESS_TOKEN,sender_psid); 1559 | return getResponseFromMessage('Hi ' + name + '! Welcome to Bright. How can I help you?'); 1560 | } 1561 | 1562 | console.log('Returning default response'); 1563 | return getDefaultResponse(); 1564 | } 1565 | ... 1566 | } 1567 | ``` 1568 | 1569 | ![](images/hello_message.jpg) 1570 | 1571 | Lastly, to customise the receipt with your user's first name, we modify the `processPayload` and `generateReceiptResponse` functions. 1572 | 1573 | ```js 1574 | async function processPayload(sender_psid, payload) { 1575 | let payload_parts = payload.split(' '); 1576 | // console.log(payload_parts); 1577 | let intent = payload_parts.shift(); 1578 | 1579 | switch(intent) { 1580 | ... 1581 | case 'paid': 1582 | // For Option 1 only: Get latest order from database and assign it to a variable named "order" 1583 | 1584 | return await generateReceiptResponse(sender_psid, order); 1585 | default: 1586 | return getDefaultResponse(); 1587 | } 1588 | 1589 | } 1590 | ``` 1591 | 1592 | ```js 1593 | // Processes and sends text message 1594 | async function generateReceiptResponse(sender_psid, order) { 1595 | let total_price = order.reduce((acc, p) => acc + p['price'] * p['quantity'], 0); 1596 | // Add getName function here to retrieve the name 1597 | let name = await getName(PAGE_ACCESS_TOKEN,sender_psid); 1598 | 1599 | return { 1600 | attachment: { 1601 | type: "template", 1602 | payload: { 1603 | template_type: "receipt", 1604 | recipient_name: name, 1605 | ... 1606 | } 1607 | } 1608 | }; 1609 | } 1610 | ``` 1611 | 1612 | ![](images/order_confirmation.jpg) 1613 | 1614 | Good job in making it thus far, we are proud of you! Now let's see if you can apply what you have learnt in the following challenges. 1615 | 1616 | ## Challenge 1: Order Enquiry 1617 | 1618 | In the previous section - [Checkout](#checkout) - we created orders for fulfillment. Now, extend the chatbot's functionality by implementing order enquiry in the chatbot, which allows users to enquire on their order status. 1619 | 1620 | > Hint: Train some utterances with a new intent (say, `enquiry_order`) and process the message. Then utilise the button template or quick replies to allow users to select the order that they are enquiring. 1621 | 1622 | ## Challenge 2: Implement Product Enquiries 1623 | 1624 | In the [Introduction to Templates](#introduction-to-templates) section, each generic template element in the carousel has a "Learn more" button that has the payload `enquiry_product `. For example, a user can enquire about the number of cookies in a box, or the number of variants a product has. 1625 | 1626 | Implement the `enquiry_product` intent in the `processPayload` function. You might need to collect enough details of the product that the user is enquiring in order to provide an answer. 1627 | 1628 | > Hint: One way to do so is to keep adding onto the payload (e.g. `enquiry_product `). Or, you could also store the postback parts in the database as a new collection/table. 1629 | 1630 | ## Wrapping Up 1631 | 1632 | In this tutorial, you have learnt to build an ecommerce chatbot that answers general enquiries, provides recommendations, and provides a seamless order process to users. You have set up the entire technology stack: Facebook Page, Facebook for Developer App, `Wit.ai` model, MongoDB database and Express.js application that includes a webhook that listens for incoming events. 1633 | 1634 | We explored some of the many features of Facebook Messenger, including templates and quick replies, and went through the differences between message and postback events. 1635 | 1636 | Additionally, you have seen how NLP is used in chatbots, converting unstructured text into categories - allowing us to create a smarter Messenger chatbot that elevates the Messenger experience. 1637 | 1638 | An NLP engine integrated with Facebook Messenger has many potential use cases and opportunity for reusability. 1639 | 1640 | For example, a CSR-supportive use case could be Community Service Volunteer Recruitment. A person who wants to volunteer, messages the Chatbot with their availability for the week and the chatbot returns a list of organisations that need volunteers on those days. The volunteer selects an organisation from the list and reads up on what type of roles are required. The volunteer selects a role and a time that they are able to attend. Organisation has gained a volunteer through the chatbot! 1641 | 1642 | For a more business-minded and advice-reliant use case, let’s refer to an Over-The-Counter e-Pharmacy example. In this scenario, the chatbot is only allowed to advise on utterances which it understands with a high level of confidence. Say, someone at home has mild symptoms such as a headache and doesn’t want to travel to a pharmacy. The would-be customer messages the chatbot with their symptoms. The chatbot advises the customer to buy paracetamol. The customer then has the option to buy paracetamol with the click of a button for home delivery. 1643 | 1644 | ## Acknowledgements 1645 | 1646 | Special shoutout to Douglas Sim and Jelissa Ong, who helped to build the Bright Social Enterprise Commerce Bot in the previous Messenger hackathon, and agreeing to allow us to use Bright chatbot as a solution for this hackathon! 1647 | 1648 | ## References 1649 | 1650 | - Bright Social Enterprise E-Commerce Chatbot: https://github.com/ngrq123/bright-social-enterprise-commerce-bot 1651 | - Built-In Entities and Traits: https://wit.ai/docs/built-in-entities/20200513/ 1652 | - Button Template: https://developers.facebook.com/docs/messenger-platform/send-messages/template/button 1653 | - Graph API User: https://developers.facebook.com/docs/graph-api/reference/v8.0/user 1654 | - How do I create a Facebook Page?: https://www.facebook.com/business/help/104002523024878 1655 | - Mongoose Getting Started: https://mongoosejs.com/docs/index.html 1656 | - Natural Language Processing: https://developers.facebook.com/docs/messenger-platform/built-in-nlp 1657 | - Postback Button: https://developers.facebook.com/docs/messenger-platform/reference/buttons/postback 1658 | - Quick Replies: https://developers.facebook.com/docs/messenger-platform/send-messages/quick-replies/ 1659 | - Receipt Template: https://developers.facebook.com/docs/messenger-platform/send-messages/template/receipt 1660 | - Send API: https://developers.facebook.com/docs/messenger-platform/reference/send-api/ 1661 | - Send Messages: https://developers.facebook.com/docs/messenger-platform/send-messages/ 1662 | - Setting Up Your Facebook App: https://developers.facebook.com/docs/messenger-platform/getting-started/app-setup 1663 | - Setting Up Your Webhook: https://developers.facebook.com/docs/messenger-platform/getting-started/webhook-setup 1664 | - `Wit.ai` Recipes: https://wit.ai/docs/recipes 1665 | 1666 | ## Tutorial GitHub Link 1667 | 1668 | https://github.com/ngrq123/bright-chatbot-tutorial 1669 | 1670 | ## Devpost Submission 1671 | 1672 | https://devpost.com/software/create-smarter-messenger-experiences-on-facebook-with-bright 1673 | --------------------------------------------------------------------------------