Microsoft Bot Framework - Use Redis to store conversation state

Bots created using Microsoft Bot Framework are by default stateless. The conversation state and it’s associated context is stored by Bot State Service in cloud. The state service stores information in 3 distinct bags keyed by their associated ids -

Property Key Description
User User Id Remembering context for a user on a channel
Conversation Conversation Id Remembering context all users in a conversation
Private Conversation Conversation Id + User Id Remembering context for a user in a conversation

Bot State Service documentation provides more detail explanation to them. Moreover all these property bags are scoped by the Bot id and Channel id, essentially making them unique.
The Dialog Stack and Dialog Data are both stored in Private Conversation bag.

If we want to store these data in our database, Bot Framework provides two extension points to do that -

  • Create a REST layer by implementing IBotState interface
  • Implement IBotDataStore<T> in your bot

In this post, we will implement IBotDataStore to store the context in Redis. However making a REST service by implementing IBotState should be similar. The source code is available here.

Redis Store

public interface IBotDataStore<T>
{
    Task<bool> FlushAsync(BotDataKey key, CancellationToken cancellationToken);
    Task<T> LoadAsync(BotDataKey key, BotStoreType botStoreType, CancellationToken cancellationToken);
    Task SaveAsync(BotDataKey key, BotStoreType botStoreType, T data, CancellationToken cancellationToken);
}

IBotDataStore is a simple interface with only three methods to implement, all of them being self explanatory. We will start with SaveAsync. We will use StackExchange.Redis for C# client.

public async Task SaveAsync(BotDataKey key, BotStoreType botStoreType, BotData data, CancellationToken cancellationToken)
{
    Connect();

    var redisKey = GetKey(key, botStoreType);
    var serializedData = Serialize(data.Data);

    var database = _connection.GetDatabase(_options.Database);
    var tran = database.CreateTransaction();
    if (data.ETag != "*")
        tran.AddCondition(Condition.HashEqual(redisKey, ETAG_KEY, data.ETag));
    tran.HashSetAsync(redisKey, new HashEntry[]
    {
        new HashEntry(ETAG_KEY, DateTime.UtcNow.Ticks.ToString()),
        new HashEntry(DATA_KEY, serializedData)
    });

    bool committed = await tran.ExecuteAsync();

    if (!committed)
        throw new ConcurrencyException("Inconsistent SaveAsync based on ETag!");
}

The critical part here is to maintain optimistic concurrency control for storing the data. This situation arises most commonly when the bot is in process of handling a user message and the user sends another message. The second message may end up in different bot instance which would start processing with older bot state. We will maintain optimistic concurrency using ETag property of BotData. ETag property will be set to DateTime.UtcNow.Ticks. If it differs while saving the new state, we will throw an exception. To achieve this we will use Transactions in Redis. Note that transactions in Redis works differently than your typical RDBMS. It gets more complicated due to how connection multiplexing is performed by StackExchange Redis client. An excellent explanation of this is given here. In our code, we add a “Condition” for the transaction to be successful. If the condition fails, the entire transaction is void and committed will be false.

The LoadAsync method is pretty simple. We return null if there is no value for a particular key (first time scenario), otherwise we return BotData.

public async Task<BotData> LoadAsync(BotDataKey key, BotStoreType botStoreType, CancellationToken cancellationToken)
{
    Connect();

    var database = _connection.GetDatabase(_options.Database);
    var redisKey = GetKey(key, botStoreType);
    var result = await database.HashGetAllAsync(redisKey);
    if (result == null || result.Count() == 0)
    {
        return null;
    }

    var botData = new BotData();
    botData.ETag = result.Where(t => t.Name.Equals(ETAG_KEY)).FirstOrDefault().Value;
    botData.Data = Deserialize((byte[])result.Where(t => t.Name.Equals(DATA_KEY)).FirstOrDefault().Value);
    return botData;
}

Integrating with Bot

To start using Redis Store instead of Bot State Service, we just need to override the existing Autofac registration of IBotDataStore with new one. The Bot Builder SDK uses CachingBotDataStore implementation for IBotDataStore which is just a decorator for ConnectorStore. We will replace ConnectorStore with RedisStore. Just call the RegisterBotDependencies in Application_Start() in Global.asax.cs and it will work.

