Share via


Administración de una operación de ejecución prolongada

SE APLICA A: SDK v4

El control adecuado de las operaciones de larga duración es un aspecto importante de un bot sólido. Cuando Azure AI Bot Service envía una actividad al bot desde un canal, se espera que el bot procese la actividad rápidamente. Si el bot no completa la operación en un plazo de 10 a 15 segundos, según el canal, azure AI Bot Service agotará el tiempo de espera y notificará al cliente un 504:GatewayTimeout, como se describe en Cómo funcionan los bots.

En este artículo se describe cómo usar un servicio externo para ejecutar la operación y notificar al bot cuando se haya completado.

Requisitos previos

Acerca de este ejemplo

Este artículo comienza con el bot de ejemplo de solicitud de varios turnos y agrega código para realizar operaciones de larga duración. También muestra cómo responder a un usuario una vez completada la operación. En el ejemplo actualizado:

  • El bot pide al usuario qué operación de ejecución prolongada se va a realizar.
  • El bot recibe una actividad del usuario y determina qué operación se va a realizar.
  • El bot notifica al usuario que la operación tardará algún tiempo y enviará la operación a una función de C#.
    • El bot guarda el estado, lo que indica que hay una operación en curso.
    • Mientras se ejecuta la operación, el bot responde a los mensajes del usuario y les notifica la operación todavía está en curso.
    • Azure Functions administra la operación de ejecución prolongada y envía una event actividad al bot, notificando que la operación se completó.
  • El bot reanuda la conversación y envía un mensaje proactivo para notificar al usuario que se completó la operación. A continuación, el bot borra el estado de la operación mencionado anteriormente.

En este ejemplo se define una LongOperationPrompt clase derivada de la clase abstracta ActivityPrompt . Cuando pone en LongOperationPrompt cola la actividad que se va a procesar, incluye una opción del usuario dentro de la propiedad value de la actividad. A continuación, esta actividad se consume mediante Azure Functions, se modifica y se encapsula en una actividad diferente event antes de que se devuelva al bot mediante un cliente de Direct Line. Dentro del bot, la actividad de eventos se usa para reanudar la conversación mediante una llamada al método continue conversation del adaptador. A continuación, se carga la pila de diálogos y LongOperationPrompt se completa.

En este artículo se tratan muchas tecnologías diferentes. Consulte la sección de información adicional para obtener vínculos a artículos asociados.

Creación de una cuenta de Azure Storage

Cree una cuenta de Azure Storage y recupere la cadena de conexión. Deberá agregar la cadena de conexión al archivo de configuración del bot.

Para más información, consulte Creación de una cuenta de almacenamiento y copia las credenciales de la Azure Portal.

Creación de un recurso de bot

  1. Configure ngrok y recupere una dirección URL que se usará como punto de conexión de mensajería del bot durante la depuración local. El punto de conexión de mensajería será la dirección URL de reenvío HTTPS con /api/messages/ anexado; el puerto predeterminado para los nuevos bots es 3978.

    Para más información, consulte cómo depurar un bot mediante ngrok.

  2. Cree un recurso de Azure Bot en el Azure Portal o con la CLI de Azure. Establezca el punto de conexión de mensajería del bot en el que creó con ngrok. Una vez creado el recurso de bot, obtenga el identificador y la contraseña de la aplicación de Microsoft del bot. Habilite el canal de Direct Line y recupere un secreto de Direct Line. Los agregará al código del bot y a la función de C#.

    Para más información, consulte cómo administrar un bot y cómo conectar un bot a Direct Line.

Creación de la función de C#

  1. Cree una aplicación de Azure Functions basada en la pila del entorno de ejecución de .NET Core.

    Para obtener más información, consulte cómo crear una aplicación de funciones y la referencia de script de C# Azure Functions.

  2. Agregue una DirectLineSecret configuración de aplicación a function App.

    Para obtener más información, consulte cómo administrar la aplicación de funciones.

  3. Dentro de function App, agregue una función basada en la plantilla de Azure Queue Storage.

    Establezca el nombre de cola deseado y elija el Azure Storage Account creado en un paso anterior. Este nombre de cola también se colocará en el archivo appsettings.json del bot.

  4. Agregue un archivo function.proj a la función.

    <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. Actualice run.csx con el código siguiente:

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

Creación del bot

  1. Comience con una copia del ejemplo multiturno de C#.

  2. Agregue el paquete NuGet Azure.Storage.Queues al proyecto.

  3. Agregue la cadena de conexión para la cuenta de Azure Storage que creó anteriormente y el nombre de la cola de Storage al archivo de configuración del bot.

    Asegúrese de que el nombre de la cola es el mismo que usó para crear la función de desencadenador de cola anteriormente. Agregue también los valores de las MicrosoftAppId propiedades y MicrosoftAppPassword que generó anteriormente al crear el recurso de 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. Agregue un IConfiguration parámetro a DialogBot.cs para recuperar .MicrsofotAppId Agregue también un OnEventActivityAsync controlador para LongOperationResponse desde la función de 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. Cree un servicio de colas de Azure para poner en cola las actividades que se van a procesar.

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

Cuadros de diálogo

Quite el cuadro de diálogo anterior y reemplácelo por diálogos nuevos para admitir las operaciones.

  1. Quite el archivo UserProfileDialog.cs .

  2. Agregue un cuadro de diálogo de solicitud personalizado que pida al usuario qué operación realizar.

    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. Agregue una clase de opciones de aviso para el símbolo del sistema personalizado.

    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. Agregue el cuadro de diálogo que usa el símbolo del sistema personalizado para obtener la elección del usuario e inicie la operación de larga duración.

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

Registro de servicios y cuadro de diálogo

En Startup.cs, actualice el ConfigureServices método para registrar LongOperationDialog y agregar .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>>();
}

Prueba del bot

  1. Si aún no lo ha hecho, instale el Bot Framework Emulator.
  2. Ejecute el ejemplo localmente en la máquina.
  3. Inicie el emulador y conéctese al bot.
  4. Elija una operación larga para iniciarse.
    • El bot envía un momento, envíe un mensaje y pone en cola la función de Azure.
    • Si el usuario intenta interactuar con el bot antes de que se complete la operación, el bot responde con un mensaje que sigue funcionando .
    • Una vez completada la operación, el bot envía un mensaje proactivo al usuario para informarle de que ha finalizado.

Transcripción de ejemplo con el usuario que inicia una operación larga y, finalmente, recibe un mensaje proactivo que la operación se ha completado.

Información adicional

Herramienta o característica Recursos
Comprobación de Creación de una aplicación de función
Azure Functions script de C#
Administración de la aplicación de funciones
Azure portal Administración de un bot
Conectar un bot con Direct Line
Azure Storage Azure Queue Storage
Cree una cuenta de almacenamiento
Copia de las credenciales desde Azure Portal
Uso de colas
Aspectos básicos de los bots Funcionamiento de los bots
Mensajes en diálogos en cascada
Mensajería proactiva
ngrok Depuración de un bot mediante ngrok