Notes on integrating a Watson Assistant bot in Messenger using Botkit

Introduction

At Justinien, we build NLP chatbots designed to solve legal issues, also known as legalbots.

The first one is distributed on our own home-made messaging channel. The latest, Cashmyflight, is based on Facebook's Messenger platform.

Why Watson Assistant ?

  • + : Powerful NLP technology
  • + : The intuitive Bluemix interface
  • + : The easy integration with all the other technologies from IBM : Cloud Foundry, Visual Recognition, Text-to-Speech, Tone Analyzer etc.
  • + : For startups : the IBM Global Entrepreneur Program, giving you access to multiple financial advantages
  • + : Easy messaging channel integration with Botkit Slack, Messenger and Twilio (complete list here)
  • - : Not free (but still worth the time it gives you)
  • - : Proprietary technology

Why Facebook Messenger ?

  • + : More than a billion users for B2C bots
  • + : The user interface : developing one yourself is hard ! (trust me, I did)
  • + : The ability to advertise your bot to Facebook's users
  • - : The review duration (slow time-to-market)

Even though the documentation from Messenger and Watson Assistant is clear and well written, I found a lack of clarity concerning the use of the stack Messenger - Watson Assistant - Botkit. Thus, the objective of theses notes is to describe in one document how we managed to integrate our bot in Messenger using the aforementioned technology stack.

Set up

Watson Assistant

First, you need to gather some parameters from Watson Assistant. In this article we consider that you have at least one workspace set up and ready to use.

The parameters are available in your Bluemix interface. We need :

  • Credentials : conversation username and conversation password
  • A conversation url : https://gateway.watsonplatform.net/conversation/api
  • The workspace id

Messenger

Dummy developer account

This step is optional but I found it useful working on my bot.

If you hate receiving notifications while you are laser-focused in your work as much as I do, setting up a "fake" Facebook account for the sole purpose of being in a distraction-free environment is the simplest way to go. Otherwise, you can use your regular Facebook account for all the steps below.

Facebook development page

Next up, we need to create a Facebook page allowing us to test our bot : tutorial by Facebook

Facebook production page

Later we will create the page used for the production environment, so don't spend a lot of time on branding your Facebook development page.

Facebook app

Once your Facebook page is ready, you can log in Facebook Developers and create a Messenger application.

All you need to obtain for now is :

  • The secret key of your app : available in Settings -> General
  • An access token : accessible from Settings -> Advanced

Botkit

Following the Watson Assistant documentation, the official solution to deploy your bot on many channels is to use Botkit as a middleware.

All you need to do is to download all the files from this repository : respository link.

Then, change the environment variables :

.env


#WATSON
CONVERSATION_URL=<watson assistant url>
CONVERSATION_USERNAME=<watson assistant username>
CONVERSATION_PASSWORD=<watson assistant password>
WORKSPACE_ID=<workspace id>

#FACEBOOK
FB_ACCESS_TOKEN=<the development page access token>
FB_APP_SECRET=<the development page secret key>

#WHAT-TO-USE
USE_FACEBOOK=true

Now, it is time to implement the message processing logic.

Basic message formatting

Text

Let's start with text messages.

We can remove this part :

bot-facebook.js


controller.hears('(.*)', 'message_received', function(bot, message) {
  if (message.watsonError) {
    console.log(message.watsonError);
    bot.reply(message, message.watsonError.description || message.watsonError.error);
  } else if (message.watsonData && 'output' in message.watsonData) {
    bot.reply(message, message.watsonData.output.text.join('\n'));
  } else {
    console.log('Error: received message in unknown format. (Is your connection with Watson Conversation up and running?)');
    bot.reply(message, "I'm sorry, but for technical reasons I can't respond to your message");
  }
});

And replace it with this one :

bot-facebook.js


// when any text message is received by the controller, it will be processed by the processWatsonResponse function
controller.hears('(.*)', 'message_received', processWatsonResponse);

Next :

bot-facebook.js


