連続して行われる会話フローの実装

この記事の対象: SDK v4

質問を投稿して情報を収集することは、ボットがユーザーとやり取りする主な手段の 1 つです。 ダイアログ ライブラリでは、質問を行いやすくなるだけでなく、応答が検証され、確実に特定のデータ型と一致するように、またはカスタム検証ルールを満たすように、prompt クラスなどの便利な組み込み機能が提供されます。

ダイアログ ライブラリを使用して、線形型の会話フローとより複雑な会話フローを管理できます。 線形型のインタラクションでは、ボットは決まった一連のステップを順番に実行していき、最後に会話が終了します。 ダイアログは、ボットがユーザーから情報を収集する必要がある場合に役立ちます。

この記事では、プロンプトを作成してウォーターフォール ダイアログから呼び出すことで、線形型の会話フローを実装する方法について説明します。 ダイアログ ライブラリを使用することなく、独自のプロンプトを作成する方法の例については、「ユーザー入力を収集するために独自のプロンプトを作成する」の記事を参照してください。

Note

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

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

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

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

前提条件

このサンプルについて

マルチターン プロンプト サンプルでは、ウォーターフォール ダイアログ、いくつかのプロンプト、コンポーネント ダイアログを使用して、ユーザーに一連の質問を行う線形型の対話を作成します。 コードはダイアログを使用して、これらの手順を順番に切り替えます。

手順 プロンプトの種類
移動手段をユーザーに聞きます 選択プロンプト
名前をユーザーに聞きます テキスト プロンプト
年齢を指定するかどうかをユーザーに聞きます 確認プロンプト
「はい」と回答した場合は、年齢を聞きます。 0 より大きく 150 未満の年齢のみを受け入れる検証を行う数値プロンプト
Microsoft Teams を使用していない場合は、プロファイル画像を依頼します 添付ファイルがないことを許可する検証を行う添付ファイル プロンプト
収集された情報が正しいかどうかを聞きます 再利用確認プロンプト

最後に、回答が「はい」なら、収集された情報を表示します。それ以外の場合は、ユーザー情報が保持されないことをユーザーに通知します。

メイン ダイアログを作成する

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

ボットは UserProfileDialog を介してユーザーと対話します。 ボットの DialogBot クラスを作成するときに、UserProfileDialog がメイン ダイアログとして設定されます。 その後、ボットは Run ヘルパー メソッドを使用して、ダイアログにアクセスします。

Class diagram for the C# sample.

Dialogs\UserProfileDialog.cs

最初に、ComponentDialog クラスから派生し、7 つのステップがある UserProfileDialog を作成します。

UserProfileDialog コンストラクターで、ウォーターフォール ステップ、プロンプト、およびウォーターフォール ダイアログを作成し、ダイアログ セットに追加します。 プロンプトは、それが使用されるダイアログ セットに追加する必要があります。

public UserProfileDialog(UserState userState)
    : base(nameof(UserProfileDialog))
{
    _userProfileAccessor = userState.CreateProperty<UserProfile>("UserProfile");

    // This array defines how the Waterfall will execute.
    var waterfallSteps = new WaterfallStep[]
    {
        TransportStepAsync,
        NameStepAsync,
        NameConfirmStepAsync,
        AgeStepAsync,
        PictureStepAsync,
        ConfirmStepAsync,
        SummaryStepAsync,
    };

    // Add named dialogs to the DialogSet. These names are saved in the dialog state.
    AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps));
    AddDialog(new TextPrompt(nameof(TextPrompt)));
    AddDialog(new NumberPrompt<int>(nameof(NumberPrompt<int>), AgePromptValidatorAsync));
    AddDialog(new ChoicePrompt(nameof(ChoicePrompt)));
    AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt)));
    AddDialog(new AttachmentPrompt(nameof(AttachmentPrompt), PicturePromptValidatorAsync));

    // The initial child Dialog to run.
    InitialDialogId = nameof(WaterfallDialog);
}

次に、ダイアログが入力を求めるために使用する手順を追加します。 プロンプトを使用するには、そのプロンプトをご自身のダイアログのステップから呼び出し、stepContext.Result を使用して、次のステップでプロンプトの結果を取得します。 バックグラウンドでは、プロンプトは 2 つのステップから成るダイアログです。 最初に、プロンプトで入力が求められます。 その後、有効な値を返すします。それ以外の場合は最初からやり直し、有効な入力を受信するまでユーザーに再入力を要求します。

ウォーターフォール ステップからは常に null 以外の DialogTurnResult を返す必要があります。 そうしないと、ダイアログは設計どおりに機能しません。 以下に、ウォーターフォール ダイアログの NameStepAsync に対する実装を示します。

private static async Task<DialogTurnResult> NameStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    stepContext.Values["transport"] = ((FoundChoice)stepContext.Result).Value;

    return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = MessageFactory.Text("Please enter your name.") }, cancellationToken);
}

AgeStepAsync で、ユーザー入力を検証できず、その理由が入力を検証できない原因は、その入力がプロンプトで解析できない形式であるか、検証基準を満たしていないかのいずれかである場合の、再試行プロンプトを指定します。 この場合、再試行プロンプトが指定されていないと、プロンプトは最初のプロンプト テキストを使用して、再びユーザーに入力を求めます。

private async Task<DialogTurnResult> AgeStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    if ((bool)stepContext.Result)
    {
        // User said "yes" so we will be prompting for the age.
        // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
        var promptOptions = new PromptOptions
        {
            Prompt = MessageFactory.Text("Please enter your age."),
            RetryPrompt = MessageFactory.Text("The value entered must be greater than 0 and less than 150."),
        };

        return await stepContext.PromptAsync(nameof(NumberPrompt<int>), promptOptions, cancellationToken);
    }
    else
    {
        // User said "no" so we will skip the next step. Give -1 as the age.
        return await stepContext.NextAsync(-1, cancellationToken);
    }
}

UserProfile.cs

ユーザーの移動手段、名前、および年齢は UserProfile クラスのインスタンスに保存されています。

public class UserProfile
{
    public string Transport { get; set; }

    public string Name { get; set; }

    public int Age { get; set; }

    public Attachment Picture { get; set; }
}

Dialogs\UserProfileDialog.cs

最後のステップで、前のウォーターフォール ステップで呼び出されたダイアログによって返された stepContext.Result を確認します。 戻り値が true の場合は、ユーザー プロファイル アクセサーを使用して、ユーザー プロファイルを取得し、更新します。 ユーザー プロファイルを取得するには、GetAsync を呼び出した後に、userProfile.TransportuserProfile.NameuserProfile.AgeuserProfile.Picture の各プロパティの値を設定します。 最後に、ダイアログを終了する EndDialogAsync を呼び出す前に、ユーザーの情報をまとめます。 ダイアログを終了すると、そのダイアログはダイアログ スタックから取り出され、ダイアログの親に省略可能な結果が返されます。 この親は、終了したばかりのダイアログを開始したダイアログまたはメソッドです。

private async Task<DialogTurnResult> SummaryStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    if ((bool)stepContext.Result)
    {
        // Get the current profile object from user state.
        var userProfile = await _userProfileAccessor.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);

        userProfile.Transport = (string)stepContext.Values["transport"];
        userProfile.Name = (string)stepContext.Values["name"];
        userProfile.Age = (int)stepContext.Values["age"];
        userProfile.Picture = (Attachment)stepContext.Values["picture"];

        var msg = $"I have your mode of transport as {userProfile.Transport} and your name as {userProfile.Name}";

        if (userProfile.Age != -1)
        {
            msg += $" and your age as {userProfile.Age}";
        }

        msg += ".";

        await stepContext.Context.SendActivityAsync(MessageFactory.Text(msg), cancellationToken);

        if (userProfile.Picture != null)
        {
            try
            {
                await stepContext.Context.SendActivityAsync(MessageFactory.Attachment(userProfile.Picture, "This is your profile picture."), cancellationToken);
            }
            catch
            {
                await stepContext.Context.SendActivityAsync(MessageFactory.Text("A profile picture was saved but could not be displayed here."), cancellationToken);
            }
        }
    }
    else
    {
        await stepContext.Context.SendActivityAsync(MessageFactory.Text("Thanks. Your profile will not be kept."), cancellationToken);
    }

    // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is the end.
    return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
}

ダイアログを実行する

Bots\DialogBot.cs

OnMessageActivityAsync ハンドラーでは、ダイアログの開始または続行に RunAsync メソッドが使用されます。 OnTurnAsync では、ボットの状態管理オブジェクトを使用して、ストレージに対するすべての状態変更を保持します。 ActivityHandler.OnTurnAsync メソッドは、OnMessageActivityAsync など、さまざまなアクティビティ ハンドラー メソッドを呼び出します。 このようにして、メッセージ ハンドラーが完了した後で、かつターン自体が完了する前の状態を保存します。

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);
    await UserState.SaveChangesAsync(turnContext, false, cancellationToken);
}

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
    Logger.LogInformation("Running dialog with Message Activity.");

    // Run the Dialog with the new message Activity.
    await Dialog.RunAsync(turnContext, ConversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
}

