你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

管理长时间运行的操作

适用于: SDK v4

正确处理长时间运行的操作是判断机器人是否可靠的一个重要因素。 当 Azure AI 机器人服务从通道向机器人发送活动时,机器人应快速处理该活动。 如果机器人未在 10 到 15 秒内完成操作,则根据通道的不同,Azure AI 机器人服务将超时,并向客户端报告 504:GatewayTimeout,如机器人工作原理中所述。

本文介绍如何使用外部服务执行操作,并在完成后通知机器人。

先决条件

关于此示例

本文从多轮次提示示例机器人开始,并添加用于执行长时间运行操作的代码。 本文还演示如何在操作完成后响应用户。 在已更新的示例中:

  • 机器人会询问用户执行哪一长时间运行的操作。
  • 机器人从用户收到某个活动,并确定要执行的操作。
  • 机器人通知用户操作需要一些时间,并将操作发送到 C# 函数。
    • 机器人保存状态,表明有一个操作正在进行。
    • 当操作正在运行时,机器人会响应用户的消息,通知他们操作仍在进行。
    • Azure Functions 管理长时间运行的操作,并向机器人发送 event 活动,通知其操作已完成。
  • 机器人会恢复对话并发送一条主动消息,通知用户操作已完成。 然后,机器人会清除前面提到的操作状态。

此示例定义了 LongOperationPrompt 类,其派生自 ActivityPrompt 抽象类。 当 LongOperationPrompt 将要处理的活动放入队列时,它在该活动的“值”属性内包含用户的一个选择。 然后,此活动由 Azure Functions 使用、修改并包装在不同的 event 活动中,然后使用 Direct Line 客户端将其发送回机器人。 在机器人内,事件活动用于通过调用适配器的“继续聊天”方法来恢复对话。 然后,对话堆栈加载,并且 LongOperationPrompt 完成。

本文涉及许多不同的技术。 有关相关文章的链接,请参阅附加信息部分。

重要

本文包含在配置文件中使用连接字符串的旧代码示例,用于实现与存储的内部连接。 Microsoft建议使用可用的最安全的身份验证流。 如果要连接到 Azure 资源,建议使用 Azure 资源的托管标识来进行身份验证。

创建 Azure 存储帐户

创建 Azure 存储帐户,并检索连接字符串。 需要将连接字符串添加到机器人的配置文件中。

有关详细信息,请参阅创建存储帐户从 Azure 门户复制凭据

创建机器人资源

  1. 设置开发隧道并检索一个 URL,在本地调试期间用作机器人的消息传送终结点。 消息传送终结点将是附加了 /api/messages/ 的 HTTPS 转发 URL,新机器人的默认端口为 3978。

    有关详细信息,请参阅如何使用 devtunnel 调试机器人

  2. 在 Azure 门户中或使用 Azure CLI 创建 Azure 机器人资源。 将机器人的消息传送终结点设置为使用开发隧道创建的终结点。 创建机器人资源后,获取机器人的 Microsoft 应用 ID 和密码。 启用 Direct Line 通道,并检索 Direct Line 机密。 将这些添加到机器人代码和 C# 函数中。

    有关详细信息,请参阅如何管理机器人和如何将机器人连接到 Direct Line

