This chapter covers
Being able to quickly build and deploy different applications is useful. As you’ve seen, building chatbots with Claudia Bot Builder is easy, and you can build a simple request/reply chatbot in just a few minutes.
But in the real world, chatbots need to do more complex operations than just reply with static data. You’ll probably need to store customer information and request data, and perhaps do some calculations or even answer some unrelated questions. This chapter covers all of that: you’ll learn how to make pizza orders from user requests, send delivery messages when the orders are ready, and integrate some basic natural language processing (NLP) to handle user input in a text format.
Your cousin Julia followed your progress and was excited when she saw you’d built the scrollable list of available pizzas. She’s already started spreading the word at school about the amazing pizza chatbot—much better than the one from Chess’s pizzeria. That’s put you in the hot seat, but Aunt Maria is happy, because she’s already seen increased website traffic as a result of that rumor.
The last thing you’d want now is to disappoint them, so you’re going to finish the pizza ordering chatbot and add a few improvements to make your bot superior to Chess’s.
Displaying a list with pizza pictures within a chatbot reply is great, because customers prefer a visual interface to a simple text reply. In the previous chapter, each pizza was displayed in a visual block that also had a Details button. If you were to tap it, though, it would do nothing.
Because your primary goal is enabling pizza ordering, you’ll do the following:
You are going to continue from the final chapter 8 listing as follows.
Listing 9.1 The current botBuilder
function that answers with the generic template
'use strict'
const pizzas = require('./data/pizzas.json')
const botBuilder = require('claudia-bot-builder')
const fbTemplate = botBuilder.fbTemplate
const api = botBuilder(message => {
const messageTemplate = new fbTemplate.Generic() ①
pizzas.forEach(pizza => {
messageTemplate.addBubble(pizza.name)
.addImage(pizza.image)
.addButton('Details', pizza.id)
}) ②
return [
'Hello, here's our pizmenu:',
messageTemplate.get() ③
]
}, {
platforms: ['facebook']
})
module.exports = api
As you can see from this code listing, the message
attribute of the botBuilder
function contains useful information about the request your chatbot received, such as whether the message was a button tap response or a text message.
To make your chatbot more useful, you should reply with the pizza details when the customer taps the Details button. Tapping the Order button should allow customers to order the selected pizza.
Now the flow of your chatbot will have three branches (figure 9.2):
Customer button actions are replied to as postback messages.
To implement the flow from figure 9.2, you’ll first need to check whether the message is a postback by checking if message.postback
is true
or false
.
If the message is a postback, you’ll check if the user wants to see the details of the pizza or to order it (let’s call that action), and you’ll also need the ID of the selected pizza. To save both values, you can update postback button value by serializing a JSON value or providing a string with both action and pizza ID, separated by a delimiter, such as pipe character (|
). In the case of the pizza chatbot, doing the latter is easier because the chatbot’s flow is quite simple. You can save the value in ACTION|ID
format, where ACTION
represents an uppercase action name (ORDER
or DETAILS
in your case) and ID
represents the pizza ID.
If the message the chatbot received is a postback, its value will be in message.text
. You’ll split it by the pipe character (|
) using the following built-in String.split
method:
const values = message.text.split('|')
The new values
array will have the action as its first item, and the pizza ID as the second. But ES6 destructuring can help with making this code more readable. If you replace the const values
with const [action, pizzaId]
, the first item of the new array will be stored directly to the action
constant and the second to pizzaId
.
Now that you’ve extracted both the action and the pizza ID, you need to check if the action is either ORDER
or DETAILS
.
In any case, you need to use the pizza ID to find a pizza from the pizzas
array. To do so, you can use the built-in Array.find
method like this:
const pizza = pizzas.find(pizza => pizza.id == pizzaId)
Note that this example uses ==
instead of ===
. That’s because the pizzaId
is of type String
, because you got it from the String.split
function, whereas the IDs from the pizzas
array are integers.
Finding a pizza by its ID should take place with both if
statements. It may seem redundant, but you might add an additional action that doesn’t have a pizza ID in the future.
When your chatbot receives the DETAILS
action, you can join all the pizza ingredients with commas and return the list of ingredients as a reply. But after receiving the pizza ingredients list, what should the customer do? What is the next conversation step?
Unlike in web applications, where the user can see the next available actions onscreen, the next step in a chatbot flow isn’t always obvious to the user. If you return just a list of pizza ingredients, the user most likely won’t know what to do next, and you may receive lots of unexpected user messages, like “Ew! Goat cheese!” and “What’s your favorite kind of pizza?” or even “I love you,” because human creativity is endless.
Even with NLP integration, chatbots are still far from being able to have a conversation on a human level, so the best thing you can do is add a nice, creative way of handling errors while trying to direct the user to the flow your chatbot can handle. Designing chatbot conversations is an interesting topic, but it is beyond the scope of this book.
The easiest way to direct users to the next chatbot action is to show them a visual menu with available options. This doesn’t guarantee that the user will tap one of those buttons, but the menu will produce much better results than simply asking a question.
The two options you should show are the possibility to order the pizza the user just previewed, or to go back to the pizza list. To do so, you can use the Button
class from fbTemplate
. The Button
class allows you to render a button template with up to three buttons, which look like the buttons from the generic template, and show a text reply. Using this class is similar to using the Generic
class, so your reply should look something like the following:
return [
`${pizza.name} has following ingredients: ` + pizza.ingredients.join(', '),
new fbTemplate.Button('What else can I do for you?')
.addButton('Order', `ORDER|${pizzaId}`)
.addButton('Show all pizzas', 'ALL_PIZZAS')
.get()
]
As you can see, the second button has the ALL_PIZZAS
value, so this won’t pass any of the if
conditions you defined, and it will show the pizzas menu. Later, you can modify that flow to show a slightly different message depending on the previous conversation flow; for example “Not a big fan of mushrooms? Here are a few other pizzas you might like more.”
In the case of an ORDER
action, your chatbot should find a pizza by the ID and tell the user that the order was successful. You’ll handle that case later in this chapter.
Finally, if the message isn’t a postback, or if the action is neither DETAILS
nor ORDER
, you can return a generic template message similar to the one you used in chapter 8. The only difference is the addition of the Order button:
pizzas.forEach(pizza => {
reply.addBubble(pizza.name)
.addImage(pizza.image)
.addButton('Details', `DETAILS|${pizza.id}`)
.addButton('Order', `ORDER|${pizza.id}`)
})
Your updated bot.js file should look like the following listing.
Listing 9.2 Chatbot flow that accepts orders and details of selected pizza
'use strict'
const pizzas = require('./data/pizzas.json')
const botBuilder = require('claudia-bot-builder')
const fbTemplate = botBuilder.fbTemplate
const api = botBuilder((message) => { ①
if (message.postback) { ②
const [action, pizzaId] = message.text.split('|') ③
if (action === 'DETAILS') { ④
const pizza = pizzas.find(pizza => pizza.id == pizzaId) ⑤
return [
`${pizza.name} has following ingredients: ` + pizza.ingredients.join(', '), ⑥
new fbTemplate.Button('What else can I do for you?') ⑦
.addButton('Order', `ORDER|${pizzaId}`)
.addButton('Show all pizzas', 'ALL_PIZZAS')
.get()
]
} else if (action === 'ORDER') { ⑧
const pizza = pizzas.find(pizza => pizza.id == pizzaId) ⑨
return `Thanks for ordering ${pizza.name}! I will let you know as soon as your pizza is ready.` ⑩
}
}
const reply = new fbTemplate.Generic() ⑪
pizzas.forEach(pizza => {
reply.addBubble(pizza.name)
.addImage(pizza.image)
.addButton('Details', `DETAILS|${pizza.id}`)
.addButton('Order', `ORDER|${pizza.id}`)
})
return [
`Hello, here's our pizza menu:`,
reply.get()
]
}, {
platforms: ['facebook']
})
module.exports = api
Now run claudia update
or npm run update
to deploy the chatbot, and try the new flow, which looks similar to figure 9.3.
As with an API, managing the whole chatbot flow in a single file is not scalable. How can you improve the structure?
The chatbot doesn’t have a router, but your if…else
conditions act like a router, and their actions look like handlers. The easiest way to improve the structure is to keep the routing in the main file and move the handlers into separate files. You should have a main bot.js file and a handlers folder containing three handler files, one for each chatbot dialog branch. Your folder structure should be similar to figure 9.4.
Create the handlers folder within your pizza-fb-chatbot project, with the following three JavaScript files:
Then update your main bot.js file with the following changes:
require
for fbTemplate
because it’s no longer used in this file.pizzaDetails
and orderPizza
handler functions, with the pizzaId
as their argument.pizzaMenu
handler.After updates, the bot.js file should look like the following listing.
Listing 9.3 Main chatbot file
'use strict'
const botBuilder = require('claudia-bot-builder')
const pizzaDetails = require('./handlers/pizza-details') ①
const orderPizza = require('./handlers/order-pizza')
const pizzaMenu = require('./handlers/pizza-menu')
const api = botBuilder((message) => {
if (message.postback) {
const [action, pizzaId] = message.text.split('|')
if (action === 'DETAILS') {
return pizzaDetails(pizzaId) ②
} else if (action === 'ORDER') {
return orderPizza(pizzaId) ③
}
}
return [
`Hello, here's our pizza menu:`,
pizzaMenu() ④
]
}, {
platforms: ['facebook']
})
module.exports = api
Now, open the handlers/pizza-details.js file. First, require the list of pizzas from the pizza.json file and the fbTemplate
from Claudia Bot Builder by adding the following:
const pizzas = require('../data/pizzas.json')
const fbTemplate = require('claudia-bot-builder').fbTemplate
Then create the pizzaDetails
function, which accepts one parameter: the pizzaId
. This function should find a single pizza by the provided pizza ID from the pizzas
array, and then return its ingredients and a button template message that allows the user to order the pizza or go back to the full pizza menu.
Finally, you’ll need to export the pizzaDetails
handler function by adding the line module.exports = pizzaDetails
at the end of your file.
Your handlers/pizza-details.js file should look like the following listing.
Listing 9.4 Pizza details handler
'use strict'
const pizzas = require('../data/pizzas.json') ①
const fbTemplate = require('claudia-bot-builder').fbTemplate ②
function pizzaDetails(id) { ③
const pizza = pizzas.find(pizza => pizza.id == id) ④
return [ ⑤
`${pizza.name} has following ingredients: ` + pizza.ingredients.join(', '),
new fbTemplate.Button('What else can I do for you?') ⑥
.addButton('Order', `ORDER|${pizza.id}`)
.addButton('Show all pizzas', 'ALL_PIZZAS')
.get()
]
}
module.exports = pizzaDetails ⑦
Next, open the handlers/order-pizza.js file and do the same:
orderPizza
handler function that will accept the pizza ID as a parameter.orderPizza
function.orderPizza
function.Your order-pizza.js file should look like the following listing.
Listing 9.5 Order pizza handler
'use strict'
const pizzas = require('../data/pizzas.json') ①
function orderPizza(id) { ②
const pizza = pizzas.find(pizza => pizza.id == id) ③
return `Thanks for ordering ${pizza.name}! I will let you know as soon as your pizza is ready.` ④
}
module.exports = orderPizza ⑤
After you update the pizza order handler, open the handlers/pizza-menu.js file and do the following:
fbTemplate
.pizzaMenu
handler function.pizzaMenu
function.Your pizza menu handler should look like the following listing.
Listing 9.6 Pizza menu handler
'use strict'
const pizzas = require('../data/pizzas.json') ①
const fbTemplate = require('claudia-bot-builder').fbTemplate ②
function pizzaMenu() { ③
const message = new fbTemplate.Generic() ④
pizzas.forEach(pizza => {
message.addBubble(pizza.name)
.addImage(pizza.image)
.addButton('Details', `DETAILS|${pizza.id}`)
.addButton('Order', `ORDER|${pizza.id}`)
})
return message.get() ⑤
}
module.exports = pizzaMenu ⑥
After you’ve updated all the files, run either the npm run update
or the claudia update
command to deploy your chatbot. When you send a message to your chatbot through Facebook Messenger, the response should stay the same, but now you have a more scalable and testable structure.
To make the chatbot useful to your customers, you’ll store pizza orders to your pizza-orders
DynamoDB table.
As shown in figure 9.5, when your chatbot receives a message, it should connect to the same DynamoDB table your Pizza API is using and save the message. After that, it should reply with an order confirmation.
For the sake of simplicity, this chapter shows you only how to save the order in the database. A real-world chatbot should probably also allow the user to see the current order and cancel it.
Saving the order requires several changes to the order-pizza.js handler.
First, you can use the DocumentClient
to connect to DynamoDB. To do so, you’ll need to install the aws-sdk
module from NPM as a dependency (or an optional dependency, if you want to optimize deployment speed). Then you need to import aws-sdk
and create an instance of the DocumentClient
by adding following code:
const AWS = require('aws-sdk')
const docClient = new AWS.DynamoDB.DocumentClient()
Now, instead of presenting the static text, you’ll use the docClient.put
method to save the pizza in the DynamoDB table.
The main difference between your chatbot and your API is that you don’t have the delivery address and the selected pizza in a single request. This means that you’ll need to save partial data to the DynamoDB table and then ask the user for an address. Facebook Messenger platform doesn’t save the state between sequential messages, so you’ll need to save incomplete order state with some additional parameters in the pizza-orders
table (which is done in this section) or in another DynamoDB table.
For the same reason, you won’t be able to use the Some Like It Hot Delivery ID, so you’ll need help from the uuid
module again.
You’ll save the following data in DynamoDB:
orderId
—Use the uuid
module to generate the unique ID.pizza
—Use the ID of the selected pizza.orderStatus
—Use in-progress
as a status, because this order is not finished.platform
—Add fb-messenger-chatbot
as a platform, because in the future your pizza bot might work on some other chat platforms.user
—Save the ID of the user who sent the message.When a pizza is successfully saved in the database, you want to ask the user for the delivery address. You can do this with a simple question, such as “Where do you want your pizza to be delivered?” You’ll handle that later in this chapter.
You also want to handle errors, so you’ll send a friendly message to the user in case of an error and show the pizza menu again.
When you’ve updated your code, the order-pizza.js handler should look like the following listing.
Listing 9.7 Order pizza handler connected to the DynamoDB database
'use strict'
const AWS = require('aws-sdk') ①
const docClient = new AWS.DynamoDB.DocumentClient() ②
const pizzas = require('../data/pizzas.json')
const pizzaMenu = require('./pizza-menu')
const uuid = require('uuid/v4') ③
function orderPizza(pizzaId, sender) {
const pizza = pizzas.find(pizza => pizza.id == pizzaId)
return docClient.put({ ④
TableName: 'pizza-orders',
Item: {
orderId: uuid(), ⑤
pizza: pizzaId,
orderStatus: 'in-progress', ⑥
platform: 'fb-messenger-chatbot', ⑦
user: sender ⑧
}
}).promise()
.then((res) => {
return 'Where do you want your pizza to be delivered?' ⑨
})
.catch((err) => {
console.log(err)
return [ ⑩
'Oh! Something went wrong. Can you please try again?',
pizzaMenu()
]
})
}
module.exports = orderPizza
In addition to the order-pizza.js handler, you’ll also need to update the main bot.js file and pass the sender ID to the orderPizza
function. The sender ID is available in the message
object as message.sender
; in the case of a Facebook Messenger chatbot, it represents a Facebook page-scoped unique user ID.
The following listing shows the updated part of your bot.js file. This code goes just under the parsing of the values
returned by your message.postback
. The rest of the file is unchanged.
Listing 9.8 Update main chatbot flow
if (values[0] === 'DETAILS') {
return pizzaDetails(values[1])
} else if (values[0] === 'ORDER') {
return orderPizza(values[1], message.sender) ①
}
Now that your chatbot code is updated, you’ll need to create a policy that allows the user that executes the Lambda function to interact with DynamoDB. Create a roles folder in your pizza-fb-chatbot folder and create a dynamodb.json file in it.
As shown in listing 9.9, the dynamodb.json file should allow the user to scan, get, put, and update items in DynamoDB. For now, your chatbot will not be able to update or cancel an order, but the dynamodb:UpdateItem
action is required because the order will be updated after the user shares their address.
Listing 9.9 DynamoDB policy
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [ ①
"dynamodb:Scan",
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem"
],
"Effect": "Allow",
"Resource": "*"
}
]
}
Finally, run the aws iam put-role-policy
command from the AWS CLI to add a policy from the roles/dynamodb.json file to the Lambda executor role. You can find the role-name
in your claudia.json file. If you remember, on the initial deployment Claudia creates the claudia.json file in your project root to store some of your Lambda data. Claudia uses that data to do the later update
deployments without requiring you to add any other parameters. The claudia.json file also stores your Lambda executor, which you need right now for your policy. Look for your role-name
in it.
The aws iam put-role-policy
command should look like the following listing.
Listing 9.10 Add a DynamoDB policy to the Pizza Bot role
aws iam put-role-policy
--role-name pizza-fb-chatbot-executor ①
--policy-name PizzaBotDynamoDB ②
--policy-document file://./roles/dynamodb.json ③
When the aws iam put-role-policy
command executes successfully, it returns an empty response, which means that the policy is in place. Your bot is ready for deployment.
Run the npm run update
or claudia update
command to deploy your chatbot, and then try to send a new message to your chatbot.
As already mentioned, a pizza order is not complete without the address to which the pizza should be delivered. In a real-world project you’ll want to build your chatbot to be as bulletproof as possible, so you’ll probably need some natural language processing that will recognize addresses sent by users. But to keep this chapter simple, you’ll use the built-in Facebook Messenger location share button.
As part of its platform, Facebook Messenger allows you to ask users to share their current location via a quick reply button. Tapping this button sends the coordinates with the rest of the payload. Claudia Bot Builder has support for that button in fbTemplate
. You can add the button by adding the .addQuickReplyLocation
method to any fbTemplate
class.
Let’s update the order-pizza.js handler. First require fbTemplate
from Claudia Bot Builder by adding const fbTemplate = require('claudia-bot-builder').fbTemplate
to the top of the file.
Then replace the reply where you ask the user to share their address with the fbTemplate.Text
class and .addQuickReplyLocation
method, as shown in the following listing.
Listing 9.11 Ask for the location after saving the order to the DynamoDB table
.then((res) => {
return new fbTemplate.Text('Where do you want your pizza to be delivered?') ①
.addQuickReplyLocation() ②
.get() ③
})
When your customer shares their location, the chatbot will receive its coordinates: its latitude and longitude. In addition to sending an address, the Some Like It Hot Delivery API also allows you to send position coordinates. (In a real-world example, you’d need to provide a lot more information, such as the floor, the apartment number, or at least a notes field for the delivery.)
In order to handle the location, you’ll need to create a new handler function. To do so, create a save-location.js file in the handlers folder within your chatbot project. This handler should accept the userId
and coordinates as parameters and use them to update the customer order.
To update the order, you’ll need to import the AWS SDK, instantiate the DocumentClient
, and do the following:
in-progress
order for the specified customer using the DocumentClient.scan
method, because the sender ID is not a part of the key.orderId
from the returned result to update the order to the new status using the DocumentClient.update
method.For now, let’s update the order status to pending
and add the latitude and longitude coordinates where the pizza should be delivered.
The following listing shows what your save-location.js handler should look like.
Listing 9.12 Save location handler
'use strict'
const AWS = require('aws-sdk') ①
const docClient = new AWS.DynamoDB.DocumentClient()
function saveLocation(userId, coordinates) { ②
return docClient.scan({ ③
TableName: 'pizza-orders',
Limit: 1, ④
FilterExpression: `user = :u, orderStatus: :s`, ⑤
ExpressionAttributeNames: { ⑥
':u': { S: userId },
':s': { S: 'in-progress' }
}
}).promise()
.then((result) => result.Items[0]) ⑦
.then((order) => {
const orderId = order.orderId ⑧
return docClient.update({ ⑨
TableName: 'pizza-orders',
Key: { ⑩
orderId: orderId
},
UpdateExpression: 'set orderStatus = :s, coords=:c', ⑪
ExpressionAttributeValues: { ⑫
':s': 'pending',
':c': coordinates
},
ReturnValues: 'ALL_NEW' ⑬
}).promise()
})
}
module.exports = saveLocation ⑭
Finally, you’ll need to update your bot.js file to do the following:
saveLocation
handler function when the customer shares their location.To import the new save-location.js handler, add the following snippet at the top of your bot.js file (for example, after the pizzaMenu
handler function):
const saveLocation = require('./handlers/save-location')
To check if the customer shared their location, first you’ll need to verify that the message is not a postback. After that check, use message.originalRequest
to get the coordinates, if they exist. Coordinates are sent as an attachment, so you can access them via the message.originalRequest.message.attachments[0].payload.coordinates
object.
The following listing shows the last few lines of your bot.js file.
Listing 9.13 Handle user location in the main chatbot file
if (
message.originalRequest.message.attachments &&
message.originalRequest.message.attachments.length &&
message.originalRequest.message.attachments[0].payload.coordinates &&
message.originalRequest.message.attachments[0].payload.coordinates.lat &&
message.originalRequest.message.attachments[0].payload.coordinates.long
) { ①
return saveLocation(message.sender, message.originalRequest.message.attachments[0].payload.coordinates) ②
}
return [
`Hello, here's our pizza menu:`,
pizzaMenu()
]
}, {
platforms: ['facebook']
})
module.exports = api
After you update your bot.js file, deploy your chatbot using the npm run update
command and then try it. The result should look like figure 9.6.
The last piece of the “making your chatbot useful” puzzle is to connect it to the Some Like It Hot Delivery API. As shown in figure 9.7, after integrating with the API your chatbot flow should look like the following:
in-progress
.As you can see from the flow, there are two things to change in your chatbot:
Start with the easier part: the save-location.js handler update should be similar to the integration you did in chapter 4. You’ll need to send a POST
request to https://some-like-it-hot-api.effortless-serverless.com/delivery. The only difference is that you’ll need to send deliveryCoords
instead of deliveryAddress
.
Another important difference is that you won’t be able to update the order’s primary key. Because you won’t be able to use deliveryId
as an orderId
, you need to save the delivery ID in your DynamoDB table. As you probably remember from chapter 3, you used deliveryId
as an orderId
so you can find orders more efficiently when the delivery status is updated.
The following listing shows the modified section of your save-location.js handler with the integration of the Some Like It Hot Delivery API.
Listing 9.14 Save location handler
// First part of the file was not changed
.then((result) => result.Items[0])
.then((order) => {
return rp.post('https://some-like-it-hot-api.effortless-serverless.com/delivery', { ①
headers: { ②
"Authorization": "aunt-marias-pizzeria-1234567890", ②
"Content-type": "application/json"
},
body: JSON.stringify({ ④
pickupTime: '15.34pm', ⑤
pickupAddress: 'Aunt Maria's Pizzeria',
deliveryCoords: coordinates, ⑥
webhookUrl: 'https://g8fhlgccof.execute-api.eu-central-1.amazonaws.com/latest/delivery', ⑦
})
})
.then(rawResponse => JSON.parse(rawResponse.body)) ⑧
.then((response) => {
order.deliveryId = response.deliveryId ⑨
return order ⑩
})
})
.then((order) => {
return docClient.update({ ⑪
TableName: 'pizza-orders',
Key: {
orderId: order.orderId
},
UpdateExpression: 'set orderStatus = :s, coords=:c, deliveryId=:d',
ExpressionAttributeValues: {
':s': 'pending',
':c': coordinates,
':d': order.deliveryId ⑫
},
ReturnValues: 'ALL_NEW'
}).promise()
})
}
module.exports = saveLocation
Now that you are saving the delivery ID in DynamoDB, you’ll need to create a webhook that you send to the Some Like It Hot Delivery API. But how should you add the route to your chatbot?
As mentioned previously, Claudia Bot Builder exports an instance of Claudia API Builder. This means that the botBuilder
function in your bot.js file returns a fully functional instance of Claudia API Builder.
Before adding the new route, you need to create a new handler for it. Create a file named delivery-webhook.js in your handlers folder. Within this handler you’ll need to find the order by its delivery ID, which is passed by the delivery API, then update the order with the new status and send a message to the customer to let them know that the delivery status has changed. Full delivery webhook flow is shown in figure 9.8.
Finding and updating an order is similar to the find and update you did in the save-location.js handler file. The only tricky part is sending the message with the new status to the user.
To send a message from Facebook Messenger, you’ll need to send an API request to the Facebook Messenger platform API. Each API request requires a user’s page-scoped ID, the message that you want to send, and a Facebook Messenger access token.
You can send the API request the same way you send a request to the delivery API, or you can use the claudia-bot-builder
library to send the message. This is an internal library, but you can require it by requiring the specific reply.js file as follows:
const reply = require('claudia-bot-builder/lib/facebook/reply')
By doing this, you’ll require just the reply.js
file, instead of a full Claudia Bot Builder, and store the function in a reply
constant.
This reply
function accepts three parameters: the sender ID, the message, and an access token.
You can get the sender ID from the database. The message is simply the message you want to send to the customer in the standard Claudia Bot Builder format. You can pass text, a template object, or an array of multiple messages. Finally, the Facebook Messenger access token is available as an API Gateway stage variable, and you can get it from the request.env
object. You can pass the token to your handler function as its second parameter.
The full delivery webhook handler is shown in the following listing.
Listing 9.15 The delivery webhook handler
'use strict'
const reply = require('claudia-bot-builder/lib/facebook/reply') ①
function deliveryWebhook(request, facebookAccessToken) { ②
if (!request.deliveryId || !request.status) ③
throw new Error('Status and delivery ID are required')
return docClient.scan({ ④
TableName: 'pizza-orders',
Limit: 1,
FilterExpression: `deliveryId = :d`, ④
ExpressionAttributeNames: {
':d': { S: deliveryId }
}
}).promise()
.then((result) => result.Items[0]) ⑥
.then((order) => {
return docClient.update({ ⑦
TableName: 'pizza-orders',
Key: {
orderId: order.orderId
},
UpdateExpression: 'set orderStatus = :s',
ExpressionAttributeValues: {
':s': request.status
},
ReturnValues: 'ALL_NEW'
}).promise()
})
.then((order) => {
return reply(order.user, `The status of your delivery is updated to:
${order.status}.`, facebookAccessToken) ⑧
})
}
module.exports = deliveryWebhook ⑨
Finally, you need to add a webhook route to your bot.js file. To do so, require the delivery-webhook.js handler at the top of the file with the following snippet:
const deliveryWebhook = require('./handlers/delivery-webhook')
Then you’ll need to add the new POST /delivery
route at the end of the file, just before the module.exports = api
line. This route invokes the deliveryWebhook
handler function with a request body and Facebook Messenger access token, and returns a status 200 as the success response or returns status 400 for an error.
The following listing shows the last few lines of your updated bot.js file.
Listing 9.16 Add a delivery webhook
return [
`Hello, here's our pizza menu:`,
pizzaMenu()
]
}, {
platforms: ['facebook']
})
api.post('/delivery', (request) => deliveryWebhook(request.body, request.env.facebookAccessToken), { ①
success: 200, ②
error: 400 ③
})
module.exports = api
Now you need to deploy the chatbot using either npm run update
or claudia update
, and it will be fully functional.
Building a more complex chatbot flow often requires some NLP integration. Building an NLP library from scratch is hard, and it’s beyond the scope of this book. Fortunately, several libraries offer easy-to-integrate NLP features, which can help you to improve your chatbot experience. For example:
Both Wit.ai and DialogFlow can be used for free with some limitations; IBM Watson has a free trial period.
Integrating any of these libraries into your chatbot is easy using their public APIs. All are good and recommended, but each has certain strengths and weaknesses that are not important at this point in the book. Claudia Bot Builder doesn’t limit or interfere with any of their integrations.
Facebook Messenger also has built-in NLP, but unfortunately it offers only basic NLP features and basic recognition. If you integrate it with your chatbot, it can recognize greetings, thanks, and goodbyes. Besides that, it can also detect dates, times, locations, amounts of money, phone numbers, and emails. For example, in the case of date and time, expressions such as “tomorrow at 2pm” will be converted to timestamps.
Though it is limited, Facebook Messenger’s built-in NLP gives you everything you need to allow customers to order a pizza for a specific time or day. Because this is already a long chapter, you’ll use this to make the chatbot reply to “thanks.”
To do this, you’ll need to do the following:
The built-in NLP will add the recognized entities as an nlp
key in your message
object. Each entity returns an array of parsed entity values, and each entity has a confidence
value and a value
. The confidence
value indicates the parser’s confidence in its recognition (the probability that it is correct). It ranges between 0 and 1. The value
attribute is the parsed entity value. In your case, if the entity is the word “thanks” it will always be true
. You’ll check if “thanks” exists, and if its confidence
value is more than 0.8 (80%), you’ll return a “You’re welcome!” message.
The following listing shows the last few lines of the updated bot.js file.
Listing 9.17 Reply to “thanks” message
if (
message.originalRequest.message.nlp &&
message.originalRequest.message.nlp.entities &&
message.originalRequest.message.nlp.entities['thanks'] &&
message.originalRequest.message.nlp.entities['thanks'].length &&
message.originalRequest.message.nlp.entities['thanks'][0].confidence > 0.8
) {
return `You're welcome!` ①
}
return [
`Hello, here's our pizza menu:`,
pizzaMenu()
] ②
}, {
platforms: ['facebook']
})
module.exports = api
Making your chatbot more interactive and a bit smarter is easy—but even though you want your customers to play around with your chatbot, it needs to quickly and efficiently fulfill a customer need.
For this exercise, your primary goal is to show each of Aunt Maria’s customers their last order in the user greeting message. When ordering from a specific restaurant chain, customers often tend to order the same food. The main goal of this exercise is to greet the customer with a reminder of their last order. In case you feel you can do something extra, an advanced exercise is presented after the primary exercise’s solution.
The solution to this exercise is simple. You need to scan the order list for the customer’s last order, using the sender ID in the message
object, and for returning customers display the pizza they last ordered while saying that you hope they liked it.
Listing 9.18 Handle the user greeting in the main chatbot file
'use strict'
const botBuilder = require('claudia-bot-builder')
const pizzaDetails = require('./handlers/pizza-details')
const orderPizza = require('./handlers/order-pizza')
const pizzaMenu = require('./handlers/pizza-menu')
const saveLocation = require('./handlers/save-location')
const getLastPizza = require('./handlers/get-last-pizza') ①
const api = botBuilder((message) => {
if (message.postback) {
const values = message.text.split('|')
if (values[0] === 'DETAILS') {
return pizzaDetails(values[1])
} else if (values[0] === 'ORDER') {
return orderPizza(values[1], message)
}
}
if (
message.originalRequest.message.attachments &&
message.originalRequest.message.attachments.length &&
message.originalRequest.message.attachments[0].payload.coordinates &&
message.originalRequest.message.attachments[0].payload.coordinates.lat &&
message.originalRequest.message.attachments[0].payload.coordinates.long
) {
return saveLocation()
}
return getLastPizza().then((lastPizza) => { ②
let lastPizzaText = lastPizza ? `Glad to have you back! Hope you liked your ${lastPizza} pizza` : '' ③
return [
`Hello, ${lastPizzaText} here's our pizza menu:`, ④
pizzaMenu()
]
})
}, {
platforms: ['facebook']
})
module.exports = api
The changes inside your main chatbot file are quite small, because you are just retrieving the exercise logic that should be contained inside your new get-last-pizza.js file.
Your get-last-pizza.js file should look like the following listing.
Listing 9.19 Get last pizza handler that retrieves the sender’s last pizza from DynamoDB
'use strict'
const AWS = require('aws-sdk') ①
const docClient = new AWS.DynamoDB.DocumentClient() ②
const pizzaMenu = require('./pizza-menu')
const pizzas = require('../data/pizzas.json') ③
function getLastPizza(sender) {
return docClient.scan({ ④
TableName: 'pizza-orders',
ScanIndexForward: false, ⑤
Limit: 1, ⑥
FilterExpression: `sender = #{sender}`, ⑦
}).promise()
.then((lastPizzaOrder) => { ⑧
let lastPizza ⑨
if (lastPizzaOrder){ ⑩
lastPizza = pizzas.find(pizza => pizza.id == lastPizzaOrder.pizzaId) ⑪
}
return lastPizza ⑫
})
.catch((err) => {
console.log(err)
return [ ⑬
'Oh! Something went wrong. Can you please try again?',
pizzaMenu()
]
})
}
module.exports = getLastPizza
For those who feel adventurous and want to implement something more difficult, this exercise is a good challenge. Its primary goal is to enable easy reordering of the customer’s last pizza order. In the initial customer greeting, if the customer has previously ordered a pizza, you’ll ask if the customer wants to order the same pizza again and provide two additional quick reply buttons as possible answers. If the customer taps “Yes, order again,” you need to implement the same pizza order with the same address. If the customer taps “No, show me the menu,” you’ll need to show the menu of available pizzas.
message.text
and message.post
. If the message is a postback, its post
value will be true
.if…else
or with a more sophisticated method.DocumentClient
.