9
Typing... Async and delayed responses

This chapter covers

  • Connecting your serverless chatbot to AWS DynamoDB
  • Sending a delayed message to the user when a pizza is ready
  • Integrating simple natural language processing (NLP)

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.

9.1 Making chatbots interactive

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.

9.1.1 Tap to order: answering a postback

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:

  1. Add an Order button below the Details one, as shown in figure 9.1.
  2. Implement the pizza ordering by storing pizza order information in your database when the Order button is tapped.
  3. Schedule an order feature.
  4. Add NLP to your pizza chatbot, which will make your chatbot seem smarter and more human-like and encourage customer interaction. It will be able to answer any kinds of questions Julia’s high school friends can think of.
figure-9.1.tif

Figure 9.1 Chatbot reply with Details and Order buttons

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):

  • The user can see the details of the selected pizza.
  • The user can order the selected pizza.
  • If the user does anything else, show the initial message with the menu.
figure-9.2.eps

Figure 9.2 A visual representation of your chatbot flow

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.

figure-9.3.tif

Figure 9.3 Screenshots of the three branches of chatbot flow

9.2 Making the chatbot structure more scalable

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.

figure-9.4.eps

Figure 9.4 The folder structure of your chatbot

Create the handlers folder within your pizza-fb-chatbot project, with the following three JavaScript files:

  • order-pizza.js
  • pizza-details.js
  • pizza-menu.js

Then update your main bot.js file with the following changes:

  • Remove the require for fbTemplate because it’s no longer used in this file.
  • Require three new handler functions from the files you just created.
  • Replace the pizza details and pizza ordering logic with the pizzaDetails and orderPizza handler functions, with the pizzaId as their argument.
  • Replace the pizza menu generic template with the 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:

  • Require the list of pizzas from the pizzas.json file.
  • Implement an orderPizza handler function that will accept the pizza ID as a parameter.
  • Find the pizza by its ID and return a text message as a result from the orderPizza function.
  • Export the 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:

  • Import both the list of pizzas and the fbTemplate.
  • Create the pizzaMenu handler function.
  • Create a new generic template inside that function.
  • Loop through all the pizzas from the pizza.json file and add bubbles for each of them in the generic template message.
  • Return the message as the result.
  • Export the 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.

9.3 Connecting your chatbot to the DynamoDB database

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.

figure-9.5.eps

Figure 9.5 The chatbot flow with a DynamoDB connection

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.

9.4 Getting the user’s location from the 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:

  1. Scan the database for the latest in-progress order for the specified customer using the DocumentClient.scan method, because the sender ID is not a part of the key.
  2. Use the 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:

  1. Import the new save-location.js handler.
  2. Invoke the new 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.

figure-9.6.tif

Figure 9.6 The chatbot with location sharing

9.5 Scheduling a delivery

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:

  1. A customer taps the Order button to order pizza.
  2. The order is saved to the database with the status in-progress.
  3. Your chatbot asks the customer to share their current location.
  4. The customer shares their location.
  5. Your chatbot contacts the delivery API.
  6. The delivery request is accepted, and the chatbot updates the database and replies to the customer.
  7. When the order is picked up by the delivery service, its API triggers your webhook.
  8. The chatbot notifies the customer.
  9. When the order is delivered, the delivery API triggers the webhook again.
  10. Your chatbot sends the final message to the customer.
figure-9.7.eps

Figure 9.7 The chatbot flow with location sharing and the integration of the Some Like It Hot Delivery API

As you can see from the flow, there are two things to change in your chatbot:

  • Update the save-location.js handler to send the request to the delivery API.
  • Create a new webhook for the chatbot that will be sent to the delivery API.

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.

figure-9.8.eps

Figure 9.8 Delivery webhook flow

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.

9.6 Small talk: Integrating simple NLP

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:

  • Wit.ai (https://wit.ai), offered by Facebook, is an API that turns natural language (speech or messages) into actionable data.
  • DialogFlow (formerly API.ai; see https://dialogflow.com), offered by Google, is a conversational user experience platform enabling natural language interactions for devices, applications, and services.
  • IBM Watson (https://www.ibm.com/watson/) is an IBM supercomputer that combines artificial intelligence (AI) and sophisticated analytical software for optimal performance as a "question answering" machine. Watson also offers natural language processing for advanced text analysis.

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:

  1. Enable the built-in NLP. The setup and configuration are described in appendix B.
  2. Update your bot.js file to check whether the message is a postback. You don’t want your NLP to get activated on your menu actions.
  3. If the message is not a postback, check if the built-in NLP recognized the “thanks” expression. If yes, it should reply with “You’re welcome!”; otherwise, you’ll show the starting pizza menu.

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

9.7 Taste it!

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.

9.7.1 Exercise

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.

9.7.2 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

9.7.3 Advanced exercise

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.

Summary

  • Postback message values are parsed as message.text and message.post. If the message is a postback, its post value will be true.
  • Larger bot flows should be split into smaller files, either by a simple if…else or with a more sophisticated method.
  • As with Claudia API Builder, chatbots built with Claudia Bot Builder can be connected to DynamoDB using DocumentClient.
  • You can use a quick reply template to ask users to share their location.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset