次の方法で共有


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

適用対象: SDK v4

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

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

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

注記

AI サービス、オーケストレーション、知識を選択してエージェントを構築するには、Microsoft 365 Agents SDK の使用を検討してください。 Agents SDK では、C#、JavaScript、または Python がサポートされています。 Agents SDK の詳細については、 aka.ms/agents を参照してください。 SaaS ベースのエージェント プラットフォームをお探しの場合は、 Microsoft Copilot Studio を検討してください。 Bot Framework SDK を使用して構築された既存のボットがある場合は、ボットを Agents SDK に更新できます。 Bot Framework SDK から Agents SDK への移行ガイダンスで、主要な変更と更新プログラムを確認できます。 Bot Framework SDK のサポート チケットは、2025 年 12 月 31 日の時点で提供されなくなります。

前提条件

このサンプルについて

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

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

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

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

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

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

C# サンプルのクラス図。

Dialogs\UserProfileDialog.cs

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

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,
        SummaryStepAsync,
        ConfirmStepAsync,
    };

    // 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 を呼び出す前に、ユーザーの情報をまとめます。 ダイアログを終了すると、そのダイアログはダイアログ スタックから取り出され、ダイアログの親に省略可能な結果が返されます。 親とは、終了したばかりのダイアログを開始したダイアログまたはメソッドを指します。

    else
    {
        msg += $" Your profile will not be kept.";
    }

    await stepContext.Context.SendActivityAsync(MessageFactory.Text(msg), 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);
}

private async Task<DialogTurnResult> SummaryStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    stepContext.Values["picture"] = ((IList<Attachment>)stepContext.Result)?.FirstOrDefault();

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

ダイアログを実行する

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

注記

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

ボットをテストする

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

マルチターン プロンプト ボットとの会話のトランスクリプトの例。

追加情報

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

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

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

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

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

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

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

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

UserProfileDialog.cs

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

    }

    // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
    return await stepContext.PromptAsync(nameof(ConfirmPrompt), new PromptOptions { Prompt = MessageFactory.Text("Is this ok?") }, cancellationToken);
}

次のステップ