function processWatsonResponse(bot, message){
    if (message.watsonError) {
        console.log(message.watsonError);
        bot.reply(message, "I'm sorry, but for technical reasons I can't respond to your message");
    } else {
        var output_size = message.watsonData.output.text.length;
        recursiveMessageReply(0, output_size, message);
    }
}

I found that when dealing with complex message structures, it is best to process the output recursively, because sometimes you might need to alternate between text messages and buttons or other UI elements. Also, it looks cleaner on Messenger when you separate all the messages instead of merging them all into one.

bot-facebook.js


function recursiveMessageReply(index, max_index, message){
    if(index < max_index){
        var current_text = message.watsonData.output.text[index];

        // do not process empty messages
        if(current_text.length > 0){
            var current_reply = {text: current_text};

            bot.reply(message, current_reply, function(err, response){
                recursiveMessageReply(++index, max_index, message);
            });
        } else {
            recursiveMessageReply(++index, max_index, message);
        }
    }
}

It's done. We can now send and receive simple text messages.

Quick replies

Quick replies are a great way to guide the user throughout the conversation.

bot-facebook.js


function processWatsonResponse(bot, message){
    if (message.watsonError) {
        console.log(message.watsonError);
        bot.reply(message, "I'm sorry, but for technical reasons I can't respond to your message");
    } else {
        var reply_object = {};

        // output.preselection is a custom json format that must be configured in your Watson Assistant nodes to indicate when and what quick replies should be displayed
        if(message.watsonData.output.preselection){
          var quick_replies_size = message.watsonData.output.preselection.length;
          var quick_replies = [];

          for(i=0 ; i<quick_replies_size ; i++){
              // integrating a quick reply object according to Facebook's documentation
              quick_replies.push({
                  content_type: "text",
                  title: message.watsonData.output.preselection[i],
                  payload: message.watsonData.output.preselection[i]
              });
          }

          reply_object.quick_replies = quick_replies;
        }

        var output_size = message.watsonData.output.text.length;
        recursiveMessageReply(0, output_size, message, reply_object);
    }
}

Link : Facebook quick replies documentation

bot-facebook.js


function recursiveMessageReply(index, max_index, message, reply_object){
    if(index < max_index){
        var current_text = message.watsonData.output.text[index];
        if(current_text.length > 0){
            var current_reply = {text: current_text};

            //display the quick replies when the last text message appears
            if(index == max_index-1){
                current_reply.quick_replies = reply_object.quick_replies;
            }

            bot.reply(message, current_reply, function(err, response){
                recursiveMessageReply(++index, max_index, message, reply_object);
            });
        } else {
            recursiveMessageReply(++index, max_index, message, reply_object);
        }
    }
}

URL button

It is not possible to use html links (<a></a>) in a Messenger message. Instead, you are expected to display an URL button.

bot-facebook.js


function processWatsonResponse(bot, message){
    if (message.watsonError) {
        bot.reply(message, "I'm sorry, but for technical reasons I can't respond to your message");
    } else {
        var reply_object = {};

        if(message.watsonData.output.preselection){
          var quick_replies_size = message.watsonData.output.preselection.length;
          var quick_replies = [];

          for(i=0 ; i<quick_replies_size ; i++){
              quick_replies.push({
                  content_type: "text",
                  title: message.watsonData.output.preselection[i],
                  payload: message.watsonData.output.preselection[i]
              });
          }

          reply_object.quick_replies = quick_replies;
        }

        var output_size = message.watsonData.output.text.length;

        // output.button is a custom json flag that must be configured in your Watson Assistant nodes to indicate when and what url button should be displayed
        if(message.watsonData.output.button){
            recursiveMessageWithButtonReply(0, output_size, message, reply_object, 1);
        } else {
            recursiveMessageReply(0, output_size, message, reply_object);
        }

    }
}

bot-facebook.js


