Chatbots: How to Make a Bot for Messenger From Scratch (Part 2)
Table of Contents
Introduction
In my last post, I showed you how to develop a basic bot using Facebook Messenger. We set up our local development environment and then ported the local bot server to AWS infrastructure. Finally, we connected it to API.ai for Natural Language Understanding (NLU) and made our chatbot live on Facebook Messenger. The chatbot, thus far, has been designed to help us keep track of our homework. However, in its current state, it cannot actually save any of our Homework items. The objective of this post will be to add a database to our bot and make sure we expand the bot’s NLU to handle most Homework requests.
Adding A Database
We need a database in order to save the Homework items our users will create. For the sake of this tutorial, we will use MongoDB as our database. For our MongoDB deployment we will use the new MongoDB Atlas service. You can sign up for free here.
Planning a Collection
Before we create a document in our MongoDB database, we need to understand what that document will look like. For this tutorial, we will create a simple document that holds our user’s data and the tasks they will be managing in their Homework list. Our document will look something like the following JSON document.
{
_id:ObjectId,
homework:[
{
subject:String,
due:ISODate
}
],
session:String
}
With this JSON structure, we can easily add and delete from the “tasks” array and keep track of our users, using the “session” key. The “session” key will come from the data sent via the Facebook webhook.
Let’s compare this to the data we receive from the API.ai context. The following document is returned to us when a homework task is successfully understood and all necessary information captured by our API.ai bot.
{
"id": "ae923ee7-5cb5-4152-bf3c-1f72e0f8c629",
"timestamp": "2017-04-26T02:51:22.249Z",
"lang": "en",
"result": {
"source": "agent",
"resolvedQuery": "Tomorrow",
"action": "save",
"actionIncomplete": false,
"parameters": {
"due": "2017-04-26",
"subject": "math"
},
"contexts": [
],
"metadata": {
"intentId": "c13099a0-8be7-43ee-9915-8a7534f7dad4",
"webhookUsed": "false",
"webhookForSlotFillingUsed": "false",
"intentName": "homework"
},
"fulfillment": {
"speech": "Your math homework was saved for 2017-04-26",
"messages": [
{
"type": 0,
"speech": "Your math homework was saved for 2017-04-26"
}
]
},
"score": 1
},
"status": {
"code": 200,
"errorType": "success"
},
"sessionId": "1605820592779632"
}
You can see in the document that there is a Key named “result,” which has another Key “parameters” as an Object. That is the Object we will push into our array of saved assignments, which we will store in our database using the MongoDB model we outlined before.
Setting Up MongoDB Atlas
After you register for MongoDB Atlas, you will be asked to create a new cluster. Select the “FREE” cluster from the options. Once your cluster has been created, you will see a similar dashboard to the image below.
Click on the “Connect” button —this is where you will find the MongoDB connection URL for your chatbot. Once you do this, you will see the following screen:
Click on “Add an Entry” to add your AWS EC2 instance public IP address to the white list. You can find that address in the EC2 dashboard as pictured below:
The IPv4 Public IP is the address you want to use here. The next security feature we need to implement is the security group for our EC2 instance, so it can connect to this MongoDB cluster. Open the Security Group for the instance you created in the last blog post and add the following entry:
Please note that this configuration is too insecure to use in production. However, it will work for demonstration purposes in this tutorial.
Viewing your data
Now, we need a way to view our data as we create it so we can debug more easily. For this, we will use MongoDB Compass. You can download it here. Download and install compass. When it launches, you can set up a new MongoDB connection to your cluster.
To find this URL in MongoDB Atlas, open your cluster and click on the link labeled PRIMARY.
This is the primary endpoint for your cluster, and the Host URL MongoDB Compass will use to make the connection.
Setting Up Our App
Connecting to the database
In our app, we need to install the MongoDB package for Node.js. Open a terminal in the root directory of your app and execute the following command:
> npm install --save mongodb
Now, in your App.js file, add the following variables to the top of the file.
var MongoClient = require('mongodb').MongoClient
var url = '<link to your Atlas cluster>';
Replace “<link to your Atlas cluster>
” with the URL provided to you when you created a cluster with MongoDB Atlas. Be sure to properly name your database in the URL.
The first thing we need to do is to initialize our database with user data when a new user sends us a message. We need to add a new function to our app.
app.initUserHomework = function(data, db, callback) {
// Get the documents collection
var collection = db.collection('homework');
// Insert some documents
collection.insertOne(data, function(err, result) {
if(err) throw err;
callback(result);
});
}
We will use this function to insert our first bit of user data without any homework assignments added. However, before we do this, we need to make sure a document with our user’s Session ID doesn’t exist. Let’s use the function below:
app.findDocument = function(sessionID, db, callback) {
// Get the documents collection
var collection = db.collection('homework');
// Find some documents
collection.findOne({'session': sessionID}, function(err, doc) {
if(err){ throw err; }
callback(doc);
});
}
This function will find a document by user session and return that document. If it fails, it will return null.
Connecting routes to the data
Now, in our “app.post(‘/fb’...
” route, we can use these functions together. Modify the route so it looks like the code below.
// accept incoming messages
app.post('/fb', function(req, res){
var id = req.body.entry[0].messaging[0].sender.id;
var text = req.body.entry[0].messaging[0].message.text;
console.log(JSON.stringify(req.body))
// here we add the logic to insert the user data into the database
MongoClient.connect(url, function(err, db) {
if(err) {
console.log(err)
}
app.findDocument(id, db, function(doc) {
if(doc === null){
app.initUserHomework({session:id, homework:[]}, db, function(doc){
db.close();
})
}
});
});
app.speechHandler(text, id, function(speech){
app.messageHandler(speech, id, function(result){
console.log("Async Handled: " + result)
})
})
res.send(req.body)
})
You should now see the document in MongoDB Compass with an empty homework array.
Now, we can save our homework assignments to our MongoDB database. In the app.speechHandler
function, we will add the logic needed to save our data. We also need to save our user session if it doesn’t exist yet.
The first bit of logic we will use is to make sure our parameters are fulfilled by API.ai. We’ll add that to our speechHandler
function so it looks like the following.
app.speechHandler = function(text, id, cb) {
var reqObj = {
url: 'https://api.api.ai/v1/query?v=20150910',
headers: {
"Content-Type":"application/json",
"Authorization":"Bearer 4485bc23469d4607b19a3d9d2d24b112"
},
method: 'POST',
json: {
"query":text,
"lang":"en",
"sessionId":id
}
};
request(reqObj, function(error, response, body) {
if (error) {
console.log('Error sending message: ', JSON.stringify(error));
cb(false)
} else {
console.log(JSON.stringify(body))
if(body.result.parameters.due !== "" && body.result.parameters.subject !== "")
{
// here we have enough information to save our homework assignment to the database.
MongoClient.connect(url, function(err, db) {
if(err) {
console.log(err)
}
app.updateHomework({due:body.result.parameters.due, subject:body.result.parameters.subject}, id, db, function(doc){
db.close();
});
});
}
cb(body.result.fulfillment.speech);
}
});
}
Now, when we engage our bot for a new assignment, we will see the array start to fill with homework assignments.
Advancing the NLU
We can add items to our homework list now, but we still can’t ask our bot to recall our list of homework assignments. To do this, let’s create a new intent in API.ai. Open your API.ai account and click on “Create Intent”.
We need to create enough example utterances so the NLU can understand this intent. Let’s use the following utterances for this tutorial.
- Show me my assignments
- Show my homework
- What’s my homework
- What homework do I have?
- List assignments
- List homework
Let’s also add an “Action” to our intent. This is how we will identify whether or not we should save data or list data in our code. Let’s name this action “list.homework
.” Your final intent should look similar to this.
Save this intent. Now, we need to build the logic into our Speech Handler function so we can differentiate between the two types of NLU we have (save homework, list homework).
Modify your app.speechHandler
function so it looks like the following.
app.speechHandler = function(text, id, cb) {
var reqObj = {
url: 'https://api.api.ai/v1/query?v=20150910',
headers: {
"Content-Type":"application/json",
"Authorization":"Bearer 4485bc23469d4607b19a3d9d2d24b112"
},
method: 'POST',
json: {
"query":text,
"lang":"en",
"sessionId":id
}
};
request(reqObj, function(error, response, body) {
if (error) {
console.log('Error sending message: ', JSON.stringify(error));
cb(false)
} else {
console.log(JSON.stringify(body))
if(body.result.action === "save"){
if(body.result.parameters.due !== "" && body.result.parameters.subject !== "")
{
// here we have enough information to save our homework assignment to the database.
MongoClient.connect(url, function(err, db) {
if(err) {
console.log(err)
}
app.updateHomework({due:body.result.parameters.due, subject:body.result.parameters.subject}, id, db, function(doc){
db.close();
});
});
}
}else if(body.result.action === "list.homework"){
MongoClient.connect(url, function(err, db) {
if(err) {
console.log(err)
}
app.findDocument(id, db, function(doc){
var iln = doc.homework.length;
var listItemsArray = [];
for(var i = 0; i < iln; i++){
listItemsArray.push(
{
"title": doc.homework[i].subject,
"subtitle": doc.homework[i].due
}
)
}
if(listItemsArray.length === 1){
listItemsArray.push({
"title":"Add more assignments",
"subtitle":":)"
})
}
app.sendListTemplate(listItemsArray, id, function(result){
console.log("List template sent")
})
db.close();
})
});
}
cb(body.result.fulfillment.speech);
}
});
}
You can see from this update that we have a function that doesn’t exist: app.sendListTemplate
. We need to add this function. This function will simply send a List Template type message through Facebook Messenger. Create the function so it looks like the following.
app.sendListTemplate = function(list, id, callback){
var data = {
"recipient":{
"id":id
}, "message": {
"attachment": {
"type": "template",
"payload": {
"template_type": "list",
"top_element_style": "compact",
"elements": list
}
}
}
}
var reqObj = {
url: 'https://graph.facebook.com/v2.6/me/messages',
qs: {access_token:token},
method: 'POST',
json: data
};
console.log(JSON.stringify(reqObj))
request(reqObj, function(error, response, body) {
if (error) {
console.log('Error sending message: ', JSON.stringify(error));
cb(false)
} else if (response.body.error) {
console.log("API Error: " + JSON.stringify(response.body.error));
cb(false)
} else{
cb(true)
}
});
}
Now, when you ask the chatbot to show you your homework, it will reply with a list of items that are due.
App Conclusion
This is as far as we will go with this app for this tutorial. There is still a lot to do in regards to cleaning up this code and making it more manageable for teams, but that is outside the scope of this tutorial. Furthermore, there is so much more we could do with the features available in our bot. I’ll leave that to you. The last piece to touch on in this tutorial is scaling our bot. We need some way to monitor the resource consumption of our server and have AWS scale it as we see fit. Luckily, we used MongoDB Atlas, so scaling our chatbot will be easy.
Scaling Our Bot
First, you need to login to your AWS account and go to the Cloud Watch application.
When you get to Cloud Watch, click on “Alarms.” Then click on “Create Alarm.”
AWS uses Cloud Watch alarms to monitor server status and resource consumption.
Select EC2 Metrics, and choose CPU Utilization.
Now click Next.
Now add a new alarm and makes sure the period for the alarm is updated every minute. Your alarm configuration should look similar to this:
We make sure the CPU is under 75% for every consecutive minute until we trigger this alarm. The goal is to launch a new server as our current server is using too much server CPU.
Create the Alarm.
Switching SSL to ELB
NOTE: Executing this section of the tutorial will incur costs on the AWS platform.
Since we need to use a Load Balancer, we will have to migrate our SSL from our EC2 instance to our load balancer. The first step is to delete the SSL certificates from the code. Change the HTTPS requirement, “https = require('https'),” to an HTTP requirement, “http=require(‘http’).” Finally, modify the server creation at the bottom of the code from this,
https.createServer(sslOpts, app).listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
});
To this,
http.createServer(app).listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
});
Finally, set the port of the app to 80 instead of 443.
Now we need to create a new SSL certificate for our domain. Navigate to “Certificate Manager” in the AWS console and click on “Get Started.” Add your domain name as “*.yourdomain.tk”, this will create a wildcard certificate that you can use for any subdomain. You will need to have an email address “admin@youdomain.tk” available to receive the certification validation. Once you click on the link in the email, your certificate will be available in the Load Balancer.
Now we need to create a new load balancer for our bot. Navigate to the EC2 section of AWS, then select Load Balancers on the left menu to create a new one. Create a “Classic Load Balancer.” First configure your load balancer name and add the subnet available.
Move on to configure security groups. Select your default security group. After choosing the default security group, move on to the SSL security and choose the certificate you made in the the Certificate Manager.
Next, configure your health check to ping the “/health” endpoint on port 80. Finally, add your current EC2 instance. Add a “Name” tag and then “Create” your Load Balancer.
Create an Auto Scaling Group
Now that we have an ELB, we need to create a Launch Configuration and an Auto Scaling group to scale our servers as they need more resources. The first step is to create an “Image” from the current running server. Open the EC2 Instances dashboard and select the current running instance, then click “create image.”
This will create an AMI that we can use in our Launch Configuration. Now, click on the Launch Configuration tab on the left panel. This will lead you through the steps it takes to create an auto scaling group with a launch configuration.
Select the AMI we just create. Choose the AMI you just created. Configure the basics.
Then create the launch configuration.
Configure the auto scaling group to use scaling policies.
Choose the alarm we created earlier and select to add 1 instance every time the alarm is triggered. You can delete the “Decrease Group Size” and there is no need for notifications at this time. You can add a Name tag to easily identify the group. Once your auto scaling group is deployed, your auto-scaling configuration is complete. You server instances will be added as needed.
Conclusion
This tutorial has covered a lot of the basics of building a complete bot infrastructure. Use this code base as a starting point for building a bigger better bot on Node.js using MongoDB Atlas and AWS. You should now have all the tools you need to build any bot you want. Good luck!
I’m thinking of logging the raw JSON returned by API.AI. This allows me to do analysis. Do you think this is a good idea?
great