Integrate LUIS, QnA Maker and Personality Chat in your Bot – Part 2

In my last post, I briefed about how can you create FAQs, LUIS models and connect them with your Bot using CLI tools. In addition to this, I also discussed a few basic features of Dispatch tool and told you how straight forward was it to integrate.

Now, we will deep dive into the code and see how you can distinguish the incoming message requests and flow them according to your business needs to the respective dialogs. 

The Code Walk

First of all, you can structure your bot according to the latest guidelines and best practices of Azure Bot Service. I have already created a few code files such as BotAccessors, BotService etc. as a boilerplate. Most of the code you would encounter has already been taken or altered after copying it from the Official Bot Builder Samples Repository

For your ease and clear understanding, I have uploaded the full working code to my repository, however, you may have to follow the first post in order to make all of your code working. 

Startup.cs

This file contains all of your Bot’s core. I have created State objects here along with the method of Initialization of Bot Services (LUIS, QnAMaker and Dispatch).

// Create and register state accesssors.
// Acessors created here are passed into the IBot-derived class on every turn.
services.AddSingleton<BotAccessors>(sp =>
{
    // Create the custom state accessor.
    // State accessors enable other components to read and write individual properties of state.
    var accessors = new BotAccessors(conversationState, userState)
    {
        CustomerInfoAccessor = userState.CreateProperty<CustomerInfo>(BotAccessors.CustomerInfoAccessorName),
        DialogStateAccessor = conversationState.CreateProperty<DialogState>(BotAccessors.DialogStateAccessorName),
    };

    return accessors;
});
private static BotServices InitBotServices(BotConfiguration config)
{
    var qnaServices = new Dictionary<string, QnAMaker>();
    var luisServices = new Dictionary<string, LuisRecognizer>();

    foreach (var service in config.Services)
    {
        switch (service.Type)
        {
            case ServiceTypes.Luis:
                {
                    var app = new LuisApplication(luis.AppId, luis.AuthoringKey, luis.GetEndpoint());
                    var recognizer = new LuisRecognizer(app);
                    luisServices.Add(luis.Name, recognizer);
                    break;
                }

            case ServiceTypes.Dispatch:
                
                var dispatchApp = new LuisApplication(dispatch.AppId, dispatch.AuthoringKey, dispatch.GetEndpoint());

                // Since the Dispatch tool generates a LUIS model, we use LuisRecognizer to resolve dispatching of the incoming utterance
                var dispatchARecognizer = new LuisRecognizer(dispatchApp);
                luisServices.Add(dispatch.Name, dispatchARecognizer);
                break;

            case ServiceTypes.QnA:
                {
                
                    var qnaEndpoint = new QnAMakerEndpoint()
                    {
                        KnowledgeBaseId = qna.KbId,
                        EndpointKey = qna.EndpointKey,
                        Host = qna.Hostname,
                    };

                    var qnaMaker = new QnAMaker(qnaEndpoint);
                    qnaServices.Add(qna.Name, qnaMaker);

                    break;
                }
        }
    }

    return new BotServices(qnaServices, luisServices);
}

BotAccessors.cs

Here, I have just created state property accessors for Conversation and User State. 

/// <summary>Gets or sets the state property accessor for the user information we're tracking.</summary>
/// <value>Accessor for user information.</value>
public IStatePropertyAccessor<CustomerInfo> CustomerInfoAccessor { get; set; }

/// <summary>Gets or sets the state property accessor for the dialog state.</summary>
/// <value>Accessor for the dialog state.</value>
public IStatePropertyAccessor<DialogState> DialogStateAccessor { get; set; }

/// <summary>Gets the conversation state for the bot.</summary>
/// <value>The conversation state for the bot.</value>
public ConversationState ConversationState { get; }

/// <summary>Gets the user state for the bot.</summary>
/// <value>The user state for the bot.</value>
public UserState UserState { get; }

BotServices.cs

This class file contains the definition of the LUIS and QnA Maker services. Please note that Dispatch is nothing but LUIS file which is a combination of our original LUIS & QnA Maker models. 