private void RegisterBotDependencies()
{
    var builder = new ContainerBuilder();

    RedisStoreOptions redisOptions = new RedisStoreOptions()
    {
        Configuration = "localhost"
    };

    builder.Register(c => new RedisStore(redisOptions))
       .As<RedisStore>()
       .SingleInstance();

    builder.Register(c => new CachingBotDataStore(c.Resolve<RedisStore>(),
                                                  CachingBotDataStoreConsistencyPolicy.ETagBasedConsistency))
        .As<IBotDataStore<BotData>>()
        .AsSelf()
        .InstancePerLifetimeScope();

    builder.Update(Conversation.Container);

    DependencyResolver.SetResolver(new AutofacDependencyResolver(Conversation.Container));
}

Wrapping Up

As mentioned before, the other way to achieve the same is by creating a REST Api and implementing IBotState. However in essence, the way we do this would remain same. Microsoft Bot Builder is highly flexible when it comes to extending it with custom logics. Again the source code is at my github repo. I have also published it as a nuget package.

I hope you liked it. Post a comment if you have any question.

Hidden Gem in Microsoft Bot Framework - QnA Maker

One of the most common use cases in Bot space is to create a bot to answer FAQ. And almost all businesses has an FAQ either in form of documents or web pages. Now it is not at all difficult to create a bot to answer simple questions. In fact there are hardly any context to maintain and most of the conversations are very shallow which makes it a very repetitive and quite boring task. All the FAQs are mostly same, well they are just list of questions and answers. Only if there would have been a way to upload this “knowledge” and create an NLP model based on the questions without us having to write any code.

Enter QnAMaker, one of the least documented and almost non-existent service offered by Microsoft. QnAMaker allows us to upload a FAQ document or just give a link to a FAQ page and it will create an NLP model and expose an endpoint to query.

QnA Maker

QnA Maker is provided as a service which is part of Microsoft Bot Framework. It is really hard to find as there are no external links to the page and absolutely no documentation. The only way to reach it by directly going to the sub-domain qnamaker.botframework.com.

QnA Maker

Once on the page, we can create a new service by clicking “Create a new QnA service”. We will be redirected a page with list of steps to create QnA Service. We will create a QnA service to answer questions about Bot Framework itself. The Bot Framework FAQ is available at it’s documentation.

QnA Edit

  • Name your service: The first step is to give a name to our service. Let’s name it “botframework”.
  • FAQ URL: This is one of the three steps to seed the QnA Maker with questions and answers. QnA Maker provides sample sites as how the FAQ page should look like. However it is not limited to the same format. Most of the FAQ pages, which clearly separates questions and answers, works with QnA Maker. Here we will provide “https://docs.botframework.com/en-us/faq/” as single line entry.
  • Question and Answer Pairs: This is the second way to provide questions and answers. The format is question:answer one per each line. We will leave this field blank.
  • Upload files: The third and last way to seed data is to upload a file(doc, docx, txt, tsv or pdf) containing questions and answers in a sequence. This too we will leave as it is.

Click on “Extract” and wait a while. The QnA maker will extract all the questions and create an NLP model (probably LUIS). It would also show how many question-answer pair were extracted just above “Next” button. Clicking on it will download a tsv file with question-answer pair.

QnA Extracted

Click on “Next” to go to fifth and last step which is to test and re-train our model.

QnA Train True

This window has three parts. In the middle is a chat bot embedded as an iframe. Enter a question from FAQ in the chat bot and it should respond back with the answer.
The right side of chat bot allows us to enter alternate phrasings to a question. This will further reinforce the model and make it better at classifying the questions correctly. As you can see in the image, I have added another phrase “what is release timeline for bot framework” to this question.
The left side can be used to choose a correct response in case the bot answers incorrectly. The QnA Maker will automatically display answers related to the question. When asked “when the bot framework be publicly available”, it returned wrong response. I selected the correct response from the list of responses which appeared on the left.
Once satisfied with the responses, click “Re-Train” to train the model again.

Click on “Publish” to make the service available over an URL.

QnA End