function recursiveMessageWithButtonReply(index, max_index, message, reply_object, attachment_counter){

    if(index < max_index){
        var current_text = message.watsonData.output.text[index];

        if(current_text.length > 0){
            var current_reply = {text: current_text};
            if(index == max_index-1){

                // the URL button Facebook formatting
                // note : observe that the current text message must be displayed in the payload, not as a regular text message
                var attachment = {
                    type: "template",
                    payload:{
                        template_type: "button",
                        text: current_text,
                        buttons:[{
                            type: "web_url",
                            url: message.watsonData.output.button.url,
                            title: message.watsonData.output.button.title,
                            webview_height_ratio : "tall"
                        }]
                    }
                };
                bot.reply(message, {attachment:attachment, quick_replies : reply_object.quick_replies}, function(err, response){
                    console.log("error:" + JSON.stringify(err) + " response:" + JSON.stringify(response));
                    recursiveMessageWithButtonReply(++index, max_index, message, reply_object, ++attachment_counter);
                });
            } else {
                bot.reply(message, current_reply, function(err, response){
                    recursiveMessageWithButtonReply(++index, max_index, message, reply_object, ++attachment_counter);
                });
            }
        } else {
            recursiveMessageWithButtonReply(++index, max_index, message, reply_object, attachment_counter);
        }
    }
}

Link : Facebook documentation on URL buttons

Attachment

Attachments allow you to process the user's document in your backend.

First, a new controller is needed :

bot-facebook.js


controller.on('message_received', processMessengerAttachment);

You also need to access Botkit's middleware logic to send asynchronous message to Watson Assistant once the attachment is dealt with :

bot-facebook.js


var middleware = require('botkit-middleware-watson')({
  username: process.env.CONVERSATION_USERNAME,
  password: process.env.CONVERSATION_PASSWORD,
  workspace_id: process.env.WORKSPACE_ID,
  url: process.env.CONVERSATION_URL || 'https://gateway.watsonplatform.net/conversation/api',
  version_date: '2017-05-26'
});

var current_asked_document_type;

function processWatsonResponse(bot, message){
    if (message.watsonError) {
        bot.reply(message, "I'm sorry, but for technical reasons I can't respond to your message");
    } else {
        var reply_object = {};

        if(message.watsonData.output.preselection){
          var quick_replies_size = message.watsonData.output.preselection.length;
          var quick_replies = [];

          for(i=0 ; i<quick_replies_size ; i++){
              quick_replies.push({
                  content_type: "text",
                  title: message.watsonData.output.preselection[i],
                  payload: message.watsonData.output.preselection[i]
              });
          }

          reply_object.quick_replies = quick_replies;

        }

        // output.document_type is a custom json flag that must be configured in your Watson Assistant nodes to indicate when and what kind of document should be processed
        if(message.watsonData.output.document_type){
          current_asked_document_type = message.watsonData.output.document_type;
        }

        var output_size = message.watsonData.output.text.length;
        if(message.watsonData.output.button){
            recursiveMessageWithButtonReply(0, output_size, message, reply_object, 1);
        } else {
            recursiveMessageReply(0, output_size, message, reply_object);
        }

    }
}

bot-facebook.js


function processMessengerAttachment(bot, message){
    if(message.attachments && current_asked_document_type){
        var attachment = message.attachments[0].payload.url;
        console.log("Received : " + JSON.stringify(message));

        // your own backend logic should be inserted here
        backend.uploadDocument(attachment, last_conversation_id, current_asked_document_type).then(function(response){
            if (!response.ok) {
                 console.log('Document upload - Error ' + response.status + ' ' + response.statusText + ' - url : ' + url + ' in ' + conversation_id);
            } else {
                // once the document is processed :

                current_asked_document_type = false;
                var newMessage = clone(message);
                newMessage.text = "I'm done !";

                // sending an asynchronous message to Watson Assistant to indicate a successful upload and continue the conversation
                middleware.sendToWatsonAsync(bot, newMessage).then(function(response){
                    processWatsonResponse(bot, newMessage);
                }, function(err){
                    console.log('Document upload - Error ' + err.status + ' ' + err.statusText);
                });
            }
        }, function(err){
            console.log('Error document upload');
        });
    } else {
        bot.reply(message, "Sorry I cannot read this file)");
    }
}

Link : Facebook documentation on attachments

Get started button

A Get Started button is necessary to validate an bot app review. It's a simple CURL command :