创建 C# 函数

  1. 基于 .NET Core 运行时堆栈创建 Azure Functions 应用。

    有关详细信息,请参阅如何创建函数应用Azure Functions C# 脚本参考

  2. DirectLineSecret 应用程序设置添加到函数应用。

    有关详细信息,请参阅如何管理函数应用

  3. 在函数应用中,基于 Azure 队列存储模板添加函数。

    设置所需的队列名称,然后选择在先前步骤中创建的 Azure Storage Account。 此队列名称还将放在机器人的“appsettings.json”文件中。

  4. 将“function.proj”文件添加到函数。

    <Project Sdk="Microsoft.NET.Sdk">
        <PropertyGroup>
            <TargetFramework>netstandard2.0</TargetFramework>
        </PropertyGroup>
    
        <ItemGroup>
            <PackageReference Include="Microsoft.Bot.Connector.DirectLine" Version="3.0.2" />
            <PackageReference Include="Microsoft.Rest.ClientRuntime" Version="2.3.4" />
        </ItemGroup>
    </Project>
    
  5. 将“run.csx”更新为以下代码:

    #r "Newtonsoft.Json"
    
    using System;
    using System.Net.Http;
    using System.Text;
    using Newtonsoft.Json;
    using Microsoft.Bot.Connector.DirectLine;
    using System.Threading;
    
    public static async Task Run(string queueItem, ILogger log)
    {
        log.LogInformation($"C# Queue trigger function processing");
    
        JsonSerializerSettings jsonSettings = new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore };
        var originalActivity =  JsonConvert.DeserializeObject<Activity>(queueItem, jsonSettings);
        // Perform long operation here....
        System.Threading.Thread.Sleep(TimeSpan.FromSeconds(15));
    
        if(originalActivity.Value.ToString().Equals("option 1", StringComparison.OrdinalIgnoreCase))
        {
            originalActivity.Value = " (Result for long operation one!)";
        }
        else if(originalActivity.Value.ToString().Equals("option 2", StringComparison.OrdinalIgnoreCase))
        {
            originalActivity.Value = " (A different result for operation two!)";
        }
    
        originalActivity.Value = "LongOperationComplete:" + originalActivity.Value;
        var responseActivity =  new Activity("event");
        responseActivity.Value = originalActivity;
        responseActivity.Name = "LongOperationResponse";
        responseActivity.From = new ChannelAccount("GenerateReport", "AzureFunction");
    
        var directLineSecret = Environment.GetEnvironmentVariable("DirectLineSecret");
        using(DirectLineClient client = new DirectLineClient(directLineSecret))
        {
            var conversation = await client.Conversations.StartConversationAsync();
            await client.Conversations.PostActivityAsync(conversation.ConversationId, responseActivity);
        }
    
        log.LogInformation($"Done...");
    }
    