Using the “Service URL” we can test the service to see what it returns.

QnA End

The question is supplied in the query string and the service returns a json containing an “answer” and a confidence “score”. The score tells us how confident is the QnA Maker that it has responded back with correct answer to the question. The score varies from 0-100, higher the number more confident it is.

Conclusion

QnA Maker is a powerful tool to quickly create an FAQ bot. However there are couple of limitations. First it does not expose the underlying NLP model. Most likely it uses LUIS internally and it creates an intent for each question. We do not get access to the LUIS or the trained model. Second it is very less documented and there are almost no mention of it anywhere. Which begs the question on how long will it be supported and whether there are any technical limitations and cost to the usage of the service. Overall this is a fine piece of work which lets us create a bot in minutes.

I hope this article was helpful. If you have any questions, please post a comment below.

Chatbot using Microsoft Bot Framework - Part 4

I finally got time to write another post and with this I will finish of my blog series on Microsoft Bot Framework. In my last post, we saw how to use other features of LUIS such as entities and phrase list. We also saw how to nest Dialogs and how to maintain context between Dialogs. If you are new to Microsoft Bot Framework, I highly recommend you go through part 1, part 2 and part 3 of my blog series before continuing. As usual, you can find source code at my github repo.

In this post, we will delve into how FormFlow works and where it can be used. We will add another feature to our bot which will let user to send feedback to me through chat.

Adding Feedback Intent

We first enhance LUIS by adding another intent called “Feedback”. I have trained LUIS for sentences such as “I want to send a feedback”, I will not go into details as I have already covered LUIS aspects in my previous posts. There are no entities to return so we just leave it to this.

Feedback

FormFlow

Till now we have only created conversations which are very shallow. In other words, it is a simple QA scenario where conversation does not flow deep and their are no context to maintain. However, there are many scenarios where we would need to take a more guided approach, where we may require multiple inputs from user and bot may take different path based on previous inputs. In short, we will need to create a state machine. A good example is ordering a pizza. We will need to ask a lot of questions such as size, crust, toppings, etc. and we will need to maintain them in the context. We will also need to provide a way for user to change previously entered data and we must only complete order once we have all the information with us. All of this would require managing a lot of state and workflow. This is where FormFlow comes in.

FormFlow sacrifices some of the flexibility of Dialogs to provide an easy way to achieve all the above. FormFlow itself is derived from IDialog so it can be nested within another dialog.

The basic idea behind FormFlow is forms i.e. collection of fields. Think of it as filling a form in any website. To order a pizza online, you would go to your favorite pizza restaurant’s website, fill out a form with details such as type of pizza, crust, size etc, put down delivery address and then order it. At any point before placing the order, you can revisit and change any aspect of pizza.

To accomplish the same using FormFlow, we start with creating a class and adding public fields or properties. Each public field and property corresponds to a field in the form. So the user will be asked to input values for each field before completing the form. The way FormFlow achieves this is by creating a state machine in background and maintaining the transition between states. It also allows user to change any previously entered value for any field and view the current status of the form. You can read more about FormFlow in the docs here.

In our scenario of implementing feedback functionality, before allowing user to send a feedback, we will ask him to enter his name and contact info. Only when we have both the information, would we allow him to send a feedback message. To achieve this, we first create a class called FeedbackForm and properties for Name, Contact and Feedback.

public class FeedbackForm
{
    [Prompt(new string[] { "What is your name?" })]
    public string Name { get; set; }

    [Prompt("How can Ankit contact you? You can enter either your email id or twitter handle (@something)")]
    public string Contact { get; set; }

    [Prompt("What's your feedback?")]
    public string Feedback { get; set; }

    public static IForm<FeedbackForm> BuildForm()
    {
        return new FormBuilder<FeedbackForm>()
            .Field(nameof(Contact), validate: ValidateContactInformation)
            .Field(nameof(Feedback), active: FeedbackEnabled)
            .AddRemainingFields()
            .Build();
    }
}

