Управление долгосрочными операциями

ПРИМЕНИМО К: Пакет SDK версии 4

Правильная обработка длительных операций является важным аспектом надежного бота. Когда Служба Bot ИИ Azure отправляет боту действие из канала, бот должен быстро обработать действие. Если бот не завершит операцию в течение 10–15 секунд, в зависимости от канала, Служба Bot Azure AI будет истекать и сообщит клиенту 504:GatewayTimeoutо , как описано в разделе Как работают боты.

В этой статье описывается, как использовать внешнюю службу для выполнения операции и уведомления бота о ее завершении.

Предварительные требования

Об этом примере

Эта статья начинается с примера бота с многоэтапным запросом и добавляет код для выполнения длительных операций. В нем также показано, как реагировать на действия пользователя после завершения операции. В обновленном примере:

  • Бот запрашивает у пользователя долго выполняющуюся операцию.
  • Бот получает действие от пользователя и определяет, какую операцию следует выполнить.
  • Бот уведомляет пользователя о том, что операция займет некоторое время, и отправляет операцию в функцию C#.
    • Бот сохраняет состояние, указывая, что выполняется операция.
    • Во время выполнения операции бот отвечает на сообщения от пользователя, уведомляя его о том, что операция все еще выполняется.
    • Функции Azure управляет длительной операцией и отправляет event действие боту, уведомляя его о завершении операции.
  • Бот возобновляет беседу и отправляет упреждающее сообщение, чтобы уведомить пользователя о завершении операции. Затем бот очищает указанное ранее состояние операции.

В этом примере определяется класс, производный LongOperationPrompt от абстрактного ActivityPrompt класса. Когда объект LongOperationPrompt помещает в очередь обрабатываемое действие, он включает выбор пользователя в свойстве значения действия. Затем это действие используется Функции Azure, изменяется и упаковывается в другое event действие перед отправкой обратно боту с помощью клиента Direct Line. В боте действие события используется для возобновления беседы путем вызова метода продолжения общения адаптера. Затем загружается стек диалогов и LongOperationPrompt завершается.

В этой статье рассматривается множество различных технологий. Ссылки на связанные статьи см. в разделе с дополнительными сведениями .

Создание учетной записи хранения Azure

Создайте учетную запись хранения Azure и получите строку подключения. Необходимо добавить строку подключения в файл конфигурации бота.

Дополнительные сведения см. в статье Создание учетной записи хранения и копирование учетных данных из портал Azure.

Создание ресурса бота

  1. Настройте ngrok и получите URL-адрес, который будет использоваться в качестве конечной точки обмена сообщениями бота во время локальной отладки. Конечной точкой обмена сообщениями будет URL-адрес переадресации HTTPS с /api/messages/ добавленным . Порт по умолчанию для новых ботов — 3978.

    Дополнительные сведения см. в статье Отладка бота с помощью ngrok.

  2. Создайте ресурс Azure Bot в портал Azure или с помощью Azure CLI. Задайте для конечной точки обмена сообщениями бота ту, которую вы создали с помощью ngrok. После создания ресурса бота получите идентификатор и пароль приложения Майкрософт бота. Включите канал Direct Line и получите секрет Direct Line. Вы добавите их в код бота и функцию C#.

    Дополнительные сведения см. в статье Управление ботом и подключение бота к Direct Line.

Создание функции C#

  1. Создайте приложение Функции Azure на основе стека среды выполнения .NET Core.

    Дополнительные сведения см. в статье Создание приложения-функции и справочнике по скриптам C#, Функции Azure.

  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. Добавьте в проект пакет NuGet Azure.Storage.Queues .

  3. Добавьте строку подключения для созданной ранее учетной записи хранения Azure и имя очереди хранилища в файл конфигурации бота.

    Убедитесь, что имя очереди совпадает с именем, которое использовалось ранее для создания функции триггера очереди. Кроме того, добавьте значения для MicrosoftAppId свойств и MicrosoftAppPassword , созданных ранее при создании ресурса Azure Bot.

    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, чтобы получить MicrsofotAppId. Кроме того, OnEventActivityAsync добавьте обработчик для из LongOperationResponse функции Azure.

    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 и добавить 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. Запустите эмулятор и подключитесь к боту.
  4. Выберите длинную операцию для запуска.
    • Бот отправляет одно мгновение, пожалуйста, сообщение и помещает в очередь функцию Azure.
    • Если пользователь пытается взаимодействовать с ботом до завершения операции, бот ответит еще рабочим сообщением.
    • После завершения операции бот отправляет пользователю упреждающее сообщение, чтобы сообщить ему о завершении.

Пример расшифровки с пользователем, инициирующим длинную операцию и получающим упреждающее сообщение о завершении операции.

Дополнительные сведения

Инструмент или компонент Ресурсы
Функции Azure Создание приложения-функции
Функции Azure скрипт C#
Управление приложением-функцией
Портал Azure Управление ботом
Подключение бота к Direct Line
Хранилище Azure Хранилище очередей Azure
создать учетную запись хранения;
Копирование учетных данных с портала Azure
Использование очередей
Основные сведения о ботах Принципы работы бота
Запросы в каскадных диалоговых окнах
Упреждающий обмен сообщениями
ngrok Отладка бота с помощью ngrok