Dialog management with QnA, LUIS, and Scorables

Figuring out how to manage your bot’s conversational flow is one of the most challenging aspects to bot development, and also related to some of the most commonly asked questions we receive from the community. In this article we’ll discuss different ways to manage your bot dialog by leveraging two popular Microsoft cognitive services – QnA Maker and LUIS.

The sample code we’ll be looking at was built using the Bot Application template and the Bot Builder SDK for .NET. We also assume some working familiarity with LUIS and QnA Maker, if you’re new to these services we highly recommend checking out some of the helpful links below to get acquainted:

Already familiar with the above? Great!

Control dialog flow with LUIS intents

LUIS is a powerful cognitive service which we can use to add natural language processing for our bots. In this case we’ll be using LUIS in order to process an intent from the user, to determine which dialogs the conversation should flow.

Shown below, in our RootDialog the bot will wait for an incoming message from a user, and we can cast that incoming message (result) as an activity. Next, using context.Forward(), we can pass that incoming message activity to LuisDialog().

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;

namespace Scorable.Dialogs
{
    [Serializable]
    public class RootDialog : IDialog<object>
    {
        public Task StartAsync(IDialogContext context)
        {
            context.Wait(MessageReceivedAsync);

            return Task.CompletedTask;
        }

        private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
        {
            var activity = await result as Activity;
            await context.Forward(new LuisDialog(), ResumeAftelLuisDialog, activity, CancellationToken.None);
        }

        private async Task ResumeAftelLuisDialog(IDialogContext context, IAwaitable<object> result)
        {
            context.Wait(MessageReceivedAsync);
        }
    }
}

Setting up a LUIS dialog is very straightforward, in LuisDialog.cs we add two new namespaces already included in the BotBuilder SDK,

  • Microsoft.Bot.Builder.Luis
  • Microsoft.Bot.Builder.Luis.Models
using System;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Luis;
using Microsoft.Bot.Builder.Luis.Models;
using Microsoft.Bot.Connector;
using System.Threading;

namespace Scorable.Dialogs
{
    [Serializable]
    [LuisModel("YourLuisAppID", "YourLuisSubscriptionKey")]
    public class LuisDialog : LuisDialog<object>
    {
        // methods to handle LUIS intents
    }
}

The LuisDialog class will inherit from LuisDialog<object>, and include a LuisModel attribute with values corresponding to your Luis App ID and Luis Subscription key respectively. At this point the dialog will be configured to handle responses from your LUIS service, next we need to create some methods to handle the intents LUIS will respond with. Our model for this sample is already pre-populated with some intents (“greeting”, “weather”, “question”, “joke”), the following code snippet demonstrates some simple methods to handle the ‘greeting’ and ‘weather’ intent.  –

[LuisIntent("greeting")]
public async Task Greeting(IDialogContext context, LuisResult result)
{
    string message = $"Hello there";

    await context.PostAsync(message);

    context.Wait(this.MessageReceived);
}

[LuisIntent("weather")]
public async Task Middle(IDialogContext context, LuisResult result)
{
    // confirm we hit weather intent
    string message = $"Weather forecast is...";

    await context.PostAsync(message);

    context.Wait(this.MessageReceived);
}

Let’s start up our bot and run it locally on the Bot Framework Emulator just to verify that this dialog is working properly and that both of these intents are being triggered as we expect.

Great! Looks like our bot is properly configured and connected to our LUIS application. Next, let’s try something a little more complex, as each intent handling method accepts IDialogContext and the original incoming IMessageActivity message. The next two intent handling methods demonstrate how we can direct the conversation into different dialogs.

[LuisIntent("question")]
public async Task QnA(IDialogContext context, LuisResult result)
{
    // confirm we hit QnA
    string message = $"Routing to QnA... ";
    await context.PostAsync(message);

    var userQuestion = (context.Activity as Activity).Text;
    await context.Forward(new QnaDialog(), ResumeAfterQnA, context.Activity, CancellationToken.None);
}

[LuisIntent("joke")]
public async Task Joke(IDialogContext context, LuisResult result)
{
    // confirm we hit joke intent
    string message = $"Let's see...I know a good joke...";

    await context.PostAsync(message);

    await context.Forward(new JokeDialog(), ResumeAfterJokeDialog, context.Activity, CancellationToken.None);
}

Back in the emulator, let’s trigger the intent to hit the QnaDialog.  The QnA service that this dialog connects to is the same one used in a previous blog post – QnA Maker with Rich Cards in .NET, so we can ask it a question that we already know the service will provide an answer for.