The Prompt attribute on top of fields allow us to specify what message would be shown to the user for asking him to enter value for the respective fields. Do note that FormFlow only accepts .NET primitive types, enum and List<enum> as Type for properties or fields. The BuildForm static method returns an IForm<> which would be used by FormDialog to build forms later. I will explain each line of the method -

  • new FormBuilder<FeedbackForm>(): Create a new FormBuilder of type FeedbackForm. FormBuilder has fluent api to help us build it.
  • .Field(nameof(Contact), validate: ValidateContactInformation): Add the property Contact to the form and set a delegate to validate the input. ValidateContactInformation delegate will be called whenever user inputs for this field.
  • .Field(nameof(Feedback), active: FeedbackEnabled): Add the property Feedback to the form and assign an ActiveDelegate to it. This means that this field will be visible only when ActiveDelegate returns true.
  • .AddRemainingFields(): Add any remaining valid public fields and properties to the form. In this case Name.
  • .Build();: Build the form and return an IForm<FeedbackForm>.

The ValidateContactInformation delegate validates that the input is either a valid email address or starts with ‘@’ to signify twitter handle.

private static Task<ValidateResult> ValidateContactInformation(FeedbackForm state, object response)
{
    var result = new ValidateResult();
    string contactInfo = string.Empty;
    if(GetTwitterHandle((string)response, out contactInfo) || GetEmailAddress((string)response, out contactInfo))
    {
        result.IsValid = true;
        result.Value = contactInfo;
    }
    else
    {
        result.IsValid = false;
        result.Feedback = "You did not enter valid email address or twitter handle. Make sure twitter handle starts with @.";
    }
    return Task.FromResult(result);
}

The ValidateAsyncDelegate must return a ValidateResult object whose property IsValid should be set appropriately. If IsValid is set to true, FormFlow will assign the field to the value in Value property. This gives us chance to transform the user input before assigning it to the form. If the IsValid is set to false, text in Feedback field will be displayed to the user. This allows us to notify user why validation failed and give clear instructions as to what to do next. If the validation fails, the FormFlow will ask user to enter the value again.

Each field can be controlled as to whether it is available to be filled or not by ActiveDelegate. The ActiveDelegate returns a bool, if it is true the field is available to be filled by the user else it will be not be shown.

 private static bool FeedbackEnabled(FeedbackForm state) => 
    !string.IsNullOrWhiteSpace(state.Contact) && !string.IsNullOrWhiteSpace(state.Name);

This completes creation of our form, next we will add an intent handler.

Add handler for Feedback intent

[LuisIntent("Feedback")]
public async Task Feedback(IDialogContext context, LuisResult result)
{
    try
    {
        await context.PostAsync("That's great. You will need to provide few details about yourself before giving feedback.");
        var feedbackForm = new FormDialog<FeedbackForm>(new FeedbackForm(), FeedbackForm.BuildForm, FormOptions.PromptInStart);
        context.Call(feedbackForm, FeedbackFormComplete);
    }
    catch (Exception)
    {
        await context.PostAsync("Something really bad happened. You can try again later meanwhile I'll check what went wrong.");
        context.Wait(MessageReceived);
    }
}

Here we create a new FormDialog object by passing the new instance of FeedbackForm and BuildFormDelegate which we have defined above. The BuildFormDelegate will be used by FormFlow to build the form. FormOptions.PromptInStart tells the bot to prompt user for the first field to be filled as soon as the dialog starts. FormDialog has another optional parameter which takes IEnumerable<EntityRecommendation>. This can be used to pass the entities returned by the LUIS and FormFlow will pre-populate the form and will not ask the user to fill in those fields.

Next we use context.Call to push our FormDialog to top of the DialogStack. Unlike context.Forward, context.Call will not pass the current message to the Dialog. Instead the next message from the user will be routed to the child dialog.

FeedbackFormComplete is called once all the fields in the our form is successfully filled and the form completes. I also get the completed form passed in the result parameter.

