Search code examples
c#botframeworkazure-language-understandingazure-qna-maker

Qna and LUIS interrupts dialog before next step can process the input


I have QnaMaker and LUIS integrated into my bot. I want the user to be able to ask questions in between conversation. I already discovered that the problem is, that the bot always looks in luis and qna first, before processing the input of the user.

For example if I have a choice prompt with "Start now" and "Stop now", Luis or qna will interrupt and process the input, reprompt the dialog again resulting in an infinite loop and never reaching the next step.

I think this is bad design on my part. is there a way for the next step to process the result first? If it did not recognize the result, luis and qna should then process the input.

    private async Task<bool> IsTurnInterruptedDispatchToQnAMakerAsync(ITurnContext turnContext, string topDispatch, string appName, CancellationToken cancellationToken = default(CancellationToken))
    {
        var dc = await _dialogs.CreateContextAsync(turnContext);
        const string qnaDispatchKey = "q_xxxxxxxx";

        if (topDispatch.Equals(qnaDispatchKey))
        {
            var results = await _services.QnAServices[appName].GetAnswersAsync(turnContext);
            if (results.Any())
            {
                await turnContext.SendActivityAsync(results.First().Answer, cancellationToken: cancellationToken);
            }

            if (dc.ActiveDialog != null)
            {
                await dc.RepromptDialogAsync();
            }

            return true;
        }

        return false;
    }

        return false;
    }

on OnTurnAsync()

            var interruptedQnaMaker = await IsTurnInterruptedDispatchToQnAMakerAsync(turnContext, topDispatch, QnaConfiguration, cancellationToken);
            if (interruptedQnaMaker)
            {
                await _basicAccessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
                await _basicAccessors.UserState.SaveChangesAsync(turnContext, false, cancellationToken);
                return;
            }

Solution

  • You kind of have two questions in there and I'll answer them both. I don't know if there's a "best" way to do this--it really depends on your code. You might need to do some combination of the two things below, as well.

    Using LUIS and QnA in a Choice Prompt

    My example shows how to do this with LUIS, but you can substitute QnAMaker in here pretty easily.

    Pass your BotServices to your dialog (in MyBot.cs's constructor):

    Dialogs.Add(new MyDialog(services));
    

    Note: Depending on where you do this, you might be able to pass in your LuisRecognizer instead of all your services.

    Use BotServices in MyDialog's constructor:

    public class MyDialog : ComponentDialog
    {
        private readonly BotServices _services;
        public MyDialog(BotServices services) : base(nameof(QuickDialog))
        {
            [...]
            _services = services;
        }
    

    Create a validator in your ChoicePrompt:

    AddDialog(new ChoicePrompt(nameof(ChoicePrompt), luisValidation));
    

    Create your validator, which allows you to adjust the user's input and set it as something else (like a LUIS intent):

    private async Task<bool> LuisValidationAsync(PromptValidatorContext<FoundChoice> promptContext, CancellationToken cancellationToken)
    {
        // ...Succeeded will only be true for a ChoicePrompt if user input matches a Choice
        if (!promptContext.Recognized.Succeeded)
        {
            // User input doesn't match a choice, so get the LUIS result
            var luisResults = await _services.LuisServices["nameOfLuisServiceInBotFile"].RecognizeAsync(promptContext.Context, cancellationToken);
            var topScoringIntent = luisResults?.GetTopScoringIntent();
            var topIntent = topScoringIntent.Value.intent;
            // Save the results and pass them onto the next waterfall step
            promptContext.Recognized.Succeeded = true;
            promptContext.Recognized.Value = new FoundChoice()
            {
                Index = 0,
                Score = 1,
                Value = topIntent
            };
            // We converted to a valid LUIS result, so return true
            return true;
        }
        // ...Succeeded was true, so return true
        return true;
    }
    

    Process the Result

    There's a few different places you can do something with the result, instead of just changing the user's input. For example, in the next step, you could:

    switch ((stepContext.Result as FoundChoice).Value)
    {
        case "Reply":
            await stepContext.Context.SendActivityAsync("Reply");
            break;
        case "Cancel":
            return await stepContext.EndDialogAsync("Cancel Me");                    
    }
    return await stepContext.NextAsync();
    

    If the user calls the "Cancel" intent, this would bubble up to MyBot.cs and dialogResult.Result would equal "Cancel Me".

    Skipping LUIS/QnA Recognition

    There's two ways that you can go about skipping LUIS recognition:

    1. Bypass LUIS and QnA Calls

    If you don't want to check for interruptions, set up conditions where you'd like to skip it. You could use something like:

    var interruptedQnaMaker = false;
    if (!<yourCondition>)
    {
        var interruptedQnaMaker = await IsTurnInterruptedDispatchToQnAMakerAsync(turnContext, topDispatch, QnaConfiguration, cancellationToken);
    }
    

    I did something fairly similar in a Node bot, where I skipped the luisRecognizer entirely for certain dialogs. For you, it might more or less look like this:

    var dc = await _dialogs.CreateContextAsync(turnContext);
    if (dc.ActiveDialog != null && dc.ActiveDialog.id == "SkipLuisDialog")
    {
        var interruptedQnaMaker = await IsTurnInterruptedDispatchToQnAMakerAsync(turnContext, topDispatch, QnaConfiguration, cancellationToken);
    }
    

    2. Adjust your LUIS app

    It looks like you have it set up so that when LUIS returns an intent (topDispatch) that matches qnaDispatchKey, that this is when you trigger an interrupt. If "Start now" and "Stop now" are returning qnaDispatchKey as the intent, you can adjust your LUIS app to prevent that.strong text