LoginSignup
6
1

More than 5 years have passed since last update.

Bot Builder v4 でボット開発 : より高度なダイアログ

Last updated at Posted at 2018-10-13

前回は単純なダイアログを紹介しましたが、今回はより複雑なダイアログを見ていきます。

ウォーターフォールダイアログ

複数のステップを順次処理する用のダイアログ。各ステップでは以下のような処理が行われる。

  • プロンプトをユーザーに送信
  • ユーザーからの入力を処理
  • ウォーターフォール処理の完了

waterfall

ウォーターフォールステップ

ウォーターフォールダイアログはウォーターフォールステップと呼ばれるメソッドを実行順に持ちます。ウォーターフォールステップは以下のシグネチャを持っています。

Task<DialogTurnResult> StepName(WaterfallStepContext stepContext, CancellationToken cancellationToken)

これらのステップを WaterfallStep 配列に設定して、ウォーターフォールダイアログを作成します。

var waterfallSteps = new WaterfallStep[]
{
    Step1,
    Step2,
    Step3,
    ....
};

dialogs.Add(new WaterfallDialog("dialog_name", waterfallSteps));

WaterfallStepContext

DialogContext を継承しており、前処理の結果などウォーターフォールダイアログ実行に必要な情報を含んでいます。

GitHub : WaterfallStepContext

コンポーネントダイアログ

再利用可能なダイアログを作成するための仕組みとして、コンポーネントダイアログが提供されています。DialogSet を独自に持てるため、ダイアログ名などの競合を避けることができる他、DialogContext も分離されます。

ウォーターフォールダイアログを使ったボットの開発

前回は名前を聞くプロンプトダイアログを実装しましたが、今回はウォーターフォールダイアログを使って、ユーザープロファイルを聞くボットを実装します。

モデルの追加とステート管理の更新

まずはユーザープロファイル用のクラス追加とアクセサーを更新します。

1. Models フォルダを追加して、UserProfile.cs を追加。以下のコードを追加。

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

    public int Age { get; set; }
}

2. MyStateAccessors.cs ファイルを以下コードで差し替え。

using System;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;

public class MyStateAccessors
{
        public MyStateAccessors(
            UserState userState,
            ConversationState conversationState)
        {
            UserState = userState ?? throw new ArgumentNullException(nameof(userState));
            ConversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState));
        }

        public IStatePropertyAccessor<UserProfile> UserProfile { get; set; }
        public IStatePropertyAccessor<DialogState> ConversationDialogState { get; set; }
        public UserState UserState { get; }
        public ConversationState ConversationState { get; }
}

3. Startup.cs の ConfigureServices を以下の様に変更してアクセサーの変更を反映。

public void ConfigureServices(IServiceCollection services)
{
    services.AddBot<MyBot>(options =>
    {
        options.Middleware.Add(new MyLoggingMiddleware());
        options.Middleware.Add(new MyMiddleware());

        // ストレージとしてインメモリを利用
        IStorage dataStore = new MemoryStorage();
        var userState = new UserState(dataStore);
        var conversationState = new ConversationState(dataStore);
        options.State.Add(userState);
        options.State.Add(conversationState);
    });

    // MyStateAccessors を IoC に登録
    services.AddSingleton(sp =>
    {
        // AddBot で登録した options を取得。
        var options = sp.GetRequiredService<IOptions<BotFrameworkOptions>>().Value;
        if (options == null)
        {
            throw new InvalidOperationException("BotFrameworkOptions を事前に構成してください。");
        }
        var userState = options.State.OfType<UserState>().FirstOrDefault();
        if (userState == null)
        {
            throw new InvalidOperationException("UserState を事前に定義してください。");
        }

        var conversationState = options.State.OfType<ConversationState>().FirstOrDefault();
        if (conversationState == null)
        {
            throw new InvalidOperationException("ConversationState を事前に定義してください。");
        }

        var accessors = new MyStateAccessors(userState, conversationState)
        {
            // DialogState を作成
            ConversationDialogState = conversationState.CreateProperty<DialogState>("DialogState"),
            // UserProfile を作成
            UserProfile = userState.CreateProperty<UserProfile>("UserProfile")
        };

        return accessors;
    });
}

ウォーターフォールダイアログの実装

次にウォーターフォールダイアログを使ったユーザープロファイル取得のコードを実装します。

1. MyBot.cs の MyBot クラスに以下メソッドを追加。それぞれがウォーターフォールダイアログのステップとなる。

private async Task<DialogTurnResult> NameStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // "name" ダイアログ(プロンプト) を返信。
    // ユーザーに対する表示は PromptOptions で指定。
    return await stepContext.PromptAsync("name", new PromptOptions { Prompt = MessageFactory.Text("名前を入力してください。") }, cancellationToken);
}