private async Task FeedbackFormComplete(IDialogContext context, IAwaitable<FeedbackForm> result)
{
    try
    {
        var feedback = await result;
        string message = GenerateEmailMessage(feedback);
        var success = await EmailSender.SendEmail(recipientEmail, senderEmail, $"Email from {feedback.Name}", message);
        if (!success)
            await context.PostAsync("I was not able to send your message. Something went wrong.");
        else
		{
            await context.PostAsync("Thanks for the feedback.");
			await context.PostAsync("What else would you like to do?");
		}

    }
    catch (FormCanceledException)
    {
        await context.PostAsync("Don't want to send feedback? That's ok. You can drop a comment below.");
    }
    catch (Exception)
    {
        await context.PostAsync("Something really bad happened. You can try again later meanwhile I'll check what went wrong.");
    }
    finally
    {
        context.Wait(MessageReceived);
    }
}

I use SendGrid to send a mail to myself with the feedback message which I get from completed form. The interesting part here is the catch block for FormCanceledException.
FormCanceledException is thrown when user quits or cancels the form. This is another feature of FormFlow. User can quit the form anytime by typing ‘quit’ or ‘bye’. Along with this, user can type ‘Help’ anytime to view all the available options if he feels stuck anywhere and ‘Status’ to view the current state of form. These commands are configurable and list of them is available in ‘Help’ menu.

Help Status

Wrapping up

This is it. We have created a bot and in the process I have explained fundamentals of bots and Microsoft Bot Framework. But in no way have I touched upon every feature of Bot Framework. There are many other features such as Scorable, BotState, IPostToBot, IBotToUser etc. which are very useful and you should definitely explore. Voice calling through Skype is an upcoming feature which can be integrated to the bot. There are many other cognitive services which can be integrated with bots to make it more smarter. Microsoft Bot Framework is a powerful and feature rich platform to build bots. The open source community around it is great and developers are quick to respond to any issues.

I will write more about above things in future. If you have any specific topic which you would like me to write about, drop a comment or send a feedback through bot :).

Chatbot using Microsoft Bot Framework - Part 3

This is third part in my series on Chat Bots. In part one I discussed how chat bots worked and basics of Microsoft Bot Framework. In part two I talked about LUIS and how it provides intelligence to our bot. I also built a simple bot using LUIS in background which answers questions of who I am. In this post, we will add more features to our bot, and see how LUIS detects entities along with intent. Before we proceed, I would mention that I have added application insight to my bot. As usual, head over to my repo to get the source code.

Since my last post, I have added application insight to the code so that I can view telemetry in Azure. Also I have updated my BotBuilder nuget package to v3.2.

The next feature will let us ask the bot to fetch us articles for a particular topic. More specifically, we can ask bot to search my blog on a particular topic and return us the list posts associated with topic. An example query can be - “show me posts related to docker” should return all the articles having “docker” tag.

Enhancing LUIS

To achieve this, not only will LUIS have to classify the sentence to an intent but also return us entities from the sentence which will act as search terms eg. “docker”. First, we will create a new entity and call it “Tag”. Next, we will add another intent to LUIS named “BlogSearch”. This time when training LUIS for this intent, we can select any word or group of words from the utterance and assign it to our entity as shown below. Just click on the word to assign it to an entity and it should get highlighted with a color.

Entity

We will train the system with few more utterances. However we quickly see that LUIS is able to recognize entities already trained, but is having hard time with new words such as “Microsoft Bot Framework” as we see in the image below.

Failed Entity

This is happening because

  1. We have not trained our system extensively.

  2. LUIS has no way to know that “Microsoft Bot Framework” can be classified as a “Tag” entity since all our previous entities are not even similar to this one.

We can quickly get around it by utilizing another feature of LUIS called “Phrase List Features”. It allow us to specify comma separated words which LUIS can use interchangeably when detecting an entity. In our case we will provide a list of tags from my blog. We will see that LUIS is now able to detect entities from phrase list we created.

Phrase List

This is an advantage we have with LUIS as it uses Conditional Random Fields(CRF) for entity detection. CRF, unlike some other algorithms, takes neighboring words into account when detecting entities. That is, words preceding and succeeding are important when detecting an entity. With enough training, this allows LUIS to detect words as entities which were not trained before, just by looking at their neighboring words. Once we have sufficiently trained model, let’s go and make changes to the code.


