Integrate multiple LUIS and QnA services with the Dispatch tool

Note

This topic is for the latest release of the SDK (v4). You can find content for the older version of the SDK (v3) here.

This tutorial demonstrates how to use a LUIS model generated by the Dispatch tool, to integrate your bot with multiple Language Understanding (LUIS) apps and QnAMaker services. This sample combines the following services.

Service type Name Description
LUIS app HomeAutomation Recognizes a home automation intent with associated entity data.
LUIS app Weather Recognizes the Weather.GetForecast and Weather.GetCondition intents with location data.
QnAMaker service FAQ Provides answers to some simple questions about the bot.

Code for this article is taken from the NLP with Dispatch sample [C#].

See language understanding, for an overview of language services. See the how to articles for LUIS and QnA Maker, for instructions on implementing these in a bot.

You can follow the instructions in the README for the sample to setup and test the bot, or you can jump ahead to notes about the code.

Create the services and test the bot

Follow the README instructions for the sample. You'll use CLI tools to create and publish these services and update information about them in your configuration (.bot) file.

  1. Clone or pull the samples repository.
  2. Install the BotBuilder CLI tools.
  3. Manually configure the required services.

Test your bot

Start your bot using either Visual Studio or Visual Studio Code.

Connect to your bot with the Bot Framework Emulator.

Here is some of the input covered by the services we've included:

  • QnA Maker
    • hi, good morning
    • what are you, what do you do
  • LUIS (home automation)
    • turn on bedroom light
    • turn off bedroom light
    • make some coffee
  • LUIS (weather)
    • whats the weather in chennai india
    • what's the forecast for bangalore
    • show me the forecast for nebraska

Notes about the code

Packages

This sample uses the following packages.

The latest v4 versions of these NuGet packages.

  • Microsoft.Bot.Builder
  • Microsoft.Bot.Builder.AI.Luis
  • Microsoft.Bot.Builder.AI.QnA
  • Microsoft.Bot.Builder.Integration.AspNet.Core
  • Microsoft.Bot.Configuration

BotBuilder CLI tools

The sample uses these BotBuilder CLI tools (available via npm) to create, train, and publish the LUIS, QnA Maker, and Dispatch services; and to record information about these services in your bot's configuration (.bot) file.

Tip

To make sure you have the latest version of npm and these CLI tools, run the following command.

npm i -g npm dispatch ludown luis-apis msbot qnamaker

Once you have used the tools to set up the services , your .bot file for this sample should look similar to this. (You can run msbot secret -n to encrypt the sensitive values in this file.)

{
    "name": "NLP-With-Dispatch-Bot",
    "description": "",
    "services": [
        {
            "type": "endpoint",
            "name": "development",
            "id": "http://localhost:3978/api/messages",
            "appId": "",
            "appPassword": "",
            "endpoint": "http://localhost:3978/api/messages"
        },
        {
            "type": "luis",
            "name": "Home Automation",
            "appId": "<your-home-automation-luis-app-id>",
            "version": "0.1",
            "authoringKey": "<your-luis-authoring-key>",
            "subscriptionKey": "<your-cognitive-services-subscription-key>",
            "region": "westus",
            "id": "110"
        },
        {
            "type": "luis",
            "name": "Weather",
            "appId": "<your-weather-luis-app-id>",
            "version": "0.1",
            "authoringKey": "<your-luis-authoring-key>",
            "subscriptionKey": "<your-cognitive-services-subscription-key>",
            "region": "westus",
            "id": "92"
        },
        {
            "type": "qna",
            "name": "Sample QnA",
            "kbId": "<your-qna-knowledge-base-id>",
            "subscriptionKey": "<your-cognitive-services-subscription-key>",
            "endpointKey": "<your-qna-endpoint-key>",
            "hostname": "<your-qna-host-name>",
            "id": "184"
        },
        {
            "type": "dispatch",
            "name": "NLP-With-Dispatch-BotDispatch",
            "appId": "<your-dispatch-app-id>",
            "authoringKey": "<your-luis-authoring-key>",
            "subscriptionKey": "<your-cognitive-services-subscription-key>",
            "version": "Dispatch",
            "region": "westus",
            "serviceIds": [
                "110",
                "92",
                "184"
            ],
            "id": "27"
        }
    ],
    "padlock": "",
    "version": "2.0"
}

Connecting to the services from your bot

To connect to the Dispatch, LUIS, and QnA Maker services, your bot pulls information from the .bot file.

In Startup.cs, ConfigureServices reads in the configuration file, and InitBotServices uses that information to initialize the services. Each time it's created, the bot is initialized with the registered BotServices object. Here are the relevant parts of these two methods.

public void ConfigureServices(IServiceCollection services)
{
    //...
    var botConfig = BotConfiguration.Load(botFilePath ?? @".\BotConfiguration.bot", secretKey);
    services.AddSingleton(sp => botConfig
        ?? throw new InvalidOperationException($"The .bot config file could not be loaded. ({botConfig})"));

    // Retrieve current endpoint.
    var environment = _isProduction ? "production" : "development";
    var service = botConfig.Services.Where(s => s.Type == "endpoint" && s.Name == environment).FirstOrDefault();
    if (!(service is EndpointService endpointService))
    {
        throw new InvalidOperationException($"The .bot file does not contain an endpoint with name '{environment}'.");
    }

    var connectedServices = InitBotServices(botConfig);

    services.AddSingleton(sp => connectedServices);
    //...
}
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 the LuisRecognizer to resolve the
                // 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);
}

