次の方法で共有


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

この記事の対象: SDK v4

実行時間の長い操作の適切な処理は、堅牢なボットに求められる重要な要素です。 Azure AI Bot Service によってチャネルからボットにアクティビティが送信されたら、ボットはアクティビティを迅速に処理する必要があります。 チャネルに応じて、ボットが 10 ~ 15 秒以内に操作を完了しなかった場合、「504:GatewayTimeout」で説明されているように、Azure AI Bot Service はタイムアウトして をクライアントに報告します。

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

前提条件

このサンプルについて

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

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

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

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

重要

この記事には、ストレージへの内部接続用の構成ファイルに 接続文字列を使用する従来のコード サンプルが含まれています。 Microsoft では、使用可能な最も安全な認証フローを使用することをお勧めします。 Azure リソースに接続する場合は、Azure リソースのマネージド ID が推奨される認証方法です。

Azure ストレージ アカウントの作成

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

詳細については、ストレージ アカウントを作成する方法および Azure ポータルから資格情報をコピーする方法に関する記事を参照してください。

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

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

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

  2. Azure ポータルまたは Azure CLI を使用して、Azure Bot リソースを作成します。 ボットのメッセージング エンドポイントは、開発トンネルで作成したものに設定します。 ボット リソースが作成されたら、ボットの 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# の Multi-Turn-Prompt サンプルのコピーから始めます。

  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.csMicrsofotAppId パラメーターを追加します。 また、Azure 関数の OnEventActivityAsync 用に 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 で資格情報をコピーする
キューの使用方法
ボットの基本 ボットのしくみ
ウォーターフォール ダイアログのプロンプト
プロアクティブ メッセージング
開発トンネル devtunnel を使用してボットをデバッグする