private async Task<DialogTurnResult> NameConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // UserProfile をステートより取得
    var userProfile = await accessors.UserProfile.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);
    // Result より名前の更新
    userProfile.Name = (string)stepContext.Result;
    // 年齢を聞いてもいいか確認のため "confirm" ダイアログを送信。
    return await stepContext.PromptAsync("confirm", new PromptOptions { Prompt = MessageFactory.Text("年齢を聞いてもいいですか?") }, cancellationToken);
}

private async Task<DialogTurnResult> AgeStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // Result より結果の確認
    if ((bool)stepContext.Result)
    {
        // 年齢を聞いてもいい場合は "age" ダイアログを送信
        return await stepContext.PromptAsync("age", new PromptOptions { Prompt = MessageFactory.Text("年齢を入力してください。") }, cancellationToken);
    }
    else
    {
        // "いいえ" を選択した場合、次のステップに進む。"age" ダイアログの結果は "-1" を指定。
        return await stepContext.NextAsync(-1, cancellationToken);
    }
}

private async Task<DialogTurnResult> ConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // 年齢の回答を取得
    var age = (int)stepContext.Result;
    // UserProfile をステートより取得
    var userProfile = await accessors.UserProfile.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);
    // 年齢をスキップしなかった場合はユーザープロファイルに設定
    if (age != -1)
        userProfile.Age = age;
    // 全て正しいか確認。"confirm" ダイアログを再利用。
    var prompt = $"次の情報で登録します。いいですか?{Environment.NewLine} 名前:{userProfile.Name} 年齢:{userProfile.Age}";
    return await stepContext.PromptAsync("confirm", new PromptOptions { Prompt = MessageFactory.Text(prompt) }, cancellationToken);
}

private async Task<DialogTurnResult> SummaryStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    if ((bool)stepContext.Result)
        await stepContext.Context.SendActivityAsync(MessageFactory.Text("プロファイルを保存します。"));
    else
        await stepContext.Context.SendActivityAsync(MessageFactory.Text("プロファイルを破棄します。"));

    // ダイアログを終了
    return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
}

2. コンストラクターを変更してウォーターフォールダイアログを定義。

public MyBot(MyStateAccessors accessors)
{
    this.accessors = accessors;
    this.dialogs = new DialogSet(accessors.ConversationDialogState);

    // ウォーターフォールのステップを定義。処理順にメソッドを追加。
    var waterfallSteps = new WaterfallStep[]
    {
        NameStepAsync,
        NameConfirmStepAsync,
        AgeStepAsync,
        ConfirmStepAsync,
        SummaryStepAsync,
    };

    // ウォーターフォールダイアログと各種プロンプトを追加
    dialogs.Add(new WaterfallDialog("profile", waterfallSteps));
    dialogs.Add(new TextPrompt("name"));
    dialogs.Add(new NumberPrompt<int>("age"));
    dialogs.Add(new ConfirmPrompt("confirm"));
}

3. OnTurnAsync で profile ダイアログを実行するように変更。

public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
    if (turnContext.Activity.Type == ActivityTypes.Message)
    {
        // DialogSet からコンテキストを作成
        var dialogContext = await dialogs.CreateContextAsync(turnContext, cancellationToken);
        // まず ContinueDialogAsync を実行して既存のダイアログがあれば継続実行。
        var results = await dialogContext.ContinueDialogAsync(cancellationToken);

        // DialogTurnStatus が Empty の場合は既存のダイアログがないため、新規に実行
        if (results.Status == DialogTurnStatus.Empty)
        {
            // ウォーターフォールダイアログを送信
            await dialogContext.BeginDialogAsync("profile", null, cancellationToken);
        }
        // DialogTurnStatus が Complete の場合、ダイアログは完了したため結果を処理
        else if (results.Status == DialogTurnStatus.Complete)
        {
            var userProfile = await accessors.UserProfile.GetAsync(turnContext);
            await turnContext.SendActivityAsync(MessageFactory.Text($"ようこそ '{userProfile.Name}' さん!"));
        }
        // 最後に現在の UserProfile と DialogState を保存
        await accessors.UserState.SaveChangesAsync(turnContext, false, cancellationToken);
        await accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
    }
}

4. デバッグを開始して BotFramework Emulator で動作を確認。
image.png

コンポーネントダイアログを使ったボットの開発

ここでは、上記で実装したウォーターフォールダイアログをコンポーネントダイアログとして再利用しやすい形に変えてみます。

1. ProfileDialog.cs を追加して、以下のコードと差し替え。内容は上記で実装したウォーターフォールダイアログとほぼ同じだが、ComponentDialog を継承しており、ダイアログ ID として nameof(ProfileDialog) を使用。また DialogSet は利用せず、AddDialog メソッドで各ダイアログを追加。

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;

public class ProfileDialog : ComponentDialog
{
    private MyStateAccessors accessors;

