microsoft / botbuilder-dotnet

Welcome to the Bot Framework SDK for .NET repository, which is the home for the libraries and packages that enable developers to build sophisticated bot applications using .NET.
https://github.com/Microsoft/botframework
MIT License
872 stars 482 forks source link

Issue With Multiple Luis Dialog and Its Entities #3270

Closed SamaritanMachine closed 4 years ago

SamaritanMachine commented 4 years ago

Github issues should be used for bugs and feature requests. Use Stack Overflow for general "how-to" questions.

Version

Latest version

Describe the bug

Briefly, when running the bookingDialog with missing parameters (entities), bot asks the missing parameter. However, when I run the weatherDialog with missing parameters conversation overs with error. Even when I look the LUIS trace in Bot Framework Emulator, intent and entity recognized successfully.

To Reproduce

I'm try to create a bot with multiple Luis intents, it means multiple dialogs naturally. I've installed the default CoreBot template (Downloaded from Azure). After setting intents and entities; created second dialog which name is 'WeatherDialog', created "WeatherDetails" which consists of getter and setter for my LUIS weather entities, implemented some of code for accessing to entities' results to partial class which is already in the project for BookingDialog.

Then in the MainDialog, I tried to AddDialog.

MainDialog:

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Logging;
using Microsoft.Recognizers.Text.DataTypes.TimexExpression;
using CoreBot;

namespace Microsoft.BotBuilderSamples.Dialogs
{
    public class MainDialog : ComponentDialog
    {
        private readonly FlightBookingRecognizer _luisRecognizer;
        protected readonly ILogger Logger;

        // Dependency injection uses this constructor to instantiate MainDialog
        public MainDialog(FlightBookingRecognizer luisRecognizer, BookingDialog bookingDialog, CoreBot.Dialogs.WeatherDialog weatherDialog, ILogger<MainDialog> logger)
            : base(nameof(MainDialog))
        {
            _luisRecognizer = luisRecognizer;
            Logger = logger;

            AddDialog(new TextPrompt(nameof(TextPrompt)));
            AddDialog(bookingDialog);
            AddDialog(weatherDialog);
            AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
            {
                IntroStepAsync,
                ActStepAsync,
                FinalStepAsync,
            }));

