Chatbotのダイアログ
複数のダイアログを使い分ける
ダイアログは、一つの主(メイン)となる話題を進める途中で、枝分かれした副(サブ)の話題を展開することが可能です。主となるメインダイアログから副となるサブダイアログを呼び出し、サブダイアログを終了するときにはメインダイアログに戻り値を引き渡して、対話を続行することが可能です。
例えばレストランでランチを注文する場合を考えてみましょう。まず初めに料理のコースを決めます。その次にサービスのドリンクを選びます。お店によっては、デザートも選べる場合があるかもしれません。ひとつひとつの枝分かれした内容の中にそれぞれ選択肢があり、決定事項が存在します。ダイアログは、それぞれ一つの話題を取り扱います。つまり、メインのダイアログから分岐して、細かい話題はサブダイアログで取り扱い、最終的にすべての決定事項を組み合わせてランチの注文が完了するということです。
複数のダイアログを組み合わせたランチの注文用のChatbotを作成してみましょう。
using System; using System.Threading.Tasks; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Connector; using System.Collections.Generic; namespace Chatbot201707.Dialogs { [Serializable] public class RootDialog : IDialog<object> { private List<string> menuList = new List<string>() { "ランチコース", "ドリンク", "デザート", "終了" }; private string lunch; private string drink; private string dessert; public Task StartAsync(IDialogContext context) { context.Wait(HelloMessage); return Task.CompletedTask; } private async Task HelloMessage(IDialogContext context, IAwaitable<object> result) { await context.PostAsync("いらっしゃいませ!ご注文を伺います。"); MenuMessage(context); } private void MenuMessage(IDialogContext context) { PromptDialog.Choice(context, SelectDialog, menuList, "ランチメニューをお選びください。"); } private async Task SelectDialog(IDialogContext context, IAwaitable<object> result) { var selectedMenu = await result; switch (selectedMenu) { case "ランチコース": context.Call(new LunchDialog(), LunchResumeAfterDialog); break; case "ドリンク": context.Call(new DrinkDialog(), DrinkResumeAfterDialog); break; case "デザート": context.Call(new DessertDialog(), DessertResumeAfterDialog); break; case "終了": await context.PostAsync("ご注文を承りました。"); context.Wait(HelloMessage); break; } } private async Task LunchResumeAfterDialog(IDialogContext context, IAwaitable<string> result) { lunch = await result; await context.PostAsync($"ランチコースは {lunch} ですね。"); MenuMessage(context); } private async Task DrinkResumeAfterDialog(IDialogContext context, IAwaitable<string> result) { drink = await result; await context.PostAsync($"お飲み物は {drink} ですね。"); MenuMessage(context); } private async Task DessertResumeAfterDialog(IDialogContext context, IAwaitable<string> result) { dessert = await result; await context.PostAsync($"デザートは {dessert} ですね。"); MenuMessage(context); } } }
RootDialog.csを修正します。
private List<string> menuList = new List<string>() { "ランチコース", "ドリンク", "デザート", "終了" };
複数のダイアログを使い分ける場合は、初めに選択肢としてメニューを表示すると使いやすいChatbotになります。メインの対話の選択肢として、「ランチコース、ドリンク、デザート、終了」をList型の文字列として定義しておきます。
private string lunch; private string drink; private string dessert;
この三つの変数は、分岐したダイアログからの戻り値(結果)を格納するために定義しておきます。
HelloMessage
挨拶のメッセージの後にMenuMessage(context)を呼び出して、選択型のプロンプトを表示します。
MenuMessage
Bot FrameworkのBot Builder SDKでは選択肢メニュー用としてPromptDialog.Choiceという便利な機能が用意されています。
PromptDialog.Choice(context, SelectDialog, menuList, "ランチメニューをお選びください。");
最初の引数contextは、スタート時から引き継がれている対話の流れです。二番目の引数には、ユーザーが選択した時の処理を記述したメソッドを指定します。例えば「ランチコースを選んだ場合」、「ドリンクを選んだ場合」のように、switch文で場合分けの処理が可能です。三番目の引数にList形式の文字列を指定すると、メニューの選択肢として表示してくれます。四番目の引数にユーザーに表示するメッセージを記述します。
SelectDialog
ユーザーが選択した内容に応じた処理を記述します。「終了」以外はサブダイアログをcontext.Callメソッドで呼び出し、対話を分岐します。context.Callメソッドの二番目の引数でサブダイアログが終了してメインダイアログに戻った際に、サブダイアログからの戻り値を受け取るなど処理をするメソッドを定義しておきます。「終了」を選択した場合は、context.PostAsyncメソッドで注文確定のメッセージを送信し、context.Wait メソッドでHelloMessageを呼び出して最初に戻ります。
LunchResumeAfterDialog
ランチコースを選択して、サブダイアログのLunchDialogを表示した後、サブダイアログが閉じられてメインダイアログに対話が戻った時の処理を記述しています。
「lunch = await result;」で変数lunchにサブダイアログからの戻り値(選択したランチコース)を格納します。
「await context.PostAsync($"ランチコースは {lunch} ですね。");」で選択内容の確認メッセージを送信します。
「MenuMessage(context);」でMenuMessageメソッドに戻って、他の項目を選択できるよう再度メニューを表示します。
「DrinkResumeAfterDialog」「DessertResumeAfterDialog」も同様にサブダイアログ終了後の処理を記述します。
次に、メインダイアログから呼び出される各サブダイアログのコードを見てみましょう。
using System; using System.Threading.Tasks; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Connector; using System.Collections.Generic; namespace Chatbot201707.Dialogs { [Serializable] public class LunchDialog : IDialog<string> { private List<string> menuList = new List<string>() { "A:チキン南蛮定食", "B:サバの塩焼定食", "C:本日のバスタセット(サラダ・スープ付き)" }; public Task StartAsync(IDialogContext context) { PromptDialog.Choice(context, this.SelectDialog, this.menuList, "ランチコースをお選びください。"); return Task.CompletedTask; } private async Task SelectDialog(IDialogContext context, IAwaitable<object> result) { var selectedMenu = await result; context.Done(selectedMenu); } } }
LunchDialogはランチのコースを選択するサブダイアログです。サブダイアログとはいっても、特にクラスの定義などが異なるわけではなく、 IDialog<T>インターフェースを装備した普通のダイアログのクラスです。メインダイアログと同様にPromptDialog.Choiceを利用して選択メニューを表示しています。
SelectDialog
「var selectedMenu = await result;」でユーザーが選択した項目を取得しています。
メインダイアログと異なるのは、終了時に「context.Done(selectedMenu);」のようにしてメインダイアログに値を引き渡すという点です。context.Doneメソッドでサブダイアログを終了します。引数でselectedMenuを渡すことで、メインダイアログとなるRootDialog.cs のLunchResumeAfterDialogメソッドで「lunch = await result;」として選択内容を取得することができるようになっています。
using System; using System.Threading.Tasks; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Connector; using System.Collections.Generic; namespace Chatbot201707.Dialogs { [Serializable] public class DrinkDialog : IDialog<string> { private List<string> menuList = new List<string>() { "ホットコーヒー", "アイスコーヒー", "オレンジジュース" }; public Task StartAsync(IDialogContext context) { PromptDialog.Choice(context, this.SelectDialog, this.menuList, "ドリンクをお選びください。"); return Task.CompletedTask; } private async Task SelectDialog(IDialogContext context, IAwaitable<object> result) { var selectedMenu = await result; context.Done(selectedMenu); } } }
using System; using System.Threading.Tasks; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Connector; using System.Collections.Generic; namespace Chatbot201707.Dialogs { [Serializable] public class DessertDialog : IDialog<string> { private List<string> menuList = new List<string>() { "季節のフルーツ", "アイスクリーム", "プリン" }; public Task StartAsync(IDialogContext context) { PromptDialog.Choice(context, this.SelectDialog, this.menuList, "デザートをお選びください。"); return Task.CompletedTask; } private async Task SelectDialog(IDialogContext context, IAwaitable<object> result) { var selectedMenu = await result; context.Done(selectedMenu); } } }
LunchDialog.csと同様にサブダイアログとして呼び出される、DrinkDialog.csとDessertDialog.csも、同仕様で作成しています。
メインダイアログと、各サブダイアログが出来上がったら実際に動かしてみましょう。Chatbotで最初に実行されるMessagesController.csのPostメソッドでRootDialogが呼び出されるように修正して、実行します。
public async Task<HttpResponseMessage> Post([FromBody]Activity activity) { if (activity.Type == ActivityTypes.Message) { await Conversation.SendAsync(activity, () => new Dialogs.RootDialog()); } else { HandleSystemMessage(activity); } var response = Request.CreateResponse(HttpStatusCode.OK); return response; }
このChatbotの主となる話題はランチを注文することですが、副次的な話題としてコース、飲み物、デザートへの分岐があります。対話の流れを維持しつつ、メインダイアログとサブダイアログを使い分けて話題を分岐して、最後にまとめています。さらに、PromptDialog.Choiceを使うことで、ユーザーからの入力を簡略化しています。
今回はChatbotで画面切り替えを行わずに、話題の分岐をダイアログで実現する基本的な方法を紹介しました。Bot FrameworkのBot Builder SDKではさらに細かいダイアログの仕様が紹介されていますので、より深くダイアログを理解したい場合にはこちらを参考にしてもらえればと思います。