As before, we will add another method and decorate it with [LuisIntent("BlogSearch")]. I have written a class which will get my blog posts and get all the articles I have written along with it’s associated tags. Then I filter the posts based on the “Tag” entity detected by LUIS.
My intent handler is pretty straightforward. I get the list of entities detected by LUIS in LuisResult. If I find an entity of type “Tag”, I filter the posts comparing its associated tags with the LUIS detected entity. I then pass the list of filtered posts and tag to a private method which formats the response and returns back a string.

[LuisIntent("BlogSearch")]
public async Task BlogSearch(IDialogContext context, LuisResult result)
{
    string tag = string.Empty;
    string replyText = string.Empty;
    List<Post> posts = new List<Post>();

    try
    {
        if (result.Entities.Count > 0)
        {
            tag = result.Entities.FirstOrDefault(e => e.Type == "Tag").Entity;
        }

        if (!string.IsNullOrWhiteSpace(tag))
        {
            var bs = new BlogSearch();
            posts = bs.GetPostsWithTag(tag);
        }

        replyText = GenerateResponseForBlogSearch(posts, tag);
        await context.PostAsync(replyText);
    }
    catch (Exception)
    {
        await context.PostAsync("Something really bad happened. You can try again later meanwhile I'll check what went wrong.");
    }
    finally
    {
        context.Wait(MessageReceived);
    }
}

Fireup emulator to check if everything is working as expected. We just added a new feature to our bot with few lines of code. Sweet!!! Blog Search


Greetings Problem

A new user would most likely start conversation with “Hi” or similar greetings. Currently our bot responds with “I’m sorry. I didn’t understand you.” for any greetings. Well it is not a very good response to give when someone says “Hi”. Let us do something about it. One way would be to create a “Greetings” intent in LUIS and train it to recognize “hi”, “hello” etc. This is what I have been doing till now. However recently I found an excellent blog post by Garry Petty. He created an implementation of IDialog to match incoming message to list of strings through regular expression and dispatch it to a handler. So let us go ahead and take his help to solve our little problem here. This approach would also allow me to demonstrate how we can create and use child dialog.

First add reference to his nuget package BestMatchDialog. Next we create GreetingsDialog and derive it from BestMatchDialog<object>.

[Serializable]
public class GreetingsDialog: BestMatchDialog<object>
{
    [BestMatch(new string[] { "Hi", "Hi There", "Hello there", "Hey", "Hello",
        "Hey there", "Greetings", "Good morning", "Good afternoon", "Good evening", "Good day" },
       threshold: 0.5, ignoreCase: false, ignoreNonAlphaNumericCharacters: false)]
    public async Task WelcomeGreeting(IDialogContext context, string messageText)
    {
        await context.PostAsync("Hello there. How can I help you?");
        context.Done(true);
    }

    [BestMatch(new string[] { "bye", "bye bye", "got to go",
        "see you later", "laters", "adios" })]
    public async Task FarewellGreeting(IDialogContext context, string messageText)
    {
        await context.PostAsync("Bye. Have a good day.");
        context.Done(true);
    }

    public override async Task NoMatchHandler(IDialogContext context, string messageText)
    {
        context.Done(false);
    }
}

In principle BestMatchDialog works in same way as LuisDialog. It would check the message against each of the strings in BestMatch attribute and calculate a score. Then the handler for the highest score is executed passing in the required context and message. If no handler is found with score above the threshold, NoMatchHandler is called.
Note in each handler we call Context.Done instead of Context.Wait. This is because we don’t want next message to arrive in this Dialog. Instead this Dialog should finish and return back to it’s parent Dialog which is MeBotLuisDialog. Context.Done will complete the current Dialog, pop it out of stack and return the result back to parent Dialog. We return True if we handled the greetings otherwise False. We then change the None intent handler in MeBotLuisDialog to one below -

[LuisIntent("None")]
[LuisIntent("")]
public async Task None(IDialogContext context, IAwaitable<IMessageActivity> message, LuisResult result)
{
    var cts = new CancellationTokenSource();
    await context.Forward(new GreetingsDialog(), GreetingDialogDone, await message, cts.Token);
}

private async Task GreetingDialogDone(IDialogContext context, IAwaitable<bool> result)
{
    var success = await result;
    if(!success)
        await context.PostAsync("I'm sorry. I didn't understand you.");

    context.Wait(MessageReceived);
}

