本記事では Bot Builder v4 のダイアログとステート管理について見ていきます。ダイアログについては、まずは単純なものを見ていき、今後の記事でより複雑なダイアログを見ていきます。
ダイアログ
ダイアログは Bot Builder でユーザーとボットの会話を管理する際に中心となる概念です。ダイアログはユーザーからの入力を処理して、次の出力を生成し、その過程を管理します。
例えばユーザープロファイルを作る場面で、ボットは名前、誕生日、仕事など複数の質問を行い、対応するユーザーの回答を記憶、最後にデータベースなどに情報を格納するという動作を行います。この際、以下のような内容を管理する必要があります。
- ユーザーの入力が、どの質問に対応するものかを理解
- ユーザー入力の検証と記憶
- 次の質問を正しく選択して、ユーザーへ送信
- 最後の質問に対する回答を得た場合、データベースの保存
Bot Builder ではダイアログクラスが以下の様に用意されています。
プロンプト
プロンプトは最小単位のダイアログであり、ユーザーからの入力に対して型チェックを行う機能を提供します。
- テキスト型
- 数値型
- ブール型
- リストからの選択肢
- 添付ファイル
- 日付型
またプロンプトは以下の性質があります。
- 入力データの型を確認するだけで、ユーザーに送る文字列は別途指定
- 上記理由からプロンプト単体は再利用が可能
- ユーザーに対する送信と、ユーザーからの入力の2ステップで完結するため、ステート管理が必須
DialogSet
複数のダイアログをまとめて管理する機能として DialogSet があります。ダイアログの状態を管理するためには、DialogSet を、後述する DialogState を引数に作成して利用します。
以下の場合は、3 つのプロンプトダイアログを、それぞれ name, age, birthday として追加しています。
DialogSet dialogs = new DialogSet(accessors.ConversationDialogState);
dialogs.Add(new TextPrompt("name"));
dialogs.Add(new NumberPrompt("age"));
dialogs.Add(new DatePrompt("birthday"));
追加の順番と実行の順番には関係がなく、取り出す際には指定した名前 (id) を使います。
DialogContext とダイアログの実行
ダイアログの実行に必要な情報を持ったコンテキストで、DialogSet に TurnContext を渡して作成します。また通常のターンに該当するもので DialogTurn という概念があり、ダイアログ全体の処理を 1 ターンをして捉えます。
var dialogContext = await dialogs.CreateContextAsync(turnContext, cancellationToken);
また DialogContext は以下のダイアログ実行メソッドをサポートします。
- BeginDialogAsync
- ContinueDialogAsync
- ResumeDialogAsync
- PromptAsync
ContinueDialogAsync
実装パターンとして、まず ContinueDialogAsync メソッドを実行します。既に開始しているダイアログがある場合は継続して実行され、ない場合は DialogTurnStatus.Empty が返ってきます。
var results = await dialogContext.ContinueDialogAsync(cancellationToken);
BeginDialogAsync
DialogTurnStatus.Empty が返ってきた場合は、新規にダイアログを開始するため、BeginDialogAsync を実行します。
await dialogContext.BeginDialogAsync("name",
// ユーザーに対する表示
new PromptOptions { Prompt = MessageFactory.Text("名前を入力してください") },
cancellationToken);
ResumeDialogAsync
ネストされたダイアログを利用する場合、子ダイアログが終わった際、親ダイアログで呼ばれます。今後の記事で詳細は紹介します。
PromptAsync
BeginDialogAsync のラッパー
public async Task<DialogTurnResult> PromptAsync(string dialogId, PromptOptions options, CancellationToken cancellationToken = default(CancellationToken))
{
if (string.IsNullOrEmpty(dialogId))
{
throw new ArgumentNullException(nameof(dialogId));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
return await BeginDialogAsync(dialogId, options, cancellationToken).ConfigureAwait(false);
}
ステート管理
ダイアログが成立するためには、会話の状態を保存するためのステートを管理する必要があります。Bot Builder では以下 2 種類のステートを定義しています。
- UserState : ユーザー単位のステート (複数の会話にまたがるステート)
- ConversationState : 会話単位のステート (特定の会話に関係するステート)
ステートにはシリアライズできるオブジェクトを保存します。またデータの保存先として、SDK レベルで以下の 3 パターンをサポートします。
- インメモリ (開発用)
- Azure テーブル
- Azure Cosmos DB
Azure のデータストアについては Microsoft.Bot.Builder.Azure モジュールで提供されます。
ダイアログステート
ダイアログのステートは ConversationState に DialogState 型として保存します。
アクセサー
ステートにデータを保存したり、保存済のデータを取得するには、IStatePropertyAccessor を使います。インターフェースでは Get/Set/Delete のメソッドがそれぞれ定義されていて、容易に情報を扱うことが可能です。BotBuilder ではアクセサー専用の UserState、ConversationState および IStatePropertyAccessor を持つクラスを定義して、IoC コンテナに登録して使うパターンがよく使われます。
以下の開発でも、このパターンを使った実装を行います。
プロンプトを使ったボットの開発
ここでは前回開発した「オウム返し」ボットを拡張して、名前を聞くボットを実装します。また現在の会話において、ユーザーとやり取りした回数を数える機能を追加します。
NuGet パッケージの追加
ダイアログ機能は Microsoft.Bot.Builder.Dialogs で提供されるため、VSCode の統合ターミナルより以下コマンドを実行。
dotnet add package Microsoft.Bot.Builder.Dialogs
dotnet restore
開発
1. MyStateAccessors.cs ファイルを追加し、クラス定義を追加。
2. コードを以下のものと差し替え。クラスに ConversationState プロパティを持ち、またダイアログを記録するプロパティを IStatePropertyAccessor として定義。
using System;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
public class MyStateAccessors{
public MyStateAccessors(
ConversationState conversationState
)
{
ConversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState));
}
public IStatePropertyAccessor<DialogState> ConversationDialogState { get; set; }
public ConversationState ConversationState { get; }
}
3. Startup.cs の ConfigurationServices に記述した AddBot を以下の様に変更。
services.AddBot<MyBot>(options =>
{
options.Middleware.Add(new MyLoggingMiddleware());
options.Middleware.Add(new MyMiddleware());
// ストレージとしてインメモリを利用
IStorage dataStore = new MemoryStorage();
// それぞれのステートを作成
var conversationState = new ConversationState(dataStore);
// オプションに追加
options.State.Add(conversationState);
});
4. 同じく ConfigurationServices メソッド内で MyStateAccessors を IoC に登録。これで動的に MyStateAccessors を取得可能。
// MyStateAccessors を IoC に登録
services.AddSingleton(sp =>
{
// AddBot で登録した options を取得。
var options = sp.GetRequiredService<IOptions<BotFrameworkOptions>>().Value;
if (options == null)
{
throw new InvalidOperationException("BotFrameworkOptions を事前に構成してください。");
}
var conversationState = options.State.OfType<ConversationState>().FirstOrDefault();
if (conversationState == null)
{
throw new InvalidOperationException("ConversationState を事前に定義してください。");
}
var accessors = new MyStateAccessors(conversationState)
{
// DialogState を ConversationState のプロパティとして設定
ConversationDialogState = conversationState.CreateProperty<DialogState>("DialogState"),
};
return accessors;
});
5. MyBot.cs に using を追加。
using Microsoft.Bot.Builder.Dialogs;
6. コンストラクターとクラスプロパティを以下の様に変更。引数として指定した MyStateAccessors は DI として自動解決される。
private MyStateAccessors accessors;
private DialogSet dialogs;
public MyBot(MyStateAccessors accessors)
{
this.accessors = accessors;
this.dialogs = new DialogSet(accessors.ConversationDialogState);
// テキスト型のプロンプトとして id=name で作成
dialogs.Add(new TextPrompt("name"));
}
7. OnTurnAsync メソッドを以下の様に変更。ユーザーに対するプロンプトの表示は、MessageFactory を使用。
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)
{
// id=name のプロンプトを送信
await dialogContext.PromptAsync(
"name",
new PromptOptions { Prompt = MessageFactory.Text("名前を入力してください") }, // ユーザーに対する表示
cancellationToken);
}
// DialogTurnStatus が Complete の場合、ダイアログは完了したため結果を処理
else if (results.Status == DialogTurnStatus.Complete)
{
if (results.Result != null)
{
await turnContext.SendActivityAsync(MessageFactory.Text($"ようこそ '{results.Result}' さん!"));
}
}
// 最後に現在のダイアログステートを保存
await accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
}
}
8. デバッグ実行して、Bot Framework Emulator で動作を確認。
まとめ
今回はまず単純なダイアログとステートの管理について見ていきました。一番シンプルに名前を聞くだけでも、ボットしてはステート管理する必要がありますが、BotBuilder を使うと簡単に実装出来ます。次回はより高度なダイアログを見ていきます。