Chatbot開発が捗るFormFlow
Chatbot開発が捗るFormFlow
連載の第3回「Chatbotのダイアログ」で紹介したダイアログは対話の分岐が可能で、単一のChatbotのインターフェースで様々なサービスを提供するための重要な機能です。ですが、ユーザーの操作を考慮した場合、必ずしも想定した流れでChatbotと対話してくれるとは限りません。時には直前の選択をやり直したり、最初に戻ってしまったり、あいまいな入力でエラーを起こしたり、複数選択をしたり…… と様々な挙動が想定されます。ダイアログだけを用いてそのすべてに対応するとなると、Chatbotが多機能であればあるほど、開発はより複雑で手間のかかるものとなります。
あらかじめ想定された流れに沿ったかたちでユーザーにChatbotを利用してもらうために、Bot Frameworkでは「FormFlow」という機能を提供しています。FormFlowは選択肢をあらかじめ設定しておき、戻る、ヘルプ表示、エラーメッセージの表示など、ダイアログでは作り込みが必要であった面倒な部分を標準機能として提供しています。
ダイアログでは細かいところまで開発者が作り込むことができるメリットもありますが、反対にそれらが手間となってしまう場合もあります。一方FormFlowは、細かい作り込みが不要な反面、カスタマイズ可能なサービスアプリケーションとしては利用しづらいといった側面もあります。Bot Frameworkでは、ダイアログとFormFlowを組み合わせることも可能なので、ある場面ではFormFlowを、ある場面ではダイアログをと使い分けることによって、柔軟性と開発のしやすさを両立させられます。
基本的なFormFlowの紹介
まずは、FormFlowがどういうものであるかを簡単なサンプルで紹介しましょう。
これまでと同様に「ChatBot201707」を利用します。今回の記事で使用するファイル一式は、以下のリンクからダウンロード可能です。
Visual StudioからDialogsフォルダーに、空白のクラスファイル「CurryForm.cs」を追加します。
using System; using Microsoft.Bot.Builder.FormFlow; namespace Chatbot201707.Dialogs { public enum CurryOptions { ビーフ, チキン, ポーク, ベジタブル }; [Serializable] public class CurryForm { public CurryOptions? Curry; public static IForm<CurryForm> BuildForm() { return new FormBuilder<CurryForm>() .Message("カレーランチのご注文をお伺いいたします。") .Build(); } } }
上記リストの内容で、注目すべきポイントをおさえておきましょう。
参照設定
FormFlowを利用するには最低限、「System」と「Microsoft.Bot.Builder.FormFlow」を参照設定しておく必要があります。
enum CurryOptions
enumで列挙型を宣言して、そのリストをFormFlowでは選択肢として利用します。
Serializable属性
FormFlowもダイアログと同様にシリアル化可能でなければならないという制約があるので、Serializable属性を付けます。Chatbotでは対話の状態を保存するために必要となります。
CurryOptions? Curry
「System.Nullable<CurryOptions> Curry」の省略形です。Nullを許容します。
public static IForm<CurryForm> BuildForm()
FormFlowを作成するメソッドです。この配下に記述された内容が、FormFlowを構成します。
return new FormBuilder<CurryForm>()
フォームを作成します。作成時のオプションでメッセージを表示しています。
このFormFlowを呼び出すために、MessagesController.csを一時的に修正します。
public async Task<HttpResponseMessage> Post([FromBody]Activity activity) { if (activity.Type == ActivityTypes.Message) { //await Conversation.SendAsync(activity, () => new Dialogs.RootDialog()); await Conversation.SendAsync(activity, () => FormDialog.FromForm(Dialogs.CurryForm.BuildForm)); } else { HandleSystemMessage(activity); } var response = Request.CreateResponse(HttpStatusCode.OK); return response; }
「await Conversation.SendAsync(activity, () => FormDialog.FromForm(Dialogs.CurryForm.BuildForm))」は、フォームを呼び出す際の決まり文句になっています。Dialogs.CurryForm.BuildFormメソッドを呼び出して、フォームを作成しています。
エミュレーターで実行してみましょう(前回Azure App Serviceにデプロイした直後では、Web.configにアプリIDとパスワードが記述される状態です。エミュレーターを利用する場合は、アプリIDとパスワードを削除しておいてください)。
<appSettings> <!-- update these with your BotId, Microsoft App Id and your Microsoft App Password--> <add key="BotId" value="YourBotId" /> <add key="MicrosoftAppId" value=" " /> <add key="MicrosoftAppPassword" value="" /> </appSettings>
何か文字を入力すると、メッセージとともに、選択肢がボタンとして表示されます。「ビーフ」を選択すると確認メッセージが表示されます。
選択肢の1番目という意味で「1」と入力しても選択が可能です。
以降の処理は記述していませんので、Chatbotの処理としてはここまでとなります。ここで、選択肢以外の項目を入力してみるとどうなるでしょうか。試しに「シーフード」と入力してみます。
このように、追加のコーディングなしで既定のエラーメッセージが表示されています。
続いて「リセット」と入力してみましょう。
Chatbotとの対話が初期化され、最初のメニューに戻りました。
このようにFormFlowでは、選択肢の作成、確認メッセージ、エラー対応などあらかじめテンプレートが用意されています。
ダイアログとの組み合わせ
ここまで説明してきたように、Chatbotは対話の流れが重要なアプリケーションの要素となります。FormFlowは単体で呼び出して利用することができますが、やはり対話の流れの中で利用してこそ、そのメリットがあると考えます。実際、ダイアログの中からFormFlowを呼び出せるので、対話の流れの中でFormFlowを利用することも可能となります。ここでは、その方法を紹介しましょう。
ここでも再び「ChatBot201707」を利用します。Visual StudioからDialogsフォルダーに、空白のクラスファイル「CurryDialog.cs」を追加します。ダイアログとして利用するので、テンプレートとなるお約束の参照設定と「Serializable」属性、「IDialog」インターフェース、「StartAsync」メソッドを追加しておきます。
using System; using System.Threading.Tasks; using Microsoft.Bot.Builder.Dialogs; namespace Chatbot201707.Dialogs { [Serializable] public class CurryDialog : IDialog<string> { public Task StartAsync(IDialogContext context) { return Task.CompletedTask; } } }
FormFlowを呼び出すための記述を追加していきます。
using System; using System.Threading.Tasks; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Builder.FormFlow; namespace Chatbot201707.Dialogs { public enum CurryOptions { ビーフ, チキン, ポーク, ベジタブル }; [Serializable] public class CurryFormQuery { public CurryOptions? Curry; } [Serializable] public class CurryDialog : IDialog<CurryFormQuery> { public Task StartAsync(IDialogContext context) { var newForm = FormDialog.FromForm(this.BuildForm, FormOptions.PromptInStart); context.Call(newForm, this.CurryResumeAfterDialog); return Task.CompletedTask; } private async Task CurryResumeAfterDialog(IDialogContext context, IAwaitable<CurryFormQuery> result) { var selectedMenu = await result; context.Done(selectedMenu); } private IForm<CurryFormQuery> BuildForm() { return new FormBuilder<CurryFormQuery>() .Message("カレーランチのご注文をお伺いいたします。") .Build(); } } }
個別に注意すべきポイントを見てみましょう。
using Microsoft.Bot.Builder.FormFlow
FormFlowを利用するための参照設定を追加します。
enum CurryOptions
enumで列挙型を宣言して、選択肢として利用します。
public class CurryFormQuery
ダイアログ終了時にRootDialogに引き渡すためのクラスを用意します。FormFlowの実行結果はこのクラス内に格納されます。「Serializable」属性を忘れずに付けておきましょう。
public class CurryDialog
IDialog<CurryFormQuery>:クラスに「IDialog」インターフェースを追加して、戻り値の型として、先ほど定義した「CurryFormQuery型」を指定します。
var newForm = FormDialog.FromForm(this.BuildForm, FormOptions.PromptInStart)
フォームのインスタンスを作成して、BuildFormメソッドを呼び出します。
context.Call(newForm, this.CurryResumeAfterDialog)
ダイアログの場合と同様にFormFlowを呼び出します。FormFlowの終了時に実行されるメソッドCurryResumeAfterDialogを指定します。
CurryResumeAfterDialog
IAwaitableインターフェースには「CurryFormQuery型」を指定します。ダイアログの終了後は、他のダイアログと同様にRootダイアログに戻ります。
BuildFormメソッドでも、「CurryFormQuery型」を指定します。ここまで記述できたら、最初に作成したCurryForm.csは不要なので、削除しておきましょう。
次にこのダイアログをこれまでのChatbotに組み込んで、ランチメニューの一つとして選択できるようにします。先ほど修正したMessagesController.csを元に戻します。
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; }
RootDialog.csも修正します。
using System; using System.Threading.Tasks; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Connector; using System.Collections.Generic; using Microsoft.Bot.Builder.FormFlow; namespace Chatbot201707.Dialogs { [Serializable] public class RootDialog : IDialog<object> { private List<string> menuList = new List<string>() { "ランチコース", "カレー", "ドリンク", "デザート", "終了" }; 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 CurryDialog(), CurryResumeAfterDialog); 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) { var lunch = await result; await context.PostAsync($"ランチコースは {lunch} ですね。"); MenuMessage(context); } private async Task CurryResumeAfterDialog(IDialogContext context, IAwaitable<CurryFormQuery> result) { var curry = await result; await context.PostAsync($"カレーは {curry.Curry.ToString()} ですね。"); MenuMessage(context); } private async Task DrinkResumeAfterDialog(IDialogContext context, IAwaitable<string> result) { var drink = await result; await context.PostAsync($"お飲み物は {drink} ですね。"); MenuMessage(context); } private async Task DessertResumeAfterDialog(IDialogContext context, IAwaitable<string> result) { var dessert = await result; await context.PostAsync($"デザートは {dessert} ですね。"); MenuMessage(context); } } }
個別に注意すべきポイントを見ていきましょう。
private List<string> menuList = new List<string>()……
メニューリストにカレーを追加します。
context.Call(new CurryDialog(), CurryResumeAfterDialog)
Switchの分岐にも「カレー」を追加して、CurryDialogを呼び出します。
private async Task CurryResumeAfterDialog……
CurryDialogが終了したときの処理です。IAwaitableインターフェースが「CurryFormQuery型」となっている点がポイントです。
ここまでできたら、エミュレーターで実行してみます。
このように、カレーのFormFlowがランチメニューのダイアログに組み込まれました。