您现在访问的是微软AZURE全球版技术文档网站,若需要访问由世纪互联运营的MICROSOFT AZURE中国区技术文档网站,请访问 https://docs.azure.cn.

Bot Framework SDK for .NET 中的对话框Dialogs in the Bot Framework SDK for .NET

备注

本主题适用于 SDK v3 版本。This topic applies to SDK v3 release. 可以在此处找到最新版 SDK (v4) 的文档。You can find the documentation for the latest version of the SDK v4 here.

使用 Bot Framework SDK for .NET 创建机器人时,可以使用对话框为聊天建模并管理聊天流When you create a bot using the Bot Framework SDK for .NET, you can use dialogs to model a conversation and manage conversation flow. 每个对话框都是一个抽象,在实现 IDialog 的 C# 类中封装自己的状态。Each dialog is an abstraction that encapsulates its own state in a C# class that implements IDialog. 对话框可以由其他对话框组成以最大限度地重用,并且对话框上下文可以维护在任何时间点都在会话中处于活动状态的对话框堆栈A dialog can be composed with other dialogs to maximize reuse, and a dialog context maintains the stack of dialogs that are active in the conversation at any point in time.

包含对话框的会话可以跨计算机移植,这使得机器人实现可以扩展。A conversation that comprises dialogs is portable across computers, which makes it possible for your bot implementation to scale. 在 Bot Framework SDK for .NET 中使用对话框时,聊天状态(对话框堆栈和堆栈中每个对话框的状态)会自动存储到所选择的状态数据存储中。When you use dialogs in the Bot Framework SDK for .NET, conversation state (the dialog stack and the state of each dialog in the stack) is automatically stored to your choice of state data storage. 这让机器人的服务代码可以是无状态,类似于不需要在 Web 服务器内存中存储会话状态的 Web 应用程序。This enables your bot's service code to be stateless, much like a web application that does not need to store session state in web server memory.

回显机器人示例Echo bot example

请考虑以下回显机器人示例,该示例介绍如何更改在快速入门教程中创建的机器人,以便它使用对话框与用户交换消息。Consider this echo bot example, which describes how to change the bot that's created in the Quickstart tutorial so that it uses dialogs to exchange messages with the user.

提示

若要按照此示例,使用快速入门教程中的说明创建机器人,然后更新其 MessagesController.cs 文件,如下所述。To follow along with this example, use the instructions in the Quickstart tutorial to create a bot, and then update its MessagesController.cs file as described below.

MessagesController.csMessagesController.cs

在 Bot Framework SDK for .NET 中,可以使用生成器库实现对话。In the Bot Framework SDK for .NET, the Builder library enables you to implement dialogs. 若要访问相关类,导入 Dialogs 命名空间。To access the relevant classes, import the Dialogs namespace.

using Microsoft.Bot.Builder.Dialogs;

接下来,将此 EchoDialog 类添加到 MessagesController.cs 来表示会话。Next, add this EchoDialog class to MessagesController.cs to represent the conversation.

[Serializable]
public class EchoDialog : IDialog<object>
{
    public async Task StartAsync(IDialogContext context)
    {
        context.Wait(MessageReceivedAsync);
    }

    public async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument)
    {
        var message = await argument;
        await context.PostAsync("You said: " + message.Text);
        context.Wait(MessageReceivedAsync);
    }
}

然后,通过调用 Conversation.SendAsync 方法将 EchoDialog 类绑定到 Post 方法。Then, wire the EchoDialog class to the Post method by calling the Conversation.SendAsync method.

public virtual async Task<HttpResponseMessage> Post([FromBody] Activity activity)
{
    // Check if activity is of type message
    if (activity != null && activity.GetActivityType() == ActivityTypes.Message)
    {
        await Conversation.SendAsync(activity, () => new EchoDialog());
    }
    else
    {
        HandleSystemMessage(activity);
    }
    return new HttpResponseMessage(System.Net.HttpStatusCode.Accepted);
}

实现详细信息Implementation details

