Chatbot開発が捗るFormFlow

2017年12月12日(火)
樋口 勝一
連載の5回目となる今回は、選択肢によるインターフェースを提供するFormFlowを用いて、Chatbotを拡張してみる。

Chatbot開発が捗るFormFlow

連載の第3回「Chatbotのダイアログ」で紹介したダイアログは対話の分岐が可能で、単一のChatbotのインターフェースで様々なサービスを提供するための重要な機能です。ですが、ユーザーの操作を考慮した場合、必ずしも想定した流れでChatbotと対話してくれるとは限りません。時には直前の選択をやり直したり、最初に戻ってしまったり、あいまいな入力でエラーを起こしたり、複数選択をしたり…… と様々な挙動が想定されます。ダイアログだけを用いてそのすべてに対応するとなると、Chatbotが多機能であればあるほど、開発はより複雑で手間のかかるものとなります。

あらかじめ想定された流れに沿ったかたちでユーザーにChatbotを利用してもらうために、Bot Frameworkでは「FormFlow」という機能を提供しています。FormFlowは選択肢をあらかじめ設定しておき、戻る、ヘルプ表示、エラーメッセージの表示など、ダイアログでは作り込みが必要であった面倒な部分を標準機能として提供しています。

ダイアログでは細かいところまで開発者が作り込むことができるメリットもありますが、反対にそれらが手間となってしまう場合もあります。一方FormFlowは、細かい作り込みが不要な反面、カスタマイズ可能なサービスアプリケーションとしては利用しづらいといった側面もあります。Bot Frameworkでは、ダイアログとFormFlowを組み合わせることも可能なので、ある場面ではFormFlowを、ある場面ではダイアログをと使い分けることによって、柔軟性と開発のしやすさを両立させられます。

基本的なFormFlowの紹介

まずは、FormFlowがどういうものであるかを簡単なサンプルで紹介しましょう。

これまでと同様に「ChatBot201707」を利用します。今回の記事で使用するファイル一式は、以下のリンクからダウンロード可能です。

Chatbot201707_05.zip

Visual StudioからDialogsフォルダーに、空白のクラスファイル「CurryForm.cs」を追加します。

リスト1:空のクラスファイルCurryForm.csを作成

01using System;
02using Microsoft.Bot.Builder.FormFlow;
03 
04namespace Chatbot201707.Dialogs
05{
06    public enum CurryOptions { ビーフ, チキン, ポーク, ベジタブル };
07 
08    [Serializable]
09    public class CurryForm
10    {
11        public CurryOptions? Curry;
12 
13        public static IForm<CurryForm> BuildForm()
14        {
15            return new FormBuilder<CurryForm>()
16                    .Message("カレーランチのご注文をお伺いいたします。")
17                    .Build();
18        }
19    }
20}

上記リストの内容で、注目すべきポイントをおさえておきましょう。

参照設定

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を一時的に修正します。

リスト2:FormFlowを呼び出すようにMessagesController.csを修正

01public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
02{
03    if (activity.Type == ActivityTypes.Message)
04    {
05        //await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
06        await Conversation.SendAsync(activity, () => FormDialog.FromForm(Dialogs.CurryForm.BuildForm));
07    }
08    else
09    {
10        HandleSystemMessage(activity);
11    }
12    var response = Request.CreateResponse(HttpStatusCode.OK);
13    return response;
14}

「await Conversation.SendAsync(activity, () => FormDialog.FromForm(Dialogs.CurryForm.BuildForm))」は、フォームを呼び出す際の決まり文句になっています。Dialogs.CurryForm.BuildFormメソッドを呼び出して、フォームを作成しています。

エミュレーターで実行してみましょう(前回Azure App Serviceにデプロイした直後では、Web.configにアプリIDとパスワードが記述される状態です。エミュレーターを利用する場合は、アプリIDとパスワードを削除しておいてください)。

リスト3:Web,configに記述したアプリIDとパスワードを削除

1<appSettings>
2  <!-- update these with your BotId, Microsoft App Id and your Microsoft App Password-->
3  <add key="BotId" value="YourBotId" />
4  <add key="MicrosoftAppId" value=" " />
5  <add key="MicrosoftAppPassword" value="" />
6</appSettings>

何か文字を入力すると、メッセージとともに、選択肢がボタンとして表示されます。「ビーフ」を選択すると確認メッセージが表示されます。

選択肢から「ビーフ」を選んだところ

選択肢から「ビーフ」を選んだところ

