实现顺序聊天流

适用于:SDK v4

通过发布问题来收集信息是机器人与用户交互的主要方式之一。 对话库提供有用的内置功能(如 prompt 类),以轻松提问和验证答复,从而确保它与特定数据类型匹配或符合自定义验证规则 。

可以使用对话库来管理线性的和较复杂的对话流。 在线性的交互中,机器人将按固定的顺序运行一组步骤,直到对话完成。 当机器人需要从用户收集信息时,对话就非常有用。

本文介绍如何通过创建提示并从瀑布对话调用这些提示来实现线性的对话流。 有关如何在不使用对话库的情况下编写自己的提示的示例,请参阅创建自己的提示来收集用户输入一文。

注意

Bot Framework JavaScript、C# 和 Python SDK 将继续受支持,但 Java SDK 即将停用,最终长期支持将于 2023 年 11 月结束。

使用 Java SDK 构建的现有机器人将继续正常运行。

要生成新的机器人,请考虑使用 Microsoft Copilot Studio 并阅读选择正确的助理解决方案

有关详细信息,请参阅机器人构建的未来

先决条件

关于此示例

在多轮次提示示例中,我们将使用一个瀑布对话、几个提示和一个组件对话来创建线性的交互,向用户提出一系列问题。 代码使用对话来循环执行以下步骤:

步骤 提示类型
请求用户提供其交通方式 选项提示
请求用户输入其姓名 文本提示
询问用户是否愿意提供其年龄 确认提示
如果他们回答“是”,则请求他们提供年龄 附带验证的数字提示仅接受大于 0 且小于 150 的年龄
如果他们不使用 Microsoft Teams,则请求他们提供个人资料图片 带有验证的附件提示允许丢失附件
询问收集的信息是否“正确” 重用确认提示

最后,如果用户回答“是”,则显示收集的信息;否则,告知用户他们的信息不会保留。

创建主对话

若要使用对话,请安装 Microsoft.Bot.Builder.Dialogs NuGet 包。

机器人通过 UserProfileDialog 来与用户交互。 创建机器人的 DialogBot 类时,UserProfileDialog 将设置为其主对话。 然后,机器人使用 Run 帮助器方法来访问该对话。

C# 示例的类图。

Dialogs\UserProfileDialog.cs

首先创建派生自 ComponentDialog 类的 UserProfileDialog,这包括 7 个步骤。

UserProfileDialog 构造函数中创建瀑布步骤、提示和瀑布对话,然后将其添加到对话集。 提示需要位于使用这些提示的同一对话集中。

public UserProfileDialog(UserState userState)
    : base(nameof(UserProfileDialog))
{
    _userProfileAccessor = userState.CreateProperty<UserProfile>("UserProfile");

    // This array defines how the Waterfall will execute.
    var waterfallSteps = new WaterfallStep[]
    {
        TransportStepAsync,
        NameStepAsync,
        NameConfirmStepAsync,
        AgeStepAsync,
        PictureStepAsync,
        SummaryStepAsync,
        ConfirmStepAsync,
    };

    // Add named dialogs to the DialogSet. These names are saved in the dialog state.
    AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps));
    AddDialog(new TextPrompt(nameof(TextPrompt)));
    AddDialog(new NumberPrompt<int>(nameof(NumberPrompt<int>), AgePromptValidatorAsync));
    AddDialog(new ChoicePrompt(nameof(ChoicePrompt)));
    AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt)));
    AddDialog(new AttachmentPrompt(nameof(AttachmentPrompt), PicturePromptValidatorAsync));

    // The initial child Dialog to run.
    InitialDialogId = nameof(WaterfallDialog);
}

接下来,添加由对话用于提示输入的步骤。 若要使用提示,请从对话中的某个步骤调用它,然后使用 stepContext.Result 检索下一步骤中的提示结果。 在幕后,提示是由两个步骤组成的对话。 首先,提示要求提供输入。 然后它返回有效值,或者使用重新提示从头开始,直到收到有效的输入为止。