Post 方法标记为 async,因为 Bot Builder 使用 C# 设施来处理异步通信。The Post method is marked async because Bot Builder uses the C# facilities for handling asynchronous communication. 它将返回 Task 对象,代表负责向传入消息发送答复的任务。It returns a Task object, which represents the task that is responsible for sending replies to the passed-in message. 如果出现异常,该方法返回的 Task 将包含异常信息。If there is an exception, the Task that is returned by the method will contain the exception information.

Conversation.SendAsync 方法是使用 Bot Framework SDK for .NET 实现对话框的关键。The Conversation.SendAsync method is key to implementing dialogs with the Bot Framework SDK for .NET. 它遵循依赖关系反转原则并执行以下步骤:It follows the dependency inversion principle and performs these steps:

  1. 实例化所需组件Instantiates the required components
  2. IBotDataStore 反序列化会话状态(对话框堆栈和堆栈中的每个对话框状态)Deserializes the conversation state (the dialog stack and the state of each dialog in the stack) from IBotDataStore
  3. 恢复机器人在其中挂起的会话过程并等待消息Resumes the conversation process where the bot suspended and waits for a message
  4. 发送答复Sends the replies
  5. 序列化已更新的会话状态并将其保存回 IBotDataStoreSerializes the updated conversation state and saves it back to IBotDataStore

第一次启动会话时,对话框不包含状态,因此 Conversation.SendAsync 构造 EchoDialog 并调用其 StartAsync 方法。When the conversation first starts, the dialog does not contain state, so Conversation.SendAsync constructs EchoDialog and calls its StartAsync method. StartAsync 方法使用延续委托调用 IDialogContext.Wait 来指定在接收新消息时应调用的方法 (MessageReceivedAsync)。The StartAsync method calls IDialogContext.Wait with the continuation delegate to specify the method that should be called when a new message is received (MessageReceivedAsync).

MessageReceivedAsync 方法等待消息、发布响应并等待下一条消息。The MessageReceivedAsync method waits for a message, posts a response, and waits for the next message. 每次调用 IDialogContext.Wait,机器人就会进入挂起状态,并且可以在接收消息的任何计算机上重新启动。Every time IDialogContext.Wait is called, the bot enters a suspended state and can be restarted on any computer that receives the message.

使用上面的代码示例创建的机器人将答复用户发送的每条消息,只需在用户消息前面加前缀文本“您说:”进行响应。A bot that's created by using the code samples above will reply to each message that the user sends by simply echoing back the user's message prefixed with the text 'You said: '. 因为机器人使用对话框创建,所以它可以演变为支持更复杂的会话,而不必显式管理状态。Because the bot is created using dialogs, it can evolve to support more complex conversations without having to explicitly manage state.

带有状态的回显机器人示例Echo bot with state example

下一个示例基于上一个示例通过添加跟踪对话框状态功能构建而成。This next example builds upon the one above by adding the ability to track dialog state. 更新 EchoDialog 类时(如下面的代码示例所示),机器人将答复用户发送的每条消息,方法是通过在用户消息前面加数字 (count) 后跟文本“您说:”进行响应。When the EchoDialog class is updated as shown in the code sample below, the bot will reply to each message that the user sends by echoing back the user's message prefixed with a number (count) followed by the text 'You said: '. 机器人将继续通过每个答复递增 count,直到用户选择重置计数。The bot will continue to increment count with each reply, until the user elects to reset the count.

MessagesController.csMessagesController.cs

[Serializable]
public class EchoDialog : IDialog<object>
{
    protected int count = 1;

    public async Task StartAsync(IDialogContext context)
    {
        context.Wait(MessageReceivedAsync);
    }