            // The initial child Dialog to run.
            InitialDialogId = nameof(WaterfallDialog);
        }

        private async Task<DialogTurnResult> IntroStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            if (!_luisRecognizer.IsConfigured)
            {
                await stepContext.Context.SendActivityAsync(
                    MessageFactory.Text("NOTE: LUIS is not configured. To enable all capabilities, add 'LuisAppId', 'LuisAPIKey' and 'LuisAPIHostName' to the appsettings.json file.", inputHint: InputHints.IgnoringInput), cancellationToken);

                return await stepContext.NextAsync(null, cancellationToken);
            }

            // Use the text provided in FinalStepAsync or the default if it is the first time.
            var messageText = stepContext.Options?.ToString() ?? "How can I help you?";
            var promptMessage = MessageFactory.Text(messageText, messageText, InputHints.ExpectingInput);
            return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = promptMessage }, cancellationToken);
        }

        private async Task<DialogTurnResult> ActStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            if (!_luisRecognizer.IsConfigured)
            {
                // LUIS is not configured, we just run the BookingDialog path with an empty BookingDetailsInstance.
                return await stepContext.BeginDialogAsync(nameof(BookingDialog), new BookingDetails(), cancellationToken);
            }

            // Call LUIS and gather any potential booking details. (Note the TurnContext has the response to the prompt.)
            var luisResult = await _luisRecognizer.RecognizeAsync<CoreBot.WeatherModel>(stepContext.Context, cancellationToken);
            switch (luisResult.TopIntent().intent)
            {
                case CoreBot.WeatherModel.Intent.BookFlight:
                    //await ShowWarningForUnsupportedCities(stepContext.Context, luisResult, cancellationToken);
                    Console.WriteLine("This is bookflight");
                    // Initialize BookingDetails with any entities we may have found in the response.
                    var bookingDetails = new BookingDetails()
                    {
                        // Get destination and origin from the composite entities arrays.
                        Destination = luisResult.ToEntities.Airport,
                        Origin = luisResult.FromEntities.Airport,
                        TravelDate = luisResult.TravelDate,
                    };

                    // Run the BookingDialog giving it whatever details we have from the LUIS call, it will fill out the remainder.
                    return await stepContext.BeginDialogAsync(nameof(BookingDialog), bookingDetails, cancellationToken);

                case CoreBot.WeatherModel.Intent.GetWeather:
                    Console.WriteLine("This is getweather");
                    var weatherDetails = new CoreBot.WeatherDetails()
                    {
                        Location = luisResult.Location,
                        TravelDate = luisResult.TravelDate,
                    };
                    return await stepContext.BeginDialogAsync(nameof(CoreBot.Dialogs.WeatherDialog), weatherDetails, cancellationToken);

                default:
                    // Catch all for unhandled intents
                    var didntUnderstandMessageText = $"Sorry, I didn't get that. Please try asking in a different way (intent was {luisResult.TopIntent().intent})";
                    var didntUnderstandMessage = MessageFactory.Text(didntUnderstandMessageText, didntUnderstandMessageText, InputHints.IgnoringInput);
                    await stepContext.Context.SendActivityAsync(didntUnderstandMessage, cancellationToken);
                    break;
            }

            return await stepContext.NextAsync(null, cancellationToken);
        }

        // Shows a warning if the requested From or To cities are recognized as entities but they are not in the Airport entity list.
        // In some cases LUIS will recognize the From and To composite entities as a valid cities but the From and To Airport values
        // will be empty if those entity values can't be mapped to a canonical item in the Airport.
        private static async Task ShowWarningForUnsupportedCities(ITurnContext context, FlightBooking luisResult, CancellationToken cancellationToken)
        {
            var unsupportedCities = new List<string>();

            var fromEntities = luisResult.FromEntities;
            if (!string.IsNullOrEmpty(fromEntities.From) && string.IsNullOrEmpty(fromEntities.Airport))
            {
                unsupportedCities.Add(fromEntities.From);
            }

            var toEntities = luisResult.ToEntities;
            if (!string.IsNullOrEmpty(toEntities.To) && string.IsNullOrEmpty(toEntities.Airport))
            {
                unsupportedCities.Add(toEntities.To);
            }

            if (unsupportedCities.Any())
            {
                var messageText = $"Sorry but the following airports are not supported: {string.Join(',', unsupportedCities)}";
                var message = MessageFactory.Text(messageText, messageText, InputHints.IgnoringInput);
                await context.SendActivityAsync(message, cancellationToken);
            }
        }

        private async Task<DialogTurnResult> FinalStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            // If the child dialog ("BookingDialog") was cancelled, the user failed to confirm or if the intent wasn't BookFlight
            // the Result here will be null.
            if (stepContext.Result is BookingDetails result)
            {
                // Now we have all the booking details call the booking service.

                // If the call to the booking service was successful tell the user.

                var timeProperty = new TimexProperty(result.TravelDate);
                var travelDateMsg = timeProperty.ToNaturalLanguage(DateTime.Now);
                var messageText = $"I have you booked to {result.Destination} from {result.Origin} on {travelDateMsg}";
                var message = MessageFactory.Text(messageText, messageText, InputHints.IgnoringInput);
                await stepContext.Context.SendActivityAsync(message, cancellationToken);
            }
            else if (stepContext.Result is CoreBot.WeatherDetails sonuc)
            {
                var timeProperty = new TimexProperty(sonuc.TravelDate);
                var weatherDateMsg = timeProperty.ToNaturalLanguage(DateTime.Now);
                var messageText = $"Thats your weather result for {weatherDateMsg} at {sonuc.Location}: 00 F";
                var message = MessageFactory.Text(messageText, messageText, InputHints.IgnoringInput);
                await stepContext.Context.SendActivityAsync(message, cancellationToken);
            }

            // Restart the main dialog with a different message the second time around
            var promptMessage = "What else can I do for you?";
            return await stepContext.ReplaceDialogAsync(InitialDialogId, promptMessage, cancellationToken);
        }
    }
}

And also my WeatherDialog:

using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
using Microsoft.BotBuilderSamples.Dialogs;
using Microsoft.Recognizers.Text.DataTypes.TimexExpression;

namespace CoreBot.Dialogs
{
    public class WeatherDialog : CancelAndHelpDialog
    {
        private const string DestinationStepMsgText = "Type the location.";

        public WeatherDialog()
            : base(nameof(WeatherDialog))
        {
            AddDialog(new TextPrompt(nameof(TextPrompt)));
            AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt)));
            AddDialog(new DateResolverDialog());
            AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
            {
                LocationStepAsync,
                TravelDateStepAsync,
                ConfirmStepAsync,
                FinalStepAsync,
            }));

            // The initial child Dialog to run.
            InitialDialogId = nameof(WaterfallDialog);
        }

        private async Task<DialogTurnResult> LocationStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            var weatherDetails = (WeatherDetails)stepContext.Options;

            if (weatherDetails.Location == null)
            {
                var promptMessage = MessageFactory.Text(DestinationStepMsgText, DestinationStepMsgText, InputHints.ExpectingInput);
                return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = promptMessage }, cancellationToken);
            }

            return await stepContext.NextAsync(weatherDetails.Location, cancellationToken);
        }

        private async Task<DialogTurnResult> TravelDateStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            var weatherDetails = (WeatherDetails)stepContext.Options;

            weatherDetails.Location = (string)stepContext.Result;

            if (weatherDetails.TravelDate == null || IsAmbiguous(weatherDetails.TravelDate))
            {
                return await stepContext.BeginDialogAsync(nameof(DateResolverDialog), weatherDetails.TravelDate, cancellationToken);
            }

            return await stepContext.NextAsync(weatherDetails.TravelDate, cancellationToken);
        }

        private async Task<DialogTurnResult> ConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            var weatherDetails = (WeatherDetails)stepContext.Options;

            weatherDetails.TravelDate = (string)stepContext.Result;

            var messageText = $"You want to know weather status at {weatherDetails.Location} for {weatherDetails.TravelDate}. Is this correct?";
            var promptMessage = MessageFactory.Text(messageText, messageText, InputHints.ExpectingInput);

            return await stepContext.PromptAsync(nameof(ConfirmPrompt), new PromptOptions { Prompt = promptMessage }, cancellationToken);
        }

        private async Task<DialogTurnResult> FinalStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            if ((bool)stepContext.Result)
            {
                var weatherDetails = (WeatherDetails)stepContext.Options;

                return await stepContext.EndDialogAsync(weatherDetails, cancellationToken);
            }

            return await stepContext.EndDialogAsync(null, cancellationToken);
        }

        private static bool IsAmbiguous(string timex)
        {
            var timexProperty = new TimexProperty(timex);
            return !timexProperty.Types.Contains(Constants.TimexTypes.Definite);
        }
    }
}

Conversation with bug

A: How is the weather today? (missing entities) Bot: The bot encounted an error or bug. -> Failed Bot: To continue to run this bot, please fix the bot source code. A: How is the weather today at Los Angeles? Bot: -> Successful A: How is the weather at Los Angeles? (missing entity) Bot: The bot encounted an error or bug. -> Failed Bot: To continue to run this bot, please fix the bot source code. A: book me flight Bot: -> Successful (Asks missing entities) A: How is the weather tomorrow at Tokyo? Bot: -> Successful ............

[bug]

xieofxie commented 4 years ago

Hi @SamaritanMachine, if you could run and debug the bot locally, you could set a breakpoint in the OnTurnError of your adapter where The bot encounted an error or bug. is sent.

Then in the exception, you will find the stacktrace and real exception type that causes the error. They would provide more information to help you identify the difference between book dialog and weather dialog.

Thanks~

SamaritanMachine commented 4 years ago

Hi @SamaritanMachine, if you could run and debug the bot locally, you could set a breakpoint in the OnTurnError of your adapter where The bot encounted an error or bug. is sent. Then in the exception, you will find the stacktrace and real exception type that causes the error. They would provide more information to help you identify the difference between book dialog and weather dialog. Thanks~

I didn't know we can debug BotFramework in real conversation enviroment. I'm really new in C# and Visual Studio :)

Thank you sir, it works like a charm now!