应始终从瀑布步骤返回非 null 的 DialogTurnResult。 否则,对话可能不按设计意图运行。 下面显示了瀑布对话中 NameStepAsync 的实现。

private static async Task<DialogTurnResult> NameStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    stepContext.Values["transport"] = ((FoundChoice)stepContext.Result).Value;

    return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = MessageFactory.Text("Please enter your name.") }, cancellationToken);
}

AgeStepAsync 中,指定当用户输入采用的是提示无法分析的格式,或者不符合验证条件,因而无法验证时,要使用的重试提示。 在这种情况下,如果未提供重试提示,则提示将使用初始提示文本来重新提示用户输入。

private async Task<DialogTurnResult> AgeStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    if ((bool)stepContext.Result)
    {
        // User said "yes" so we will be prompting for the age.
        // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
        var promptOptions = new PromptOptions
        {
            Prompt = MessageFactory.Text("Please enter your age."),
            RetryPrompt = MessageFactory.Text("The value entered must be greater than 0 and less than 150."),
        };

        return await stepContext.PromptAsync(nameof(NumberPrompt<int>), promptOptions, cancellationToken);
    }
    else
    {
        // User said "no" so we will skip the next step. Give -1 as the age.
        return await stepContext.NextAsync(-1, cancellationToken);
    }
}

UserProfile.cs

用户的交通方式、姓名和年龄将保存在 UserProfile 类的实例中。

public class UserProfile
{
    public string Transport { get; set; }

    public string Name { get; set; }

    public int Age { get; set; }

    public Attachment Picture { get; set; }
}

Dialogs\UserProfileDialog.cs

最后一个步骤检查前一瀑布步骤中调用的对话返回的 stepContext.Result。 如果返回值为 true,则个人资料访问器将获取并更新用户个人资料。 若要获取用户个人资料,请调用 GetAsync,然后设置 userProfile.TransportuserProfile.NameuserProfile.AgeuserProfile.Picture 属性的值。 最后,在调用用于结束对话的 EndDialogAsync 之前汇总用户的信息。 结束对话会从对话堆栈中弹出该对话,并将可选结果返回到该对话的父级。 父级是启动了刚刚结束的对话的对话或方法。

    else
    {
        msg += $" Your profile will not be kept.";
    }

    await stepContext.Context.SendActivityAsync(MessageFactory.Text(msg), cancellationToken);

    // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is the end.
    return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
}