public class BotServices
    {
        public BotServices(Dictionary<string, QnAMaker> qnaServices, Dictionary<string, LuisRecognizer> luisServices)
        {
            QnAServices = qnaServices ?? throw new ArgumentNullException(nameof(qnaServices));
            LuisServices = luisServices ?? throw new ArgumentNullException(nameof(luisServices));
        }

        public Dictionary<string, QnAMaker> QnAServices { get; } = new Dictionary<string, QnAMaker>();

        public Dictionary<string, LuisRecognizer> LuisServices { get; } = new Dictionary<string, LuisRecognizer>();
    }

The Dialog Walk

So let’s talk about some concepts, techniques and best practices here. I explored many ways to accomplish a single task. However, as per business use-case and the need, there can be different ways to accomplish the same task. That’s why there’s no best answer to this. Although, the approach I chose for this bot was quite reasonable for me and I will explain why. 

The bot will have two Component Dialogs, one for Flight Status and the other one for Booking. I will not go in detail for the booking one as my main topic of this blog post is to brief you about using multiple features into one bot.  

Why ComponentDialog? Better organization, better control, re-usable and independent DialogSets. 

StatusDialog.cs

This dialog will be called if the customer is inquiring about flight status i.e. if LUIS intent does not have a BookFlight or None intent fired. You can then look at the request and see if it fulfills your requirement of checking out the status of the flight. If it does not then you can request the customer for more information in the same dialog (as it will be on the top of the stack). 

BookFlight.cs

Similarly, you can setup the Waterfall Dialog steps for booking details and then respond with the final booking offer. 

AMABot.cs

This is actually the heart, our main bot file. In this file, OnTurnAsync method will be invoked as soon as it receives any activity. If it’s of message type, then that message will be analyzed using the DispatchToTopIntentAsync method. As I told you earlier that Dispatch is nothing but LUIS therefore we will use the LuisRecognizer to identify the intent whether it is our LUIS app, QnA Maker or None. Based upon the intent, the business flow will proceed.

if (dialogTurnResult.Result is null)
 {
     // Get the intent recognition result
     var recognizerResult = await botServices.LuisServices["AMAAirlinesDispatch"].RecognizeAsync(turnContext, cancellationToken);
     var topIntent = recognizerResult?.GetTopScoringIntent();

     if (topIntent == null)
     {
         await turnContext.SendActivityAsync("Unable to get the top intent.");
     }
     else
     {
         await DispatchToTopIntentAsync(turnContext, dc, topIntent, cancellationToken);
     }
 }

In case of LUIS, it will be redirected to the respective dialog (from DispatchToLuisModelAsync) where as in case of QnA Maker, customer will be responded accordingly. This way, we have covered important scenarios (if not all). 

/// <summary>
 /// Dispatches the turn to the request QnAMaker app.
 /// </summary>
 private async Task DispatchToQnAMakerAsync(ITurnContext context, string appName, DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
 {
     if (!string.IsNullOrEmpty(context.Activity.Text))
     {
         var results = await botServices.QnAServices[appName].GetAnswersAsync(context).ConfigureAwait(false);
         if (results.Any())
         {
             await context.SendActivityAsync(results.First().Answer, cancellationToken: cancellationToken);
         }
         else
         {
             await context.SendActivityAsync($"Couldn't find an answer in the {appName}.");
         }
     }
 }

 /// <summary>
 /// Dispatches the turn to the requested LUIS model.
 /// </summary>
 private async Task DispatchToLuisModelAsync(ITurnContext context, string appName, DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
 {
     var result = await botServices.LuisServices[appName].RecognizeAsync(context, cancellationToken);
     var intent = result.Intents?.FirstOrDefault();

     if (intent?.ToString() == "BookFlight")
     {
         await dc.BeginDialogAsync(BookingDialogID, null, cancellationToken);
     }
     else
     {
         await dc.BeginDialogAsync(StatusDialogID, result, cancellationToken);
     }
 }

To sum it up, I explained step by step how you can integrate LUIS, QnA Maker & Personality Chat to your bot by using the CLI tools to connect to your configuration file, create dispatch and then write few lines of code with Dialogs. I hope it would save your time in research. 

In case you have any suggestions / comments, you can either reach me out on twitter or just post it here. Happy coding!