Dialog を使わない会話管理
Bot Builder SDK の UserState や ConversationState を使う と、ユーザーや会話ごとの状態管理を行うことできるため、複数回のやりとりが必要な会話を実現することができます。
例えば、下記のような会話ができるボットを実装してみます。
User: こんにちは
Bot: あなたの名前は?
User: まく
Bot: こんにちは まく さん
User: おやすみなさい
Bot: また来てね まく さん
次のボット実装は、UserState
クラスを使って、pos
という名前のプロパティを作成し、会話がどこまで進んでいるかを管理しています。
会話の流れに従って、pos
プロパティの値は init
→ askName
→ end
と変化していきます(最後に init
に戻る)。
このように、UserState
を使った状態管理だけでも、会話の流れを表現することはできるのですが、こんな状態管理を各トピックごとに実装するのは非常に骨が折れます。
そこで Bot Builder SDK は、Dialog
という会話の流れを簡潔に実装するための便利クラスを用意しています。
Dialog を使った会話管理
ここでは、順序通り会話を進めるためのダイアログである WaterfallDialog
を使います。
BotBuilder SDK の提供するダイアログ系クラスを使用するには、botbuilder-dialogs
パッケージをインストールしておく必要があります。
下記は、WaterfallDialog
を使って、ユーザーから名前を聞き出すボットの実装例です。
大まかな流れは次のような感じになります。
- ボット起動時に
DialogSet
に必要な Dialog
(会話トピックごとの実装)をセットアップする。 - ユーザーからのメッセージを受信 (
ActvityHandler#onMessage
) するたびに、DialogContext#continueDialog()
を呼び出して、処理を 1 ステップずつ進める。
ポイントは、コンストラクタから呼び出している _setupDialogSet()
メソッドで、この中で DialogSet というダイアログ群を管理するオブジェクトを構築しています。
DialogSet
のコンストラクタには、ダイアログの状態を管理するための、StatePropertyAccessor
オブジェクトを渡します。
このオブジェクトは、UserState#createProperty()
あるいは ConversationState#createProperty()
で作成できます。
DialogSet#add()
を使い、ボットが使用する一連の Dialog
を追加しておく必要があります。
ここでは、SDK が提供する WaterfallDialog と TextPrompt という Dialog
オブジェクトを追加しています。
TextPrompt
はユーザーにテキストの入力を促すための Dialog
実装です(正確には Dialog
を継承した Prompt
クラスを継承しています)。
TextPrompt
は WaterfallDialog
の処理の中から使用するのですが、後から呼び出すために名前を付けて DialogSet
に追加しておく必要があります。
名前は DialogSet
中で一意であれば OK です(ここでは、askName
という名前を付けています)。
起点となる WaterfallDialog
の方も名前を付けて DialogSet
に追加します。
そして、第2引数のコールバック配列で、各ステップのボット応答を実装します。
各ステップには WaterfallStepContext が渡され、これを使って各ステップをどう進んでいくかを定義します。
上記の例では、最初のステップとして、ユーザーに名前の入力を促すためのプロンプト (TextPrompt
) を表示するように指定しています。
最初のパラメータで、あらかじめ登録しておいた askName
というプロンプト名を指定しています。
ユーザーが名前を入力すると、今度はその次に指定したコールバックが呼び出されます。
このようなステップ連鎖を実装していき、最後に endDialog()
でダイアログ全体の処理を完了します。
step.result
には、前のステップでユーザーが入力したテキストが格納されています。
ここでは、その名前を表示して、全体のダイアログ処理を終了 (step.endDialog()
) しています(実際には、必要に応じて UserState
に保存したりします)。
この WaterfallDialog
を起動するエントリポイントとなっているのは、ボットクラスの onMessage
ハンドラです。
現在アクティブになっているダイアログの処理を 1 ステップずつ進めていくには、ボットクラスの onMessage
が呼び出されるたびに、DialogContext#continueDialog()
を呼び出さなければいけません。
この関数は、戻り値として (DialogTurnResult) オブジェクトを返し、ダイアログの各ステップの結果や、ダイアログ全体の進捗状態を取得することができます。
ダイアログがまだ起動していない(アクティブなダイアログが存在しない)場合は、DialogTurnResult#state
プロパティの値が empty
となるため、このときに DialogContext#beginDialog()
でダイアログを起動するようにします。
これはお決まりの実装パターンです。
最後に忘れてはいけないのが、onDialog
ハンドラの実装です。
onDialog
ハンドラは各ターンの終わりに呼び出されます。
ダイアログの状態(どのステップまで進んだかという情報)は、BotState
オブジェクトで管理されるので、ここで忘れずに保存するようにします。
これを忘れると、WaterfallDialog
のステップがいつまでたっても進まなくなります。
Dialog の実装を別クラスに切り出す (ComponentDialog)
ボットクラス(ActivityHandler
を継承したクラス)の中に、WaterfallDialog
などを使用したダイアログ関連の実装を入れてしまうと、コードが煩雑になってしまいます。
ここでは、ダイアログに関する実装を、Dialog
クラスを継承した別クラスとして抽出します。
ダイアログクラス側の実装
ダイアログクラスは、ComponentDialog を継承して作成するのが楽です。
ComponentDialog
は Dialog
を継承したクラスで、addDialog()
を使って自分自身が複数の Dialog
(あるいは Prompt
)を子ダイアログとして保持することができるようになっています。
下記の UserProfileDialog
クラスは、ユーザーの名前と年齢を収集する WaterfallDialog
を含んだダイアログクラスの実装例です。
間接的に Dialog
クラスを継承しているため、DialogSet
に格納して起動することができます。
WaterfallDialog
の各ステップで渡される WaterfallStepContext
パラメータの result
プロパティには、前のステップでユーザーから取得した値が含まれています。
values
プロパティ(連想配列)に、その値を格納しておくと、次のステップへ値を引き継いでいくことができます。
step.values['name'] = step.result;
最後のステップでダイアログを終了させるときに、収集した値を endDialog()
のパラメータとして渡してやることで、ダイアログの起動元へ結果を返すことができます。
return await step.endDialog(step.values);
ボットクラス側の実装
次に、このダイアログクラスを起動するためのボットクラスを作成します。
Microsoft のサンプルコードとして、DialogBot という任意のダイアログを起動するボットクラス実装が紹介されているので、これを参考にすることにします。
サンプルコードでは、ルートの DialogSet
に関する処理を、ダイアログクラス側の run()
メソッドに実装していますが、特にダイアログクラス側に入れる必要はないと思いますので、ここでは DialogBot
側の実装で DialogSet
に関する処理もやってしまうことにします。
ちなみにこの実装では、ダイアログの状態を保存するために、UserState
ではなく ConversationState
を使っていることに注意してください。
用途によっては使用する BotState
オブジェクトを変更する必要があるかもしれません。
この DialogBot
クラスは、任意の Dialog
を起動するための汎用的なクラスとして使用することができます。
やっていることはとてもシンプルで、onMessage
ハンドラの中で、DialogContext#beginDialog()
や DialogContext#continueDialog()
を呼び出して、対象のダイアログのステップを順番に進めているだけです。
DialogContext#continueDialog()
は、ダイアログの進捗具合 (status
) や WaterfallDialog
の各ステップの結果 (result
) を含む DialogTurnResult
オブジェクトを返します。
WaterfallDialog
のすべてのステップが完了すると(endDialog()
が呼び出されると)、status
プロパティは completed
となり、ダイアログ側から endDialog()
で渡された最終結果を result
プロパティで参照することができます(DialogTurnResult
の中に result
プロパティがあるのでちょっとネーミングが気持ち悪い)。
ダイアログ側で収集したユーザー情報は、ダイアログ側の実装で使ってしまえばよいのですが、UserProfileDialog
の動作を確認するために、上記のボットクラスではコンソールおよびユーザーへの返答として表示するようにしています。
おまけ: WaterfallDialog の各ステップの登録方法いろいろ
WaterfallDialog の API ドキュメント にも書いてありますが、各ステップのコールバック関数は、コンストラクタの第 2 引数の配列でまとめて渡す以外にも、次のような設定方法があります。
クロージャーとして 1 つずつ追加する方法
通常の関数を追加する方法(メソッドじゃない関数)
各ステップをメソッド化して this に bind して追加する方法
各ステップをメソッド化してまとめて追加
こんな感じでメソッドで追加する方法がオススメかなぁ。
ダイアログはクラス化して実装したいので。
関連記事