Verwalten eines Vorgangs mit langer Ausführungszeit

GILT FÜR: SDK v4

Die ordnungsgemäße Verarbeitung von Vorgängen mit langer Ausführungsdauer ist ein wichtiger Aspekt eines robusten Bots. Wenn die Azure KI-Bot Service eine Aktivität von einem Kanal an Ihren Bot sendet, wird erwartet, dass der Bot die Aktivität schnell verarbeitet. Wenn der Bot den Vorgang je nach Kanal nicht innerhalb von 10 bis 15 Sekunden abgeschlossen hat, wird für die Azure KI-Bot Service ein Timeout ausgeführt und dem Client a 504:GatewayTimeoutgemeldet, wie unter Funktionsweise von Bots beschrieben.

In diesem Artikel wird beschrieben, wie Sie einen externen Dienst verwenden, um den Vorgang auszuführen und den Bot zu benachrichtigen, wenn er abgeschlossen ist.

Voraussetzungen

Informationen zu diesem Beispiel

Dieser Artikel beginnt mit dem Beispiel-Bot mit mehreren Eingabeaufforderungen und fügt Code für die Ausführung von Vorgängen mit langer Ausführungsdauer hinzu. Außerdem wird veranschaulicht, wie sie nach Abschluss des Vorgangs auf einen Benutzer reagieren. Im aktualisierten Beispiel:

  • Der Bot fragt den Benutzer, welchen Vorgang mit langer Ausführungsdauer ausgeführt werden soll.
  • Der Bot empfängt eine Aktivität vom Benutzer und bestimmt, welcher Vorgang ausgeführt werden soll.
  • Der Bot benachrichtigt den Benutzer, dass der Vorgang einige Zeit in Anspruch nimmt, und sendet den Vorgang an eine C#-Funktion.
    • Der Bot speichert den Zustand, der angibt, dass ein Vorgang ausgeführt wird.
    • Während der Vorgang ausgeführt wird, antwortet der Bot auf Nachrichten des Benutzers und benachrichtigt diesen, dass der Vorgang noch ausgeführt wird.
    • Azure Functions verwaltet den vorgang mit langer Ausführungsdauer und sendet eine event Aktivität an den Bot und benachrichtigt ihn darüber, dass der Vorgang abgeschlossen wurde.
  • Der Bot setzt die Unterhaltung fort und sendet eine proaktive Nachricht, um den Benutzer darüber zu informieren, dass der Vorgang abgeschlossen wurde. Der Bot löscht dann den zuvor erwähnten Vorgangsstatus.

In diesem Beispiel wird eine LongOperationPrompt Klasse definiert, die von der abstrakten ActivityPrompt Klasse abgeleitet wird. Wenn die LongOperationPrompt zu verarbeitende Aktivität in die Warteschlange eingeht, enthält sie eine Auswahl des Benutzers innerhalb der value-Eigenschaft der Aktivität. Diese Aktivität wird dann von Azure Functions genutzt, geändert und in eine andere event Aktivität eingeschlossen, bevor sie mithilfe eines Direct Line-Clients an den Bot zurückgesendet wird. Innerhalb des Bots wird die Ereignisaktivität verwendet, um die Konversation fortzusetzen, indem die Continue Conversation-Methode des Adapters aufgerufen wird. Der Dialogstapel wird dann geladen, und der LongOperationPrompt Vorgang wird abgeschlossen.

In diesem Artikel werden viele verschiedene Technologien behandelt. Links zu zugehörigen Artikeln finden Sie im Abschnitt zu zusätzlichen Informationen .

Erstellen eines Azure-Speicherkontos

Erstellen Sie ein Azure Storage-Konto, und rufen Sie die Verbindungszeichenfolge ab. Sie müssen die Verbindungszeichenfolge zur Konfigurationsdatei Ihres Bots hinzufügen.

Weitere Informationen finden Sie unter Erstellen eines Speicherkontos und Kopieren Ihrer Anmeldeinformationen aus dem Azure-Portal.

Erstellen einer Botressource

  1. Richten Sie ngrok ein, und rufen Sie eine URL ab, die beim lokalen Debuggen als Messagingendpunkt des Bots verwendet werden soll. Der Messagingendpunkt ist die HTTPS-Weiterleitungs-URL mit /api/messages/ angefügt– der Standardport für neue Bots ist 3978.

    Weitere Informationen finden Sie unter Debuggen eines Bots mithilfe von ngrok.

  2. Erstellen Sie eine Azure Bot-Ressource im Azure-Portal oder mit der Azure CLI. Legen Sie den Messagingendpunkt des Bots auf den endpunkt fest, den Sie mit ngrok erstellt haben. Nachdem die Botressource erstellt wurde, rufen Sie die Microsoft-App-ID und das Kennwort des Bots ab. Aktivieren Sie den Direct Line Kanal, und rufen Sie ein Direct Line Geheimnis ab. Sie fügen diese Ihrem Botcode und Ihrer C#-Funktion hinzu.

    Weitere Informationen finden Sie unter Verwalten eines Bots und Verbinden eines Bots mit Direct Line.

Erstellen der C#-Funktion

  1. Erstellen Sie eine Azure Functions-App basierend auf dem .NET Core-Runtimestapel.

    Weitere Informationen finden Sie unter Erstellen einer Funktions-App und Azure Functions C#-Skriptreferenz.

  2. Fügen Sie der Funktions-App eine Anwendungseinstellung hinzu DirectLineSecret .

    Weitere Informationen finden Sie unter Verwalten Ihrer Funktions-App.

  3. Fügen Sie in der Funktions-App eine Funktion basierend auf der Azure Queue Storage-Vorlage hinzu.

    Legen Sie den gewünschten Warteschlangennamen fest, und wählen Sie den Azure Storage Account in einem früheren Schritt erstellten aus. Dieser Warteschlangenname wird auch in der Datei appsettings.json des Bots platziert.

  4. Fügen Sie der Funktion eine function.proj-Datei hinzu.

    <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. Aktualisieren Sie run.csx mit dem folgenden Code:

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

Erstellen des Bots

  1. Beginnen Sie mit einer Kopie des C# -Beispiels für Mehrere Turn-Eingabeaufforderungen .

  2. Fügen Sie Ihrem Projekt das NuGet-Paket Azure.Storage.Queues hinzu.

  3. Fügen Sie der Konfigurationsdatei Ihres Bots die Verbindungszeichenfolge für das zuvor erstellte Azure Storage-Konto und den Namen der Speicherwarteschlange hinzu.

    Stellen Sie sicher, dass der Warteschlangenname mit dem Namen übereinstimmt, den Sie zuvor zum Erstellen der Warteschlangentriggerfunktion verwendet haben. Fügen Sie außerdem die Werte für die MicrosoftAppId Eigenschaften und MicrosoftAppPassword hinzu, die Sie zuvor beim Erstellen der Azure Bot-Ressource generiert haben.

    appsettings.json

    {
      "MicrosoftAppId": "<your-bot-app-id>",
      "MicrosoftAppPassword": "<your-bot-app-password>",
      "StorageQueueName": "<your-azure-storage-queue-name>",
      "QueueStorageConnection": "<your-storage-connection-string>"
    }
    
  4. Fügen Sie DialogBot.cs einen IConfiguration Parameter hinzu, um den MicrsofotAppIdabzurufen. Fügen Sie auch einen OnEventActivityAsync Handler für die LongOperationResponse aus der Azure-Funktion hinzu.

    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. Erstellen Sie einen Azure Queues-Dienst für zu verarbeitende Aktivitäten.

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

Dialogfelder

Entfernen Sie das alte Dialogfeld, und ersetzen Sie es durch neue Dialogfelder, um die Vorgänge zu unterstützen.

  1. Entfernen Sie die Datei UserProfileDialog.cs .

  2. Fügen Sie ein benutzerdefiniertes Eingabeaufforderungsdialogfeld hinzu, in dem der Benutzer gefragt wird, welcher Vorgang ausgeführt werden soll.

    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. Fügen Sie eine Eingabeaufforderungsoptionenklasse für die benutzerdefinierte Eingabeaufforderung hinzu.

    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. Fügen Sie das Dialogfeld hinzu, das die benutzerdefinierte Eingabeaufforderung verwendet, um die Auswahl des Benutzers zu erhalten, und initiiert den Vorgang mit langer Ausführungsdauer.

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

Registrieren von Diensten und Dialog

Aktualisieren Sie in Startup.cs die ConfigureServices -Methode, um die LongOperationDialog zu registrieren, und fügen Sie hinzu 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>>();
}

So testen Sie den Bot

  1. Wenn Sie dies noch nicht getan haben, installieren Sie die Bot Framework Emulator.
  2. Führen Sie das Beispiel lokal auf Ihrem Computer aus.
  3. Starten Sie den Emulator, und stellen Sie eine Verbindung mit Ihrem Bot her.
  4. Wählen Sie einen langen Vorgang aus, der gestartet werden soll.
    • Der Bot sendet einen Moment, bitte eine Nachricht und stellt die Azure-Funktion in die Warteschlange.
    • Wenn der Benutzer versucht, vor Abschluss des Vorgangs mit dem Bot zu interagieren, antwortet der Bot mit einer noch funktionierenden Nachricht.
    • Sobald der Vorgang abgeschlossen ist, sendet der Bot eine proaktive Nachricht an den Benutzer, um ihn darüber zu informieren, dass er abgeschlossen ist.

Beispieltranskript mit dem Benutzer, der einen langen Vorgang initiiert und schließlich eine proaktive Nachricht erhält, dass der Vorgang abgeschlossen wurde.

Zusätzliche Informationen

Tool oder Feature Ressourcen
Azure-Funktionen Erstellen einer Funktions-App
Azure Functions C#-Skript
Verwalten Ihrer Funktions-App
Azure-Portal Verwalten eines Bots
Herstellen der Verbindung eines Bots mit Direct Line
Azure Storage Azure Queue Storage
Erstellen eines Speicherkontos
Kopieren Ihrer Anmeldeinformationen aus dem Azure-Portal
Verwenden von Warteschlangen
Grundlagen von Bots Funktionsweise von Bots
Eingabeaufforderungen in Wasserfalldialogen
Proaktives Messaging
ngrok Debuggen eines Bots mithilfe von ngrok