Управление долгосрочными операциями
ОБЛАСТЬ ПРИМЕНЕНИЯ: ПАКЕТ SDK версии 4
Правильная обработка длительных операций является важным аспектом надежного бота. Когда azure AI Служба Bot отправляет действие боту из канала, бот должен быстро обработать действие. Если бот не завершит операцию в течение 10–15 секунд, в зависимости от канала504:GatewayTimeout
, Служба Bot Azure AI будет истекать и сообщать клиенту, как описано в разделе "Как боты работают".
В этой статье описывается, как использовать внешнюю службу для выполнения операции и уведомления бота о завершении.
Необходимые компоненты
- Если у вас нет подписки Azure, создайте бесплатную учетную запись, прежде чем приступить к работе.
- Знакомство с запросами в каскадных диалогах и упреждающим обменом сообщениями.
- Знакомство с хранилищем очередей Azure и Функции Azure скриптом C#.
- Копия примера запроса с несколькими поворотами в C#.
Об этом примере
Эта статья начинается с примера бота с несколькими поворотами и добавляет код для выполнения длительных операций. В нем также показано, как реагировать на пользователя после завершения операции. В обновленном примере:
- Бот запрашивает у пользователя, какой длительный запуск операции требуется выполнить.
- Бот получает от пользователя действие и определяет, какая операция выполняется.
- Бот уведомляет пользователя, что операция займет некоторое время и отправляет операцию в функцию C#.
- Бот сохраняет состояние, указывающее, что выполняется операция.
- Пока операция выполняется, бот отвечает на сообщения от пользователя, уведомляя их о выполнении операции.
- Функции Azure управляет длительной операцией и отправляет
event
действие боту, уведомляя его о завершении операции.
- Бот возобновляет беседу и отправляет упреждающее сообщение, чтобы уведомить пользователя о завершении операции. Затем бот очищает состояние операции, указанное ранее.
В этом примере определяется класс, производный LongOperationPrompt
от абстрактного ActivityPrompt
класса. LongOperationPrompt
Когда действие будет обработано в очереди, оно включает в себя выбор пользователя в свойстве значения действия. Затем это действие используется Функции Azure, изменения и упаковки в другое event
действие перед отправкой боту с помощью клиента Direct Line. В боте действие события используется для возобновления беседы путем вызова метода продолжения беседы адаптера. Затем стек диалогов загружается и LongOperationPrompt
завершается.
Эта статья касается многих различных технологий. Дополнительные сведения см. в разделе "Ссылки на связанные статьи".
Создание учетной записи хранения Azure
Создайте учетную запись служба хранилища Azure и получите строка подключения. Вам потребуется добавить строка подключения в файл конфигурации бота.
Дополнительные сведения см. в статье о создании учетной записи хранения и копировании учетных данных из портал Azure.
Создание ресурса бота
Настройте туннели разработки и получите URL-адрес, который будет использоваться в качестве конечной точки обмена сообщениями бота во время локальной отладки. Конечная точка обмена сообщениями будет URL-адрес пересылки HTTPS с
/api/messages/
добавленным — порт по умолчанию для новых ботов — 3978.Дополнительные сведения см. в статье об отладке бота с помощью devtunnel.
Создайте ресурс Azure Bot в портал Azure или с помощью Azure CLI. Задайте конечную точку обмена сообщениями бота, созданную с помощью туннелей разработки. После создания ресурса бота получите идентификатор и пароль приложения Майкрософт бота. Включите канал Direct Line и получите секрет Direct Line. Вы добавите их в код бота и функцию C#.
Дополнительные сведения см. в статье об управлении ботом и подключении бота к Direct Line.
Создание функции C#
Создайте приложение Функции Azure на основе стека среды выполнения .NET Core.
Дополнительные сведения см. в статье о создании приложения-функции и справочнике по скрипту C#Функции Azure.
DirectLineSecret
Добавьте параметр приложения в приложение-функцию.Дополнительные сведения см. в статье об управлении приложением-функцией.
В приложении-функции добавьте функцию на основе шаблона хранилища очередей Azure.
Задайте нужное имя очереди и выберите
Azure Storage Account
созданный на предыдущем шаге. Это имя очереди также будет помещено в файл appsettings.json бота.Добавьте файл 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>
Обновите 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..."); }
Создание бота
Начните с копии примера C# Multi-Turn-Prompt .
Добавьте пакет NuGet Azure.Storage.Queues в проект.
Добавьте строка подключения для созданной ранее учетной записи служба хранилища 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>" }
Добавьте параметр для
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); } }
Создайте службу очередей 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; } }
Диалоговые окна
Удалите старый диалог и замените его новыми диалогами для поддержки операций.
Удалите файл UserProfileDialog.cs.
Добавьте пользовательское диалоговое окно запроса, которое запрашивает у пользователя, какую операцию необходимо выполнить.
Диалоговые окна\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); } }
Добавьте класс параметров запроса для пользовательского запроса.
Диалоговые окна\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; } }
Добавьте диалоговое окно, использующее пользовательский запрос, чтобы получить выбор пользователя и инициировать долговременную операцию.
Диалоговые окна\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>>();
}
Тестирование бота
- Если это еще не сделано, установите эмулятор Bot Framework.
- Выполните этот пример на локальном компьютере.
- Запустите эмулятор и подключитесь к боту.
- Выберите длинную операцию для запуска.
- Бот отправляет одно мгновение, сообщение и очередь функции Azure.
- Если пользователь пытается взаимодействовать с ботом до завершения операции, бот отвечает с сообщением о работе .
- После завершения операции бот отправляет пользователю упреждающее сообщение, чтобы сообщить ему о завершении.
Дополнительная информация:
Инструмент или функция | Ресурсы |
---|---|
Функции Azure | Создание приложения-функции скрипт Функции Azure C# Управление приложением-функцией |
Портал Azure | Управление ботом Подключение бота к Direct Line |
Хранилище Azure | Хранилище очередей Azure Создание учетной записи хранилища Копирование учетных данных из портал Azure Использование очередей |
Основные сведения о ботах | Принципы работы бота Запросы в каскадных диалогах Упреждающее обмен сообщениями |
Туннели разработки | Отладка бота с помощью devtunnel |