Search code examples
c#botframeworkchatbotazure-language-understanding

Design Advice on Architecture on Bot Framework for Best conversation flow


I am in the process of building a chatbot which is more conversational using the Microsoft Bot Framework v4.

What form of structure can I implement in the architecture to achieve a bot that can handle complex scenarios as described below

User: I'd like to add a Mac to my renters insurance

{detects intent}

Bot: Sure, which laptop do you want to get.

User: A used Pro from 2018

{extracts "pro" and "2018" from user utterance}

Bot:Alright, It will cost $5 per month to add your MacBook Pro 2018. Does that work for you?

User: Oh Sorry, It is actually a 2017

{Bot detect intent and extracts entities}

Bot:No worries, it would still be $5 per month to add your MacBook Pro 2017, Works for you?

User: By the way what is the weather in New York? {Bot detect interruption}

Bot: The weather in New York is 28 Degrees Celsius

Bot:Do you still want to add your MacBook to your rentals?

User:Sounds great

Bot: Perfect, Your MacBook is not insured.

Bot: By the way, Happy Birthday!!.

I understand that bot framework can be integrated with LUIS to detect user intents from utterance however how can I place Luis in the chatbot project for it to handle all user utterances as the dialog flows.

So in scenarios as described above, LUIS can help continuing extracting entities from user utterances.


Solution

  • The core bot here shows both how to handle LUIS and interruptions.

    Basically, instead of adding LUIS in as middleware, you're going to add it in as a "helper" class:

    picture of file structure

    The LUIS helper class will keep track of the intents:

    
    public static class LuisHelper
    {
        public static async Task<BookingDetails> ExecuteLuisQuery(IConfiguration configuration, ILogger logger, ITurnContext turnContext, CancellationToken cancellationToken)
        {
            var bookingDetails = new BookingDetails();
    
            try
            {
                // Create the LUIS settings from configuration.
                var luisApplication = new LuisApplication(
                    configuration["LuisAppId"],
                    configuration["LuisAPIKey"],
                    "https://" + configuration["LuisAPIHostName"]
                );
    
                var recognizer = new LuisRecognizer(luisApplication);
    
                // The actual call to LUIS
                var recognizerResult = await recognizer.RecognizeAsync(turnContext, cancellationToken);
    
                var (intent, score) = recognizerResult.GetTopScoringIntent();
                if (intent == "Book_flight")
                {
                    // We need to get the result from the LUIS JSON which at every level returns an array.
                    bookingDetails.Destination = recognizerResult.Entities["To"]?.FirstOrDefault()?["Airport"]?.FirstOrDefault()?.FirstOrDefault()?.ToString();
                    bookingDetails.Origin = recognizerResult.Entities["From"]?.FirstOrDefault()?["Airport"]?.FirstOrDefault()?.FirstOrDefault()?.ToString();
    
                    // This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop the Time part.
                    // TIMEX is a format that represents DateTime expressions that include some ambiguity. e.g. missing a Year.
                    bookingDetails.TravelDate = recognizerResult.Entities["datetime"]?.FirstOrDefault()?["timex"]?.FirstOrDefault()?.ToString().Split('T')[0];
                }
            }
            catch (Exception e)
            {
                logger.LogWarning($"LUIS Exception: {e.Message} Check your LUIS configuration.");
            }
    
            return bookingDetails;
        }
    }
    
    

    In your main dialog, you're going to call that LUIS helper like this:

    private async Task<DialogTurnResult> ActStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // Call LUIS and gather any potential booking details. (Note the TurnContext has the response to the prompt.)
        var bookingDetails = stepContext.Result != null
            ?
        await LuisHelper.ExecuteLuisQuery(Configuration, Logger, stepContext.Context, cancellationToken)
            :
        new BookingDetails();
    
        // In this sample we only have a single Intent we are concerned with. However, typically a scenario
        // will have multiple different Intents each corresponding to starting a different child Dialog.
    
        // 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);
    }
    

    As for interruptions, the simple way to handle those are by creating them as a component dialog, and then extending that dialog on all the other dialogs:

    picture of component dialog

    I recommend giving the Core Bot a look over, as it gives a basic outline of what you're looking for. For a more complicated example, the Bot Framework also has the Virtual Assistant, here.