In None intent handler we call context.Forward which will create a child dialog of type GreetingsDialog, push it to top of stack and call it’s StartAsync method passing message as argument. GreetingDialogDone is called once the child dialog completes i.e. child dialog calls context.Done.

Well this solves our little problem of handling greetings. One last thing we need to do. There should be a way for user to ask for help. We will create another LUIS intent called “Help” and train it with few utterances such as “need help”. This will allow user flexibility to ask for help anytime. From our bot, we would return the functionality that our bot can do similar to what we return for ConversationUpdate ActivityType.

In this article we enhanced our bot to search through my blog and filter articles based on associated tags. We also created a child dialog to handle greetings and a way for user to get help. In next post, we will get into FormFlow and make our bot bit more conversational. Till then, if you have any questions or feedback post a comment.

Chatbot using Microsoft Bot Framework - Part 2

This is second post in the series of building a chat bot. If you haven’t gone through Part 1, you can find it here. It sets the context and talks about basics of Microsoft Bot Framework. The source code for bot can be found at my github repo.

In this article, we will add first feature to our bot which is to answer question about me. The bot should answer questions such as “Who is Ankit”, “Who is author of this blog”, etc. As we saw previously, the bot in itself is dumb. To understand such questions we will need to take help from LUIS.

Language Understanding and Intelligence Service

LUIS Natural Language Processing Service is one of the cognitive services by Microsoft. In general, there are two things LUIS can possibly do -

  • Intent Recognition: Whenever a user sends a message to our bot, he has an intent. For instance if user types “I want to order a pizza” his intent is “OrderPizza”, “I want to rent a car” intent is “RentCar” etc. Given a sentence, LUIS will to classify the sentence into one of the trained intents and give us probability score for each intent. The way we achieve this is by defining the intents and training the LUIS with some sentences (called utterances) by manually classifying them. This type of learning is called Supervised Learning and the algorithm which LUIS uses to classify is Logistic Regression.

  • Entity Detection: In a sentence, we might be interested in a word or group of words. For example in “I want to order a pepperoni pizza” text “pepperoni” is the word we are interested in. Another example, “Rent a car from London airport tomorrow” - “London airport” and “Tomorrow” are the words which are contextually important to us. LUIS can help us by recognizing these words (called entities) from the sentence. LUIS uses Conditional Random Fields (CRF) algorithm to detect entities, which falls under Supervised Learning. Therefore this is again achieved by training LUIS with some words manually before it can start detecting.

A great tutorial explaining these in detail is present at Luis Help. It is a short tutorial which I recommend you go through it later for better understanding.

Create a LUIS App

Go over to luis.ai and create a new App. By default, we get one intent called None. Any utterances which we feel does not classify into any other intent in our app should be trained under None. Training None is important, I have seen many people not training it sufficiently.

Training Luis

Let us create a new intent named AboutMe. While creating intent we have to enter an example sentence that would be classified to it. Enter “Who is Ankit” and created the intent. Click on Submit to classify the sentence to the intent. We can add more utterances by entering it into input box and classifying it to the intent as shown below. Add some utterances for None intent too.

Intent

Train the LUIS by clicking on “Train” button on bottom left. Next, publish the app so that it is available via HTTP. We can test the app and see what result LUIS returns. LUIS will classify the query into each intent and return us probability score for each.

Luis Result

I have exported the LUIS app and added the JSON to the solution.


LuisDialog

Once we have created the LUIS app, next step is to integrate it with our bot. Fortunately Bot Builder SDK provides us an easy way to integrate with LUIS. Enter LuisDialog. It derives from IDialog and does the low level plumbing work of interfacing with LUIS and deserializing the result back to LuisResult. Let’s go ahead and create a new class called MeBotLuisDialog and derive it from LuisDialog. Next we add the following method to the class -

[LuisIntent("None")]
public async Task None(IDialogContext context, LuisResult result)
{
    await context.PostAsync("I'm sorry. I didn't understand you.");
    context.Wait(MessageReceived);
}