Calling the services from your bot

The bot logic checks the user input against the combined Dispatch model.

In the NlpDispatchBot.cs file, the bot's constructor gets the BotServices object that we registered at startup.

private readonly BotServices _services;

public NlpDispatchBot(BotServices services)
{
    _services = services ?? throw new System.ArgumentNullException(nameof(services));

    //...
}

In the bot's OnTurnAsync method, we check incoming messages from the user against the Dispatch model.

// Get the intent recognition result
var recognizerResult = await _services.LuisServices[DispatchKey].RecognizeAsync(context, cancellationToken);
var topIntent = recognizerResult?.GetTopScoringIntent();

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

Working with the recognition results

When the model produces a result, it indicates which service can most appropriately process the utterance. The code in this bot routes the request to the corresponding service, and then summarizes the response from the called service.

/// <summary>
/// Depending on the intent from Dispatch, routes to the right LUIS model or QnA service.
/// </summary>
private async Task DispatchToTopIntentAsync(
    ITurnContext context,
    (string intent, double score)? topIntent,
    CancellationToken cancellationToken = default(CancellationToken))
{
    const string homeAutomationDispatchKey = "l_Home_Automation";
    const string weatherDispatchKey = "l_Weather";
    const string noneDispatchKey = "None";
    const string qnaDispatchKey = "q_sample-qna";

    switch (topIntent.Value.intent)
    {
        case homeAutomationDispatchKey:
            await DispatchToLuisModelAsync(context, HomeAutomationLuisKey);

            // Here, you can add code for calling the hypothetical home automation service, passing in any entity
            // information that you need.
            break;
        case weatherDispatchKey:
            await DispatchToLuisModelAsync(context, WeatherLuisKey);

            // Here, you can add code for calling the hypothetical weather service,
            // passing in any entity information that you need
            break;
        case noneDispatchKey:
            // You can provide logic here to handle the known None intent (none of the above).
            // In this example we fall through to the QnA intent.
        case qnaDispatchKey:
            await DispatchToQnAMakerAsync(context, QnAMakerKey);
            break;

        default:
            // The intent didn't match any case, so just display the recognition results.
            await context.SendActivityAsync($"Dispatch intent: {topIntent.Value.intent} ({topIntent.Value.score}).");
            break;
    }
}

/// <summary>
/// Dispatches the turn to the request QnAMaker app.
/// </summary>
private async Task DispatchToQnAMakerAsync(
    ITurnContext context,
    string appName,
    CancellationToken cancellationToken = default(CancellationToken))
{
    if (!string.IsNullOrEmpty(context.Activity.Text))
    {
        var results = await _services.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,
    CancellationToken cancellationToken = default(CancellationToken))
{
    await context.SendActivityAsync($"Sending your request to the {appName} system ...");
    var result = await _services.LuisServices[appName].RecognizeAsync(context, cancellationToken);

    await context.SendActivityAsync($"Intents detected by the {appName} app:\n\n{string.Join("\n\n", result.Intents)}");

    if (result.Entities.Count > 0)
    {
        await context.SendActivityAsync($"The following entities were found in the message:\n\n{string.Join("\n\n", result.Entities)}");
    }
}

Evaluate the dispatcher's performance

Sometimes there are user messages that are provided as examples in both the LUIS apps and the QnA maker services, and the combined LUIS app that Dispatch generates won't perform well for those inputs. You can check your app's performance using the eval option.

dispatch eval

Running dispatch eval generates a Summary.html file that provides statistics on the predicted performance of the language model.

Tip

You can run dispatch eval on any LUIS app, not just LUIS apps created by the dispatch tool.

Edit intents for duplicates and overlaps

Review example utterances that are flagged as duplicates in Summary.html, and remove similar or overlapping examples. For example, let's say that in the Home Automation LUIS app requests like "turn my lights on" map to a "TurnOnLights" intent, but requests like "Why won't my lights turn on?" map to a "None" intent so that they can be passed on to QnA Maker. When you combine the LUIS app and the QnA Maker service using dispatch, you need to do one of the following:

  • Remove the "None" intent from the original Home Automation LUIS app, and add the utterances in that intent to the "None" intent in the dispatcher app.
  • If you don't remove the "None" intent from the original LUIS app, you need to add logic in your bot to pass those messages that match that intent on to the QnA maker service.

Tip

Review Best practices for Language Understanding for tips on improving your language model's performance.

To clean up resources from this sample

This sample creates a number of applications and resources. You can follow these instructions to delete them.

LUIS resources

  1. Sign in to the luis.ai portal.
  2. Go to the My Apps page.
  3. Select the apps created by this sample.
    • Home Automation
    • Weather
    • NLP-With-Dispatch-BotDispatch
  4. Click Delete, and click Ok to confirm.

QnA Maker resources

  1. Sign in to the qnamaker.ai portal.
  2. Go to the My knowledge bases page.
  3. Click the delete button for the Sample QnA knowledge base, and click Delete to confirm.

Azure resources

Warning

Do not delete any of these resources that any other other apps or services rely on.

  1. Sign in to the Azure portal.
  2. Go to the Overview page for the Cognitive Services resource you created for the sample.
  3. Click Delete, and click Yes to confirm.

Additional resources