Aracılığıyla paylaş


Uzun süre çalışan bir işlemi yönetme

ŞUNLAR IÇIN GEÇERLIDIR: SDK v4

Uzun süre çalışan işlemlerin düzgün işlenmesi, sağlam bir botun önemli bir yönüdür. Azure AI Bot Hizmeti bir kanaldan botunuza etkinlik gönderdiğinde botunun etkinliği hızlı bir şekilde işlemesi beklenir. Bot işlemi kanala bağlı olarak 10-15 saniye içinde tamamlamazsa Azure AI Bot Hizmeti zaman aşımına uğrar ve botların nasıl çalıştığı konusunda açıklandığı gibi istemciye bir 504:GatewayTimeoutrapor gönderir.

Bu makalede, işlemi yürütmek ve tamamlandığında bota bildirim göndermek için dış hizmetin nasıl kullanılacağı açıklanır.

Önkoşullar

Bu örnek hakkında

Bu makale, çok aşamalı istem örnek botuyla başlar ve uzun süre çalışan işlemler gerçekleştirmek için kod ekler. Ayrıca, işlem tamamlandıktan sonra kullanıcıya nasıl yanıt ver yapılacağını da gösterir. Güncelleştirilmiş örnekte:

  • Bot, kullanıcıya uzun süre çalışan işlemi sorar.
  • Bot kullanıcıdan bir etkinlik alır ve gerçekleştirilecek işlemi belirler.
  • Bot, işlemin biraz zaman alacağını kullanıcıya bildirir ve işlemi bir C# işlevine gönderir.
    • Bot durumu kaydeder ve devam eden bir işlem olduğunu gösterir.
    • İşlem çalışırken bot, kullanıcıdan gelen iletilere yanıt vererek işlemin devam etmekte olduğunu bildirir.
    • Azure İşlevleri uzun süre çalışan işlemi yönetir ve bota işlemin tamamlandığını bildiren bir event etkinlik gönderir.
  • Bot konuşmayı sürdürür ve kullanıcıya işlemin tamamlandığını bildirmek için proaktif bir ileti gönderir. Bot daha sonra daha önce bahsedilen işlem durumunu temizler.

Bu örnek soyut sınıftan ActivityPrompt türetilmiş bir LongOperationPrompt sınıfı tanımlar. LongOperationPrompt İşlenecek etkinliği kuyruğa alırsa, etkinliğin value özelliği içinde kullanıcıdan bir seçenek içerir. Bu etkinlik daha sonra Azure İşlevleri tarafından kullanılır, değiştirilir ve Direct Line istemcisi kullanılarak bota geri gönderilmeden önce farklı event bir etkinlikte sarmalanır. Bot içinde olay etkinliği, bağdaştırıcının konuşmaya devam etme yöntemini çağırarak konuşmayı sürdürmek için kullanılır. ardından iletişim kutusu yığını yüklenir ve LongOperationPrompt tamamlanır.

Bu makalede birçok farklı teknolojiye değinilmiştir. İlişkili makalelerin bağlantıları için ek bilgi bölümüne bakın.

Azure Depolama hesabı oluşturma

Bir Azure Depolama hesabı oluşturun ve bağlantı dizesini alın. Bağlantı dizesini botunuzun yapılandırma dosyasına eklemeniz gerekir.

Daha fazla bilgi için bkz. Depolama hesabı oluşturma ve Azure portal kimlik bilgilerinizi kopyalama.

Bot kaynağı oluşturma

  1. Ngrok'u kurun ve yerel hata ayıklama sırasında bot'un mesajlaşma uç noktası olarak kullanılacak url'yi alın. Mesajlaşma uç noktası ekli HTTPS iletme URL'si /api/messages/ olacaktır; yeni botlar için varsayılan bağlantı noktası 3978'dir.

    Daha fazla bilgi için bkz. ngrok kullanarak bir botta hata ayıklama.

  2. Azure portal veya Azure CLI ile bir Azure Bot kaynağı oluşturun. Bot'un mesajlaşma uç noktasını ngrok ile oluşturduğunuza ayarlayın. Bot kaynağı oluşturulduktan sonra botunun Microsoft uygulama kimliğini ve parolasını alın. Direct Line kanalını etkinleştirin ve Direct Line bir gizli dizi alın. Bunları bot kodunuza ve C# işlevinize ekleyeceksiniz.

    Daha fazla bilgi için bkz. Botu yönetme ve botuDirect Line bağlama.

C# işlevini oluşturma

  1. .NET Core çalışma zamanı yığınını temel alan bir Azure İşlevleri uygulaması oluşturun.

    Daha fazla bilgi için bkz. İşlev uygulaması oluşturma ve Azure İşlevleri C# betik başvurusu.

  2. İşlev Uygulamasına bir DirectLineSecret uygulama ayarı ekleyin.

    Daha fazla bilgi için bkz. İşlev uygulamanızı yönetme.

  3. İşlev Uygulaması'nın içine Azure Kuyruk Depolama şablonunu temel alan bir işlev ekleyin.

    İstenen kuyruk adını ayarlayın ve önceki bir adımda oluşturulanı Azure Storage Account seçin. Bu kuyruk adı botunun appsettings.json dosyasına da yerleştirilir.

  4. İşleve bir function.proj dosyası ekleyin.

    <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. run.csx dosyasını aşağıdaki kodla güncelleştirin:

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

Botu oluşturma

  1. C# Çoklu Dönüş İstemi örneğinin bir kopyasıyla başlayın.

  2. Azure.Storage.Queues NuGet paketini projenize ekleyin.

  3. Daha önce oluşturduğunuz Azure Depolama hesabının bağlantı dizesini ve Depolama Kuyruğu Adı'nı botunuzun yapılandırma dosyasına ekleyin.

    Kuyruk adının daha önce Kuyruk Tetikleyicisi İşlevi'ni oluşturmak için kullandığınız adla aynı olduğundan emin olun. Ayrıca, Daha önce Azure Bot kaynağını oluştururken oluşturduğunuz ve MicrosoftAppPassword özellikleri için MicrosoftAppId değerleri ekleyin.

    appsettings.json

    {
      "MicrosoftAppId": "<your-bot-app-id>",
      "MicrosoftAppPassword": "<your-bot-app-password>",
      "StorageQueueName": "<your-azure-storage-queue-name>",
      "QueueStorageConnection": "<your-storage-connection-string>"
    }
    
  4. öğesini almak MicrsofotAppIdiçin DialogBot.cs dosyasına bir IConfiguration parametre ekleyin. Ayrıca Azure İşlevi'nden LongOperationResponse için bir OnEventActivityAsync işleyici ekleyin.

    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. İşlenecek etkinlikleri kuyruğa almak için bir Azure Kuyrukları hizmeti oluşturun.

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

İletişim Kutuları

İşlemleri desteklemek için eski iletişim kutusunu kaldırın ve yeni iletişim kutularıyla değiştirin.

  1. UserProfileDialog.cs dosyasını kaldırın.

  2. Kullanıcıya hangi işlemin gerçekleştirildiğini soran özel bir istem iletişim kutusu ekleyin.

    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. Özel istem için bir istem seçenekleri sınıfı ekleyin.

    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. Kullanıcının seçimini almak için özel istem kullanan ve uzun süre çalışan işlemi başlatan iletişim kutusunu ekleyin.

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

Hizmetleri kaydetme ve İletişim Kutusu

Startup.cs dosyasında, öğesini kaydetmek LongOperationDialog ve eklemek için yöntemini güncelleştirin ConfigureServicesAzureQueuesService.

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

Botu test etmek için

  1. Henüz yapmadıysanız Bot Framework Emulator yükleyin.
  2. Örneği makinenizde yerel olarak çalıştırın.
  3. Öykünücüyü başlatın ve botunuza bağlanın.
  4. Başlamak için uzun bir işlem seçin.
    • Bot bir dakika gönderir, lütfen Azure işlevini iletip kuyruğa alır.
    • Kullanıcı işlem tamamlanmadan botla etkileşim kurmaya çalışırsa, bot çalışmaya devam eden bir iletiyle yanıt verir.
    • İşlem tamamlandıktan sonra bot kullanıcıya proaktif bir ileti göndererek işlemin tamamlanmasını bildirir.

Uzun bir işlem başlatan ve sonunda işlemin tamamlandığını belirten proaktif bir ileti alan kullanıcı ile örnek transkript.

Ek bilgiler

Araç veya özellik Kaynaklar
Azure İşlevleri İşlev uygulaması oluşturma
C# betiğini Azure İşlevleri
İşlev uygulamanızı yönetme
Azure portal Botu yönetme
Botu Direct Line bağlama
Azure Depolama Azure Kuyruk Depolama
Depolama hesabı oluşturma
Azure portalından kimlik bilgilerinizi kopyalama
Kuyrukları Kullanma
Bot temel bilgileri Botlar nasıl çalışır?
Şelale iletişim kutularındaki istemler
Proaktif mesajlaşma
ngrok ngrok kullanarak botta hata ayıklama