    public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument)
    {
        var message = await argument;
        if (message.Text == "reset")
        {
            PromptDialog.Confirm(
                context,
                AfterResetAsync,
                "Are you sure you want to reset the count?",
                "Didn't get that!",
                promptStyle: PromptStyle.None);
        }
        else
        {
            await context.PostAsync($"{this.count++}: You said {message.Text}");
            context.Wait(MessageReceivedAsync);
        }
    }

    public async Task AfterResetAsync(IDialogContext context, IAwaitable<bool> argument)
    {
        var confirm = await argument;
        if (confirm)
        {
            this.count = 1;
            await context.PostAsync("Reset count.");
        }
        else
        {
            await context.PostAsync("Did not reset count.");
        }
        context.Wait(MessageReceivedAsync);
    }
}

实现详细信息Implementation details

在第一个示例中,收到新消息时调用 MessageReceivedAsync 方法。As in the first example, the MessageReceivedAsync method is called when a new message is received. 不过,这次 MessageReceivedAsync 方法会在响应之前评估用户消息。This time though, the MessageReceivedAsync method evaluates the user's message before responding. 如果用户消息为“重置”,内置 PromptDialog.Confirm 提示会生成一个子对话框,提示用户确认计数重置。If the user's message is "reset", the built-in PromptDialog.Confirm prompt spawns a sub-dialog that asks the user to confirm the count reset. 子对话框有其自己的私有状态,该状态不会影响父对话框状态。The sub-dialog has its own private state that does not interfere with the parent dialog's state. 当用户响应提示,子对话框的结果会传递给 AfterResetAsync 方法,该方法将消息发送给用户以指示是否已重置计数,然后在下一条消息中调用 IDialogContext.Wait 并延续回 MessageReceivedAsyncWhen the user responds to the prompt, the result of the sub-dialog is passed to the AfterResetAsync method, which sends a message to the user to indicate whether or not the count was reset and then calls IDialogContext.Wait with a continuation back to MessageReceivedAsync on the next message.

对话框上下文Dialog context

传递到每个对话框方法的 IDialogContext 接口提供对服务的访问权限,对话框需要这些服务来保存状态并与该通道进行通信。The IDialogContext interface that is passed into each dialog method provides access to the services that a dialog requires to save state and communicate with the channel. IDialogContext 接口包含三个接口:Internals.IBotDataInternals.IBotToUserInternals.IDialogStackThe IDialogContext interface comprises three interfaces: Internals.IBotData, Internals.IBotToUser, and Internals.IDialogStack.

Internals.IBotDataInternals.IBotData

Internals.IBotData 接口提供对每个用户、每个会话以及由 Connector 维护的私人会话状态数据的访问。The Internals.IBotData interface provides access to the per-user, per-conversation, and private conversation state data that's maintained by Connector. 每个用户状态数据可用于存储与特定会话无关的用户数据,而每个会话数据用于存储有关会话的常规数据,私人会话数据则用于存储与特定会话相关的用户数据。Per-user state data is useful for storing user data that is not related to a specific conversation, while per-conversation data is useful for storing general data about a conversation, and private conversation data is useful for storing user data that is related to a specific conversation.

Internals.IBotToUserInternals.IBotToUser

Internals.IBotToUser 提供方法向用户发送来自机器人的消息。Internals.IBotToUser provides methods to send a message from bot to user. 消息可能使用对 Web API 方法调用的响应以内联方式发送,或直接通过 Connector 客户端发送。Messages may be sent inline with the response to the web API method call or directly by using the Connector client. 通过对话框上下文发送和接收消息可确保 Internals.IBotData 状态通过 Connector 传递。Sending and receiving messages through the dialog context ensures that the Internals.IBotData state is passed through the Connector.

Internals.IDialogStackInternals.IDialogStack

Internals.IDialogStack 提供方法来管理对话框堆栈Internals.IDialogStack provides methods to manage the dialog stack. 大多数情况下会自动管理对话框堆栈。Most of the time, the dialog stack will automatically be managed for you. 但是,也有可能你希望显式管理堆栈。However, there may be cases where you want to explictly manage the stack. 例如,你可能想要调用子对话框并将其添加到对话框堆栈顶部,将当前对话框标记为“完成”(从而在对话框堆栈中将其删除,并将结果返回到堆栈中的先前对话框)、挂起当前对话框直到收到用户消息,甚至完全重置对话框堆栈。For example, you might want to call a child dialog and add it to the top of the dialog stack, mark the current dialog as complete (thereby removing it from the dialog stack and returning the result to the prior dialog in the stack), suspend the current dialog until a message from the user arrives, or even reset the dialog stack altogether.