private async Task<DialogTurnResult> SummaryStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    stepContext.Values["picture"] = ((IList<Attachment>)stepContext.Result)?.FirstOrDefault();

    // Get the current profile object from user state.
    var userProfile = await _userProfileAccessor.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);

    userProfile.Transport = (string)stepContext.Values["transport"];
    userProfile.Name = (string)stepContext.Values["name"];
    userProfile.Age = (int)stepContext.Values["age"];
    userProfile.Picture = (Attachment)stepContext.Values["picture"];

    var msg = $"I have your mode of transport as {userProfile.Transport} and your name as {userProfile.Name}";

    if (userProfile.Age != -1)
    {
        msg += $" and your age as {userProfile.Age}";
    }

    msg += ".";

    await stepContext.Context.SendActivityAsync(MessageFactory.Text(msg), cancellationToken);

    if (userProfile.Picture != null)
    {
        try
        {
            await stepContext.Context.SendActivityAsync(MessageFactory.Attachment(userProfile.Picture, "This is your profile picture."), cancellationToken);
        }
        catch
        {
            await stepContext.Context.SendActivityAsync(MessageFactory.Text("A profile picture was saved but could not be displayed here."), cancellationToken);

运行对话

Bots\DialogBot.cs

OnMessageActivityAsync 处理程序使用 RunAsync 方法来启动或继续对话。 OnTurnAsync 使用机器人的状态管理对象将所有状态更改保存到存储中。 ActivityHandler.OnTurnAsync 方法调用各种活动处理程序方法,例如 OnMessageActivityAsync。 这样,在消息处理程序完成之后、轮次本身完成之前将保存状态。

public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
{
    await base.OnTurnAsync(turnContext, cancellationToken);

    // Save any state changes that might have occurred during the turn.
    await ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
    await UserState.SaveChangesAsync(turnContext, false, cancellationToken);
}

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
    Logger.LogInformation("Running dialog with Message Activity.");

    // Run the Dialog with the new message Activity.
    await Dialog.RunAsync(turnContext, ConversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
}

注册机器人的服务

此机器人使用以下服务:

  • 机器人的基本服务:凭据提供程序、适配器和机器人实现。
  • 用于管理状态的服务:存储、用户状态和聊天状态。
  • 机器人使用的对话。

Startup.cs

Startup 中注册机器人的服务。 可以通过依赖项注入将这些服务用于其他代码部分。

{
    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient().AddControllers().AddNewtonsoftJson(options =>
        {
            options.SerializerSettings.MaxDepth = HttpHelper.BotMessageSerializerSettings.MaxDepth;
        });

        // Create the Bot Framework Authentication to be used with the Bot Adapter.
        services.AddSingleton<BotFrameworkAuthentication, ConfigurationBotFrameworkAuthentication>();

        // Create the Bot Adapter with error handling enabled.
        services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();

        // Create the storage we'll be using for User and Conversation state. (Memory is great for testing purposes.)
        services.AddSingleton<IStorage, MemoryStorage>();

        // Create the User state. (Used in this bot's Dialog implementation.)
        services.AddSingleton<UserState>();

        // Create the Conversation state. (Used by the Dialog system itself.)
        services.AddSingleton<ConversationState>();

注意

内存存储仅用于测试,不用于生产。 请务必对生产用机器人使用持久型存储。

测试机器人

  1. 安装 Bot Framework Emulator(如果尚未安装)。
  2. 在计算机本地运行示例。
  3. 按如下所示启动模拟器,连接到机器人,然后发送消息。

与多轮次提示机器人对话的示例脚本。

其他信息

关于对话和机器人状态

在此机器人中,定义了两个状态属性访问器:

  • 一个访问器是在对话状态属性的聊天状态中创建的。 对话状态跟踪用户在对话集的对话中所处的位置,由对话上下文更新,例如,在调用 begin dialog 或 continue dialog 方法时,会更新对话状态。
  • 一个访问器是在用户配置文件属性的用户状态中创建的。 机器人使用此访问器来跟踪有关用户的信息,必须在对话代码中显式管理此状态。

状态属性访问器的 getset 方法在状态管理对象的缓存中获取和设置属性值。 首次在某个轮次中请求状态属性的值时,将填充该缓存,但是,必须显式持久保存该值。 为了持久保存对这两个状态属性所做的更改,我们将调用相应状态管理对象的 save changes 方法。

此示例从对话内部更新用户配置文件状态。 这种做法适用于某些机器人;如果你想要在多个机器人中重复使用某个对话,则此做法不适用。

有多种选项可将对话步骤与机器人状态相分离。 例如,在对话收集完整信息后,你可以:

  • 使用 end dialog 方法将收集的数据作为返回值返回给父上下文。 此上下文可能是机器人的轮次处理程序,或对话堆栈中以前的某个活动对话,这就是提示类的设计方式。
  • 向相应的服务生成请求。 如果机器人充当较大服务的前端,此选项可能很适合。

提示验证程序方法的定义

UserProfileDialog.cs

下面是 AgePromptValidatorAsync 方法定义的验证程序代码示例。 promptContext.Recognized.Value 包含已解析的值,此值是一个整数,用于数值提示。 promptContext.Recognized.Succeeded 指示提示是否能够解析用户的输入。 验证程序应返回 false 以指示未接受该值,且提示对话应重新提示用户;否则,返回 true 以指示接受输入并从提示对话返回。 可以根据方案更改验证程序中的值。

    }

    // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
    return await stepContext.PromptAsync(nameof(ConfirmPrompt), new PromptOptions { Prompt = MessageFactory.Text("Is this ok?") }, cancellationToken);
}

后续步骤