使用調適型、元件、瀑布式和自訂對話建立 Bot

適用于: SDK v4

所有對話都衍生自基底「對話」類別。 如果您使用「對話管理員」來執行根對話,則所有對話類別都可以一起運作。 本文說明如何在一個 Bot 中一起使用元件、瀑布式、自訂和調適型對話。

本文著重於可讓這些對話一起運作的程式碼。 如需詳細說明每種對話類型的文章,請參閱其他資訊

必要條件

用於將調適型對話新增至 Bot 的預備步驟

您必須遵循下面所述的步驟,將調適型對話新增至 Bot。 在如何使用調適型對話建立 Bot 中,將會更詳細地討論這些步驟。

  1. 將所有 Bot Builder NuGet 套件更新為 4.9.x 版。
  2. Microsoft.Bot.Builder.Dialogs.Adaptive 套件新增至 Bot 專案。
  3. 更新 Bot 配接器,將儲存體及使用者和交談狀態物件新增至每個回合內容。
  4. 在 Bot 程式碼中使用對話管理員,以啟動或繼續每個回合的根對話。

關於範例

藉由圖解,此範例會將各種對話類型結合在一個 Bot 中。 其不會示範設計交談流程的最佳做法。 此範例:

  • 定義自訂「填槽」對話類別。
  • 建立根元件對話:
    • 瀑布式對話會管理最上層的交談流程。
    • 調適型對話會與 2 個自訂填槽對話一起管理交談流程的其餘部分。

對話流程

自訂填槽對話會接受一些屬性 (要填入的槽值)。 每個自訂對話都會提示輸入任何遺漏值,直到所有槽值都填入為止。 範例會將屬性「繫結」至調適型對話,讓調適型對話也可以填入槽值。

本文著重於各種對話類型一起運作的方式。 如需將 Bot 設定為使用調適型對話的相關資訊,請參閱如何使用調適型對話建立 bot。 如需使用調適型對話來收集使用者輸入的詳細資訊,請參閱關於調適型對話中的輸入

自訂填槽對話

自訂對話是任何從 SDK 中的其中一個對話類別衍生的對話,可覆寫一或多種基本對話方法:「開始對話」、「繼續對話」、「恢復對話」或「結束對話」。

當您建立填槽對話時,您會針對對話將會填入的「槽值」提供一些定義。 此對話會覆寫開始、繼續和恢復對話方法,以反覆提示使用者填入每個槽值。 當所有槽值都已填入時,對話就會結束並傳回所收集的資訊。

每個槽值定義都包含用來收集資訊的對話提示名稱。

根對話會建立 2 個填槽對話,一個用來收集使用者的完整名稱,另一個用來收集其位址。 其也會建立這兩個對話用於填入其槽值的「文字提示」。

Dialogs\SlotDetails.cs

SlotDetails 類別描述要收集的資訊,以及用來收集資訊的提示。

/// <summary>
/// A list of SlotDetails defines the behavior of our SlotFillingDialog.
/// This class represents a description of a single 'slot'. It contains the name of the property we want to gather
/// and the id of the corresponding dialog that should be used to gather that property. The id is that used when the
/// dialog was added to the current DialogSet. Typically this id is that of a prompt but it could also be the id of
/// another slot dialog.
/// </summary>
public class SlotDetails
{
    public SlotDetails(string name, string dialogId, string prompt = null, string retryPrompt = null)
        : this(name, dialogId, new PromptOptions
        {
            Prompt = MessageFactory.Text(prompt),
            RetryPrompt = MessageFactory.Text(retryPrompt),
        })
    {
    }

    public SlotDetails(string name, string dialogId, PromptOptions options)
    {
        Name = name;
        DialogId = dialogId;
        Options = options;
    }

    public string Name { get; set; }

    public string DialogId { get; set; }

    public PromptOptions Options { get; set; }
}

Dialogs\SlotFillingDialog.cs

SlotFillingDialog 類別衍生自基底 Dialog 類別。

其會追蹤所收集的值、最後提示輸入的槽值,以及要填入的槽值詳細資料。

// Custom dialogs might define their own custom state.
// Similarly to the Waterfall dialog we will have a set of values in the ConversationState. However, rather than persisting
// an index we will persist the last property we prompted for. This way when we resume this code following a prompt we will
// have remembered what property we were filling.
private const string SlotName = "slot";
private const string PersistedValues = "values";

// The list of slots defines the properties to collect and the dialogs to use to collect them.
private readonly List<SlotDetails> _slots;

public SlotFillingDialog(string dialogId, List<SlotDetails> slots)
    : base(dialogId)
{
    _slots = slots ?? throw new ArgumentNullException(nameof(slots));
}

用於收集遺漏資訊的核心邏輯位於 RunPromptAsync 協助程式方法中。 收集所有資訊後,就會結束對話並傳回資訊。

