Developing a serverless WhatsApp chatbot

Luis Beltran - Sep 17 '19 - - Dev Community

This article is part of #ServerlessSeptember. You'll find other helpful articles, detailed tutorials, and videos in this all-things-Serverless content collection. New articles are published every day — that's right, every day — from community members and cloud advocates in the month of September.

Find out more about how Microsoft Azure enables your Serverless functions at https://docs.microsoft.com/azure/azure-functions/.

Chatbots are AI software capable of interacting with users in natural language. They are able to detect users' intentions by extracting keywords from their messages and then provide an appropriate response to their requests. They are important nowadays as they can be used for repetitive, time-consuming tasks in effective ways, allowing companies to focus in other activities and optimize resources (especially human resources).

Microsoft provides two key technologies, LUIS and Bot Framework to develop and create bots the easy way. Without being an AI-expert, you can develop and deploy conversational bots which interact with your users and process their needs. Add Azure Bot Service to the equation and you'll have a bot connected to the cloud which can be inserted in several channels, such as Skype, Microsoft Teams, Facebook Messenger and even to your website through the WebChat channel with very few configuration steps.

However, some channels are not currently available in the Azure Bot Service offer. WhatsApp, the most popular messenger app with 1.6 billion monthly users as of July 2019 is a notable example. And our customers would love to communicate with our apps through this messenger application.

So, how can we tackle this problem? Azure Functions to the rescue! We can simplify things by creating a serverless code that is connected to a WhatsApp number through webhooks. This means that an Azure Function will be triggered every time a user sends a message to this number. And we can infuse intelligence in the reply by connecting it to a LUIS model for natural language processing. In short, we will be using three technologies:

  • LUIS (Language Understanding Intelligent Service)
  • Azure Functions
  • Twilio API (for accessing WhatsApp)

Let's get started!

Part 1: LUIS

LUIS stands for Language Understanding Intelligent Service. It is part of the Microsoft AI Platform and allows us to identify user's intentions and key elements from their messages. In order to create a smart language processing model, we need to train it before by feeding it with examples (utterances)

Step 1. Create a new LUIS application.
LUIS app

Step 2. Add the geographyV2 prebuilt entity to the project. An entity represents a part of the text that will be identified (for instance, a city)
Entity

Step 3. Create a new Intent: GetCityWeather. An intent represents what our users are asking for, such as booking a hotel room, looking for a product, or requesting the weather conditions for a specific city.

Intent

Step 4. Add at least 5 sample utterances for this intent. Note that the cities are automatically detected as geographyV2 entities. The model gets smarter by providing sample sentences that the users might say for a specific intent.
Entities

Step 5. Click on the Train button in order to create the LUIS model. When the process finishes, test it with a new request and see if it's working (it should detect the intent and entity):
Train and Test

Step 6. Publish the model. Select the Production slot and then go to Azure Resources. Copy the URL from Example query box, since we will use it later for our queries and requests from Azure Functions in Part 3.
Publish

Part 2: OpenWeatherMap

OpenWeatherMap is a service that we can use to obtain weather information about a specific city.

Step 1. Sign up to the service.
OpenWeatherMap

Step 2. Click on API and then Subscribe to the Current weather data API.
API

Step 3. Select the Free tier and obtain an API key
Free subscription

Step 4. Copy the API key, we will use it in the next part.
API key

Part 3: Azure Functions

Azure Functions is a serverless compute service that enables you to run a script or piece of code on-demand or in response to an event without having to explicitly provision or manage infrastructure.

Step 1. From the Azure Portal, create a new Function App. Its name is unique, so serverlesschatbot won't work for you, use another one :-)
Function App

Step 2. Once the resource is created, add a new HTTP trigger called receive-message. Then click on View files and add a function.proj file
New file

Step 3. This file is used to include a Nuget package into our project. We are adding the Twilio extension so our code can interact with a WhatsApp number later.

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Twilio" Version="3.0.0" />
  </ItemGroup>
</Project>
Enter fullscreen mode Exit fullscreen mode

Step 4. Next, we have the code for run.csx, which is the main part of an Azure Function. In this case, we are using both the URL from Part 1 (Step 6) and the API key from Part 2 (Step 4), so replace them in the code.

In the first section of the code we include several classes needed to deserialize the LUIS and OpenWeatherMap JSON responses after calling their services. Then, the code in the Run method parses the payload generated after a message to a WhatsApp number is sent. The text part is extracted and we have the evaluateMessage method, in which we send the text to the LUIS published model, which process the message and extracts the city part. If successful, a request to OpenWeatherMap is made in order to obtain the weather in a particular city. Finally, this information is sent as a response to the user.

#r "System.Runtime"
#r "Newtonsoft.Json"

using System.Net;
using System.Text; 
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Twilio.TwiML; 

// LUIS classes
public class LuisModel
{
    public string query { get; set; }
    public TopScoringIntent topScoringIntent { get; set; }
    public List<Intent> intents { get; set; }
    public List<Entity> entities { get; set; }
}

public class TopScoringIntent
{
    public string intent { get; set; }
    public double score { get; set; }
}

public class Intent
{
    public string intent { get; set; }
    public double score { get; set; }
}

