Compartilhar via


Gerenciar uma operação de execução prolongada

APLICA-SE A: SDK v4

O tratamento adequado de operações de longa execução é um aspecto importante de um bot robusto. Quando o Serviço de Bot de IA do Azure envia uma atividade para o bot de um canal, espera-se que o bot processe a atividade rapidamente. Se o bot não concluir a operação dentro de 10 a 15 segundos, dependendo do canal, o Serviço de Bot de IA do Azure atingirá o tempo limite e relatará ao cliente a 504:GatewayTimeout, conforme descrito em Como os bots funcionam.

Este artigo descreve como usar um serviço externo para executar a operação e notificar o bot quando ela for concluída.

Pré-requisitos

Sobre este exemplo

Este artigo começa com o bot de exemplo de prompt de vários turnos e adiciona código para executar operações de longa duração. Ele também demonstra como responder a um usuário após a conclusão da operação. No exemplo atualizado:

  • O bot pergunta ao usuário qual operação de execução longa executar.
  • O bot recebe uma atividade do usuário e determina qual operação executar.
  • O bot notifica o usuário de que a operação levará algum tempo e envia a operação para uma função C#.
    • O bot salva o estado, indicando que há uma operação em andamento.
    • Enquanto a operação está em execução, o bot responde às mensagens do usuário, notificando-o de que a operação ainda está em andamento.
    • O Azure Functions gerencia a operação de execução longa e envia uma event atividade para o bot, notificando-o de que a operação foi concluída.
  • O bot retoma a conversa e envia uma mensagem proativa para notificar o usuário de que a operação foi concluída. Em seguida, o bot limpa o estado de operação mencionado anteriormente.

Este exemplo define uma LongOperationPrompt classe, derivada da classe abstrata ActivityPrompt . Quando o enfileira LongOperationPrompt a atividade a ser processada, ele inclui uma opção do usuário na propriedade value da atividade. Essa atividade é consumida pelo Azure Functions, modificada e encapsulada em uma atividade diferente event antes de ser enviada de volta ao bot usando um cliente Direct Line. Dentro do bot, a atividade de evento é usada para retomar a conversa chamando o método de conversa de continuação do adaptador. A pilha de diálogos é então carregada e concluída LongOperationPrompt .

Este artigo aborda muitas tecnologias diferentes. Consulte a seção de informações adicionais para obter links para artigos associados.

Crie uma conta de Armazenamento do Azure

Crie uma conta de Armazenamento do Azure e recupere a cadeia de conexão. Você precisará adicionar a cadeia de conexão ao arquivo de configuração do bot.

Para obter mais informações, consulte criar uma conta de armazenamento e copiar suas credenciais do portal do Azure.

Criar um recurso de bot

  1. Configure Túneis de Desenvolvimento e recupere uma URL a ser usada como o ponto de extremidade de mensagens do bot durante a depuração local. O endpoint de mensagens será a URL de encaminhamento HTTPS com /api/messages/ anexado — a porta padrão para novos bots é 3978.

    Para obter mais informações, consulte como depurar um bot usando devtunnel.

  2. Crie um recurso de Bot do Azure no portal do Azure ou com a CLI do Azure. Defina o ponto de extremidade de mensagens do bot como aquele que você criou com os Túneis de Desenvolvimento. Depois que o recurso de bot for criado, obtenha a ID e a senha do aplicativo Microsoft do bot. Habilite o canal de Linha Direta e recupere um segredo de Linha Direta. Você os adicionará ao código do bot e à função C#.

    Para obter mais informações, consulte como gerenciar um bot e como conectar um bot ao Direct Line.