get_started_button.sh


#!/bin/sh
curl -X POST -H "Content-Type: application/json" -d '{
    "get_started" : {"payload": "GET_STARTED"}
}'
"https://graph.facebook.com/v2.6/me/messenger_profile?access_token=<your_facebook_access_token>";

Persistent menu

A persistent menu is useful to help your user get a quick access to additional informations or conversation entry points. It's also a simple curl command :

persistent_menu.sh


#!/bin/sh
curl -X POST -H "Content-Type: application/json" -d '{
    "persistent_menu":[{
        "locale":"default",
        "composer_input_disabled": false,
        "call_to_actions":
            [{
                "title":"Conversation",
                "type":"nested",
                "call_to_actions":[{
                    "title":"Restart the conversation",
                    "type":"postback",
                    "payload":"Restart the conversation"
                }]
            },{
                "title":"Contact",
                "type":"nested",
                "call_to_actions":[{
                    "type":"web_url",
                    "title":"Our company",
                    "url":"http://justinien.co",
                    "webview_height_ratio":"full"
                },{
                    "title":"Contact us",
                    "type":"postback",
                    "payload":"Contact us"
                }]
            }]
    }]
}' "https://graph.facebook.com/v2.6/me/messenger_profile?access_token=<your_facebook_access_token>"

Bot description

A greeting message is also necessary to validate an bot app review. A simple CURL command as well :

greeting.sh


#!/bin/sh
curl -X POST -H "Content-Type: application/json" -d '{
  "greeting": [
    {
      "locale":"default",
      "text":"The first legalbot that gets you money from your plane issues (delay, cancellation, etc.) in a few minutes !"
    }
  ]
}' "https://graph.facebook.com/v2.6/me/messenger_profile?access_token=<your_facebook_access_token>"

Testing

Development environment

Setting up a localtunnel

During the development phase, you can use temporarily your own machine as a web server using localtunnel :

Console terminal


    #install
    npm install -g localtunnel

    #create a localtunnel
    lt -p 8001 -s <a custom subdomain>

    #launch your web server
    node server.js
    

Setting up a webhook

Go to developers.facebook.com and configure a webhook by navigating to Messenger -> Parameters -> Webhooks

You will need to enter a callback link (https://<your custom subdomain>.localtunnel.me/facebook/receive) and a verification token (it can be anything you want).

Update your .env file with the verification token and relaunch your local server.

.env


        FB_VERIFY_TOKEN=<your webhook token>
    

Testing

Once you are done with the configuration, you can communicate with your bot directly from your Facebook page.

Debugging

When the need for debugging arises, the most straightforward way to do it is to have a look at your server logs in the console terminal (below your node server.js instruction). You can set up logs by simply using the console.log() function in your js files. Don't forget to restart your local server whenever you make a change to your javascript files.

Production environment

Facebook production page

Similarly to the development page, we must create a production page that will be used by the end-users. All you have to do is repeat the same process than the one we used previously (facebook page, facebook app, webhook), and update the .env file.

.env


        #PRODUCTION ENV
        #FB_ACCESS_TOKEN=<the production page access token>
        #FB_APP_SECRET=<the production page secret key>

        #DEV ENV
        FB_ACCESS_TOKEN=<the development page access token>
        FB_APP_SECRET=<the development page secret key>

        FB_VERIFY_TOKEN=<your webhook token>
    

App hosting

Instead of using a localtunnel, we are going to host the app on a real web server. At Justinien we use IBM's Cloud Foundry but the choice is up to you.

App review

Once your bot is ready, it has to be submitted to the Facebook App review before going public. The process can take a long time (one month and a week in our case), so make sure to have a clean bot that respects all of Facebook terms and conditions.

Conclusion

Congratulations, you now have own a smart bot available to the public ! Feel free to reach out to me if you have any questions or need further details.


I am Basile, a young software craftsman documenting his entrepreneurship journey. If you liked this article, you can follow my adventures in real time on Twitter. I’m always looking forward to meeting new people and learning from others !

My personal website : basilesamel.com