Thursday, 31 August 2017

Combining other services with IBM Watson Conversation Service

It is becoming increasingly popular to offer an interface to computer applications which resembles the way that we converse with a fellow human. The IBM Watson Conversation Service is an excellent way to program such an interface because it allows the developer an easy way to specify the conversation flow and is also very good at doing fuzzy matching on input text to guess what the user is really trying to find out. However, the graphical way that conversation flows are specified doesn't allow the user to make calls to external services in order to get information to be included in the reply.

People often need to call external services to get the information that their users are looking for and so in this article I describe a simple sample application written by myself and my colleague David Curran which shows a common pattern whereby the conversation service provides a template response along with parameters which can be used by the calling application to retrieve the necessary information to give the end user the answer that they are looking for.

This pattern is useful in a lot of different situations, but we will use a fictitious application of where people want to use a conversational interface to track their parcels. We will leverage the simple conversation application as a starting point to minimise the amount of work. You can either download that sample and follow the steps below to add the interface to the conversation agent, or if your prefer you can download the completed example from our GitHub repository.

Adding the Parcel Intent to the conversation

In order to modify the conversation agent to handle parcel requests, you first need to add a parcel intent to the list of intents. The original sample contains 25 intents which is the maximum allowed with the free plan, so you will need to delete one of the existing intents. I deleted the weather intent since it is not being used and then I added a parcel intent with a few sample inputs as you can see below,


The next step is to add a node to the dialog to specify how parcel queries are to be dealt with. Our logic is quite simple. If a number is detected in the input we assume that this is the parcel number so we set a context variable parcel_num with this value and then we send back a response message with placeholders where the parcel location should be inserted. However, if no number is detected in the input stream, we simply reply saying that they need to supply us with a parcel number. For simplicity sake we won't consider holding context from one question to the next.



Implementing the dummy parcel lookup service

We don't want to use a real parcel lookup service for this sample, because when testing we won't know the parcel number for parcels in transit. Instead we will implement a very simple lookup service.

To implement the parcel lookup service you need to add the following function near the end of app.js
 what this does is respond to get requests on /api/parcel and respond with one of the sample location names e.g. requesting http://localhost:3000/api/parcel?parcel_num=6 will return the string "Buckingham Palace". Just to illustrate how we should deal with errors, we have implemented the rule that if the parcel number is divisible by 13 it will return a status code 404 and an error message saying that the parcel number is unlucky.

/**
 * A dummy parcel tracking service
 */
 app.get('/api/parcel', function(req, res) {
   var parcel_num = parseInt(req.query.parcel_num);
   if (!req.query.parcel_num || isNaN(parcel_num)) {
     return res.status(400).end("Not a valid parcel number "
                                  +req.query.parcel_num);
   }
   if (0 == (parcel_num %13)) {
     return res.status(404).end("We can't find parcel number "
                                  +parcel_num+" it is unlucky!");
   }

   var locations = [
     'Anfield', 'Stamford Bridge', 'Old Trafford', 'Parkhead',
     'Heathrow Airport', 'Westminister, London', 'Buckingham Palace',
     'Lands End, Cornwall', 'John O\'Groats'
   ];

   parcel_num = parcel_num % locations.length;
   var location  = locations[parcel_num];
   res.end(location);
});

You should experiment with this service and/or customise it before moving on to the next steps.

Recognising a parcel location request and filling in the details

