Skip to main content

使用Dialog库控制聊天机器人的会话流程

分类:  Azure机器人 标签:  #Azure Bot Framework SDK #Azure Bot Service #机器人 发布于: 2023-08-07 22:57:01

我们之前学习过了如何进行状态管理,状态管理的最佳场景就是在和机器人会话的过程中保持参与对话双方的状态,这也利于我们设计更多的组件,并利用状态管理来维持组件的运行状态。我们本节就来介绍一下Bot Framework SDK提供的最为重要的流程管理概念Dialog。在了解Dialog系统之前,建议大家熟悉一下如下的文章:

当我们开发聊天机器人(无论是通过语音还是文字)时候,最重要的一点是机器人在理解了用户对话的主题之后,如何围绕主题对对话进行跟进。为了更真实的模拟真人对话,在很多场景下并不限制主题范围(当然我们也是可以限制的), 当用户突然改变话题,机器人需要保存当前话题的状态并进入下一个话题的,并在某些时候再恢复之前的话题并继续。举一个例子:用户在进行某项业务操作,例如购买商品,预订酒店等等动作,类似场景要给用户一个引导的过程,添加必要的小组件,例如各种卡片,方便用户进行操作,并管理用户的状态。完成这些场景我们无法通过在代码中一直使用if-else,我们可以利用Dialog组件处理这些场景,请记住所有的Dialog组件都是有状态的, 必须依赖状态管理。

一睹为快

为了更好的说明Dialog是干嘛的以及如何使用Dialog,我们先看一个快速的例子: 我在这个例子里是需要使用Dialog收集用户的名称,年纪两个信息,并展示给用户。

在开始之前,请先使用文档:https://www.azuredeveloper.cn/article/create-full-function-chat-bot-template, 创建一个机器人模板。创建好了之后,你会拥有一个目录CoreBot, 该目录里即是我们的项目模板。

在根目录CoreBot下运行命令:

dotnet add package Microsoft.Bot.Builder.Dialogs

添加一个新的软件包引用,该包主要定义Dialog库相关的类。

在根目录下创建一个新的目录: Dialogs, 然后在该目录创建一个文件MainDialog.cs, 内容如下:

using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder;
namespace CoreBot.Dialogs;
public class MainDialog: ComponentDialog
{
    public MainDialog() : base(nameof(MainDialog))
    {
        AddDialog(new TextPrompt("get-nick-name", NickNameNotNull));
        AddDialog(new NumberPrompt<int>("get-age"));

        AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]{
            GetNickName,
            GetAge,
            ShowNickNameAndAge,
        }));

        InitialDialogId = nameof(WaterfallDialog);
    }

    public async Task<DialogTurnResult> GetNickName(WaterfallStepContext waterfallStepContext, CancellationToken cancellationToken)
    {
        var options = new PromptOptions()
        {
            Prompt = MessageFactory.Text("请输入您的昵称:"),
        };

        return await waterfallStepContext.PromptAsync("get-nick-name", options, cancellationToken);
    }

    public async Task<DialogTurnResult> GetAge(WaterfallStepContext waterfallStepContext, CancellationToken cancellationToken)
    {
        var NickName  = waterfallStepContext.Result.ToString();
        waterfallStepContext.Values["NickName"] = NickName;

        var options = new PromptOptions() 
        {
            Prompt = MessageFactory.Text("请输入您的年龄:"),
        };

        return await waterfallStepContext.PromptAsync("get-age", options, cancellationToken);
    }

    public async Task<DialogTurnResult> ShowNickNameAndAge(WaterfallStepContext waterfallStepContext, CancellationToken cancellationToken)
    {
        var Age = waterfallStepContext.Result.ToString();
        var NickName = waterfallStepContext.Values["NickName"];

        var reply = waterfallStepContext.Context.Activity.CreateReply();
        reply.Text = $"您好,您输入的昵称是:{NickName}, 您输入的年龄是: {Age}";

        await waterfallStepContext.Context.SendActivityAsync(reply, cancellationToken);

        return await waterfallStepContext.EndDialogAsync(null, cancellationToken);
    }


    public async Task<bool> NickNameNotNull(PromptValidatorContext<string> promptContext, CancellationToken cancellationToken)
    {
        var NickName = promptContext.Recognized.Value;

        if ( !string.IsNullOrEmpty(NickName))
        {
            return await Task.FromResult<bool>(true);
        }
        else
        {
            return await Task.FromResult<bool>(false);
        }

    }
}

这个代码有点长,但是主要是分成如下几个部分:

  • 定义Dialog类,并且从ComponentDialog类继承: 每一个Dialog类都需要从CompontentDialog继承,当然你也可以从Dialog根类集成,但是这样就少了很多已经实现的方法,因此建议所有的DialogComponentDialog从这个类继承。这个类有一些已经实现了的方法,例如内置了一个数据集合,用于放置该Dialog拥有的多个Dialog变量。
  • 实现构造函数,在构造函数里添加需要用于展示的dialog类,例如本例中的TextPromtp类和NumberPrompt<int>类,以及我们经常使用的WaterfallDialog类,请记住WaterfallDialog是一个非常重要的流程类。
  • WaiterfallDialog类里定义会执行的方法,这些方法会被顺序执行。
  • 完善每一个在WaterfallDialog里定义的顺序执行的方法。

如上这四个部分是定义一个Dialog类全部的步骤,步骤很清晰,要做的就是填充代码,仅此而已。

定义了Dialog类之后,我们需要应用该类,首先要加入到自动注入依赖的容器里:请打开program.cs, 在builder.Services.AddTransient<IBot, TestBot>();之前添加一行: builder.Services.AddSingleton<MainDialog>();, 如下所示:

builder.Services.AddSingleton<MainDialog>();
builder.Services.AddTransient<IBot, TestBot>();

然后我们需要在TestBot.cs中使用这个Dialog类。

注意
我们前面讨论如果要保持状态,那么我们必须要添加状态管理的组件,这个部分我们之前一篇文章也详细的讨论过了,如果您还不熟悉,那么请再复习一遍之前讨论的状态管理的部分。

要在TestBot.cs中使用Dialog, 我们首先要注入状态管理的组件,主要是ConversationStateUserState的实例,同时为了同步每一个turn的结束后,turn的状态,并持久化,我们需要在每一个turn结束后保存状态。为了达到这些目的,我们给TestBot.cs添加如下的代码:

  • 添加两个字段,用于注入状态的引用。
  • 添加方法OnTurnAsync, 用于保存状态。
  • 构造函数中注入状态管理器和dialog的实例。
  • 定义对于Dialog的引用。
    private readonly Dialog _mainDialog;
    private readonly ConversationState _conversationState;
    private readonly UserState _userState;

    public TestBot(MainDialog mainDialog, ConversationState conversationState, UserState userState)
    {
        _mainDialog = mainDialog;
        _conversationState = conversationState;
        _userState = userState;
    }

    public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
    {
        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);
    }

最后我们需要启动Dialog, 为此我们需要更改方法OnMessageActivityAsync:

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
    await _mainDialog.RunAsync(turnContext, _conversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
}

注意要引用包:using Microsoft.Bot.Builder.Dialogs;

调用dialog的扩展方法RunAsync来启动一个Dialog

到这里你可以运行一下你的应用了:

dontnet run

按照之前的文章打开Bot Framework Emulator来访问一下地址,即可以看到对话框时如何运作的。


我们先看一个简单的例子,后面再详细的解释一下整个的代码架构和需要注意的点。