ボット用のサービスを登録する

このボットでは、次のサービスを使用します。

  • ボット用の基本サービス: 資格情報プロバイダー、アダプター、およびボット実装。
  • 状態を管理するためのサービス: ストレージ、ユーザー状態、および会話の状態。
  • ボットで使用されるダイアログ。

Startup.cs

Startup でボット用のサービスを登録します。 これらのサービスは、依存関係の挿入を通じてコードの他の部分で使用できます。

{
    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient().AddControllers().AddNewtonsoftJson(options =>
        {
            options.SerializerSettings.MaxDepth = HttpHelper.BotMessageSerializerSettings.MaxDepth;
        });

        // Create the Bot Framework Authentication to be used with the Bot Adapter.
        services.AddSingleton<BotFrameworkAuthentication, ConfigurationBotFrameworkAuthentication>();

        // Create the Bot Adapter with error handling enabled.
        services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();

        // Create the storage we'll be using for User and Conversation state. (Memory is great for testing purposes.)
        services.AddSingleton<IStorage, MemoryStorage>();

        // Create the User state. (Used in this bot's Dialog implementation.)
        services.AddSingleton<UserState>();

        // Create the Conversation state. (Used by the Dialog system itself.)
        services.AddSingleton<ConversationState>();

Note

メモリ ストレージはテストにのみ使用され、実稼働を目的としたものではありません。 運用環境のボットでは、必ず永続タイプのストレージを使用してください。

ボットをテストする

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

An example transcript of a conversation with the multi-turn prompt bot.

追加情報

ダイアログとボットの状態について

このボットでは、2 つの状態プロパティ アクセサーが定義されています。

  • 1 つはダイアログ状態プロパティ用の会話状態内で作成されます。 ダイアログ状態が追跡するのは、ダイアログ セットのダイアログ内におけるユーザーの場所です。この状態は、begin dialog メソッドや continue dialog メソッドを呼び出すときなど、ダイアログ コンテキストによって更新されます。
  • もう 1 つはユーザー プロファイル プロパティ用のユーザー状態内で作成されます。 これは、保持しているユーザー情報を追跡するためにボットによって使用されます。この状態は、ダイアログ コードで明示的に管理しなければなりません。

状態管理オブジェクトのキャッシュのプロパティ値は、状態プロパティ アクセサーの get メソッドおよび set メソッドによって取得および設定されます。 キャッシュはターンで状態プロパティの値が最初に要求されたときに設定されますが、明示的に保持する必要があります。 この両方の状態プロパティに対する変更を保持するために、対応する状態管理オブジェクトの save changes メソッド呼び出しが実行されます。

このサンプルでは、ダイアログ内からユーザー プロファイルの状態が更新されます。 この方法は一部のボットには有効ですが、複数ボットにわたりダイアログを再利用する場合は機能しません。

ダイアログ ステップとボットの状態を別々に維持するオプションには、さまざまな種類があります。 たとえば、お使いのダイアログで完全な情報を収集すると、以下を行うことができます。

  • end dialog メソッドを使用して、収集したデータを戻り値として親コンテキストに提供する。 これは、ボットのターン ハンドラーでもダイアログ スタック上の以前のアクティブ ダイアログでもかまいません。これは、プロンプト クラスの設計上の仕組みです。
  • 適切なサービスへの要求を生成する。 お使いのボットが大規模なサービスへのフロントエンドとして動作している場合にうまく機能することがあります。

プロンプト検証コントロールのメソッドの定義

UserProfileDialog.cs

以下に、AgePromptValidatorAsync メソッドの定義用の検証コントロールのコード例を示します。 promptContext.Recognized.Value には、解析済みの値が格納されます。数値のプロンプトの場合、この値は整数になります。 promptContext.Recognized.Succeeded は、プロンプトがユーザーの入力を解析できたかどうかを示します。 その値が受け入れられなかったことを示すために、検証コントロールは false を返し、プロンプト ダイアログは、ユーザーに再度入力を要求します。それ以外の場合は、true を返して入力を受け取り、プロンプト ダイアログから復帰します。 検証コントロールの値は、実際のシナリオに応じて変更することができます。

private static Task<bool> AgePromptValidatorAsync(PromptValidatorContext<int> promptContext, CancellationToken cancellationToken)
{
    // This condition is our validation rule. You can also change the value at this point.
    return Task.FromResult(promptContext.Recognized.Succeeded && promptContext.Recognized.Value > 0 && promptContext.Recognized.Value < 150);
}

次のステップ