実行時間の長い操作を管理する

適用対象: SDK v4

実行時間の長い操作を適切に処理することは、堅牢なボットの重要な側面です。 Azure AI Bot Serviceがチャネルからボットにアクティビティを送信すると、ボットはアクティビティをすばやく処理することが期待されます。 チャネルに応じて、ボットが 10 から 15 秒以内に操作を完了しない場合、「ボットのしくみ」で説明されているように、Azure AI Bot Serviceはタイムアウトしてクライアントに504:GatewayTimeout報告します。

この記事では、外部サービスを使用して操作を実行し、完了したときにボットに通知する方法について説明します。

前提条件

このサンプルについて

この記事は、マルチターン プロンプト サンプル ボットから始まり、実行時間の長い操作を実行するためのコードを追加します。 また、操作が完了した後にユーザーに応答する方法も示します。 更新されたサンプルでは、次の手順を実行します。

  • ボットは、実行する実行時間の長い操作をユーザーに要求します。
  • ボットはユーザーからアクティビティを受け取り、実行する操作を決定します。
  • ボットは、操作に時間がかかるとユーザーに通知し、操作を C# 関数に送信します。
    • ボットは状態を保存し、進行中の操作があることを示します。
    • 操作の実行中、ボットはユーザーからのメッセージに応答し、操作がまだ進行中であることを通知します。
    • Azure Functions実行時間の長い操作を管理し、操作が完了したことを通知するアクティビティをボットに送信eventします。
  • ボットは会話を再開し、プロアクティブ メッセージを送信して、操作が完了したことをユーザーに通知します。 ボットは、前に説明した操作状態をクリアします。

この例では、抽象ActivityPromptクラスから派生した クラスを定義LongOperationPromptします。 LongOperationPrompt処理するアクティビティをキューに入れると、アクティビティの value プロパティ内のユーザーからの選択肢が含まれます。 その後、このアクティビティは、Direct Line クライアントを使用してボットに送り返される前に、Azure Functionsによって使用され、変更され、別eventのアクティビティにラップされます。 ボット内では、イベント アクティビティを使用して、アダプターの continue conversation メソッドを呼び出して会話を再開します。 ダイアログ スタックが読み込まれ、完了 LongOperationPrompt します。

この記事では、さまざまなテクノロジについて説明します。 関連する記事へのリンクについては、 追加情報 に関するセクションを参照してください。

Azure Storage アカウントの作成

Azure Storage アカウントを作成し、接続文字列を取得します。 接続文字列をボットの構成ファイルに追加する必要があります。

詳細については、「ストレージ アカウントを作成し、Azure portalから資格情報をコピーする」を参照してください。

ボット リソースを作成する

  1. ngrok をセットアップし、ローカル デバッグ中にボットの メッセージング エンドポイント として使用される URL を取得します。 メッセージング エンドポイントは、追加された HTTPS 転送 URL /api/messages/ になります。新しいボットの既定のポートは 3978 です。

    詳細については、「 ngrok を使用してボットをデバッグする方法」を参照してください。

  2. Azure portalまたは Azure CLI を使用して Azure Bot リソースを作成します。 ボットのメッセージング エンドポイントを、ngrok で作成したエンドポイントに設定します。 ボット リソースが作成されたら、ボットの Microsoft アプリ ID とパスワードを取得します。 Direct Line チャネルを有効にして、Direct Line シークレットを取得します。 これらをボット コードと C# 関数に追加します。

    詳細については、ボットを管理する方法とボットをDirect Lineに接続する方法に関するページを参照してください。

C# 関数を作成する

  1. .NET Core ランタイム スタックに基づいてAzure Functions アプリを作成します。

    詳細については、「関数アプリを作成する方法」および「Azure Functions C# スクリプト リファレンス」を参照してください

  2. 関数アプリに DirectLineSecret アプリケーション設定を追加します。

    詳細については、 関数アプリを管理する方法に関するページを参照してください。

  3. 関数アプリ内で、 Azure Queue Storage テンプレートに基づいて関数を追加します。

    目的のキュー名を設定し、前の手順で作成した Azure Storage Account を選択します。 このキュー名は、ボットの appsettings.json ファイルにも配置されます。

  4. function.proj ファイルを 関数に追加します。

    <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 を次のコードで更新します。

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

ボットを作成する

  1. C# マルチ ターン プロンプト サンプルのコピーから始めます。

  2. Azure.Storage.Queues NuGet パッケージをプロジェクトに追加します。

  3. 前に作成した Azure Storage アカウントの接続文字列とストレージ キュー名をボットの構成ファイルに追加します。

    キュー名が、先ほどキュー トリガー関数を作成するために使用した名前と同じであることを確認します。 また、Azure Bot リソースの作成時に MicrosoftAppId 前に生成した プロパティと MicrosoftAppPassword プロパティの値も追加します。

    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 取得するために 、DialogBot.cs に パラメーターを追加します MicrsofotAppId。 また、 OnEventActivityAsync Azure 関数から の LongOperationResponse ハンドラーを追加します。

    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. 処理するアクティビティをキューに入れる Azure Queues サービスを作成します。

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

ダイアログ

古いダイアログを削除し、新しいダイアログに置き換えて操作をサポートします。

  1. UserProfileDialog.cs ファイルを削除します。

  2. 実行する操作をユーザーに求めるカスタム プロンプト ダイアログを追加します。

    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. カスタム プロンプトのプロンプト オプション クラスを追加します。

    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. カスタム プロンプトを使用してユーザーの選択を取得し、実行時間の長い操作を開始するダイアログを追加します。

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

サービスとダイアログの登録

Startup.cs で、 メソッドをConfigureServices更新して をLongOperationDialog登録し、 を追加します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>>();
}

ボットをテストする

  1. まだインストールしていない場合は、Bot Framework Emulatorをインストールします。
  2. ご自身のマシンを使ってローカルでサンプルを実行します。
  3. エミュレーターを起動し、ボットに接続します。
  4. 開始する長い操作を選択します。
    • ボットからしばらく送信されます。Azure 関数にメッセージを送信してキューに入 れます
    • 操作が完了する前にユーザーがボットと対話しようとすると、ボットは 、まだ動作している メッセージで応答します。
    • 操作が完了すると、ボットはプロアクティブ メッセージをユーザーに送信して、完了したことを通知します。

ユーザーが長い操作を開始し、最終的に操作が完了したことを示すプロアクティブ メッセージを受信するサンプル トランスクリプト。

追加情報

ツールまたは機能 リソース
Azure Functions 関数アプリの作成
Azure Functions C# スクリプト
お使いの Function App の管理
Azure portal ボットの管理
ボットを Direct Line に接続する
Azure Storage Azure Queue Storage
ストレージ アカウントの作成
Azure Portal で資格情報をコピーする
キューを使用する方法
ボットの基本 ボットのしくみ
ウォーターフォール ダイアログのプロンプト
プロアクティブ メッセージング
ngrok ngrok を使用してボットをデバッグする