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

適用対象: SDK v4

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

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

この記事では、プロンプトを作成し、ウォーターフォール ダイアログから呼び出すことで、単純な会話フローを実装する方法について説明します。

ヒント

ダイアログ ライブラリを使用することなく、独自のプロンプトを作成する方法の例については、「ユーザー入力を収集するために独自のプロンプトを作成する」の記事を参照してください。

前提条件

このサンプルについて

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

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

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

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

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

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

C# user profile dialog

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,
        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.Transport値を userProfile.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();

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

        // The Dialog that will be run by the bot.
        services.AddSingleton<UserProfileDialog>();

注意

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

ボットをテストする

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

Sample run of the multi-turn prompt dialog

関連情報

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

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

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

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

このサンプルでは、ダイアログ内からユーザー プロファイルの状態が更新されます。 この方法は単純なボットでも機能しますが、ボット間でダイアログを再利用する場合は機能しません。

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

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

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

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

次のステップ