/// <summary>
/// This helper function contains the core logic of this dialog. The main idea is to compare the state we have gathered with the
/// list of slots we have been asked to fill. When we find an empty slot we execute the corresponding prompt.
/// </summary>
/// <param name="dialogContext">A handle on the runtime.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A DialogTurnResult indicating the state of this dialog to the caller.</returns>
private Task<DialogTurnResult> RunPromptAsync(DialogContext dialogContext, CancellationToken cancellationToken)
{
    var state = GetPersistedValues(dialogContext.ActiveDialog);

    // Run through the list of slots until we find one that hasn't been filled yet.
    var unfilledSlot = _slots.FirstOrDefault((item) => !state.ContainsKey(item.Name));

    // If we have an unfilled slot we will try to fill it
    if (unfilledSlot != null)
    {
        // The name of the slot we will be prompting to fill.
        dialogContext.ActiveDialog.State[SlotName] = unfilledSlot.Name;

        // If the slot contains prompt text create the PromptOptions.

        // Run the child dialog
        return dialogContext.BeginDialogAsync(unfilledSlot.DialogId, unfilledSlot.Options, cancellationToken);
    }
    else
    {
        // No more slots to fill so end the dialog.
        return dialogContext.EndDialogAsync(state);
    }
}

如需有關實作自訂對話的詳細資訊,請參閱如何處理使用者中斷中關於 cancel 和 help 對話的討論。

根元件對話

根對話會:

  • 針對本身、2 個填槽對話及調適型對話,定義所有要填入的槽值。
  • 建立使用者狀態屬性存取子,以儲存所收集的資訊。
  • 建立調適型對話、2 個填槽對話、瀑布式對話,以及要搭配瀑布式和填槽對話使用的提示。
  • 將瀑布式對話設定為第一次啟動元件時要執行的初始對話。

瀑布將彙總所有收集的資訊並儲存其使用者狀態。

下列各節會說明瀑布式和調適型對話。

Dialogs\RootDialog.cs

RootDialog 類別是 ComponentDialog。 其會定義使用者狀態屬性,以儲存所收集的資訊。

public class RootDialog : ComponentDialog
{
    private IStatePropertyAccessor<JObject> _userStateAccessor;

    public RootDialog(UserState userState)
        : base("root")
    {
        _userStateAccessor = userState.CreateProperty<JObject>("result");

其建構函式會建立其所需的所有對話方塊,包括調適型對話方塊 adaptiveSlotFillingDialog

然後會將所有對話方塊新增至其對話方塊集。

    // Add the various dialogs that will be used to the DialogSet.
    AddDialog(new SlotFillingDialog("address", address_slots));
    AddDialog(new SlotFillingDialog("fullname", fullname_slots));
    AddDialog(new TextPrompt("text"));
    AddDialog(new NumberPrompt<int>("number", defaultLocale: Culture.English));
    AddDialog(new NumberPrompt<float>("shoesize", ShoeSizeAsync, defaultLocale: Culture.English));

    // We will instead have adaptive dialog do the slot filling by invoking the custom dialog
    // AddDialog(new SlotFillingDialog("slot-dialog", slots));

    // Add adaptive dialog
    AddDialog(adaptiveSlotFillingDialog);

    // Defines a simple two step Waterfall to test the slot dialog.
    AddDialog(new WaterfallDialog("waterfall", new WaterfallStep[] { StartDialogAsync, DoAdaptiveDialog, ProcessResultsAsync }));

    // The initial child Dialog to run.
    InitialDialogId = "waterfall";
}

瀑布式對話

瀑布式對話包含 3 個步驟:

  1. 啟動 "fullname" 填槽對話,其會收集並傳回使用者的完整名稱。
  2. 記錄使用者的名稱並開始調適型對話,這將會收集使用者的其餘資訊。
  3. 將使用者的資訊寫入使用者狀態屬性存取子,並摘要說明使用者所收集的資訊。

Dialogs\RootDialog.cs

private async Task<DialogTurnResult> StartDialogAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // Start the child dialog. This will just get the user's first and last name.
    return await stepContext.BeginDialogAsync("fullname", null, cancellationToken);
}

private async Task<DialogTurnResult> DoAdaptiveDialog(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    object adaptiveOptions = null;
    if (stepContext.Result is IDictionary<string, object> result && result.Count > 0)
    {
        adaptiveOptions = new { fullname = result };
    }
    // begin the adaptive dialog. This in-turn will get user's age, shoe-size using adaptive inputs and subsequently
    // call the custom slot filling dialog to fill user address.
    return await stepContext.BeginDialogAsync(nameof(AdaptiveDialog), adaptiveOptions, cancellationToken);
}