    public ProfileDialog(MyStateAccessors accessors) : base(nameof(ProfileDialog))
    {
        this.accessors = accessors;

        // ウォーターフォールのステップを定義。処理順にメソッドを追加。
        var waterfallSteps = new WaterfallStep[]
        {
            NameStepAsync,
            NameConfirmStepAsync,
            AgeStepAsync,
            ConfirmStepAsync,
            SummaryStepAsync,
        };

        // ウォーターフォールダイアログと各種プロンプトを追加
        AddDialog(new WaterfallDialog("profile", waterfallSteps));
        AddDialog(new TextPrompt("name"));
        AddDialog(new NumberPrompt<int>("age"));
        AddDialog(new ConfirmPrompt("confirm"));
    }

    private async Task<DialogTurnResult> NameStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // "name" ダイアログ(プロンプト) を返信。
        // ユーザーに対する表示は PromptOptions で指定。
        return await stepContext.PromptAsync("name", new PromptOptions { Prompt = MessageFactory.Text("名前を入力してください。") }, cancellationToken);
    }

    private async Task<DialogTurnResult> NameConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // UserProfile をステートより取得
        var userProfile = await accessors.UserProfile.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);
        // Result より名前の更新
        userProfile.Name = (string)stepContext.Result;
        // 年齢を聞いてもいいか確認のため "confirm" ダイアログを送信。
        return await stepContext.PromptAsync("confirm", new PromptOptions { Prompt = MessageFactory.Text("年齢を聞いてもいいですか?") }, cancellationToken);
    }

    private async Task<DialogTurnResult> AgeStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // Result より結果の確認
        if ((bool)stepContext.Result)
        {
            // 年齢を聞いてもいい場合は "age" ダイアログを送信
            return await stepContext.PromptAsync("age", new PromptOptions { Prompt = MessageFactory.Text("年齢を入力してください。") }, cancellationToken);
        }
        else
        {
            // "いいえ" を選択した場合、次のステップに進む。"age" ダイアログの結果は "-1" を指定。
            return await stepContext.NextAsync(-1, cancellationToken);
        }
    }

    private async Task<DialogTurnResult> ConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // 年齢の回答を取得
        var age = (int)stepContext.Result;
        // UserProfile をステートより取得
        var userProfile = await accessors.UserProfile.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);
        // 年齢をスキップしなかった場合はユーザープロファイルに設定
        if (age != -1)
            userProfile.Age = age;
        // 全て正しいか確認。"confirm" ダイアログを再利用。
        var prompt = $"次の情報で登録します。いいですか?{Environment.NewLine} 名前:{userProfile.Name} 年齢:{userProfile.Age}";
        return await stepContext.PromptAsync("confirm", new PromptOptions { Prompt = MessageFactory.Text(prompt) }, cancellationToken);
    }

    private async Task<DialogTurnResult> SummaryStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        if ((bool)stepContext.Result)
            await stepContext.Context.SendActivityAsync(MessageFactory.Text("プロファイルを保存します。"));
        else
            await stepContext.Context.SendActivityAsync(MessageFactory.Text("プロファイルを破棄します。"));

        // ダイアログを終了
        return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
    }
}

2. MyBot.cs をコンポーネントダイアログを呼び出すように変更。

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
public class MyBot : IBot
{
    private MyStateAccessors accessors;
    private DialogSet dialogs;
    // DI で MyStateAccessors は自動解決
    public MyBot(MyStateAccessors accessors)
    {
        this.accessors = accessors;
        this.dialogs = new DialogSet(accessors.ConversationDialogState);

        // コンポーネントダイアログを追加
        dialogs.Add(new ProfileDialog(accessors));      
    }

    public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
    {
        if (turnContext.Activity.Type == ActivityTypes.Message)
        {
            // DialogSet からコンテキストを作成
            var dialogContext = await dialogs.CreateContextAsync(turnContext, cancellationToken);
            // まず ContinueDialogAsync を実行して既存のダイアログがあれば継続実行。
            var results = await dialogContext.ContinueDialogAsync(cancellationToken);

            // DialogTurnStatus が Empty の場合は既存のダイアログがないため、新規に実行
            if (results.Status == DialogTurnStatus.Empty)
            {
                // コンポーネントダイアログを送信
                await dialogContext.BeginDialogAsync(nameof(ProfileDialog), null, cancellationToken);
            }
            // DialogTurnStatus が Complete の場合、ダイアログは完了したため結果を処理
            else if (results.Status == DialogTurnStatus.Complete)
            {
                var userProfile = await accessors.UserProfile.GetAsync(turnContext, () => new UserProfile(), cancellationToken);
                await turnContext.SendActivityAsync(MessageFactory.Text($"ようこそ '{userProfile.Name}' さん!"));
            }
            // 最後に現在の UserProfile と DialogState を保存
            await accessors.UserState.SaveChangesAsync(turnContext, false, cancellationToken);
            await accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
        }
    }    
}

3. BotFramework Emulator で動作を確認。

まとめ

今回はウォーターフォールダイアログとコンポーネントダイアログの使い方を見ていきました。またプロンプトの再利用シナリオもカバーしたため、より柔軟なダイアログを作れるようになりました。次回は複数ダイアログの管理について見ていきます。

次の記事へ
目次へ戻る

この記事のサンプルコード

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1