如何在瀑布对话框中替换对话框后在上下文中找到 ActiveDialog(瀑布步骤)

How to find ActiveDialog (waterfall-step) in context after replacedialog in waterfall dialog

提示中的上下文帮助

我需要为聊天机器人实现上下文帮助。我的策略是使用活动提示作为带有帮助文本行的 table 的索引。在瀑布对话框中的 stepContext.replaceDialog() 之后,我正在努力寻找活动提示。

我将以 Compex Dialog sample 为例。

在下面的 reviewSelectionDialog 中是一个名为 CHOICE_PROMPT 的提示。这是我想添加上下文帮助的提示。如果用户输入帮助,则应显示有关该提示的帮助文本。

在同一个对话框中是一个循环步骤。根据用户的决定,对话通过 replaceDialog() 方法重复(循环)。

ReviewSelectionDialog 扩展了 CancelAndHelpDialog。因此,我能够检查并处理任何用户中断,例如 'help'.

在 CancelAndHelpDialog 中,当用户输入帮助时,我需要活动提示,以便我能够显示相关帮助。 (本例中为CHOICE_PROMPT)。

我的问题

在ReviewSelectionDialog 的第一次传递中,在发送'help' 之后,我能够通过innerDc.activeDialog.id 在CancelAndHelpDialog 中获得活动提示。但是在 loopStep 中的 stepContext.replaceDialog() 并在 CHOICE_PROMPT 中再次发送 'help' 之后,innerDc.activeDialog.id 显示 REVIEW_SELECTION_DIALOG。 我在哪里可以找到 replace_dialog() 之后的活动提示?

ReviewSelectionDialog

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

const { ChoicePrompt, WaterfallDialog } = require('botbuilder-dialogs');

const REVIEW_SELECTION_DIALOG = 'REVIEW_SELECTION_DIALOG';
const { CancelAndHelpDialog } = require('./cancelAndHelpDialog');

const CHOICE_PROMPT = 'CHOICE_PROMPT';
const WATERFALL_DIALOG = 'WATERFALL_DIALOG';

class ReviewSelectionDialog extends CancelAndHelpDialog {
    constructor() {
        super(REVIEW_SELECTION_DIALOG);

        // Define a "done" response for the company selection prompt.
        this.doneOption = 'done';

        // Define value names for values tracked inside the dialogs.
        this.companiesSelected = 'value-companiesSelected';

        // Define the company choices for the company selection prompt.
        this.companyOptions = ['Adatum Corporation', 'Contoso Suites', 'Graphic Design Institute', 'Wide World Importers'];

        this.addDialog(new ChoicePrompt(CHOICE_PROMPT));
        this.addDialog(new WaterfallDialog(WATERFALL_DIALOG, [
            this.selectionStep.bind(this),
            this.loopStep.bind(this)
        ]));

        this.initialDialogId = WATERFALL_DIALOG;
    }

    async selectionStep(stepContext) {
        // Continue using the same selection list, if any, from the previous iteration of this dialog.
        const list = Array.isArray(stepContext.options) ? stepContext.options : [];
        stepContext.values[this.companiesSelected] = list;

        // Create a prompt message.
        let message = '';
        if (list.length === 0) {
            message = `Please choose a company to review, or \`${ this.doneOption }\` to finish.`;
        } else {
            message = `You have selected **${ list[0] }**. You can review an additional company, or choose \`${ this.doneOption }\` to finish.`;
        }

        // Create the list of options to choose from.
        const options = list.length > 0
            ? this.companyOptions.filter(function(item) { return item !== list[0]; })
            : this.companyOptions.slice();
        options.push(this.doneOption);

        // Prompt the user for a choice.
        return await stepContext.prompt(CHOICE_PROMPT, {
            prompt: message,
            retryPrompt: 'Please choose an option from the list.',
            choices: options
        });
    }

    async loopStep(stepContext) {
        // Retrieve their selection list, the choice they made, and whether they chose to finish.
        const list = stepContext.values[this.companiesSelected];
        const choice = stepContext.result;
        const done = choice.value === this.doneOption;

        if (!done) {
            // If they chose a company, add it to the list.
            list.push(choice.value);
        }

        if (done || list.length > 1) {
            // If they're done, exit and return their list.
            return await stepContext.endDialog(list);
        } else {
            // Otherwise, repeat this dialog, passing in the list from this iteration.
            return await stepContext.replaceDialog(REVIEW_SELECTION_DIALOG, list);
        }
    }
}

module.exports.ReviewSelectionDialog = ReviewSelectionDialog;
module.exports.REVIEW_SELECTION_DIALOG = REVIEW_SELECTION_DIALOG;


CancelAndHelpDialog

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

const { InputHints } = require('botbuilder');
const { ComponentDialog, DialogTurnStatus } = require('botbuilder-dialogs');

/**
 * This base class watches for common phrases like "help" and "cancel" and takes action on them
 * BEFORE they reach the normal bot logic.
 */