创建机器人

  1. 从 C# 多轮次提示示例的副本开始。

  2. 将“Azure.Storage.Queues”NuGet 包添加到项目。

  3. 将之前创建的 Azure 存储帐户的连接字符串和存储队列名称添加到机器人的配置文件。

    确保队列名称与之前用于创建队列触发器函数的名称相同。 还要添加之前创建 Azure 机器人资源时生成的 MicrosoftAppIdMicrosoftAppPassword 属性的值。

    appsettings.json

    {
      "MicrosoftAppId": "<your-bot-app-id>",
      "MicrosoftAppPassword": "<your-bot-app-password>",
      "StorageQueueName": "<your-azure-storage-queue-name>",
      "QueueStorageConnection": "<your-storage-connection-string>"
    }
    
  4. IConfiguration 参数添加到“DialogBot.cs”,以检索 。 另外,从 Azure 函数为 OnEventActivityAsync 添加 LongOperationResponse 处理程序。

    Bots\DialogBot.cs

    protected readonly IStatePropertyAccessor<DialogState> DialogState;
    protected readonly Dialog Dialog;
    protected readonly BotState ConversationState;
    protected readonly ILogger Logger;
    private readonly string _botId;
    
    /// <summary>
    /// Create an instance of <see cref="DialogBot{T}"/>.
    /// </summary>
    /// <param name="configuration"><see cref="IConfiguration"/> used to retrieve MicrosoftAppId
    /// which is used in ContinueConversationAsync.</param>
    /// <param name="conversationState"><see cref="ConversationState"/> used to store the DialogStack.</param>
    /// <param name="dialog">The RootDialog for this bot.</param>
    /// <param name="logger"><see cref="ILogger"/> to use.</param>
    public DialogBot(IConfiguration configuration, ConversationState conversationState, T dialog, ILogger<DialogBot<T>> logger)
    {
        _botId = configuration["MicrosoftAppId"] ?? Guid.NewGuid().ToString();
        ConversationState = conversationState;
        Dialog = dialog;
        Logger = logger;
        DialogState = ConversationState.CreateProperty<DialogState>(nameof(DialogState));
    }
    
    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);
    }
    
    protected override async Task OnEventActivityAsync(ITurnContext<IEventActivity> turnContext, CancellationToken cancellationToken)
    {
        // The event from the Azure Function will have a name of 'LongOperationResponse'
        if (turnContext.Activity.ChannelId == Channels.Directline && turnContext.Activity.Name == "LongOperationResponse")
        {
            // The response will have the original conversation reference activity in the .Value
            // This original activity was sent to the Azure Function via Azure.Storage.Queues in AzureQueuesService.cs.
            var continueConversationActivity = (turnContext.Activity.Value as JObject)?.ToObject<Activity>();
            await turnContext.Adapter.ContinueConversationAsync(_botId, continueConversationActivity.GetConversationReference(), async (context, cancellation) =>
            {
                Logger.LogInformation("Running dialog with Activity from LongOperationResponse.");
    
                // ContinueConversationAsync resets the .Value of the event being continued to Null, 
                //so change it back before running the dialog stack. (The .Value contains the response 
                //from the Azure Function)
                context.Activity.Value = continueConversationActivity.Value;
                await Dialog.RunAsync(context, DialogState, cancellationToken);
    
                // Save any state changes that might have occurred during the inner turn.
                await ConversationState.SaveChangesAsync(context, false, cancellationToken);
            }, cancellationToken);
        }
        else
        {
            await base.OnEventActivityAsync(turnContext, cancellationToken);
        }
    }
    
  5. 创建 Azure 队列服务,将要处理的活动排队。

    AzureQueuesService.cs

    /// <summary>
    /// Service used to queue messages to an Azure.Storage.Queues.
    /// </summary>
    public class AzureQueuesService
    {
        private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings()
            {
                Formatting = Formatting.Indented,
                NullValueHandling = NullValueHandling.Ignore
            };
    
        private bool _createQueuIfNotExists = true;
        private readonly QueueClient _queueClient;
    
        /// <summary>
        /// Creates a new instance of <see cref="AzureQueuesService"/>.
        /// </summary>
        /// <param name="config"><see cref="IConfiguration"/> used to retrieve
        /// StorageQueueName and QueueStorageConnection from appsettings.json.</param>
        public AzureQueuesService(IConfiguration config)
        {
            var queueName = config["StorageQueueName"];
            var connectionString = config["QueueStorageConnection"];
    
            _queueClient = new QueueClient(connectionString, queueName);
        }
    
        /// <summary>
        /// Queue and Activity, with option in the Activity.Value to Azure.Storage.Queues
        ///
        /// <seealso cref="https://github.com/microsoft/botbuilder-dotnet/blob/master/libraries/Microsoft.Bot.Builder.Azure/Queues/ContinueConversationLater.cs"/>
        /// </summary>
        /// <param name="referenceActivity">Activity to queue after a call to GetContinuationActivity.</param>
        /// <param name="option">The option the user chose, which will be passed within the .Value of the activity queued.</param>
        /// <param name="cancellationToken">Cancellation token for the async operation.</param>
        /// <returns>Queued <see cref="Azure.Storage.Queues.Models.SendReceipt.MessageId"/>.</returns>
        public async Task<string> QueueActivityToProcess(Activity referenceActivity, string option, CancellationToken cancellationToken)
        {
            if (_createQueuIfNotExists)
            {
                _createQueuIfNotExists = false;
                await _queueClient.CreateIfNotExistsAsync().ConfigureAwait(false);
            }
    
            // create ContinuationActivity from the conversation reference.
            var activity = referenceActivity.GetConversationReference().GetContinuationActivity();
            // Pass the user's choice in the .Value
            activity.Value = option;
    
            var message = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(activity, jsonSettings)));
    
            // Aend ResumeConversation event, it will get posted back to us with a specific value, giving us 
            // the ability to process it and do the right thing.
            var reciept = await _queueClient.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
            return reciept.Value.MessageId;
        }
    }
    

对话框