序列化Serialization

对话堆栈和所有活动对话的状态都会序列化到每个用户、每个会话 IBotDataBagThe dialog stack and the state of all active dialogs are serialized to the per-user, per-conversation IBotDataBag. 序列化机器人在机器人发送到 Connector 和从中接收的消息中保持不变。The serialized blob is persisted in the messages that the bot sends to and receives from the Connector. 若要进行序列化,Dialog 类必须包含 [Serializable] 属性。To be serialized, a Dialog class must include the [Serializable] attribute. 生成器库中的所有 IDialog 实现都标记为可序列化。All IDialog implementations in the Builder library are marked as serializable.

Chain 方法提供在 LINQ 查询语法中可用的流畅对话框接口。The Chain methods provide a fluent interface to dialogs that is usable in LINQ query syntax. LINQ 查询语法的已编译形式通常使用匿名方法。The compiled form of LINQ query syntax often uses anonymous methods. 如果这些匿名方法不引用局部变量的环境,这些匿名方法就没有状态,并且完全可序列化。If these anonymous methods do not reference the environment of local variables, then these anonymous methods have no state and are trivially serializable. 但是,如果匿名方法捕获环境中的任何局部变量,生成的闭包对象(由编译器生成)并没有被标记为可序列化。However, if the anonymous method captures any local variable in the environment, the resulting closure object (generated by the compiler) is not marked as serializable. 在这种情况下,Bot Builder 会引发 ClosureCaptureException 来标识问题。In this situation, Bot Builder will throw a ClosureCaptureException to identify the issue.

为了使用反射来序列化未标记为可序列化的类,生成器库包含了一个基于反射的序列化代理,可以使用它来注册 AutofacTo use reflection to serialize classes that are not marked as serializable, the Builder library includes a reflection-based serialization surrogate that you can use to register with Autofac.

var builder = new ContainerBuilder();
builder.RegisterModule(new DialogModule());
builder.RegisterModule(new ReflectionSurrogateModule());

对话框链Dialog chains

虽然可以使用 IDialogStack.Call<R>IDialogStack.Done<R> 来显式管理活动对话堆栈,但也可以通过使用这些流畅的 Chain 方法隐式管理活动对话堆栈。While you can explicitly manage the stack of active dialogs by using IDialogStack.Call<R> and IDialogStack.Done<R>, you can also implicitly manage the stack of active dialogs by using these fluent Chain methods.

方法Method 类型Type 说明Notes
Chain.Select<T, R>Chain.Select<T, R> LINQLINQ 支持 LINQ 查询语法中的“select”和“let”。Supports "select" and "let" in LINQ query syntax.
Chain.SelectMany<T, C, R>Chain.SelectMany<T, C, R> LINQLINQ 支持 LINQ 查询语法中的连续“from”。Supports successive "from" in LINQ query syntax.
Chain.WhereChain.Where LINQLINQ 支持 LINQ 查询语法中的“where”。Supports "where" in LINQ query syntax.
Chain.FromChain.From ChainsChains 实例化一个新的对话框实例。Instantiates a new instance of a dialog.
Chain.ReturnChain.Return ChainsChains 向链返回常量值。Returns a constant value into the chain.
Chain.DoChain.Do ChainsChains 允许链中的副作用。Allows for side-effects within the chain.
Chain.ContinueWith<T, R>Chain.ContinueWith<T, R> ChainsChains 简单对话框链。Simple chaining of dialogs.
Chain.UnwrapChain.Unwrap ChainsChains 解包嵌套在对话框中的对话框。Unwrap a dialog nested in a dialog.
Chain.DefaultIfExceptionChain.DefaultIfException ChainsChains 吞并前面结果中的异常,并返回 default(T)。Swallows an exception from the previous result and returns default(T).
Chain.LoopChain.Loop 分支Branch 循环整个对话框链。Loops the entire chain of dialogs.
Chain.FoldChain.Fold 分支Branch 将对话框枚举中的结果折叠到单个结果中。Folds results from an enumeration of dialogs into a single result.
Chain.Switch<T, R>Chain.Switch<T, R> 分支Branch 支持分支到不同对话框链。Supports branching into different dialog chains.
Chain.PostToUserChain.PostToUser 消息Message 将消息发送给用户。Posts a message to the user.
Chain.WaitToBotChain.WaitToBot 消息Message 等待消息发送给机器人。Waits for a message to the bot.
Chain.PostToChainChain.PostToChain 消息Message 从用户消息开始一个链。Starts a chain with a message from the user.