class CancelAndHelpDialog extends ComponentDialog {
    async onContinueDialog(innerDc) {
        const result = await this.interrupt(innerDc);
        if (result) {
            return result;
        }
        return await super.onContinueDialog(innerDc);
    }

    async interrupt(innerDc) {
        if (innerDc.context.activity.text) {
            const text = innerDc.context.activity.text.toLowerCase();

            switch (text) {
            case 'help':
            case '?': {
                const helpMessageText = 'Show help about prompt: ' + innerDc.activeDialog.id;
                await innerDc.context.sendActivity(helpMessageText, helpMessageText, InputHints.ExpectingInput);
                return { status: DialogTurnStatus.waiting };
            }
            case 'cancel':
            case 'quit': {
                const cancelMessageText = 'Cancelling...';
                await innerDc.context.sendActivity(cancelMessageText, cancelMessageText, InputHints.IgnoringInput);
                return await innerDc.cancelAllDialogs();
            }
            }
        }
    }
}

module.exports.CancelAndHelpDialog = CancelAndHelpDialog;


我要感谢您使用示例代码,因为您实际上已经揭示了我在此处报告的错误:https://github.com/microsoft/BotBuilder-Samples/issues/2457

这里的根本问题是对话框库有两种堆叠对话框的方法。通常,一个对话框像这样堆叠在另一个对话框之上:

[  CHOICE_PROMPT   ]
[ WATERFALL_DIALOG ]

但是,组件对话框形成一个向内堆叠而不是向上堆叠的嵌套对话框堆栈:

        [ REVIEW_SELECTION_DIALOG  ]
    [        TOP_LEVEL_DIALOG          ]
[               MAIN_DIALOG                ]

由于并非所有对话框都是组件对话框,所以这两种方式结合起来如下所示:

            [  CHOICE_PROMPT   ]
            [ WATERFALL_DIALOG ]
        [ REVIEW_SELECTION_DIALOG  ]
    [        TOP_LEVEL_DIALOG          ]
[               MAIN_DIALOG                ]

我想指出,如果您习惯于编写如下所示的分层列表(最近添加的项目位于底部),那么此堆栈的顺序不一定是您所期望的:

  • MAIN_DIALOG
    • TOP_LEVEL_DIALOG
      • REVIEW_SELECTION_DIALOG
        • WATERFALL_DIALOG
        • CHOICE_PROMPT

有些人可能不会考虑第二种堆叠方式实际堆叠,因为它是父子关系而不是堆叠。我在这里称它为第二种堆叠方式的原因是因为它与对话框堆栈在概念上相似。设计机器人的对话框时,您可以选择是要将每个新对话框添加到现有对话框堆栈的顶部,还是作为子项嵌套在内部对话框堆栈中。这两种方式的行为相似,因为组件对话框在其最后一个子对话框结束时结束,因此当您从堆栈中弹出对话框时,堆栈向外展开的方式与向下展开的方式大致相同。 (请记住,新对话框会添加到堆栈的顶部,因此 "downwards" 这里的意思是从较新的对话框返回到较旧的对话框,就像我开始时使用的堆栈图一样。)

"active dialog" 是堆栈顶部的对话框。由于每个组件对话框都有自己的对话框集和对话框状态以及对话框堆栈和对话框上下文,因此每个组件对话框对什么是活动对话框有不同的想法。因为活动对话是根据特定对话堆栈定义的,所以当存在多个对话堆栈时,活动对话取决于您询问的人。

当您在最里面的组件对话框中寻找活动对话框时,这对您来说没有问题。但是随后您用组件对话框本身替换了该组件对话框的子项。之后,您的(完整)堆栈如下所示:

                [  CHOICE_PROMPT   ]
                [ WATERFALL_DIALOG ]
            [ REVIEW_SELECTION_DIALOG  ]
        [     REVIEW_SELECTION_DIALOG      ]
    [            TOP_LEVEL_DIALOG              ]
[                   MAIN_DIALOG                    ]

当您的 CancelAndHelpDialog 试图访问其内部对话框上下文的活动对话框时,它正确地 return 编辑了一个 ReviewSelectionDialog,因为那是其堆栈中唯一的对话框。您想要 return 选择提示,但该选择提示位于子 ReviewSelectionDialog 而不是父 ReviewSelectionDialog.

的对话框堆栈中

错误在于您应该将瀑布对话框替换为其自身而不是父组件对话框。所以它可能看起来像这样:

return await stepContext.replaceDialog(WATERFALL_DIALOG, list);

或者像这样:

return await stepContext.replaceDialog(this.initialDialogId, list);

最终,这仍然没有回答您可能想问的问题。由于您已经看到在中间对话框上下文中获取活动对话框时可能会出现问题,因此您可能需要一种方法来获取 "real" 最里面的活动对话框。这可以通过一些简单的递归来完成:

function getInnermostActiveDialog(dc) {
    const child = dc.child;
    return child ? getInnermostActiveDialog(child) : dc.activeDialog;
}