The main code  modification we need to do is in the app.post('/api/message',  function in app.js. However we first need to do some housekeeping changed due to the fact that we will be using the requestify library.

Add the following line to the dependencies section of package.json:
    "requestify": "^0.2.5",

Then add this line near the top of app.js
var requestify = require('requestify');

The nub of the code is contained in the function below. You should paste this into app.js to replace the call to conversation.message which is around line 56 of the original file.

  // Send the input to the conversation service
  conversation.message(payload, function(err, data) {
    if (err) {
      // the conversation service returned an error
      return res.status(err.code || 500).json(err);
    }
    var parcel_num = data.context.parcel_num;
    if (data.intents && (data.intents.length>0) && data.intents[0].intent
                  && (data.intents[0].intent === 'parcel') && parcel_num) {
      var server = 'localhost';
      var port = process.env.PORT || process.env.VCAP_APP_PORT || 3000;
      var url = 'http://' + server + ':' + port +'/api/parcel?parcel_num='+parcel_num;
      requestify.get(url)
        .then(function(response) {
          var location = response.body;
          data.output.text[0] = data.output.text[0].replace( /\{0\}/g, location);
          return res.json(data);
        })
        .catch(function(err){
          data.output.text[0] = "Parcel lookup service returned an error: "+err.body;
          return res.json(data);
        });
    } else {
      return res.json(data);
    }
  });

The original code did nothing other than calling the updateMessage function before passing the data received from the Conversation service back to the UI layer. However, the updateMessage function didn't do anything useful so we can delete it and instead we will call our dummy parcel location service to find the location of the parcel whose number appears in the context variable.

If the http call succeeds we assume that we have a good location and we replace any placeholder {0} strings in the message received from the conversation service with this location. If we get an error status from the http call, we replace the entire string received from the conversation service with a message saying we failed to locate the parcel.

Summary

You have a conversation service which can reply to questions such as "where is my parcel number 543210" with details of where the parcel is located. It is currently only using a toy implementation which pseudo-randomly selects locations in the UK. However, it should be relatively easy to extend it to any real parcel tracing service you want. In fact, the methods used can easily be applied to interfacing with any 3rd party service.

You can go to https://github.com/bodonova/conversation-parcel to download the complete working sample.

Sending an Email from Watson Conversation Service

Sending emails is a simple way to connect a chatbot to a business process. This post shows how to extend the Watson Conversation Sample Application to get it to send email from a Gmail account. We will use the nodemailer library and a slightly modified version of the code from w3 schools to help us.

The first step is to download the original sample application from GitHub. Follow the instructions in the Readme file in the repository to get this application running on your local machine and/or on the BlueMix service. Make sure you have the original application working correctly before you go on to make any changes.

Modify the code below to reference the username and password of an Gmail account you have access to (or create a new account) and then add it near the top of the app.js file.

var nodemailer = require ('nodemailer');

var transporter = nodemailer.createTransport({  
  service: 'Gmail',  
  auth: {  
   user: 'chatbot@gmail.com',  
   pass: 'secretpassword'  
  }  
 });  
 var mailOptions = {  
  from: 'chatbot@gmail.com',  
  to: 'name@email.com',  
  subject: 'Sending Email from the chatbot',  
  text: 'That was easy!'  
 }  

If you use a gmail email and two factor authentication in Gmail you have to get an app password for your gmail address. Google are worried you will share your Gmail password with people so they give you a special password just for your app that has limited powers.

Next update the Watson application's package.json to say that you want to install the nodemailer package and force the server to use a version of node.js that supports nodemailer i.e.:

  "dependencies": {
...,
  "nodemailer": ">0.3.x"
  },
...
  "engines": {
    "node": ">= 6.9.x",
    "npm": "> 5.30.x"
  }


Having done this, the next step is to change the conversation flow in the Watson Conversation Service so that when the user says ‘send an email’, we will set a context variable called 'email' to say an email should be set. (A production version would probably send different emails to different addresses depending upon context, but we will stick to a simple example for now)

If your WCS has an intent that you want to send an email. This contains example user utterances like

#sendEmail
Can you email for me?
email dave important info
Ping dave an email to tell him to do important business things
Send an email to find my package
Send an email reminder message
Message Dave a mail to tell him the stuff he needs to do

In WCS dialog tab we want a node that recognises this intent and sets a context variable to signal to the node application to send an email.

In the json editor of the dialog node add a context variable for the email.

{
  "context": {
    "email": "TRUE"
  },
  "output": {
    "text": {
      "values": [
        "Ill email david now"
      ],
      "selection_policy": "sequential"
    }
  }
}



Next, you need to change app.js to say that when this ‘send an email' context variable is send it. At the start of the function updateMessage add in the following code check to see if the context variable email from WCS' response has been set and send an email if it has.

if(response.context.email){
    response.context.email=null;//set send variable to null so we don't send email every time
  transporter.sendMail(mailOptions, function(error, info){

    if (error) {
      console.log(error);
    } else {
      console.log('Email sent: ' + info.response);

    }
  });  
}


You can download the complete sample from here

This code just sends the same email every time. In practice you would probably have to include some information from the conversation in the mail. Usually this would something from context variables. You would add in the email information the user had told the chatbot such as the value in response.context.query

I am not a node developer and this code needs some error correction and security checks added to it. But for a quick demo it should allow you to show your chatbot emailing.

Welcome to this Blog

IBM Watson services are amazing. Most people who saw Watson winning the Jeopardy game on television thought  that they would love to have that advanced technology applied to their problems. Unfortunately advanced technology can sometimes be complex and people struggle to see how they can adapt the IBM Watson services to their needs. As a result we decided to establish this blog which will focus on tips and tricks to help you get the most out the IBM Watson services.

It is not an official blog so there is no guarantee that the tips will continue to work. Treat the advice with caution and use the tips at your own risk.