Dela via


Hantera en långvarig åtgärd

GÄLLER FÖR: SDK v4

Korrekt hantering av långvariga åtgärder är en viktig aspekt av en robust robot. När Azure AI-Bot Service skickar en aktivitet till din robot från en kanal förväntas roboten bearbeta aktiviteten snabbt. Om roboten inte slutför åtgärden inom 10 till 15 sekunder kommer Azure AI-Bot Service att överskrida tidsgränsen och rapportera tillbaka till klienten enligt 504:GatewayTimeoutbeskrivningen i Så här fungerar robotar.

Den här artikeln beskriver hur du använder en extern tjänst för att utföra åtgärden och meddela roboten när den har slutförts.

Förutsättningar

Om det här exemplet

Den här artikeln börjar med exempelroboten med flera varv och lägger till kod för att utföra långvariga åtgärder. Den visar också hur du svarar en användare när åtgärden har slutförts. I det uppdaterade exemplet:

  • Roboten frågar användaren vilken långvarig åtgärd som ska utföras.
  • Roboten tar emot en aktivitet från användaren och avgör vilken åtgärd som ska utföras.
  • Roboten meddelar användaren att åtgärden tar lite tid och skickar åtgärden till en C#-funktion.
    • Roboten sparar tillstånd, vilket indikerar att en åtgärd pågår.
    • Medan åtgärden körs svarar roboten på meddelanden från användaren och meddelar dem att åtgärden fortfarande pågår.
    • Azure Functions hanterar den långvariga åtgärden och skickar en event aktivitet till roboten och meddelar den att åtgärden har slutförts.
  • Roboten återupptar konversationen och skickar ett proaktivt meddelande för att meddela användaren att åtgärden har slutförts. Roboten rensar sedan åtgärdstillståndet som nämnts tidigare.

Det här exemplet definierar en LongOperationPrompt klass som härleds från den abstrakta ActivityPrompt klassen. När köar LongOperationPrompt aktiviteten som ska bearbetas innehåller den ett val från användaren i aktivitetens värdeegenskap. Den här aktiviteten förbrukas sedan av Azure Functions, ändras och omsluts i en annan event aktivitet innan den skickas tillbaka till roboten med hjälp av en Direct Line-klient. I roboten används händelseaktiviteten för att återuppta konversationen genom att anropa adapterns fortsätt-konversationsmetod . Sedan läses dialogstacken in och LongOperationPrompt slutförs.

Den här artikeln berör många olika tekniker. Se avsnittet ytterligare information för länkar till associerade artiklar.

Skapa ett Azure Storage-konto

Skapa ett Azure Storage-konto och hämta anslutningssträngen. Du måste lägga till anslutningssträngen i robotens konfigurationsfil.

Mer information finns i Skapa ett lagringskonto och kopiera dina autentiseringsuppgifter från Azure Portal.

Skapa en robotresurs

  1. Konfigurera ngrok och hämta en URL som ska användas som robotens slutpunkt för meddelanden under lokal felsökning. Slutpunkten för meddelanden är HTTPS-vidarebefordran med /api/messages/ tillagd – standardporten för nya robotar är 3978.

    Mer information finns i så här felsöker du en robot med ngrok.

  2. Skapa en Azure Bot-resurs i Azure Portal eller med Azure CLI. Ange robotens slutpunkt för meddelanden till den som du skapade med ngrok. När robotresursen har skapats hämtar du robotens App-ID och lösenord för Microsoft. Aktivera Direct Line-kanalen och hämta en Direct Line hemlighet. Du lägger till dessa i robotkoden och C#-funktionen.

    Mer information finns i hantera en robot och hur du ansluter en robot till Direct Line.

Skapa C#-funktionen

  1. Skapa en Azure Functions app baserat på .NET Core-körningsstacken.

    Mer information finns i skapa en funktionsapp och Azure Functions C#-skriptreferens.

  2. Lägg till en DirectLineSecret programinställning i funktionsappen.

    Mer information finns i hantera din funktionsapp.

  3. Lägg till en funktion som baseras på Azure Queue Storage-mallen i funktionsappen.

    Ange önskat könamn och välj den som skapades Azure Storage Account i ett tidigare steg. Könamnet placeras också i robotens appsettings.json-fil .

  4. Lägg till en function.proj-fil i funktionen .

    <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. Uppdatera run.csx med följande kod:

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

Skapa roboten

  1. Börja med en kopia av exemplet C# Multi-Turn-Prompt .

  2. Lägg till NuGet-paketet Azure.Storage.Queues i projektet.

  3. Lägg till anslutningssträngen för Det Azure Storage-konto som du skapade tidigare och Lagringskönamn i robotens konfigurationsfil.

    Kontrollera att könamnet är samma som det du använde för att skapa köutlösarfunktionen tidigare. Lägg även till värdena för MicrosoftAppId egenskaperna och MicrosoftAppPassword som du genererade tidigare när du skapade Azure Bot-resursen.

    appsettings.json

    {
      "MicrosoftAppId": "<your-bot-app-id>",
      "MicrosoftAppPassword": "<your-bot-app-password>",
      "StorageQueueName": "<your-azure-storage-queue-name>",
      "QueueStorageConnection": "<your-storage-connection-string>"
    }
    
  4. Lägg till en IConfiguration parameter i DialogBot.cs för att hämta MicrsofotAppId. Lägg även till en OnEventActivityAsync hanterare för LongOperationResponse från Azure-funktionen.

    Robotar\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. Skapa en Azure Queues-tjänst för att köa aktiviteter som ska bearbetas.

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

Dialogrutor

Ta bort den gamla dialogrutan och ersätt den med nya dialogrutor för att stödja åtgärderna.

  1. Ta bort filen UserProfileDialog.cs .

  2. Lägg till en anpassad dialogruta där användaren uppmanas att utföra åtgärden.

    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. Lägg till en promptalternativklass för den anpassade prompten.

    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. Lägg till dialogrutan som använder den anpassade prompten för att hämta användarens val och initierar den långvariga åtgärden.

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

Registrera tjänster och dialogruta

I Startup.cs uppdaterar ConfigureServices du metoden för att registrera LongOperationDialog och lägga till 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>>();
}

Testa roboten

  1. Om du inte redan har gjort det installerar du Bot Framework Emulator.
  2. Kör exemplet lokalt på datorn.
  3. Starta emulatorn och anslut till din robot.
  4. Välj en lång åtgärd att starta.
    • Roboten skickar ett ögonblick. Skicka ett meddelande och placera Azure-funktionen i kö.
    • Om användaren försöker interagera med roboten innan åtgärden slutförs svarar roboten med ett meddelande som fortfarande fungerar .
    • När åtgärden har slutförts skickar roboten ett proaktivt meddelande till användaren för att meddela dem att den har slutförts.

Exempelavskrift där användaren initierar en lång åtgärd och så småningom får ett proaktivt meddelande om att åtgärden har slutförts.

Ytterligare information

Verktyg eller funktion Resurser
Azure Functions Skapa en funktionsapp
Azure Functions C#-skript
Hantera din funktionsapp
Azure Portal Hantera en robot
Ansluta en robot till Direct Line
Azure Storage Azure Queue Storage
Skapa ett lagringskonto
Kopiera dina autentiseringsuppgifter från Azure-portalen
Använda köer
Grunderna i roboten Så här fungerar robotar
Frågor i vattenfallsdialogrutor
Proaktiva meddelanden
ngrok Felsöka en robot med ngrok