Een langlopende bewerking beheren
VAN TOEPASSING OP: SDK v4
Een goede afhandeling van langlopende bewerkingen is een belangrijk aspect van een robuuste bot. Wanneer de Azure AI-Bot Service een activiteit vanuit een kanaal naar uw bot verzendt, wordt verwacht dat de bot de activiteit snel verwerkt. Als de bot de bewerking niet binnen 10 tot 15 seconden voltooit, treedt er, afhankelijk van het kanaal, een time-out op voor de Azure AI-Bot Service en rapporteert deze aan de client, 504:GatewayTimeout
zoals beschreven in Hoe bots werken.
In dit artikel wordt beschreven hoe u een externe service gebruikt om de bewerking uit te voeren en de bot te waarschuwen wanneer deze is voltooid.
Vereisten
- Als u nog geen Azure-abonnement hebt, maakt u een gratis account voordat u begint.
- Bekendheid met prompts in watervaldialoogvensters en proactieve berichten.
- Bekendheid met Azure Queue Storage en Azure Functions C#-script.
- Een kopie van het promptvoorbeeld met meerdere keren in C#.
Over dit voorbeeld
Dit artikel begint met de voorbeeldbot met meerdelige prompts en voegt code toe voor het uitvoeren van langlopende bewerkingen. Het laat ook zien hoe u op een gebruiker kunt reageren nadat de bewerking is voltooid. In het bijgewerkte voorbeeld:
- De bot vraagt de gebruiker welke langlopende bewerking moet worden uitgevoerd.
- De bot ontvangt een activiteit van de gebruiker en bepaalt welke bewerking moet worden uitgevoerd.
- De bot meldt de gebruiker dat de bewerking enige tijd duurt en verzendt de bewerking naar een C#-functie.
- De bot slaat de status op, wat aangeeft dat er een bewerking wordt uitgevoerd.
- Terwijl de bewerking wordt uitgevoerd, reageert de bot op berichten van de gebruiker, met een melding dat de bewerking nog steeds wordt uitgevoerd.
- Azure Functions beheert de langlopende bewerking en verzendt een
event
activiteit naar de bot, met een melding dat de bewerking is voltooid.
- De bot hervat het gesprek en stuurt een proactief bericht om de gebruiker te laten weten dat de bewerking is voltooid. De bot wist vervolgens de eerder genoemde bewerkingsstatus.
In dit voorbeeld wordt een LongOperationPrompt
klasse gedefinieerd, afgeleid van de abstracte ActivityPrompt
klasse. Wanneer de activiteit in de LongOperationPrompt
wachtrij wordt geplaatst die moet worden verwerkt, bevat deze een keuze van de gebruiker in de eigenschap waarde van de activiteit. Deze activiteit wordt vervolgens verbruikt door Azure Functions, gewijzigd en verpakt in een andere event
activiteit voordat deze wordt teruggestuurd naar de bot met behulp van een Direct Line-client. Binnen de bot wordt de gebeurtenisactiviteit gebruikt om het gesprek te hervatten door de continue gespreksmethode van de adapter aan te roepen. De dialoogvensterstack wordt vervolgens geladen en de LongOperationPrompt
bewerkingen worden voltooid.
In dit artikel worden veel verschillende technologieën besproken. Zie de sectie aanvullende informatie voor koppelingen naar gekoppelde artikelen.
Een Azure Storage-account maken
Maak een Azure Storage-account en haal de connection string op. U moet de connection string toevoegen aan het configuratiebestand van uw bot.
Zie Een opslagaccount maken en uw referenties kopiëren uit de Azure Portal voor meer informatie.
Een botresource maken
Stel ngrok in en haal een URL op die moet worden gebruikt als berichteindpunt van de bot tijdens lokale foutopsporing. Het eindpunt voor berichten wordt de HTTPS-doorstuur-URL met
/api/messages/
toegevoegd. De standaardpoort voor nieuwe bots is 3978.Zie fouten opsporen in een bot met behulp van ngrok voor meer informatie.
Maak een Azure-botresource in de Azure Portal of met de Azure CLI. Stel het berichteneindpunt van de bot in op het eindpunt dat u met ngrok hebt gemaakt. Nadat de botresource is gemaakt, haalt u de Microsoft-app-id en het wachtwoord van de bot op. Schakel het Direct Line-kanaal in en haal een Direct Line geheim op. U voegt deze toe aan uw botcode en C#-functie.
Zie Een bot beheren en een bot verbinden met Direct Line voor meer informatie.
De C#-functie maken
Maak een Azure Functions-app op basis van de .NET Core-runtimestack.
Zie Een functie-app maken en de Azure Functions C#-scriptreferentie voor meer informatie.
Voeg een
DirectLineSecret
toepassingsinstelling toe aan de functie-app.Zie Uw functie-app beheren voor meer informatie.
Voeg in de functie-app een functie toe op basis van de Azure Queue Storage-sjabloon.
Stel de gewenste wachtrijnaam in en kies de
Azure Storage Account
die in een eerdere stap is gemaakt. Deze wachtrijnaam wordt ook in het bestand appsettings.json van de bot geplaatst.Voeg een function.proj-bestand toe aan de functie.
<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>
Werk run.csx bij met de volgende 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..."); }
De bot maken
Begin met een kopie van het C# Multi-Turn-Prompt-voorbeeld .
Voeg het NuGet-pakket Azure.Storage.Queues toe aan uw project.
Voeg de connection string voor het Azure Storage-account dat u eerder hebt gemaakt en de naam van de opslagwachtrij toe aan het configuratiebestand van uw bot.
Zorg ervoor dat de naam van de wachtrij hetzelfde is als de naam die u eerder hebt gebruikt om de wachtrijtriggerfunctie te maken. Voeg ook de waarden toe voor de
MicrosoftAppId
eigenschappen enMicrosoftAppPassword
die u eerder hebt gegenereerd bij het maken van de Azure Bot-resource.appsettings.json
{ "MicrosoftAppId": "<your-bot-app-id>", "MicrosoftAppPassword": "<your-bot-app-password>", "StorageQueueName": "<your-azure-storage-queue-name>", "QueueStorageConnection": "<your-storage-connection-string>" }
Voeg een
IConfiguration
parameter toe aan DialogBot.cs om deMicrsofotAppId
op te halen. Voeg ook eenOnEventActivityAsync
handler toe voor deLongOperationResponse
van de Azure-functie.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); } }
Maak een Azure Queues-service om activiteiten in de wachtrij te plaatsen die moeten worden verwerkt.
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; } }
Dialoogvensters
Verwijder het oude dialoogvenster en vervang dit door nieuwe dialoogvensters ter ondersteuning van de bewerkingen.
Verwijder het bestand UserProfileDialog.cs .
Voeg een dialoogvenster voor een aangepaste prompt toe waarin de gebruiker wordt gevraagd welke bewerking moet worden uitgevoerd.
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); } }
Voeg een promptoptiesklasse toe voor de aangepaste prompt.
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; } }
Voeg het dialoogvenster toe waarin de aangepaste prompt wordt gebruikt om de keuze van de gebruiker op te vragen en waarmee de langdurige bewerking wordt gestart.
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); } }
Services en dialoogvenster registreren
Werk in Startup.cs de methode bij om de ConfigureServices
LongOperationDialog
te registreren en voeg de AzureQueuesService
toe.
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>>();
}
De bot testen
- Als u dit nog niet hebt gedaan, installeert u de Bot Framework Emulator.
- Voer het voorbeeld lokaal uit op uw computer.
- Start de emulator en maak verbinding met uw bot.
- Kies een lange bewerking om te starten.
- De bot verzendt één moment een bericht en zet de Azure-functie in de wachtrij.
- Als de gebruiker probeert te communiceren met de bot voordat de bewerking is voltooid, reageert de bot met een bericht dat nog steeds werkt .
- Zodra de bewerking is voltooid, stuurt de bot een proactief bericht naar de gebruiker om te laten weten dat de bewerking is voltooid.
Aanvullende informatie
Hulpprogramma of functie | Resources |
---|---|
Azure Functions |
Een functie-app maken Azure Functions C#-script Uw functie-app beheren |
Azure Portal |
Manage a bot (Een bot beheren) Een bot verbinden met Direct Line |
Azure Storage |
Azure Queue Storage Een opslagaccount maken Kopieer uw referenties van de Azure Portal Wachtrijen gebruiken |
Basisprincipes van bot |
Hoe bots werken Prompts in watervaldialoogvensters Proactieve berichten |
ngrok | Fouten opsporen in een bot met behulp van ngrok |