Excellent! To recap what’s happening, our RootDialog is setup to forward incoming messages to the LuisDialog, which processes the message via a LUIS service we can train. The result we get back from LUIS will describe what the user’s intent was, and one of the intent handler methods will be triggered to determine where the conversation will be directed to next. In the above example, LUIS recognized a ‘question’ intent, and forwarded the message activity to the QnaDialog, which is configured to a QnA Maker service.

QnA maker as a back up conversation handler

Another popular way to use both LUIS and QnA maker in your bot, is to use a QnA service as a fall back conversation handler. If LUIS fails to find a matching intent, then pass the user’s message to a QnA service to handle the rest!

[LuisIntent("")]
[LuisIntent("None")]
public async Task None(IDialogContext context, LuisResult result)
{
    // You can forward to QnA Dialog, and let Qna Maker handle the user's query if no intent is found
    await context.Forward(new QnaDialog(), ResumeAfterQnaDialog, context.Activity, CancellationToken.None);
}

Adding a global message handler using Scorables

Gary Pretty, Microsoft MVP wrote an article on his blog which provides a great introduction to Scorables, which we also discussed in a previous blog post. Scorables allow us to create global message handlers, meaning that no matter where on the dialog stack the conversation state is at, a scorable dialog can be called and interrupt the the dialog stack. The following sample borrows heavily from Garry’s sample code, but we wanted to use this opportunity to provide a quick example of a ‘help’ scenario to demonstrate how you could also include activity properties to a scorable dialog, such as a card attachment.

In CommonResponsesScorable, we include some logic to call a scorable dialog if a user types in ‘help’

protected override async Task PostAsync(IActivity item, string state, CancellationToken token)
{
    var message = item as IMessageActivity;
    IDialog<IMessageActivity> interruption = null;
    if (message != null)
    {
        var incomingMessage = message.Text.ToLowerInvariant();
        var messageToSend = string.Empty;

        if (incomingMessage.Contains("help"))
        {
            var commonResponsesDialog = new CommonResponsesDialog((Activity)message);
            interruption = commonResponsesDialog.Void<object, IMessageActivity>();
        }
        else
        {
            if (incomingMessage == "hello")
                messageToSend = "Hi! I am a bot";

...

In CommonResponsesDialog, we create an overloaded constructor to accept an Activity object. Next, we can add a card attachment to post it back to the user.

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;

namespace Scorable.Dialogs
{
    public class CommonResponsesDialog : IDialog<object>
    {
        private readonly string _messageToSend;
        private Activity _activity;

        public CommonResponsesDialog(string message)
        {
            _messageToSend = message;
        }

        // overload constructor to handle activities (card attachments)
        public CommonResponsesDialog(Activity activity)
        {
            var heroCard = new HeroCard
            {
                Title = "Help",
                Text = "Need assisstance?",
                Buttons = new List<CardAction> {
                    new CardAction(ActionTypes.OpenUrl, "Contact Us", value: "https://..."),
                    new CardAction(ActionTypes.OpenUrl, "FAQ", value: "https://...")
                }
            };
            var reply = activity.CreateReply();

            reply.Attachments.Add(heroCard.ToAttachment());

            _activity = reply;
        }

        public async Task StartAsync(IDialogContext context)
        {
            if (string.IsNullOrEmpty(_messageToSend))
            {
                await context.PostAsync(_activity);
            }
            else
            {
                await context.PostAsync(_messageToSend);
            }
            context.Done<object>(null);
        }
    }
}

Let’s test this out and run the emulator again. From the joke dialog, we type in ‘help’, and we expect the current dialog to be interrupted.

The bot recognized the scorable dialog, and responded to the user with a card attachment. When this scorable dialog is then resolved, the conversation will then resume from the dialog the conversation state was at prior to the scorable being called.

Summary

Click here to view the sample code on Git Hub.

Determining how to build your dialog flow is one of the most difficult challenges to overcome when designing your bot. LUIS and QnA Maker are two of the most popular cognitive services developers using the Bot Framework have been using, and one of the most frequent questions we’ve been receiving lately are how to integrate both of these powerful tools into their bots. Hopefully this post helps to shed some light on how you can manage your bot conversation flow.

Happy Making!

Matthew Shim and Jason Sowers from the Bot Framework Team

References

QnA Maker

LUIS

Scorables