Partager via


Gérer une opération de longue durée

S'APPLIQUE À : SDK v4

La gestion appropriée des opérations de longue durée est un aspect important d’un bot robuste. Quand Azure AI Bot Service envoie une activité à votre bot à partir d’un canal, le bot est censé traiter l’activité rapidement. Si le bot ne termine pas l’opération dans les 10 à 15 secondes, selon le canal, Azure AI Bot Service expire et renvoie au client un 504:GatewayTimeoutrapport, comme décrit dans le fonctionnement des bots.

Cet article explique comment utiliser un service externe pour exécuter l’opération et informer le bot lorsqu’il est terminé.

Prérequis

À propos de cet exemple

Cet article commence par l’exemple de bot d’invite à plusieurs tour et ajoute du code pour effectuer des opérations de longue durée. Il montre également comment répondre à un utilisateur une fois l’opération terminée. Dans l’exemple mis à jour :

  • Le bot demande à l’utilisateur quelle opération de longue durée effectuer.
  • Le bot reçoit une activité de l’utilisateur et détermine l’opération à effectuer.
  • Le bot avertit l’utilisateur que l’opération prendra un certain temps et envoie l’opération à une fonction C#.
    • Le bot enregistre l’état, indiquant qu’une opération est en cours.
    • Pendant l’exécution de l’opération, le bot répond aux messages de l’utilisateur, en les informant que l’opération est toujours en cours d’exécution.
    • Azure Functions gère l’opération de longue durée et envoie une event activité au bot, en l’informant que l’opération s’est terminée.
  • Le bot reprend la conversation et envoie un message proactif pour informer l’utilisateur que l’opération s’est terminée. Le bot efface ensuite l’état de l’opération mentionné précédemment.

Cet exemple définit une LongOperationPrompt classe dérivée de la classe abstraite ActivityPrompt . Lorsque l’activité LongOperationPrompt doit être traitée en file d’attente, elle inclut un choix de l’utilisateur dans la propriété valeur de l’activité. Cette activité est ensuite consommée par Azure Functions, modifiée et encapsulée dans une autre event activité avant qu’elle ne soit renvoyée au bot à l’aide d’un client Direct Line. Dans le bot, l’activité d’événement est utilisée pour reprendre la conversation en appelant la méthode de conversation continue de l’adaptateur. La pile de boîtes de dialogue est ensuite chargée et la LongOperationPrompt fin.

Cet article traite de nombreuses technologies différentes. Consultez la section d’informations supplémentaires pour obtenir des liens vers des articles associés.

Création d’un compte de stockage Azure

Créez un compte Stockage Azure et récupérez le chaîne de connexion. Vous devez ajouter la chaîne de connexion au fichier de configuration de votre bot.

Pour plus d’informations, consultez créer un compte de stockage et copier vos informations d’identification à partir du Portail Azure.

Créer une ressource de bot

  1. Configurez Dev Tunnels et récupérez une URL à utiliser comme point de terminaison de messagerie du bot pendant le débogage local. Le point de terminaison de messagerie est l’URL de transfert HTTPS avec /api/messages/ ajout : le port par défaut pour les nouveaux bots est 3978.

    Pour plus d’informations, consultez comment déboguer un bot à l’aide de devtunnel.

  2. Créez une ressource Azure Bot dans le Portail Azure ou avec Azure CLI. Définissez le point de terminaison de messagerie du bot sur celui que vous avez créé avec Dev Tunnels. Une fois la ressource de bot créée, obtenez l’ID et le mot de passe de l’application Microsoft du bot. Activez le canal Direct Line et récupérez un secret Direct Line. Vous les ajouterez à votre code de bot et à votre fonction C#.

    Pour plus d’informations, consultez comment gérer un bot et comment connecter un bot à Direct Line.

Créer la fonction C#

  1. Créez une application Azure Functions basée sur la pile du runtime .NET Core.

    Pour plus d’informations, consultez comment créer une application de fonction et la référence de script C# Azure Functions.

  2. Ajoutez un paramètre d’application DirectLineSecret à l’application de fonction.

    Pour plus d’informations, consultez comment gérer votre application de fonction.

  3. Dans l’application de fonction, ajoutez une fonction basée sur le modèle Stockage File d’attente Azure.

    Définissez le nom de file d’attente souhaité, puis choisissez le Azure Storage Account fichier créé à une étape antérieure. Ce nom de file d’attente est également placé dans le fichier appsettings.json du bot.

  4. Ajoutez un fichier function.proj à la fonction.

    <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. Mettez à jour run.csx avec le code suivant :

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

Créer un bot

  1. Commencez par une copie de l’exemple C# Multi-Turn-Prompt .

  2. Ajoutez le package NuGet Azure.Storage.Queues à votre projet.

  3. Ajoutez le chaîne de connexion pour le compte Stockage Azure que vous avez créé précédemment, ainsi que le nom de la file d’attente de stockage, au fichier de configuration de votre bot.

    Vérifiez que le nom de la file d’attente est identique à celui que vous avez utilisé pour créer la fonction de déclencheur de file d’attente précédemment. Ajoutez également les valeurs des MicrosoftAppId propriétés que MicrosoftAppPassword vous avez générées précédemment lors de la création de la ressource 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. Ajoutez un IConfiguration paramètre à DialogBot.cs pour récupérer le MicrsofotAppId. Ajoutez également un OnEventActivityAsync gestionnaire pour la LongOperationResponse fonction 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. Créez un service Files d’attente Azure pour traiter les activités de file d’attente.

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

Boîtes de dialogue

Supprimez l’ancien dialogue et remplacez-le par de nouveaux dialogues pour prendre en charge les opérations.

  1. Supprimez le fichier UserProfileDialog.cs .

  2. Ajoutez une boîte de dialogue d’invite personnalisée qui demande à l’utilisateur quelle opération effectuer.

    Boîtes de dialogue\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. Ajoutez une classe d’options d’invite pour l’invite personnalisée.

    Boîtes de dialogue\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. Ajoutez la boîte de dialogue qui utilise l’invite personnalisée pour obtenir le choix de l’utilisateur et lance l’opération de longue durée.

    Boîtes de dialogue\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);
        }
    }
    

Inscrire des services et boîte de dialogue

Dans Startup.cs, mettez à jour la ConfigureServices méthode pour inscrire et LongOperationDialog ajouter le 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>>();
}

Pour tester le bot

  1. Si vous ne l'avez pas encore fait, installez Bot Framework Emulator.
  2. Exécutez l’exemple en local sur votre machine.
  3. Démarrez l'émulateur et connectez-vous à votre bot.
  4. Choisissez une longue opération à démarrer.
    • Le bot envoie un instant, veuillez envoyer un message et mettre en file d’attente la fonction Azure.
    • Si l’utilisateur tente d’interagir avec le bot avant la fin de l’opération, le bot répond avec un message toujours opérationnel .
    • Une fois l’opération terminée, le bot envoie un message proactif à l’utilisateur pour lui faire savoir qu’il a terminé.

Exemple de transcription avec l’utilisateur qui lance une longue opération et reçoit finalement un message proactif que l’opération s’est terminée.

Informations supplémentaires

Fonctionnalité ou outil Ressources
Azure Functions Créer une application de fonction
Script C# Azure Functions
Gérer votre application de fonction
Portail Azure Gérer un bot
Connecter un bot à Direct Line
Stockage Azure Stockage File d’attente Azure
Créez un compte de stockage
Copiez vos informations d’identification à partir du Portail Azure
Comment utiliser des files d’attente
Concepts de base des bots Fonctionnement des bots
Invites dans les dialogues en cascade
Messagerie proactive
Tunnels de développement Déboguer un bot à l’aide de devtunnel