Search code examples
.netbotframework

Bot framework v4 .NET handling user interruptions by redirecting to a different dialog


I'm developing a bot using bot framework v4 with .NET. The conversation is in norwegian and is strict with different menu options the user can follow. For example in the welcome menu the user can choose between "contact us", "show products", "subscribe to newsletter" and follow the dialogs for those three. Now I want the user to be able to just write for example "products" at any part of the conversation, even though the user is in a "subscribe to newsletter" dialog and then get directed to my ProductDialog.

I've followed Microsoft's documentation on handling user interruptions and used Core-bot as the fundament. The same approach worked in my code and I successfully was able to interrupt and write "help" at any point in the conversation og get an answer. But I can't seem to work out a simple way to direct to another dialog though when the CancelAndHelpDialog is triggered by an interruption word.

Here's my version of CancelAndHelpDialog from the CoreBot, I've added a BookingDialog in the constructor and added BeginDialog when the user writes the word help in the conversation. The rest of the files are the same as in CoreBot.

public class CancelAndHelpDialog : ComponentDialog
{
    public CancelAndHelpDialog(string id)
        : base(id)
    {
        AddDialog(new BookingDialog());
    }

    protected override async Task<DialogTurnResult> OnBeginDialogAsync(DialogContext innerDc,
        object options, CancellationToken cancellationToken = default(CancellationToken))
    {
        var result = await InterruptAsync(innerDc, cancellationToken);
        if (result != null)
        {
            return result;
        }

        return await base.OnBeginDialogAsync(innerDc, options, cancellationToken);
    }

    protected override async Task<DialogTurnResult> OnContinueDialogAsync(DialogContext innerDc,
        CancellationToken cancellationToken)
    {
        var result = await InterruptAsync(innerDc, cancellationToken);
        if (result != null)
        {
            return result;
        }

        return await base.OnContinueDialogAsync(innerDc, cancellationToken);
    }

    private async Task<DialogTurnResult> InterruptAsync(DialogContext innerDc, CancellationToken cancellationToken)
    {
        if (innerDc.Context.Activity.Type == ActivityTypes.Message)
        {
            var text = innerDc.Context.Activity.Text.ToLowerInvariant();

            switch (text)
            {
                case "help":
                case "?":
                    await innerDc.Context.SendActivityAsync($"Show Help...", cancellationToken: cancellationToken);
                    //return new DialogTurnResult(DialogTurnStatus.Waiting);
                    return await innerDc.BeginDialogAsync(nameof(BookingDialog), cancellationToken);

                case "cancel":
                case "quit":
                    await innerDc.Context.SendActivityAsync($"Cancelling", cancellationToken: cancellationToken);
                    return await innerDc.CancelAllDialogsAsync();
            }
        }

        return null;
    }
}

Solution

  • I implemented something similar via the old version of Core-Bot, but it's checking for the interruption within my intent selection, not as a separate dialog. It's also node.js, but providing this example here as I think it can help you get to a working solution for your use case.

    After I get intent back I'm calling a function to start the appropriate dialog or action. But before them I'm checking for interruption:

    async dispatchToTopIntentAsync(context, intent, recognizerResult) {
        const dc = await this.dialogs.createContext(context);
        const userDialog = await this.userDialogStateAccessor.get(context, {});
            if (context.activity.type === ActivityTypes.Message) {
                let dialogResult;
                const interrupted = await this.isTurnInterrupted(dc, recognizerResult);
                if (interrupted) {
                    if (dc.activeDialog !== undefined) {
                        // issue a re-prompt on the active dialog
                        dialogResult = await dc.repromptDialog();
                    } // Else: We dont have an active dialog so nothing to continue here.
                } else {
                    // No interruption. Continue any active dialogs.
                    dialogResult = await dc.continueDialog();
                }
    

    And the interrupt function:

        async isTurnInterrupted(dc, luisResults) {
            const topIntent = LuisRecognizer.topIntent(luisResults);
            const topIntentScore = luisResults.intents[topIntent].score;
    
            // see if there are any conversation interrupts we need to handle
            if (topIntent === CANCEL_INTENT & topIntentScore > 0.6) {
                if (dc.activeDialog) {
                    // cancel all active dialog (clean the stack)
                    await dc.cancelAllDialogs();
                    await dc.context.sendActivity('Ok. I\'ve cancelled our last activity.');
                } else {
                    await dc.context.sendActivity('I don\'t have anything to cancel. If you\'re not trying to cancel something, please ask your question again.');
                }
                return true; // this is an interruption
            }
    
            if (topIntent === HELP_INTENT & topIntentScore > 0.5) {
                await dc.context.sendActivity('Let me try to provide some help.');
                await dc.context.sendActivity('Right now I am trained to help you with order status and tracking. If you are stuck in a conversation, type "Cancel" to start over.');
                return true; // this is an interruption
            }
    
            if (topIntent === EXPEDITE_INTENT & topIntentScore > 0.5) {
                await dc.beginDialog(INTERRUPT_DIALOG, topIntent);
                return false; // pushing new dialog so not an interruption
            }
    
            if (topIntent === ESCALATE_INTENT & topIntentScore > 0.5) {
                await dc.beginDialog(INTERRUPT_DIALOG, topIntent);
                return false; // pushing new dialog so not an interruption
            }
    
            return false; // this is not an interruption
        }
    

    Note here that in the first two interrupts I'm sending a message directly and in the last two I'm starting a new dialog. I'm returning false in those cases because I don't want to reprompt the dialog I'm pushing on the stack.

    In this case I'm pushing, but you could also replace the current active dialog or cancel all dialogs and start a new one.