Anthony Chu Contact Me

Adding Natural Language Processing to a Bot with Microsoft Cognitive Services (LUIS)

Tuesday, April 19, 2016

Last time we created a bot using the Microsoft Bot Framework and Azure Functions. Today, we'll improve our bot by adding natural language processing using Microsoft Cognitive Services' LUIS (Language Understanding Intelligent Service). It will be able to tell us about weather conditions from around the world.

Update: Note that this example is part of a proof-of-concept to get Bot Framework to run on Azure Functions. A typical implementation of Bot Framework and LUIS is much simpler. Check out this article for details.

LUIS

Microsoft Cognitive Services is currently free. To sign up for LUIS, we'll go to luis.ai and sign in with our Microsoft Account.

We can create and train complex natural language processing models using LUIS. That won't be necessary today... there's a pre-built model based on Microsoft's work on Cortana that we'll use to build our app.

Intents and Entities

LUIS is capable of interpreting a sentence and parsing out its intent and entities. An intent is something an user wants, sometimes it could be an action that they want to perform. An entity is the subject of the intent.

For example, if someone asks "What's the weather in Vancouver?", the intent could be check_weather and the entity is Vancouver.

Using LUIS

We can test out the Cortana pre-built model by selecting it in the menu. A dialogue will open. We can type a question about the weather and click the link at the bottom to test it out...

Testing LUIS

Testing LUIS

The URL is all that we need to use this model from inside our bot.

We can see the response includes the intent (builtin.intent.weather.check_weather) and the entity (of type builtin.weather.absolute_location with a value of Seattle)...

Testing LUIS

Calling LUIS from the Bot

To incorporate a call to LUIS, we can start by adding this function to our bot hosted in Azure Functions. It simply calls LUIS, and returns the city name if the message is a weather query for a city...

private static async Task<string> QueryLuisWeatherLocation(string message, TraceWriter log)
{
    using (var client = new HttpClient())
    {
        dynamic response = JObject.Parse(await client.GetStringAsync(@"https://api.projectoxford.ai/luis/v1/application?id=<appid>&subscription-key=<key>&q="
            + System.Uri.EscapeDataString(message)));

        var intent = response.intents?.First?.intent;

        if (intent == "builtin.intent.weather.check_weather")
        {
            var entity = response.entities?.First;
            if (entity?.type == "builtin.weather.absolute_location")
            {
                return entity.entity;
            }
        }

        return null;
    }
}

We'll call this function a bit later.

Getting the Weather

There are many APIs available for getting the weather for a given city. Today we'll be using Weather Underground.

Before using it, we'll need an API key. Sign up for a free account to get one.

Now that we have an API key, we can add this function to our Azure Function:

private static async Task<string> GetCurrentWeather(string location, TraceWriter log)
{
    using (var client = new HttpClient())
    {
        var escapedLocation = Regex.Replace(location, @"\W+", "_");

        dynamic response = JObject.Parse(await client.GetStringAsync($"http://api.wunderground.com/api/<key>/conditions/q/{escapedLocation}.json"));

        dynamic observation = response.current_observation;
        dynamic results = response.response.results;

        if (observation != null)
        {
            string displayLocation = observation.display_location?.full;
            decimal tempC = observation.temp_c;
            string weather = observation.weather;

            return $"It is {weather} and {tempC} degrees in {displayLocation}.";
        }
        else if (results != null)
        {
            return $"There is more than one '{location}'. Can you be more specific?";
        }

        return null;
    }
}

This gets the current weather for the specified city as a string. If the API indicates that the city is ambiguous (it returns multiple results), we'll let the user know. Otherwise the call didn't work and we return null.

Putting It Together

In the main section of our code, we'll call the 2 functions we created and return the correct messages:

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
    if (!HasValidAuthHeader(req.Headers))
    {
        return req.CreateResponse(HttpStatusCode.Forbidden, "Forbidden");
    }

    var message = await req.Content.ReadAsAsync<Message>();

    if (message.Type == "Message")
    {
        var location = await QueryLuisWeatherLocation(message.Text, log);

        Message replyMessage = null;
        if (location != null)
        {
            var weatherMessage = await GetCurrentWeather(location, log);
            if (weatherMessage != null)
            {
                replyMessage = message.CreateReplyMessage(weatherMessage);
            }
        }

        if (replyMessage == null)
        {
            replyMessage = message.CreateReplyMessage("Sorry, I don't understand.");
        }

        return req.CreateResponse(HttpStatusCode.OK, replyMessage);
    }
    else
    {
        return req.CreateResponse(HttpStatusCode.OK, HandleSystemMessage(message));
    }
}

Testing the Bot

Now it's time to test this out. Let's go back to the web chat control we were using in the previous article.

This time we'll ask it about weather in a city. Notice we can ask about the weather in a few different ways and it'll still understand:

Testing LUIS