删除旧对话框,并将其替换为新对话框以支持操作。

  1. 删除“UserProfileDialog.cs”文件。

  2. 添加一个自定义提示对话框,询问用户要执行的操作。

    Dialogs\LongOperationPrompt.cs

    /// <summary>
    /// <see cref="ActivityPrompt"/> implementation which will queue an activity,
    /// along with the <see cref="LongOperationPromptOptions.LongOperationOption"/>,
    /// and wait for an <see cref="ActivityTypes.Event"/> with name of "ContinueConversation"
    /// and Value containing the text: "LongOperationComplete".
    ///
    /// The result of this prompt will be the received Event Activity, which is sent by
    /// the Azure Function after it finishes the long operation.
    /// </summary>
    public class LongOperationPrompt : ActivityPrompt
    {
        private readonly AzureQueuesService _queueService;
    
        /// <summary>
        /// Create a new instance of <see cref="LongOperationPrompt"/>.
        /// </summary>
        /// <param name="dialogId">Id of this <see cref="LongOperationPrompt"/>.</param>
        /// <param name="validator">Validator to use for this prompt.</param>
        /// <param name="queueService"><see cref="AzureQueuesService"/> to use for Enqueuing the activity to process.</param>
        public LongOperationPrompt(string dialogId, PromptValidator<Activity> validator, AzureQueuesService queueService) 
            : base(dialogId, validator)
        {
            _queueService = queueService;
        }
    
        public async override Task<DialogTurnResult> BeginDialogAsync(DialogContext dc, object options, CancellationToken cancellationToken = default)
        {
            // When the dialog begins, queue the option chosen within the Activity queued.
            await _queueService.QueueActivityToProcess(dc.Context.Activity, (options as LongOperationPromptOptions).LongOperationOption, cancellationToken);
    
            return await base.BeginDialogAsync(dc, options, cancellationToken);
        }
    
        protected override Task<PromptRecognizerResult<Activity>> OnRecognizeAsync(ITurnContext turnContext, IDictionary<string, object> state, PromptOptions options, CancellationToken cancellationToken = default)
        {
            var result = new PromptRecognizerResult<Activity>() { Succeeded = false };
    
            if(turnContext.Activity.Type == ActivityTypes.Event
                && turnContext.Activity.Name == "ContinueConversation"
                && turnContext.Activity.Value != null
                // Custom validation within LongOperationPrompt.  
                // 'LongOperationComplete' is added to the Activity.Value in the Queue consumer (See: Azure Function)
                && turnContext.Activity.Value.ToString().Contains("LongOperationComplete", System.StringComparison.InvariantCultureIgnoreCase))
            {
                result.Succeeded = true;
                result.Value = turnContext.Activity;
            }
    
            return Task.FromResult(result);
        }
    }
    
  3. 为自定义提示添加提示选项类。

    Dialogs\LongOperationPromptOptions.cs

    /// <summary>
    /// Options sent to <see cref="LongOperationPrompt"/> demonstrating how a value
    /// can be passed along with the queued activity.
    /// </summary>
    public class LongOperationPromptOptions : PromptOptions
    {
        /// <summary>
        /// This is a property sent through the Queue, and is used
        /// in the queue consumer (the Azure Function) to differentiate 
        /// between long operations chosen by the user.
        /// </summary>
        public string LongOperationOption { get; set; }
    }
    
  4. 添加使用自定义提示获取用户的选择并启动长时间运行的操作的对话框。

    Dialogs\LongOperationDialog.cs

    /// <summary>
    /// This dialog demonstrates how to use the <see cref="LongOperationPrompt"/>.
    ///
    /// The user is provided an option to perform any of three long operations.
    /// Their choice is then sent to the <see cref="LongOperationPrompt"/>.
    /// When the prompt completes, the result is received as an Activity in the
    /// final Waterfall step.
    /// </summary>
    public class LongOperationDialog : ComponentDialog
    {
        public LongOperationDialog(AzureQueuesService queueService)
            : base(nameof(LongOperationDialog))
        {
            // This array defines how the Waterfall will execute.
            var waterfallSteps = new WaterfallStep[]
            {
                OperationTimeStepAsync,
                LongOperationStepAsync,
                OperationCompleteStepAsync,
            };
    
            // Add named dialogs to the DialogSet. These names are saved in the dialog state.
            AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps));
            AddDialog(new LongOperationPrompt(nameof(LongOperationPrompt), (vContext, token) =>
            {
                return Task.FromResult(vContext.Recognized.Succeeded);
            }, queueService));
            AddDialog(new ChoicePrompt(nameof(ChoicePrompt)));
    
            // The initial child Dialog to run.
            InitialDialogId = nameof(WaterfallDialog);
        }
    
        private static async Task<DialogTurnResult> OperationTimeStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it's a Prompt Dialog.
            // Running a prompt here means the next WaterfallStep will be run when the user's response is received.
            return await stepContext.PromptAsync(nameof(ChoicePrompt),
                new PromptOptions
                {
                    Prompt = MessageFactory.Text("Please select a long operation test option."),
                    Choices = ChoiceFactory.ToChoices(new List<string> { "option 1", "option 2", "option 3" }),
                }, cancellationToken);
        }
    
        private static async Task<DialogTurnResult> LongOperationStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            var value = ((FoundChoice)stepContext.Result).Value;
            stepContext.Values["longOperationOption"] = value;
    
            var prompt = MessageFactory.Text("...one moment please....");
            // The reprompt will be shown if the user messages the bot while the long operation is being performed.
            var retryPrompt = MessageFactory.Text($"Still performing the long operation: {value} ... (is the Azure Function executing from the queue?)");
            return await stepContext.PromptAsync(nameof(LongOperationPrompt),
                                                        new LongOperationPromptOptions
                                                        {
                                                            Prompt = prompt,
                                                            RetryPrompt = retryPrompt,
                                                            LongOperationOption = value,
                                                        }, cancellationToken);
        }
    
        private static async Task<DialogTurnResult> OperationCompleteStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            stepContext.Values["longOperationResult"] = stepContext.Result;
            await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Thanks for waiting. { (stepContext.Result as Activity).Value}"), cancellationToken);
    
            // Start over by replacing the dialog with itself.
            return await stepContext.ReplaceDialogAsync(nameof(WaterfallDialog), null, cancellationToken);
        }
    }
    