示例Examples

LINQ 查询语法使用 Chain.Select<T, R> 方法。The LINQ query syntax uses the Chain.Select<T, R> method.

var query = from x in new PromptDialog.PromptString(Prompt, Prompt, attempts: 1)
            let w = new string(x.Reverse().ToArray())
            select w;

Chain.SelectMany<T, C, R> 方法。Or the Chain.SelectMany<T, C, R> method.

var query = from x in new PromptDialog.PromptString("p1", "p1", 1)
            from y in new PromptDialog.PromptString("p2", "p2", 1)
            select string.Join(" ", x, y);

Chain.PostToUser<T>Chain.WaitToBot<T> 方法将消息从机器人发送给用户,反之亦然。The Chain.PostToUser<T> and Chain.WaitToBot<T> methods post messages from the bot to the user and vice versa.

query = query.PostToUser();

Chain.Switch<T, R> 方法对会话对话框流进行分支。The Chain.Switch<T, R> method branches the conversation dialog flow.

var logic =
    toBot
    .Switch
    (
        new RegexCase<string>(new Regex("^hello"), (context, text) =>
        {
            return "world!";
        }),
        new Case<string, string>((txt) => txt == "world", (context, text) =>
        {
            return "!";
        }),
        new DefaultCase<string, string>((context, text) =>
        {
            return text;
        }
    )
);

如果 Chain.Switch<T, R> 返回嵌套的 IDialog<IDialog<T>>,则内部 IDialog<T> 可以使用 Chain.Unwrap<T> 解包。If Chain.Switch<T, R> returns a nested IDialog<IDialog<T>>, then the inner IDialog<T> can be unwrapped with Chain.Unwrap<T>. 这允许将会话分支到对话框链的不同路径,可能是不等长度。This allows branching conversations to different paths of chained dialogs, possibly of unequal length. 此示例演示了一个更完整的分支对话框,它用流畅的链样式编写,包含隐式堆栈管理。This example shows a more complete example of branching dialogs written in the fluent chain style with implicit stack management.

var joke = Chain
    .PostToChain()
    .Select(m => m.Text)
    .Switch
    (
        Chain.Case
        (
            new Regex("^chicken"),
            (context, text) =>
                Chain
                .Return("why did the chicken cross the road?")
                .PostToUser()
                .WaitToBot()
                .Select(ignoreUser => "to get to the other side")
        ),
        Chain.Default<string, IDialog<string>>(
            (context, text) =>
                Chain
                .Return("why don't you like chicken jokes?")
        )
    )
    .Unwrap()
    .PostToUser().
    Loop();

后续步骤Next steps

对话框管理机器人和用户之间的会话流。Dialogs manage conversation flow between a bot and a user. 对话框定义如何与用户进行交互。A dialog defines how to interact with a user. 机器人可以使用堆栈中组织的许多对话框来指导与用户的会话。A bot can use many dialogs organized in stacks to guide the conversation with the user. 在下节中,了解在堆栈中创建和删除对话框时,对话框堆栈如何增长和缩减。In the next section, see how the dialog stack grows and shrinks as you create and dismiss dialogs in the stack.