Let me explain each line in above -

  • [LuisIntent("None")]: Apart from calling LUIS API, LuisDialog also takes the result returned by LUIS, calculates the best intent based on probability score and calls the corresponding method defined in our dialog decorated with LuisIntent attribute matching the intent name passed as argument with the best intent detected. So, if LUIS classifies a sentence and scores it highest to “None” intent, our above method will get called automatically.

  • public async Task None(IDialogContext context, LuisResult result): Our method accepts two parameters, first is of type IDialogContext which as discussed before contains the stack of active dialog. It also has helper methods to send reply back to the user which we do in the next line. The second parameter is the result returned by the LUIS which is deserialized as LuisResult.

  • await context.PostAsync("I'm sorry. I didn't understand you."): We use the the Dialog Context to send a reply back to the user. Since this method is called when our bot did not understand the user’s intent, we return back a friendly response. Later we will modify it to return more detailed response.

  • context.Wait(MessageReceived): This is important. Before exiting from dialog, we must mention which method will be called when the next message arrives. If you forget it, you will get a very ambiguous runtime error something in the line of “need ‘Wait’ have ‘Done’”. We again use dialog context to specify it. MessageReceived method is defined in LuisDialog class and is the same method which calls the LUIS endpoint, calculates the best intent from the result and calls the relevant method for the intent.

So in short, we reply back to user saying that we didn’t understand and specify that next message should also be sent to the LUIS to understand the user intent. Let us add method to handle “AboutMe” intent.

[LuisIntent("AboutMe")]
public async Task AboutMe(IDialogContext context, LuisResult result)
{
    await context.PostAsync(@"Ankit is a Software Engineer currently working in Microsoft Center of Excellence team at Mindtree. He started his professional career in 2013 after completing his graduation as Bachelor in Computer Science.");
    await context.PostAsync(@"He is a technology enthusiast and loves to dig in emerging technologies. Most of his working hours are spent on creating architecture, evaluating upcoming products and developing frameworks.");
    context.Wait(MessageReceived);
}

This is also similar to the “None” intent handler. Instead of sending only one response, I send two since the sentences are quite big. The response is quire simple but let us keep it this way. Decorate the MeBotLuisDialog class with [LuisModel("modelid", "subskey")] with correct LUIS ModelId and Subscription Key. You can get the keys from published URL of your LUIS app.
There is just one more place that we need to change before we can test our bot that is in MessageController. Replace the entire If section of the method with the one below -

if (activity.Type == ActivityTypes.Message)
{
    await Conversation.SendAsync(activity, () => new MeBotLuisDialog());
}

Done. Press F5 to run the bot. Open the emulator and send a text to check if the bot is replying properly.

About me

One last feature to add. When a user add/open our bot for the first time, it is good practice to show a welcome text having information about what our bot can do and how to interact with it. Let us add a small help text and send it to the user when he first interacts with our bot. The place to do it is in ActivityTypes.ConversationUpdate block in MessageController. Microsoft Bot Framework supports Markdown, which we can utilize to give a richer experience to our user. I have added the relevant welcome text as below.

else if (message.Type == ActivityTypes.ConversationUpdate)
{
    // Handle conversation state changes, like members being added and removed
    // Use Activity.MembersAdded and Activity.MembersRemoved and Activity.Action for info
    // Not available in all channels
    string replyMessage = string.Empty;
    replyMessage += $"Hi there\n\n";
    replyMessage += $"I am MeBot. Designed to answer questions about this blog.  \n";
    replyMessage += $"Currently I have following features  \n";
    replyMessage += $"* Ask question about the author of this blog: Try 'Who is Ankit'\n\n";
    replyMessage += $"I will get more intelligent in future.";
    return message.CreateReply(replyMessage);
}

Registering the bot

Before registering, we need to publish our bot and make it accessible from internet over HTTPS. Once done, head over to bot registration portal. An excellent article on how to register the bot is here. Once registered, update the web.config with correct id and secret and publish the bot again.

Registering the bot will auto-configure it with skype. But let us go a step further and configure the Web Chat Channel. Configuring web chat gives us an iframe which we can include in our web site. I have added the iframe to my blog and the bot appears at the bottom right corner. This is so cool.

I have tagged the code till this point as part2 in my repo. In next post we will add more features to our bot.
Meanwhile if you have any questions or feedbacks, post a comment below.