注册服务和对话框

在“Startup.cs”中,更新 方法以注册 ConfigureServices 并添加 LongOperationDialog

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers().AddNewtonsoftJson();

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

    // In production, this should be a persistent storage provider.bot
    services.AddSingleton<IStorage>(new MemoryStorage());

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

    // The Dialog that will be run by the bot.
    services.AddSingleton<LongOperationDialog>();

    // Service used to queue into Azure.Storage.Queues
    services.AddSingleton<AzureQueuesService>();

    // Create the bot as a transient. In this case the ASP Controller is expecting an IBot.
    services.AddTransient<IBot, DialogBot<LongOperationDialog>>();
}

测试机器人

  1. 安装 Bot Framework Emulator(如果尚未安装)。
  2. 在计算机本地运行示例。
  3. 启动 Emulator 并连接到机器人。
  4. 选择要启动的长操作。
    • 机器人发送了一条请稍等消息,并将 Azure 函数排队。
    • 如果用户在操作完成之前尝试与机器人交互,机器人会回复一条仍在工作消息。
    • 一旦操作完成,机器人就会向用户发送一条主动消息,让他们知道操作已经完成。

用户发起长时间操作并最终收到操作完成的主动消息的示例记录。

其他信息

工具或功能 资源
Azure Functions 创建函数应用
Azure Functions C# 脚本
管理函数应用
Azure 门户 管理机器人
将机器人连接到 Direct Line
Azure 存储 Azure 队列存储
创建存储帐户
从 Azure 门户复制凭据
如何使用队列
机器人基础知识 机器人工作原理
瀑布对话框中的提示
主动消息传递
开发隧道 使用 devtunnel 调试机器人