private async Task<DialogTurnResult> ProcessResultsAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // To demonstrate that the slot dialog collected all the properties we will echo them back to the user.
    if (stepContext.Result is IDictionary<string, object> result && result.Count > 0)
    {
        // Now the waterfall is complete, save the data we have gathered into UserState.
        // This includes data returned by the adaptive dialog.
        var obj = await _userStateAccessor.GetAsync(stepContext.Context, () => new JObject());
        obj["data"] = new JObject
        {
            { "fullname",  $"{result["fullname"]}" },
            { "shoesize", $"{result["shoesize"]}" },
            { "address", $"{result["address"]}" },
        };

        await stepContext.Context.SendActivityAsync(MessageFactory.Text(obj["data"]["fullname"].Value<string>()), cancellationToken);
        await stepContext.Context.SendActivityAsync(MessageFactory.Text(obj["data"]["shoesize"].Value<string>()), cancellationToken);
        await stepContext.Context.SendActivityAsync(MessageFactory.Text(obj["data"]["address"].Value<string>()), cancellationToken);
    }

    // Remember to call EndAsync to indicate to the runtime that this is the end of our waterfall.
    return await stepContext.EndDialogAsync();
}

調適型對話

調適型對話會定義一個在對話開始時執行的觸發程序。 此觸發程序將會執行下列動作:

  1. 使用輸入對話來詢問使用者的年齡。
  2. 使用輸入對話來詢問使用者的鞋子尺寸。
  3. 開始 "address" 填槽對話,以收集使用者的地址。
  4. 設定觸發程序的結果值並結束。

由於沒有其他動作要排入佇列,調適型對話也將結束並傳回此結果值。

調適型對話會使用語言產生器來格式化文字,並包含來自 Bot 和對話狀態的值。 (如需詳細資訊,請參閱如何在調適型對話中使用產生器。)

Dialogs\RootDialog.cs

// define adaptive dialog
var adaptiveSlotFillingDialog = new AdaptiveDialog();
adaptiveSlotFillingDialog.Id = nameof(AdaptiveDialog);

// Set a language generator
// You can see other adaptive dialog samples to learn how to externalize generation resources into .lg files.
adaptiveSlotFillingDialog.Generator = new TemplateEngineLanguageGenerator();

// add set of actions to perform when the adaptive dialog begins.
adaptiveSlotFillingDialog.Triggers.Add(new OnBeginDialog()
{
    Actions = new List<Dialog>()
    {
        // any options passed into adaptive dialog is automatically available under dialog.xxx
        // get user age
        new NumberInput()
        {
            Property = "dialog.userage",

            // use information passed in to the adaptive dialog.
            // See https://aka.ms/language-generation to learn more.
            Prompt = new ActivityTemplate("Hello ${dialog.fullname.first}, what is your age?"),
            Validations = new List<BoolExpression>()
            {
                // Conditions are written using Adaptive Expressions.
                // See https://aka.ms/adaptive-expressions to learn more.
                "int(this.value) >= 1",
                "int(this.value) <= 150"
            },
            InvalidPrompt = new ActivityTemplate("Sorry, ${this.value} does not work. Looking for age to be between 1-150. What is your age?"),
            UnrecognizedPrompt = new ActivityTemplate("Sorry, I did not understand ${this.value}. What is your age?"),
            MaxTurnCount = 3,
            DefaultValue = "=30",
            DefaultValueResponse = new ActivityTemplate("Sorry, this is not working. For now, I'm setting your age to ${this.defaultValue}"),
            AllowInterruptions = false
        },
        new NumberInput()
        {
            Property = "dialog.shoesize",
            Prompt = new ActivityTemplate("Please enter your shoe size."),
            InvalidPrompt = new ActivityTemplate("Sorry ${this.value} does not work. You must enter a size between 0 and 16. Half sizes are acceptable."),
            Validations = new List<BoolExpression>()
            {
                // size can only between 0-16
                "int(this.value) >= 0 && int(this.value) <= 16",
                // can only full or half size
                "isMatch(string(this.value), '^[0-9]+(\\.5)*$')"
            },
            AllowInterruptions = false
        },
        // get address - adaptive is calling the custom slot filling dialog here.
        new BeginDialog()
        {
            Dialog = "address",
            ResultProperty = "dialog.address"
        },
        // return everything under dialog scope. 
        new EndDialog()
        {
            Value = "=dialog"
        }
    }
}) ;

測試 Bot

  1. 如果您尚未安裝 Bot Framework Emulator,請進行安裝。
  2. 在您的電腦本機執行範例。
  3. 啟動模擬器、連線到您的 bot,並回應提示:名字和姓氏、鞋大小、街道、城市和郵遞區號。
  4. Bot 會顯示其所收集的資訊。
  5. 將任何訊息傳送給 Bot,再次啟動程序。

其他資訊

如需如何使用每種對話類型的詳細資訊,請參閱:

對話類型 發行項
調適型和輸入對話 使用調適型對話建立 Bot
元件對話 管理對話方塊複雜度
自訂對話 處理使用者中斷
瀑布式和提示對話 實作循序對話流程