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

管理长时间运行的操作

适用于: SDK v4

正确处理长时间运行的操作是可靠机器人的一个重要方面。 当 Azure 机器人服务将活动从通道发送到机器人时,机器人应该会迅速处理活动。 如果机器人在10到15秒内未完成操作,Azure 机器人服务将超时并向客户端报告 504:GatewayTimeout ,如 bot 的工作方式中所述。

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

先决条件

关于此示例

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

  • 机器人会要求用户执行运行时间较长的操作。
  • 机器人接收来自用户的活动,并确定要执行的操作。
  • 机器人通知用户操作将需要一段时间,并将操作发送到 c # 函数。
    • 机器人保存状态,指示有一个操作正在进行。
    • 当操作正在运行时,bot 会响应用户的消息,并通知他们操作仍在进行。
    • Azure Functions 管理长时间运行的操作,并 event 向机器人发送活动,通知其操作已完成。
  • 机器人会恢复会话并发送一条主动消息,通知用户操作已完成。 然后,bot 会清除前面提到的操作状态。

此示例定义了一个 LongOperationPrompt 派生自抽象类的类 ActivityPrompt 。 当对 LongOperationPrompt 要处理的活动进行排队时,它将在该活动的 value 属性内包含用户的一个选择。 然后,在 event 使用直接线路客户端将此活动发送回机器人之前,Azure Functions、修改和包装该活动。 在机器人中,事件活动用于通过调用适配器的 continue 会话 方法来恢复会话。 然后,将加载对话框堆栈,并 LongOperationPrompt 完成。

本文涉及许多不同的技术。 有关指向相关文章的链接,请参阅 " 其他资源 " 部分。

创建 Azure 存储帐户

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

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

创建机器人通道注册

  1. 在创建注册之前,请在本地调试过程中设置 ngrok 并检索要用作 bot 消息传递终结点 的 URL。 消息传递终结点将是附加的 HTTPS 转发 URL /api/messages/ 。 请注意,新的 bot 的默认端口为3978。

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

  2. 使用 Azure CLI 在 Azure 门户或中创建机器人频道注册。 将机器人的消息传递终结点设置为你用 ngrok 创建的终结点。 创建机器人通道注册资源后,获取机器人的 Microsoft 应用 ID 和密码。 启用直接行通道,并检索直接行机密。 将它们添加到机器人代码和 c # 函数中。

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

创建 c # 函数

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

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

  2. DirectLineSecret Function App 添加应用程序设置。

    有关详细信息,请参阅如何 管理 function app

  3. 在 Function App 中,添加一个基于 Azure 队列存储模板的函数。

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

  4. 函数 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", CompareOptions.OrdinalIgnoreCase))
        {
            originalActivity.Value = " (Result for long operation one!)";
        }
        else if(originalActivity.Value.ToString().Equals("option 2", CompareOptions.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 node.js NuGet 包添加到项目。

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

    确保队列名称与之前用于创建队列触发器函数的名称相同。 另外,添加在 MicrosoftAppId MicrosoftAppPassword 你先前创建机器人通道注册资源时生成的和属性的值。

    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 添加参数,以便检索 MicrsofotAppId 。 还可 OnEventActivityAsync 从 Azure 函数为添加处理程序 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

    /// <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 文件。

  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. 为自定义提示符添加 prompt 选项类。

    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 is 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 中,更新 ConfigureServices 方法以注册 LongOperationDialog ,并添加 AzureQueuesService

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. 按如下所示启动模拟器,连接到机器人,然后发送消息。

    机器人示例

其他资源

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