public class Entity
{
    public string entity { get; set; }
    public string type { get; set; }
    public int startIndex { get; set; }
    public int endIndex { get; set; }
}

// OpenWeatherMap classes
public class WeatherModel
{
    public Coord coord { get; set; }
    public List<Weather> weather { get; set; }
    public string @base { get; set; }
    public Main main { get; set; }
    public int visibility { get; set; }
    public Wind wind { get; set; }
    public Clouds clouds { get; set; }
    public int dt { get; set; }
    public Sys sys { get; set; }
    public int id { get; set; }
    public string name { get; set; }
    public int cod { get; set; }
}

public class Weather
{
    public int id { get; set; }
    public string main { get; set; }
    public string description { get; set; }
    public string icon { get; set; }
}

public class Coord
{
    public double lon { get; set; }
    public double lat { get; set; }
}

public class Main
{
    public double temp { get; set; }
    public double pressure { get; set; }
    public double humidity { get; set; }
    public double temp_min { get; set; }
    public double temp_max { get; set; }
}

public class Wind
{
    public double speed { get; set; }
}

public class Clouds
{
    public double all { get; set; }
}

public class Sys
{
    public int type { get; set; }
    public int id { get; set; }
    public double message { get; set; }
    public string country { get; set; }
    public long sunrise { get; set; }
    public long sunset { get; set; }
}

// Main code
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
    var data = await req.Content.ReadAsStringAsync();

    var formValues = data.Split('&') 
        .Select(value => value.Split('='))
        .ToDictionary(pair => Uri.UnescapeDataString(pair[0]).Replace("+", " "), 
                      pair => Uri.UnescapeDataString(pair[1]).Replace("+", " "));

    var text = formValues["Body"].ToString();
    var message = await evaluateMessage(text);
    var response = new MessagingResponse().Message(message);

    var twiml = response.ToString();
    twiml = twiml.Replace("utf-16", "utf-8");

    return new HttpResponseMessage
    { 
        Content = new StringContent(twiml, Encoding.UTF8, "application/xml")
    };
}

private static readonly HttpClient httpClient = new HttpClient();

private static async Task<string> evaluateMessage(string text)
{
    try
    {
        var luisURL = "Your-LUIS-URL-From-Step6-Part1";
        var luisResult = await httpClient.GetStringAsync($"{luisURL}{text});
        var luisModel = JsonConvert.DeserializeObject<LuisModel>(luisResult);

        if (luisModel.topScoringIntent.intent == "GetCityWeather")
        {
            var entity = luisModel.entities.FirstOrDefault();

            if (entity != null)
            {
                if (entity.type == "builtin.geographyV2.city")
                {
                    var city = entity.entity;
                    var apiKey = "Your-OpenWeatherMapKey-From-Step4-Part2";

                    var weatherURL = $"http://api.openweathermap.org/data/2.5/weather?appid={apiKey}&q={city}";

                    var weatherResult = await httpClient.GetStringAsync(weatherURL);
                    var weatherModel = JsonConvert.DeserializeObject<WeatherModel>(weatherResult);
                    weatherModel.main.temp -= 273.15;

                    var weather = $"{weatherModel.weather.First().main} ({weatherModel.main.temp.ToString("N2")} °C)";
                    return $"Weather of {city} is: {weather}";
                }
            }
        }
        else
            return "Sorry, I could not understand you!";
    }
    catch(Exception ex)
    {

    }

    return "Sorry, there was an error!";
}

Enter fullscreen mode Exit fullscreen mode

Step 5. Copy the Function URL, we will use it in the final part.
Function URL

Final Part: Twilio

In order to communicate with a WhatsApp number, we can use the Twilio API.

Step 1. Create a free Twilio account
Twilio Account

Step 2. Access the Programmable SMS Dashboard and then select WhatsApp Beta followed by a click on Get started.
WhatsApp Beta

Step 3. Activate the Twilio Sandbox for WhatsApp.
Sandbox

Step 4. Set up the testing sandbox by sending the specific WhatsApp message from your device to the indicated number
Join

Joined

Step 5. After joining the conversation, click on Sandbox again to access the Configuration. Replace the URL under "When a message comes in" with your Azure Function URL from Step 5 in the previous part.
Alt Text

Step 6. That's it! Let's test our work!
Alt Text

Success! Yay!

It certainly takes some time to set up everything as several technologies are involved. However, you can now imagine the possibilities. How would your users feel after you tell them that they can interact with your app through WhatsApp? Or that they can send their questions to a specific number which will handle all of them? You can replace LUIS by another technology, such as QnA Maker to process users' questions. It is even possible to send images and get them analyzed by Cognitive Services for instance!

The sky is the limit! :-)

And everything, of course, is managed under a serverless experience thanks to Azure Functions.

Thank you for your time and hopefully this post was useful for you (let me know your thoughts in the comment section :-D). If you want to learn more about Azure, Xamarin, Artificial Intelligence and more, visit my blog and YouTube channel, where I usually have fun sharing my knowledge and experiences.

Happy coding!

Luis

PS: I would also like to thank the [Azure Advocates (https://twitter.com/azureadvocates) for the #ServerlessSeptember initiative! It's awesome to learn something new everyday from the community and experts.

References used for this publication:
Twilio

. . . . . . . . . . . . . . . . . . . . . . . . . . .