選択肢の1番目という意味で「1」と入力しても選択が可能です。

「1」を入力しても同じ結果になる

「1」を入力しても同じ結果になる

以降の処理は記述していませんので、Chatbotの処理としてはここまでとなります。ここで、選択肢以外の項目を入力してみるとどうなるでしょうか。試しに「シーフード」と入力してみます。

存在しない選択肢を選ぶとエラーが表示される

存在しない選択肢を選ぶとエラーが表示される

このように、追加のコーディングなしで既定のエラーメッセージが表示されています。

続いて「リセット」と入力してみましょう。

「リセット」を入力すると、最初のメニューに戻る

「リセット」を入力すると、最初のメニューに戻る

Chatbotとの対話が初期化され、最初のメニューに戻りました。

このようにFormFlowでは、選択肢の作成、確認メッセージ、エラー対応などあらかじめテンプレートが用意されています。

ダイアログとの組み合わせ

ここまで説明してきたように、Chatbotは対話の流れが重要なアプリケーションの要素となります。FormFlowは単体で呼び出して利用することができますが、やはり対話の流れの中で利用してこそ、そのメリットがあると考えます。実際、ダイアログの中からFormFlowを呼び出せるので、対話の流れの中でFormFlowを利用することも可能となります。ここでは、その方法を紹介しましょう。

ダイアログとFormFlowを組み合わせる

ダイアログとFormFlowを組み合わせる

ここでも再び「ChatBot201707」を利用します。Visual StudioからDialogsフォルダーに、空白のクラスファイル「CurryDialog.cs」を追加します。ダイアログとして利用するので、テンプレートとなるお約束の参照設定と「Serializable」属性、「IDialog」インターフェース、「StartAsync」メソッドを追加しておきます。

リスト4:空のクラスファイルCurryDialog.csを作成

01using System;
02using System.Threading.Tasks;
03using Microsoft.Bot.Builder.Dialogs;
04 
05namespace Chatbot201707.Dialogs
06{
07    [Serializable]
08    public class CurryDialog : IDialog<string>
09    {
10        public Task StartAsync(IDialogContext context)
11        {
12            return Task.CompletedTask;
13        }
14    }
15}

FormFlowを呼び出すための記述を追加していきます。

リスト5:FormFlowを呼び出すための記述を追加

01using System;
02using System.Threading.Tasks;
03using Microsoft.Bot.Builder.Dialogs;
04using Microsoft.Bot.Builder.FormFlow;
05 
06namespace Chatbot201707.Dialogs
07{
08    public enum CurryOptions { ビーフ, チキン, ポーク, ベジタブル };
09 
10    [Serializable]
11    public class CurryFormQuery
12    {
13        public CurryOptions? Curry;
14    }
15 
16    [Serializable]
17    public class CurryDialog : IDialog<CurryFormQuery>
18    {
19        public  Task StartAsync(IDialogContext context)
20        {
21            var newForm = FormDialog.FromForm(this.BuildForm, FormOptions.PromptInStart);
22            context.Call(newForm, this.CurryResumeAfterDialog);
23            return Task.CompletedTask;
24        }
25 
26        private async Task CurryResumeAfterDialog(IDialogContext context, IAwaitable<CurryFormQuery> result)
27        {
28            var selectedMenu = await result;
29            context.Done(selectedMenu);
30        }
31 
32        private IForm<CurryFormQuery> BuildForm()
33        {
34            return new FormBuilder<CurryFormQuery>()
35                    .Message("カレーランチのご注文をお伺いいたします。")
36                    .Build();
37        }
38    }
39}

個別に注意すべきポイントを見てみましょう。

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を元に戻します。

リスト6:MessagesController.csを元に戻す

01public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
02{
03    if (activity.Type == ActivityTypes.Message)
04    {
05        await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
06    }
07    else
08    {
09        HandleSystemMessage(activity);
10    }
11    var response = Request.CreateResponse(HttpStatusCode.OK);
12    return response;
13}

RootDialog.csも修正します。

リスト7:RootDialog.csを修正

