Anthony Chu Contact Me

Easy Natural Language Processing with Bot Framework and LUIS

Sunday, June 5, 2016

I noticed my previous article on hooking up LUIS to an Azure Functions Bot Framework bot was mentioned in a great article comparing different bot frameworks by the folks at Aspect Software. There was one problem, however; it wasn't immediately obvious to the author of the article or their readers that my code was a hack to get the Bot Framework to work with Azure Functions. It's not representative of how one would write a Bot Framework bot in C# or Node.

So I thought today I'd write about how Bot Framework and LUIS were meant to be used together. I presented a similar example at the Vancouver Polyglot Unconference last week on chatbots. I am choosing to use Node here for no other reason than I'm more familiar with the Node Bot Framework SDK right now. A C# version would be quite similar.

Setting up the Prebuilt LUIS Model

I walked through how to set up a prebuilt Cortana model in LUIS in my previous article. Follow the instructions there if you don't have one set up already.

Building the Bot

We need a couple of dependencies in our Node app. The first is the BotBuilder itself. The second is Restify.

npm install botbuilder --save
npm install restify --save

Here's the basic outline of the Node app:

var builder = require('botbuilder');
var restify = require('restify');

var bot = new builder.BotConnectorBot({
    appId: 'YourAppId',
    appSecret: 'YourAppSecret'
});

var dialog = new builder.LuisDialog('https://api.projectoxford.ai/luis/v1/application?id=ID&subscription-key=KEY&q=');

bot.add('/', dialog);

// insert intent handlers here

var server = restify.createServer();
server.post('/v1/messages', bot.verifyBotFramework(), bot.listen());

server.listen(process.env.port || 8080, function () {
    console.log('%s listening to %s', server.name, server.url);
});

It's pretty straight-forward. We're creating a BotConnectorBot, providing it our Bot Connector credentials. Check out the Bot Framework docs or my previous post for details on how to set this up.

There's a few lines of code to set up the restify server and hook the bot up to it.

The BotBuilder SDK is tightly integrated with LUIS. For Node, we simply have to create a LuisDialog.

To handle LUIS intents, we can add handlers to the dialog. A handler can be a function or a sequence of functions called a waterfall:

var weatherClient = require('./wunderground-client');

// ...

dialog.on('builtin.intent.weather.check_weather', [
    (session, args, next) => {
        var locationEntity = builder.EntityRecognizer.findEntity(args.entities, 'builtin.weather.absolute_location');
        if (locationEntity) {
            return next({ response: locationEntity.entity });
        } else {
            builder.Prompts.text(session, 'What location?');
        }
    },
    (session, results) => {
        weatherClient.getCurrentWeather(results.response, (responseString) => {
            session.send(responseString);
        });
    }
]);

Whenever the bot receives a message, our LUIS model is automatically called by the SDK and the top detected intent's handler is triggered. In the above example, we're responding to the builtin.intent.weather.check_weather intent with a waterfall.

The first function in the waterfall looks for the location in the entities. If it's matched, we'll simply call the next function in the waterfall. If it is missing (e.g., the message was something like "what's the weather?"), we'll use the built-in text prompt dialog to ask for a value. That value is passed to the next function in the waterfall.

The weather client is split out into another module but it's pretty simple as well:

var restify = require('restify');

var wundergroundClient = restify.createJsonClient({ url: 'http://api.wunderground.com' });

function getCurrentWeather(location, callback) {
    var escapedLocation = location.replace(/\W+/, '_');
    wundergroundClient.get(`/api/dd328fca2cbee1db/conditions/q/${escapedLocation}.json`, (err, req, res, obj) => {
        console.log(obj);
        var observation = obj.current_observation;
        var results = obj.response.results;
        if (observation) {
            callback(`It is ${observation.weather} and ${observation.temp_c} degrees in ${observation.display_location.full}.`);
        } else if (results) {
            callback(`There is more than one '${location}'. Can you be more specific?`);
        } else {
            callback("Couldn't retrieve weather.");
        }
    })
}

module.exports = {
    getCurrentWeather: getCurrentWeather
};

Testing it out

We can test it out using the Bot Framework Emulator:

Meetup

Downloads

The source code can be found here: https://github.com/anthonychu/bot-framework-luis-node

And my Polyglot Unconference presentation and code: