Udostępnij za pośrednictwem


Zarządzanie długotrwałą operacją

DOTYCZY: Zestaw SDK w wersji 4

Właściwa obsługa długotrwałych operacji jest ważnym aspektem niezawodnego bota. Gdy usługa Azure AI Bot Service wysyła działanie do bota z kanału, bot ma szybko przetworzyć działanie. Jeśli bot nie ukończy operacji w ciągu od 10 do 15 sekund, w zależności od kanału, usługa Azure AI Bot Service upłynął limit czasu i zgłosi powrót do klienta jako , zgodnie z opisem 504:GatewayTimeoutw temacie Jak działają boty.

W tym artykule opisano, jak używać usługi zewnętrznej do wykonywania operacji i powiadamiania bota po zakończeniu.

Wymagania wstępne

Informacje o tym przykładzie

Ten artykuł rozpoczyna się od przykładowego bota z monitem wielowrotnym i dodaje kod do wykonywania długotrwałych operacji. Pokazuje również, jak reagować na użytkownika po zakończeniu operacji. W zaktualizowanym przykładzie:

  • Bot pyta użytkownika, który długotrwała operacja ma być wykonywana.
  • Bot odbiera działanie od użytkownika i określa, która operacja ma być wykonywana.
  • Bot powiadamia użytkownika, że operacja zajmie trochę czasu i wyśle operację do funkcji języka C#.
    • Bot zapisuje stan wskazujący, że trwa operacja.
    • Gdy operacja jest uruchomiona, bot odpowiada na komunikaty od użytkownika, powiadamiając go, że operacja jest nadal w toku.
    • Azure Functions zarządza długotrwałą operacją event i wysyła działanie do bota, powiadamiając go o zakończeniu operacji.
  • Bot wznawia konwersację i wysyła proaktywną wiadomość, aby powiadomić użytkownika o zakończeniu operacji. Bot czyści stan operacji wymieniony wcześniej.

W tym przykładzie zdefiniowano klasę pochodzącą LongOperationPrompt z klasy abstrakcyjnej ActivityPrompt . Gdy LongOperationPrompt działanie ma zostać przetworzone w kolejce, zawiera on wybór od użytkownika we właściwości value działania. To działanie jest następnie używane przez Azure Functions, zmodyfikowane i opakowane w inne event działanie, zanim zostanie wysłane z powrotem do bota przy użyciu klienta Direct Line. W ramach bota działanie zdarzenia jest używane do wznowienia konwersacji przez wywołanie metody kontynuuj konwersacji karty. Następnie zostanie załadowany stos okna dialogowego LongOperationPrompt i zostanie ukończony.

Ten artykuł dotyczy wielu różnych technologii. Zobacz sekcję dodatkowe informacje, aby uzyskać linki do skojarzonych artykułów.

Tworzenie konta usługi Azure Storage

Utwórz konto usługi Azure Storage i pobierz parametry połączenia. Musisz dodać parametry połączenia do pliku konfiguracji bota.

Aby uzyskać więcej informacji, zobacz tworzenie konta magazynu i kopiowanie poświadczeń z Azure Portal.

Tworzenie zasobu bota

  1. Skonfiguruj narzędzie ngrok i pobierz adres URL, który ma być używany jako punkt końcowy obsługi komunikatów bota podczas debugowania lokalnego. Punkt końcowy obsługi komunikatów będzie adresem URL przekazywania HTTPS z /api/messages/ dołączonym — domyślnym portem dla nowych botów jest 3978.

    Aby uzyskać więcej informacji, zobacz jak debugować bota przy użyciu narzędzia ngrok.

  2. Utwórz zasób usługi Azure Bot w Azure Portal lub za pomocą interfejsu wiersza polecenia platformy Azure. Ustaw punkt końcowy obsługi komunikatów bota na utworzony za pomocą narzędzia ngrok. Po utworzeniu zasobu bota uzyskaj identyfikator aplikacji i hasło bota firmy Microsoft. Włącz kanał Direct Line i pobierz wpis tajny Direct Line. Dodasz je do kodu bota i funkcji języka C#.

    Aby uzyskać więcej informacji, zobacz jak zarządzać botem i jak połączyć bota z Direct Line.

Tworzenie funkcji języka C#

  1. Utwórz aplikację Azure Functions opartą na stosie środowiska uruchomieniowego platformy .NET Core.

    Aby uzyskać więcej informacji, zobacz, jak utworzyć aplikację funkcji i dokumentację skryptu Azure Functions C#.

  2. DirectLineSecret Dodaj ustawienie aplikacji do aplikacji funkcji.

    Aby uzyskać więcej informacji, zobacz jak zarządzać aplikacją funkcji.

  3. W aplikacji funkcji dodaj funkcję opartą na szablonie usługi Azure Queue Storage.

    Ustaw żądaną nazwę kolejki i wybierz utworzoną Azure Storage Account we wcześniejszym kroku. Ta nazwa kolejki zostanie również umieszczona w pliku appsettings.json bota.

  4. Dodaj plik function.proj do funkcji.

    <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. Zaktualizuj plik run.csx przy użyciu następującego kodu:

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

Tworzenie bota

  1. Zacznij od kopii przykładu multi-turn-prompt języka C#.

  2. Dodaj pakiet NuGet Azure.Storage.Queues do projektu.

  3. Dodaj parametry połączenia dla utworzonego wcześniej konta usługi Azure Storage oraz nazwę kolejki magazynu do pliku konfiguracji bota.

    Upewnij się, że nazwa kolejki jest taka sama jak wcześniej użyta do utworzenia funkcji wyzwalacza kolejki. Dodaj również wartości dla MicrosoftAppId właściwości i MicrosoftAppPassword wygenerowanych wcześniej podczas tworzenia zasobu usługi 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. IConfiguration Dodaj parametr do pliku DialogBot.cs, aby pobrać plik MicrsofotAppId. Dodaj również procedurę OnEventActivityAsync obsługi dla LongOperationResponse funkcji platformy 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. Utwórz usługę Azure Queues w celu przetworzenia działań kolejki.

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

Okna dialogowe

Usuń stare okno dialogowe i zastąp je nowymi oknami dialogowymi, aby obsługiwać operacje.

  1. Usuń plik UserProfileDialog.cs .

  2. Dodaj niestandardowe okno dialogowe monitu z pytaniem użytkownika, który ma wykonać operację.

    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. Dodaj klasę opcji monitu dla monitu niestandardowego.

    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. Dodaj okno dialogowe, które używa niestandardowego monitu, aby uzyskać wybór użytkownika i inicjuje długotrwałą operację.

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

Rejestrowanie usług i okno dialogowe

W pliku Startup.cs zaktualizuj metodę ConfigureServicesLongOperationDialog , aby zarejestrować element i dodać element 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>>();
}

Aby przetestować bota

  1. Jeśli jeszcze tego nie zrobiono, zainstaluj Bot Framework Emulator.
  2. Uruchom przykład lokalnie na maszynie.
  3. Uruchom emulator i połącz się z botem.
  4. Wybierz długą operację do uruchomienia.
    • Bot wysyła wiadomość i kolejkuje funkcję platformy Azure.
    • Jeśli użytkownik spróbuje wchodzić w interakcję z botem przed zakończeniem operacji, bot odpowiada komunikatem nadal działającym .
    • Po zakończeniu operacji bot wysyła użytkownikowi proaktywny komunikat, aby poinformować go o zakończeniu.

Przykładowa transkrypcja z użytkownikiem inicjującym długą operację i ostatecznie otrzymuje proaktywny komunikat, że operacja została ukończona.

Dodatkowe informacje

Narzędzie lub funkcja Zasoby
Azure Functions Tworzenie aplikacji funkcji
Azure Functions skrypt języka C#
Zarządzanie aplikacją funkcji
Azure Portal Zarządzanie botem
Łączenie bota z Direct Line
Azure Storage Azure Queue Storage
Tworzenie konta magazynu
Kopiowanie poświadczeń z witryny Azure Portal
Jak używać kolejek
Podstawy bota Jak działają boty
Monity w oknach dialogowych kaskadowych
Proaktywne komunikaty
ngrok Debugowanie bota przy użyciu narzędzia ngrok