Criar a função C#

  1. Crie um aplicativo do Azure Functions com base na pilha de runtime do .NET Core.

    Para obter mais informações, consulte como criar um aplicativo de funções e a referência de script C# do Azure Functions.

  2. Adicione uma DirectLineSecret configuração de aplicativo ao Aplicativo de Funções.

    Para obter mais informações, consulte como gerenciar seu aplicativo de funções.

  3. No Aplicativo de Funções, adicione uma função com base no modelo de Armazenamento de Filas do Azure.

    Defina o nome da fila desejado e escolha o Azure Storage Account criado em uma etapa anterior. Esse nome de fila também será colocado no arquivo appsettings.json do bot.

  4. Adicione um arquivo function.proj à função.

    <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. Atualize run.csx com o seguinte código:

    #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...");
    }
    

Criar o bot

  1. Comece com uma cópia do exemplo de prompt de vários turnos do C#.

  2. Adicione o pacote NuGet Azure.Storage.Queues ao seu projeto.

  3. Adicione a cadeia de conexão para a conta de Armazenamento do Azure que você criou anteriormente e o Nome da Fila de Armazenamento ao arquivo de configuração do bot.

    Verifique se o nome da fila é o mesmo que você usou para criar a Função de Gatilho de Fila anteriormente. Adicione também os valores para as MicrosoftAppId propriedades and MicrosoftAppPassword que você gerou anteriormente ao criar o recurso de Bot do Azure.

    appsettings.json

    {
      "MicrosoftAppId": "<your-bot-app-id>",
      "MicrosoftAppPassword": "<your-bot-app-password>",
      "StorageQueueName": "<your-azure-storage-queue-name>",
      "QueueStorageConnection": "<your-storage-connection-string>"
    }
    
  4. Adicione um IConfiguration parâmetro a DialogBot.cs para recuperar o MicrsofotAppIdarquivo . Adicione também um OnEventActivityAsync manipulador para a LongOperationResponse função do 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. Crie um serviço de filas do Azure para enfileirar as atividades a serem processadas.

    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;
        }
    }
    

Diálogos

Remova a caixa de diálogo antiga e substitua-a por novas caixas de diálogo para dar suporte às operações.

  1. Remova o arquivo UserProfileDialog.cs .

  2. Adicione uma caixa de diálogo de prompt personalizada que pergunta ao usuário qual operação executar.

    Diálogos\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. Adicione uma classe de opções de prompt para o prompt personalizado.

    Diálogos\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. Adicione a caixa de diálogo que usa o prompt personalizado para obter a escolha do usuário e iniciar a operação de execução longa.

    Diálogos\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);
        }
    }
    

Registrar serviços e caixa de diálogo

Em Startup.cs, atualize o ConfigureServices método para registrar o LongOperationDialog e adicionar o 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>>();
}

Para testar o bot

  1. Caso ainda não tenha feito isso, instale o Bot Framework Emulator.
  2. Execute o exemplo localmente em seu computador.
  3. Inicie o Emulator e conecte-se ao seu bot.
  4. Escolha uma operação longa para iniciar.
    • O bot envia uma mensagem de um momento, por favor , e enfileira a função do Azure.
    • Se o usuário tentar interagir com o bot antes da conclusão da operação, o bot responderá com uma mensagem ainda funcionando .
    • Depois que a operação é concluída, o bot envia uma mensagem proativa ao usuário para que ele saiba que foi concluída.

Transcrição de exemplo com o usuário iniciando uma operação longa e, eventualmente, recebendo uma mensagem proativa de que a operação foi concluída.

Informações adicionais

Ferramenta ou recurso Recursos
Azure Functions Criar um aplicativo de funções
Script C# do Azure Functions
Gerenciar seu aplicativo de funções
Portal do Azure Gerenciar um bot
Conectar um bot ao Direct Line
Armazenamento do Azure Armazenamento de Filas do Azure
Criar uma conta de armazenamento
Copiar suas credenciais do portal do Azure
Como usar filas
Noções básicas sobre os bots Como funcionam os bots
Prompts em caixas de diálogo em cascata
Mensagens proativas
Túneis de desenvolvimento Depurar um bot usando devtunnel