01using System;
02using System.Threading.Tasks;
03using Microsoft.Bot.Builder.Dialogs;
04using Microsoft.Bot.Connector;
05using System.Collections.Generic;
06using Microsoft.Bot.Builder.FormFlow;
07 
08namespace Chatbot201707.Dialogs
09{
10    [Serializable]
11    public class RootDialog : IDialog<object>
12    {
13        private List<string> menuList = new List<string>() { "ランチコース", "カレー", "ドリンク", "デザート", "終了" };
14 
15        public Task StartAsync(IDialogContext context)
16        {
17            context.Wait(HelloMessage);
18            return Task.CompletedTask;
19        }
20 
21        private async Task HelloMessage(IDialogContext context, IAwaitable<object> result)
22        {
23            await context.PostAsync("いらっしゃいませ!ご注文を伺います。");
24            MenuMessage(context);
25        }
26 
27        private void MenuMessage(IDialogContext context)
28        {
29            PromptDialog.Choice(context, SelectDialog, menuList, "ランチメニューをお選びください。");
30        }
31 
32        private async Task SelectDialog(IDialogContext context, IAwaitable<object> result)
33        {
34            var selectedMenu = await result;
35            switch (selectedMenu)
36            {
37                case "ランチコース":
38                    context.Call(new LunchDialog(), LunchResumeAfterDialog);
39                    break;
40                case "カレー":
41                    context.Call(new CurryDialog(), CurryResumeAfterDialog);
42                    break;
43                case "ドリンク":
44                    context.Call(new DrinkDialog(), DrinkResumeAfterDialog);
45                    break;
46                case "デザート":
47                    context.Call(new DessertDialog(), DessertResumeAfterDialog);
48                    break;
49                case "終了":
50                    await context.PostAsync("ご注文を承りました。");
51                    context.Wait(HelloMessage);
52                    break;
53            }
54        }
55 
56        private async Task LunchResumeAfterDialog(IDialogContext context, IAwaitable<string> result)
57        {
58            var lunch = await result;
59            await context.PostAsync($"ランチコースは {lunch} ですね。");
60            MenuMessage(context);
61        }
62 
63        private async Task CurryResumeAfterDialog(IDialogContext context, IAwaitable<CurryFormQuery> result)
64        {
65            var curry = await result;
66            await context.PostAsync($"カレーは {curry.Curry.ToString()} ですね。");
67            MenuMessage(context);
68        }
69 
70        private async Task DrinkResumeAfterDialog(IDialogContext context, IAwaitable<string> result)
71        {
72            var drink = await result;
73            await context.PostAsync($"お飲み物は {drink} ですね。");
74            MenuMessage(context);
75        }
76 
77        private async Task DessertResumeAfterDialog(IDialogContext context, IAwaitable<string> result)
78        {
79            var dessert = await result;
80            await context.PostAsync($"デザートは {dessert} ですね。");
81            MenuMessage(context);
82        }
83    }
84}

個別に注意すべきポイントを見ていきましょう。

private List<string> menuList = new List<string>()……

メニューリストにカレーを追加します。

context.Call(new CurryDialog(), CurryResumeAfterDialog)

Switchの分岐にも「カレー」を追加して、CurryDialogを呼び出します。

private async Task CurryResumeAfterDialog……

CurryDialogが終了したときの処理です。IAwaitableインターフェースが「CurryFormQuery型」となっている点がポイントです。

ここまでできたら、エミュレーターで実行してみます。

ランチメニューダイアログに組み込まれたカレーのFormFlow

ランチメニューダイアログに組み込まれたカレーのFormFlow

このように、カレーのFormFlowがランチメニューのダイアログに組み込まれました。

GMOインターネット株式会社 Windowsソリューション チーフエグゼクティブ

GMOインターネットでWindowsのサービス開発運用に関わって16年、数年単位で進化し続けるMicrosoftのWindowsは新しもの好きにはたまらない製品です。自動販売機に見たことのないジュースがあれば、迷わすボタンを押します。そんなチャレンジが僕の人生を明るく、楽しくしてくれています。

お名前.com デスクトップクラウド
http://www.onamae-desktop.com/

お名前.com VPS Hyper-V
http://www.onamae-server.com/vps/hyperv/

連載バックナンバー

Web開発技術解説
第10回

Chatbotから話しかける(プロアクティブメッセージの送信)

2018/5/18
連載10回目となる今回は、Chatbotから先に話しかけるための手順を紹介します。
Web開発技術解説
第9回

Chatbotで画像コミュニケーションを実現する

2018/5/14
連載9回目となる今回は、Chatbotとの文字でのやり取りに画像を追加する方法を解説します。
Web開発技術解説
第8回

FacebookでChatbotを公開する

2018/4/17
連載8回目となる今回は、FacebookのMessengerを介してChatbotとやり取りする方法を解説します。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています