ユーザーによる割り込みの処理

この記事の対象: SDK v4

割り込みの処理は、堅牢なボットの重要な側面です。 定義された会話フローの手順に従ってユーザーが操作するとは限りません。 プロセスの途中で質問しようとする場合も、操作を完了せずに、途中でキャンセルしたいだけの場合もあります。 この記事では、ボットでのユーザーによる中断の一般的な処理方法をいくつか取り上げます。

Note

Bot Framework JavaScript SDK、C#、Python SDK は引き続きサポートされますが、Java SDK については、最終的な長期サポートは 2023 年 11 月に終了する予定です。 このリポジトリ内の重要なセキュリティとバグの修正のみが行われます。

Java SDK を使用して構築された既存のボットは引き続き機能します。

新しいボットの構築については、Power Virtual Agents の使用を検討し、適切なチャットボット ソリューションの選択についてお読みください。

詳細については、「The future of bot building」をご覧ください。

前提条件

コア ボット サンプルでは、Language Understanding (LUIS) を使用してユーザーの意図を識別します。ただし、ユーザーの意図を特定することは、この記事の主目的ではありません。 ユーザーの意図を識別する方法については、「自然言語の理解」および「ボットに自然言語の理解を追加する」をご覧ください。

Note

Language Understanding (LUIS) は、2025 年 10 月 1 日に廃止されます。 2023 年 4 月 1 日以降は、新しい LUIS リソースを作成することはできません。 より新しいバージョンの言語理解が、現在、Azure AI Language の一部として提供されています。

Azure AI Language の機能である会話言語理解 (CLU) は、LUIS の更新バージョンです。 Bot Framework SDK での言語理解のサポートの詳細については、「自然言語の理解」を参照してください。

このサンプルについて

この記事のサンプルでは、ダイアログを使用してユーザーからフライト情報を取得する航空券予約ボットをモデル化します。 ボットとの会話中、ユーザーはいつでも help コマンドまたは cancel コマンドを発行できます。 処理される中断には以下の 2 種類があります。

  • ターン レベル: ターン レベルでは処理をバイパスしますが、スタック上のダイアログはそのままで、提供された情報は保持されます。 次のターンで、会話が中断された場所から再開されます。
  • ダイアログ レベル: 処理が完全にキャンセルされるため、ボットは最初からやり直すことができます。

中断ロジックを定義して実装する

最初に、helpcancel 中断を定義して実装します。

ダイアログを使用するには、Microsoft.Bot.Builder.Dialogs NuGet パッケージをインストールします。

Dialogs\CancelAndHelpDialog.cs

ユーザーによる中断を処理する CancelAndHelpDialog クラスを実装します。 キャンセル可能なダイアログ、BookingDialog および DateResolverDialog は、このクラスから派生します。

public class CancelAndHelpDialog : ComponentDialog

CancelAndHelpDialog クラスでは、OnContinueDialogAsync メソッドが InterruptAsync メソッドを呼び出して、ユーザーが通常のフローを中断をしたかどうかを確認します。 フローが中断された場合は、基底クラス メソッドが呼び出されます。それ以外の場合は、戻り値が InterruptAsync から返されます。

protected override async Task<DialogTurnResult> OnContinueDialogAsync(DialogContext innerDc, CancellationToken cancellationToken = default)
{
    var result = await InterruptAsync(innerDc, cancellationToken);
    if (result != null)
    {
        return result;
    }

    return await base.OnContinueDialogAsync(innerDc, cancellationToken);
}

ユーザーが「help」と入力すると、InterruptAsync メソッドはメッセージを送信し、DialogTurnResult (DialogTurnStatus.Waiting) を呼び出します。これは、上部のダイアログがユーザーからの応答を待っていることを示します。 この方法では、ターンについてのみ会話フローが中断され、次のターンでは、会話が中断された場所から再開されます。

ユーザーが「cancel」と入力すると、内部ダイアログ コンテキストで CancelAllDialogsAsync が呼び出され、そのダイアログ スタックがクリアされ処理が終了します。この場合、取り消し済み状態になり、結果値は返されません。 MainDialog (後で説明します) の場合は、予約ダイアログが終了し、null を返したように見えます。これはユーザーが予約を確認しないことを選択した場合の処理と似ています。

private async Task<DialogTurnResult> InterruptAsync(DialogContext innerDc, CancellationToken cancellationToken)
{
    if (innerDc.Context.Activity.Type == ActivityTypes.Message)
    {
        var text = innerDc.Context.Activity.Text.ToLowerInvariant();

        switch (text)
        {
            case "help":
            case "?":
                var helpMessage = MessageFactory.Text(HelpMsgText, HelpMsgText, InputHints.ExpectingInput);
                await innerDc.Context.SendActivityAsync(helpMessage, cancellationToken);
                return new DialogTurnResult(DialogTurnStatus.Waiting);

            case "cancel":
            case "quit":
                var cancelMessage = MessageFactory.Text(CancelMsgText, CancelMsgText, InputHints.IgnoringInput);
                await innerDc.Context.SendActivityAsync(cancelMessage, cancellationToken);
                return await innerDc.CancelAllDialogsAsync(cancellationToken);
        }
    }

    return null;
}

ターンごとに中断を確認する

中断を処理するクラスが実装されたら、このボットがユーザーから新しいメッセージを受信したときの動作を確認します。

Dialogs\MainDialog.cs

新しいメッセージ アクティビティが届くと、ボットは MainDialog を実行します。 MainDialog は、ユーザーに対して、ボットに役立つ情報を入力するよう要求します。 そして、次のように MainDialog.ActStepAsync メソッドで BookingDialog を開始して、BeginDialogAsync を呼び出します。

private async Task<DialogTurnResult> ActStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    if (!_luisRecognizer.IsConfigured)
    {
        // LUIS is not configured, we just run the BookingDialog path with an empty BookingDetailsInstance.
        return await stepContext.BeginDialogAsync(nameof(BookingDialog), new BookingDetails(), cancellationToken);
    }

    // Call LUIS and gather any potential booking details. (Note the TurnContext has the response to the prompt.)
    var luisResult = await _luisRecognizer.RecognizeAsync<FlightBooking>(stepContext.Context, cancellationToken);
    switch (luisResult.TopIntent().intent)
    {
        case FlightBooking.Intent.BookFlight:
            await ShowWarningForUnsupportedCities(stepContext.Context, luisResult, cancellationToken);

            // Initialize BookingDetails with any entities we may have found in the response.
            var bookingDetails = new BookingDetails()
            {
                // Get destination and origin from the composite entities arrays.
                Destination = luisResult.ToEntities.Airport,
                Origin = luisResult.FromEntities.Airport,
                TravelDate = luisResult.TravelDate,
            };

            // Run the BookingDialog giving it whatever details we have from the LUIS call, it will fill out the remainder.
            return await stepContext.BeginDialogAsync(nameof(BookingDialog), bookingDetails, cancellationToken);

        case FlightBooking.Intent.GetWeather:
            // We haven't implemented the GetWeatherDialog so we just display a TODO message.
            var getWeatherMessageText = "TODO: get weather flow here";
            var getWeatherMessage = MessageFactory.Text(getWeatherMessageText, getWeatherMessageText, InputHints.IgnoringInput);
            await stepContext.Context.SendActivityAsync(getWeatherMessage, cancellationToken);
            break;

        default:
            // Catch all for unhandled intents
            var didntUnderstandMessageText = $"Sorry, I didn't get that. Please try asking in a different way (intent was {luisResult.TopIntent().intent})";
            var didntUnderstandMessage = MessageFactory.Text(didntUnderstandMessageText, didntUnderstandMessageText, InputHints.IgnoringInput);
            await stepContext.Context.SendActivityAsync(didntUnderstandMessage, cancellationToken);
            break;
    }

    return await stepContext.NextAsync(null, cancellationToken);
}

次に、MainDialog クラスの FinalStepAsync メソッドで、予約ダイアログが終了し、予約は完了または取り消し済みと見なされます。

private async Task<DialogTurnResult> FinalStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // If the child dialog ("BookingDialog") was cancelled, the user failed to confirm or if the intent wasn't BookFlight
    // the Result here will be null.
    if (stepContext.Result is BookingDetails result)
    {
        // Now we have all the booking details call the booking service.

        // If the call to the booking service was successful tell the user.

        var timeProperty = new TimexProperty(result.TravelDate);
        var travelDateMsg = timeProperty.ToNaturalLanguage(DateTime.Now);
        var messageText = $"I have you booked to {result.Destination} from {result.Origin} on {travelDateMsg}";
        var message = MessageFactory.Text(messageText, messageText, InputHints.IgnoringInput);
        await stepContext.Context.SendActivityAsync(message, cancellationToken);
    }

    // Restart the main dialog with a different message the second time around
    var promptMessage = "What else can I do for you?";
    return await stepContext.ReplaceDialogAsync(InitialDialogId, promptMessage, cancellationToken);
}

BookingDialog のコードは中断処理に直接関連しないため、ここには示されていません。 これは、ユーザーに予約の詳細の入力を求めるときに使用されます。 このコードは、Dialogs\bookingDialogs.cs にあります。

予期しないエラーを処理する

アダプターのエラー ハンドラーでは、ボットでキャッチされなかったすべての例外が処理されます。

AdapterWithErrorHandler.cs

このサンプルでは、アダプターの OnTurnError ハンドラーは、ボットのターン ロジックによってスローされたすべての例外を受け取ります。 例外がスローされると、ハンドラーは、ボットが無効な状態になることでエラー ループに陥らないように、現在の会話の会話状態を削除します。

    {
        // Log any leaked exception from the application.
        // NOTE: In production environment, you should consider logging this to
        // Azure Application Insights. Visit https://aka.ms/bottelemetry to see how
        // to add telemetry capture to your bot.
        logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}");

        // Send a message to the user
        var errorMessageText = "The bot encountered an error or bug.";
        var errorMessage = MessageFactory.Text(errorMessageText, errorMessageText, InputHints.IgnoringInput);
        await turnContext.SendActivityAsync(errorMessage);

        errorMessageText = "To continue to run this bot, please fix the bot source code.";
        errorMessage = MessageFactory.Text(errorMessageText, errorMessageText, InputHints.ExpectingInput);
        await turnContext.SendActivityAsync(errorMessage);

        if (conversationState != null)
        {
            try
            {
                // Delete the conversationState for the current conversation to prevent the
                // bot from getting stuck in a error-loop caused by being in a bad state.
                // ConversationState should be thought of as similar to "cookie-state" in a Web pages.
                await conversationState.DeleteAsync(turnContext);
            }
            catch (Exception e)
            {
                logger.LogError(e, $"Exception caught on attempting to Delete ConversationState : {e.Message}");
            }
        }

        // Send a trace activity, which will be displayed in the Bot Framework Emulator
        await turnContext.TraceActivityAsync("OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError");
    };
}

サービスを登録する

Startup.cs

最後に、Startup.cs では、ボットは一時的なものとして作成され、ターンごとに、ボットの新しいインスタンスが作成されます。


// Register the BookingDialog.

参照用に、上記のボットを作成するときに呼び出しで使用されるクラスの定義を次に示します。

public class DialogAndWelcomeBot<T> : DialogBot<T>
public class DialogBot<T> : ActivityHandler
    where T : Dialog
public class MainDialog : ComponentDialog

ボットのテスト

  1. Bot Framework Emulator をインストールします (まだインストールしていない場合)。
  2. ご自身のマシンを使ってローカルでサンプルを実行します。
  3. 以下に示すように、エミュレーターを起動し、お使いのボットに接続して、メッセージを送信します。

追加情報

  • C#JavaScriptPython、または Java24.bot-authentication-msgraph サンプルは、ログアウト要求の処理方法を示しています。 これは、ここに示しているものと同じようなパターンを使用して、中断を処理しています。

  • 何も行わない、または何が起こっているかとユーザーに思わせたままにするのではなく、既定の応答を送信する必要があります。 既定の応答では、ユーザーが本来の会話に戻ることができるように、ボットで認識されるコマンドをユーザーに伝える必要があります。

  • ターンの任意の時点で、ターン コンテキストの responded プロパティは、ボットがこのターンでユーザーにメッセージを送信したかどうかを示します。 ターンの終了前に、ボットは、何らかのメッセージをユーザーに送信する必要があります。そのメッセージは、簡単な入力の受信確認でもかまいません。