前回は、サンプルコードを少しだけいじった単純なbotを、AzureにあげてSkype上で会話できるようにしました。
今回はBot Builderに用意されているDialogクラスを使って、シンプルな会話を可能にする能力を実装してみたいと思います。
Dialogを使ってみる
Dialogとは「対話」というような意味で、C#のBotBuilderでは対話を簡単に実装するためのインターフェースが提供されています。オリジナルのドキュメントはこちら。
Dialogは、IDialogインターフェースを継承します。
まずは前回作成したbotの、電力使用状況を教えてくれる部分を、Dialogを使用して作り直します。
せっかくなので、Dialogを使って簡単にできる「確認」ステップや、対話の状態を記憶する機能も使ってみます。
「確認」ステップを追加する
PromptDialogクラスを使うと、botに「確認」をするためのワンステップを追加できるようになります。今回作っているbotだと、次のような形になります。
「電力使用状況は?」と聞かれたときに、「~~よろしいですか?」と確認するような返答をします。はい/いいえと答えると、それぞれ用意した応答を返してくれます。
この確認に答えるための語彙はエミュレータのLocaleをja-JPにしていると「はい/いいえ」という日本語で設定されますが、ほかの言語にするとほかの言語で選択肢が用意されます。
このためのコードは次の通りです。前回も使用したMessageController.csに加え、新しくDialog.csというファイルを追加し、そこにSimpleDialogというクラスを定義しています。
MessageController.cs
public async Task<Message> Post([FromBody]Message message) { if (message.Type == "Message") { Message reply = null; if (message.Text == "電力使用状況は?" || message.GetBotPerUserInConversationData<BotState>("state") == BotState.Usage) { message.SetBotPerUserInConversationData("state", BotState.Usage); // Dialog.csで定義したSimpleDialogクラスを呼び出す return await Conversation.SendAsync(message, () => new SimpleDialog()); } else { reply = message.CreateReplyMessage("(。・ ω<)ゞてへぺろ♡"); } return reply; } else { return HandleSystemMessage(message); } }
Dialog.cs
[Serializable] public class SimpleDialog : IDialog<object> { public async Task StartAsync(IDialogContext context) { context.Wait(MessageReceivedAsync); } public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable<Message> argument) { var message = await argument; var usage = GetElectricityUsage(); PromptDialog.Confirm( context, ShowUsageAsync, "東京電力の電力使用状況を取得します。よろしいですか?", "わかりませんでした。「はい」か「いいえ」でお答えください。", promptStyle: PromptStyle.None); } public async Task ShowUsageAsync(IDialogContext context, IAwaitable<bool> argument) { var confirm = await argument; if (confirm) { var usage = GetElectricityUsage(); await context.PostAsync(usage != null ? string.Format("{0}kWです", usage) : "取得できませんでした。。"); } else { await context.PostAsync("では、やめておきますね。。"); } context.PerUserInConversationData.SetValue("state", BotState.Default); context.Wait(MessageReceivedAsync); } private int? GetElectricityUsage() { // 前回と同じなので省略 } }
電力使用状況は?と聞かれると、まずはmessage.SetBotPerUserInConversationData("state", BotState.Usage);の部分でユーザのステートを変更します。
IDialogContext.PerUserInConversationDataには、ユーザーごとのデータが保存されており、ここに現在のステートを保存することでユーザーがメッセージを送ってきた時の挙動を変えています。
使用状況を聞いている状態(=BotState.Usage)だったら、何を言っていてもDialog.csで定義したSimpleDialogに飛ばしており、そうでない場合のみてへぺろを表示するようにしています。
SimpleDialogクラスは、PromptDialog.Confirmという場所で確認ステップを処理しています。確認ステップで得られた答えはShowUsageAsync関数のconfirm関数にbool値として格納されるので、その結果によって答えを返します。
電力使用状況を返しても返さなくても、電力使用状況を聞かれている状況が終わるので、ステートをリセットするのは忘れないようにします。
対話のステートを保持する
Dialogを使用すると、対話のステートも簡単に保持することができます。例えば、電力使用状況を聞かれた時刻を記憶しておき、それを対話の中で使いたいとしましょう。あんまりいい例じゃないんですが、、だいたいこんなイメージです。
これを実現するためのコードは次の通りです。
[Serializable] public class SimpleDialog : IDialog<object> { // 会話を始めた時刻 protected DateTime PrevTime; protected bool ResetTime = false; public async Task StartAsync(IDialogContext context) { context.Wait(MessageReceivedAsync); } public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable<Message> argument) { if (ResetTime) { this.PrevTime = DateTime.Now; this.ResetTime = false; } // 省略 } public async Task ShowUsageAsync(IDialogContext context, IAwaitable<bool> argument) { var confirm = await argument; // 省略 context.PerUserInConversationData.SetValue("state", BotState.Default); this.ResetTime = true; context.Wait(MessageReceivedAsync); } private int? GetElectricityUsage() { // 省略 } }
実は、このSimpleDialogのメンバ変数は、ユーザーとの対話の間失われることなく保存されています。ここではPrevTimeというメンバ変数を定義してそれを使うことによって、ユーザーと以前会話した時の情報を記憶しています。
なぜこの情報が記憶されているかというと、実はこのDialogはエミュレータでも確認できるJSONの形でやりとりされています。botPerUserInConversationDataの中のDialogStateという値を見てもらうと、何やら長い文字列が入っているかと思いますが、それがシリアライズされたDialogクラスです。だからDialogクラスには[Serializable]属性が指定されているんですね。
Tips
Dialogのデバッグをしていると、Dialogを書き換えたのに前の状態が保持されてしまっていて、期待通りの動きをしてくれない場面に遭遇するかと思います。(僕はこれに結構悩まされました)
その場合は、エミュレータのChannelConversationIdという部分の文字列を適当に書き換えてください。
DialogはこのConversationIdごとにシリアライズして保存されているので、このIDを書き換えてしまえばDialogの状態はリセットされるはずです。
今後の流れ
今回は、Dialogを使って簡単な対話なようなものを実装しました。
ソースコードへの変更は、このコミット履歴で確認できます。
次回以降は、FormFlowを使った対話の実装、そしてそのあとはLUISとDialogを使用した対話の実装に挑戦します。
www.madopro.net