MicrosoftのBot作成用FrameworkのBot Framework入門してみましょう。公式サイトは以下になります。
導入
導入は至って簡単です。公式の導入手順は以下です。
Getting started with the Connector | Bot Builder SDK C# Reference Library | Bot Framework
以下に手順を示します。
プロジェクトテンプレートのインストール
Bot Framework用のC#のプロジェクトテンプレートが以下からダウンロードできます。
http://aka.ms/bf-bc-vstemplate
zipファイルをダウンロードしたら、ドキュメントフォルダの下のVisual Studio 2015\Templates\ProjectTemplates\Visual C#にコピーします。
エミュレータのダウンロード
デバッグに便利なエミュレータも入れておきます。以下からインストールできます。
Bot Framework Channel Emulator
Hello world
ではHello worldをしてみます。BotのHello worldということで、名前を入力したら「こんにちは〇〇さん」と表示するBotでも作ってみようと思います。
プロジェクトの新規作成からBot Applicationを選択します。プロジェクト名はHelloBotAppにしました。
とりあえず、エミュレータの動作を見るため、このままF5を押して実行します。以下のような画面が出れば起動は完了です。
URLを控えておいてエミュレータを起動しましょう。赤線の部分のURLをhttp://localhost:起動したポート番号/api/messages
と入力します。(Bot Applicationのプロジェクトテンプレートはデフォルトで3979番ポートを使っていて、エミュレータはデフォルトで、そのポート番号を指定されてると思うので、何もしてなければそのままでいいと思います)
画面下部の入力欄に「こんにちは」と打ち込んでEnterを押します。するとボットにメッセージが送られて「You sent こんにちは which was 5 characters」というレスポンスが返ってきます。メッセージを選択すると、そのときのJSONも見ることができます。
では、プログラムを見ていきます。Controllers
フォルダにMessagesController
がいます。これがボットの本体です。コードは以下のようになっています。
using System; using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; using System.Web.Http; using System.Web.Http.Description; using Microsoft.Bot.Connector; using Newtonsoft.Json; namespace HelloBotApp { [BotAuthentication] publicclass MessagesController : ApiController { /// <summary>/// POST: api/Messages/// Receive a message from a user and reply to it/// </summary>public async Task<HttpResponseMessage> Post([FromBody]Activity activity) { if (activity.Type == ActivityTypes.Message) { ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl)); // calculate something for us to returnint length = (activity.Text ?? string.Empty).Length; // return our reply to the user Activity reply = activity.CreateReply($"You sent {activity.Text} which was {length} characters"); await connector.Conversations.ReplyToActivityAsync(reply); } else { HandleSystemMessage(activity); } var response = Request.CreateResponse(HttpStatusCode.OK); return response; } private Activity HandleSystemMessage(Activity message) { if (message.Type == ActivityTypes.DeleteUserData) { // Implement user deletion here// If we handle user deletion, return a real message } elseif (message.Type == ActivityTypes.ConversationUpdate) { // Handle conversation state changes, like members being added and removed// Use Activity.MembersAdded and Activity.MembersRemoved and Activity.Action for info// Not available in all channels } elseif (message.Type == ActivityTypes.ContactRelationUpdate) { // Handle add/remove from contact lists// Activity.From + Activity.Action represent what happened } elseif (message.Type == ActivityTypes.Typing) { // Handle knowing tha the user is typing } elseif (message.Type == ActivityTypes.Ping) { } returnnull; } } }
見てわかる通り、ボット用の認証属性のついた、ただのWebAPIです。その中でBot FrameworkのAPIを呼び出して色々やります。
Post
メソッドのわたってきているActivity
がいろいろな情報が詰まっています。そのType
がMessage
の時にチャットの応答を返せばOKです。デフォルトでは入力文字列(Activity
のText
プロパティで取得可能)の長さを算出してレスポンスを返しています。
ConnectorClient
がクライアントとの接続を表していて、そこのConversations
プロパティのReplyToActivityAsync
にActivity
のCreateReply
メソッドで作れるActivity
を渡すことで返事を返せます。
ということで、最初に書いた通り名前を入力してもらうとして「こんにちは〇〇さん」と返すようにしましょう。
以下のようなPost
メソッドになります。
public async Task<HttpResponseMessage> Post([FromBody]Activity activity) { if (activity.Type == ActivityTypes.Message) { var connector = new ConnectorClient(new Uri(activity.ServiceUrl)); var reply = activity.CreateReply($"こんにちは{activity.Text}さん"); await connector.Conversations.ReplyToActivityAsync(reply); } else { HandleSystemMessage(activity); } var response = Request.CreateResponse(HttpStatusCode.OK); return response; }
実行してエミュレータに、名前を入力して送ってみましょう。以下のようになります。
ダイアログ
次に、よく使うと思われるダイアログについて説明します。ダイアログは簡単に言ってしまえばGUIで言うところのウィザード形式のダイアログみたいなものです。IDialog<T>
インターフェースを実装したクラスがダイアログになります。簡単に名前を入力するダイアログを作ってみようと思います。
DialogAppという名前でプロジェクトを作ります。Dialogs
フォルダを作成して、そこにInputNameDialog
クラスを作成します。ダイアログはIDialog
インターフェースを実装します。IDialog
インターフェースはStartAsync
メソッドがあるので、それを実装します。StartAsync
メソッドの引数のIDialogContext
がダイアログの処理が色々詰まっています。あと、ダイアログはシリアル化可能でなければならないという制約があるのでSerializable
属性をつけておきます。
using Microsoft.Bot.Builder.Dialogs; using System; using System.Threading.Tasks; namespace DialogApp.Dialogs { [Serializable] publicclass InputNameDialog : IDialog { public Task StartAsync(IDialogContext context) { } } }
では、StartAsync
を実装していきます。ここでも名前を入力したら「こんにちは〇〇さん」と出すだけのダイアログを作りたいと思います。ダイアログの作り方は基本的にcontext
のWait
メソッドにメッセージを処理するメソッドを渡す形で定義します。こんな感じになります。
using Microsoft.Bot.Builder.Dialogs; using System.Threading.Tasks; using System; using Microsoft.Bot.Connector; namespace DialogApp.Dialogs { publicclass InputNameDialog : IDialog { public Task StartAsync(IDialogContext context) { context.Wait(this.ReceiveMessageAsync); return Task.CompletedTask; } private Task ReceiveMessageAsync(IDialogContext context, IAwaitable<IMessageActivity> result) { thrownew NotImplementedException(); } } }
ReceiveMessageAsync
メソッドに処理を書いていきます。基本的には第二引数をawait
してメッセージの入ったIMessageActivity
を取得して、そのText
プロパティを見て処理を行います。レスポンスの返し方は、IDialogContext
にPostAsync
というメソッドがあるので、そこに文字列を渡せばOKです。最初より簡単ですね。ということでコードは以下のようになります。
using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Connector; using System; using System.Threading.Tasks; namespace DialogApp.Dialogs { [Serializable] publicclass InputNameDialog : IDialog { public Task StartAsync(IDialogContext context) { context.Wait(this.ReceiveMessageAsync); return Task.CompletedTask; } private async Task ReceiveMessageAsync(IDialogContext context, IAwaitable<IMessageActivity> result) { var activity = await result; await context.PostAsync($"こんにちは{activity.Text}さん"); } } }
ではMessagesController
でダイアログを使うように変更します。Conversation
クラスのSendAsync
を使うことで出来ます。SendAsync
は第一引数にActivity
を受け取り、第二引数にダイアログを生成するデリゲートを受け取ります。ということで、先ほど作成したInputNameDialog
を使うようにするコードは以下のようになります。
public async Task<HttpResponseMessage> Post([FromBody]Activity activity) { if (activity.Type == ActivityTypes.Message) { await Conversation.SendAsync(activity, () => new InputNameDialog()); } else { HandleSystemMessage(activity); } var response = Request.CreateResponse(HttpStatusCode.OK); return response; }
実行結果は先ほどと一緒になります。
複数の値を入力させてみよう
ダイアログはウィザードみたいなものだと言いました。ウィザードといえば複数の入力値を受け付けるのが一般的だと思います。ダイアログを改造して名前と年齢を受け取るようにしたいと思います。やり方は簡単で1回のやり取りが終わったら次の入力処理をcontext.Wait
で渡してやればいいだけです。あと、一連のダイアログの処理が終わったらcontext.Done
を呼んでやる感じでいけます。コードは以下のようになります。
using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Connector; using System; using System.Threading.Tasks; namespace DialogApp.Dialogs { [Serializable] publicclass InputNameDialog : IDialog { publicstring Name { get; set; } publicint Age { get; set; } public async Task StartAsync(IDialogContext context) { context.Wait(this.InputNameAsync); } private async Task InputNameAsync(IDialogContext context, IAwaitable<IMessageActivity> result) { var activity = await result; this.Name = activity.Text; await context.PostAsync($"こんにちは{this.Name}さん。続けて年齢を入力してください"); context.Wait(this.InputAgeAsync); } private async Task InputAgeAsync(IDialogContext context, IAwaitable<IMessageActivity> result) { var activity = await result; int age; if (int.TryParse(activity.Text ?? "", out age)) { this.Age = age; await context.PostAsync($"こんにちは{this.Age}歳の{this.Name}さん"); context.Done((object)null); } else { await context.PostAsync($"年齢は数字で入力してください"); context.Wait(this.InputAgeAsync); } } } }
実行すると以下のような感じになります。
ダイアログの連携
ウィザードといえば一連の入力したデータが終わったら後続の処理を実行するようなイメージがあります。Bot Frameworkにも、そんな感じの機能があります。Chain
というものを使ってダイアログを連結することで実現できます。ダイアログ間の値の受け渡しは、context.Done
メソッドで渡したものが後続に渡るイメージです。
ということで値受け渡し用のデータの入れ物のPerson
クラスを用意します。これもSerializable
にしておきます。
using System; namespace DialogApp.Models { [Serializable] publicclass Person { publicstring Name { get; set; } publicint Age { get; set; } } }
そして、InputPersonInfoDialogというクラスを作成して以下のように実装します。結果を返すダイアログはIDialog<T>
を実装する点がこれまでのダイアログと異なっています。
using DialogApp.Models; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Connector; using System; using System.Threading.Tasks; namespace DialogApp.Dialogs { [Serializable] publicclass InputPersonInfoDialog : IDialog<Person> { private Person Person { get; set; } = new Person(); public Task StartAsync(IDialogContext context) { context.Wait(this.InputNameAsync); return Task.CompletedTask; } private async Task InputNameAsync(IDialogContext context, IAwaitable<IMessageActivity> result) { var activity = await result; this.Person.Name = activity.Text; await context.PostAsync($"こんにちは{this.Person.Name}さん。続けて年齢を入力してください"); context.Wait(this.InputAgeAsync); } private async Task InputAgeAsync(IDialogContext context, IAwaitable<IMessageActivity> result) { var activity = await result; int age; if (int.TryParse(activity.Text ?? "", out age)) { this.Person.Age = age; context.Done(this.Person); } else { await context.PostAsync("年齢は数字で入れてください"); context.Wait(this.InputAgeAsync); } } } }
後続の処理を行うダイアログを作成します。といっても入力された値をダンプするだけのシンプルなダイアログです。以下のように実装します。
using DialogApp.Models; using Microsoft.Bot.Builder.Dialogs; using System; using System.Threading.Tasks; namespace DialogApp.Dialogs { [Serializable] publicclass CompletedDialog : IDialog<object> { public Person Person { get; set; } public CompletedDialog(Person person) { this.Person = person; } public async Task StartAsync(IDialogContext context) { await context.PostAsync($"こんにちは{this.Person.Age}歳の{this.Person.Name}さん"); context.Done((object)null); } } }
そして、ダイアログの連携部分を書いていきます。
MessagesController
クラスにダイアログをChain
するファクトリメソッドを作成します。以下のようなメソッドです。
privatestatic IDialog<object> CreateDialog() { return Chain.From(() => new InputPersonInfoDialog()) .ContinueWith<Person, object>(async (ctx, r) => { var p = await r; returnnew CompletedDialog(p); }); }
ContinueWith
メソッドでダイアログを連結するようなイメージです。型引数は、前のダイアログが返す型と自分が返す型です。今回は最初のダイアログがPerson
を返して、次のダイアログは何も返さないのでobject
としています。ContinueWith
はIBotContext
とIAwaitable<第二型引数>
なのでawait
して前のダイアログの結果を受け取ります。そして、それをもとに次のダイアログを作ります。
MessagesController
のPost
メソッドを以下のように書き換えてChain
メソッドでつないだダイアログを使うようにしましょう。
public async Task<HttpResponseMessage> Post([FromBody]Activity activity) { if (activity.Type == ActivityTypes.Message) { await Conversation.SendAsync(activity, CreateDialog); } else { HandleSystemMessage(activity); } var response = Request.CreateResponse(HttpStatusCode.OK); return response; }
実行すると以下のようになります。繰り返し入力もいけるようになりました。
フォーム入力に特化したダイアログ
さて、クラスに定義されたプロパティの値を埋めていくという処理は結構やることが多いと思います。FormDialog
というクラスがこの処理をやってくれます。FormDialog
はIForm<T>
から作ることができます。IForm<T>
はFormBuilder<T>
というビルダークラスがいて、これを使って細かなカスタマイズが可能になっています。とりあえず一番簡単な使い方は以下のようになります。
privatestatic IForm<Person> CreateForm() { returnnew FormBuilder<Person>() .Build(); } privatestatic IDialog<object> CreateDialog() { return Chain.From(() => FormDialog.FromForm(CreateForm)) .ContinueWith<Person, object>(async (ctx, r) => { var p = await r; returnnew CompletedDialog(p); }); }
InputPersonInfoDialog
はいらなくなったので消してください。今までの挙動の違いとしては、最初に何かを話しかけるまで入力シーケンスが始まらないという点です。なので最初に、こんにちはとかhiとかと話しかけてあげましょう。実行すると以下のようになります。
ちなみにenum型を持たせると、それを選択するようにできます。Person
クラスを以下のように書き換えると(= 1重要)
using System; namespace DialogApp.Models { [Serializable] publicclass Person { publicstring Name { get; set; } publicint Age { get; set; } public Gender Gender { get; set; } } publicenum Gender { Man = 1, Woman } }
こんな選択肢が追加されます。
フォームのカスタマイズ
色々カスタマイズが可能ですが、いくつかのカスタマイズ例を示したいと思います。
メッセージのカスタマイズ
プロパティ名がそのまま出るのって日本語では不自然ですよね。FormBuilder
を使うと色々メッセージや値の範囲のチェックなどができるようになっています。細かく解説はしませんが、以下のような感じのコードが可能です。
using System; using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; using System.Web.Http; using System.Web.Http.Description; using Microsoft.Bot.Connector; using Newtonsoft.Json; using Microsoft.Bot.Builder.Dialogs; using DialogApp.Dialogs; using DialogApp.Models; using Microsoft.Bot.Builder.FormFlow; using Microsoft.Bot.Builder.FormFlow.Advanced; namespace DialogApp { [BotAuthentication] publicclass MessagesController : ApiController { privatestatic IForm<Person> CreateForm() { returnnew FormBuilder<Person>() .Message("あなたの情報を入力してください") .Field(nameof(Person.Name), "名前を入力してください", validate: (p, value) => // nullっていう名前の人はいないという無理な条件 Task.FromResult(((string)value) == "null" ? new ValidateResult { Feedback = "nullという名前はダメです" } : new ValidateResult { IsValid = true, Value = value })) .Field(new FieldReflector<Person>(nameof(Person.Age)) .SetValidate((p, value) => Task.FromResult((long)value< 0 ? new ValidateResult { Feedback = "0歳以下はダメです" } : new ValidateResult { IsValid = true, Value = value })) .SetPrompt(new PromptAttribute("年齢を入力してください"))) .Field(new FieldReflector<Person>(nameof(Person.Gender)) .SetPrompt(new PromptAttribute("性別を入力してください{||}")) .SetDefine((p, field) => { field.AddDescription(Gender.Man, "男性"); field.AddDescription(Gender.Woman, "女性"); return Task.FromResult(true); })) .OnCompletion(async (context, p) => { await context.PostAsync($"こんにちは{p.Gender}の{p.Age}歳の{p.Name}さん"); }) .Confirm(p => { return Task.FromResult(new PromptAttribute($@"入力した情報は以下でいいですか?(はい/いいえ)名前: {p.Name}年齢: {p.Age}歳性別: {(p.Gender == Gender.Man ? "男性" : "女性")}")); }) .Build(); } privatestatic IDialog<object> CreateDialog() { return Chain.From(() => FormDialog.FromForm(CreateForm)) .ContinueWith<Person, object>(async (ctx, r) => { var p = await r; returnnew CompletedDialog(p); }); } /// <summary>/// POST: api/Messages/// Receive a message from a user and reply to it/// </summary>public async Task<HttpResponseMessage> Post([FromBody]Activity activity) { if (activity.Type == ActivityTypes.Message) { await Conversation.SendAsync(activity, CreateDialog); } else { HandleSystemMessage(activity); } var response = Request.CreateResponse(HttpStatusCode.OK); return response; } private Activity HandleSystemMessage(Activity message) { if (message.Type == ActivityTypes.DeleteUserData) { // Implement user deletion here// If we handle user deletion, return a real message } elseif (message.Type == ActivityTypes.ConversationUpdate) { // Handle conversation state changes, like members being added and removed// Use Activity.MembersAdded and Activity.MembersRemoved and Activity.Action for info// Not available in all channels } elseif (message.Type == ActivityTypes.ContactRelationUpdate) { // Handle add/remove from contact lists// Activity.From + Activity.Action represent what happened } elseif (message.Type == ActivityTypes.Typing) { // Handle knowing tha the user is typing } elseif (message.Type == ActivityTypes.Ping) { } returnnull; } } }
プロパティのPromptAttribute
の{||}
は選択肢を出すものです。PromptAttribute
内で改行するには、改行2つで改行できます。
こんな感じになります。
複雑なダイアログ
Chain
には様々なメソッドがあります。
Dialogs | Bot Builder SDK C# Reference Library | Bot Framework
PostToChain
で始まり、LINQを使って処理を組み立てることができます。Switch
を使って以下のように分岐をすることもできます。
privatestatic IDialog<object> CreateDialog() { return Chain.PostToChain() .Select(x => x.Text) .Switch( new RegexCase<string>(new Regex("^助けて$"), (ctx, value) => "食べたいものを言ってね。"), new Case<string, string>(x => x.Contains("寿司"), (ctx, value) => $"かずあきさんの財布で{value}"), new DefaultCase<string, string>((ctx, x) => x)) .PostToUser() .Loop(); }
Switch
でダイアログを返すこともできます。そのときはUnwrap
とセットで使う感じです。
privatestatic IDialog<object> CreateDialog() { return Chain.PostToChain() .Select(x => x.Text) .Switch( Chain.Case(new Regex("^助けて$"), (ctx, x) => Chain.Return("食べたいものを言ってね")), Chain.Case((string x) => x == "寿司", (ctx, x) => Chain .Return($"かずあきさんの財布で{x}が食べたい?(はい/いいえ)") .PostToUser() .WaitToBot() .Select(y => y.Text) .Select(y => y == "はい" ? "かずあきさんの財布で寿司が食べたい!!" : "そうはいっても本音では食べたい!!")), Chain.Default<string, IDialog<string>>((ctx, x) => Chain.Return($"{x}が食べたい"))) .Unwrap() .PostToUser() .Loop(); }
Botのデプロイ
普通にAzureのApp Serviceにデプロイできます。
仮に、https://okazukibottest.azurewebsites.net/
にデプロイしたものとして、公開手順を説明します。
Bot FrameworkのサイトでRegister Botを選択します。
必要事項を入れていきます。Nameは適当に、bothandleはボットのハンドルネームです。
Messaging endpointは、URL/api/messagesです。今回の場合はhttps://okazukibottest.azurewebsites.net/api/messages
になります。
Configurationの項目の下にある「Create Microsoft App ID and password」をクリックして、アプリを登録します。 「アプリシークレットを作成して続行」をクリックしてアプリのシークレットを控えておきます。アプリIDが入力されます。あとで使うので控えておきます。
Publisher Profileを適当に入力したら完成です。同意してRegisterしましょう。
控えてアプリIDとシークレットをWeb.configのappSettingsのMicrosoftAppIdとMicrosoftAppPasswordに設定して再デプロイします。
Botの画面でTestを押して成功すれば準備完了です。
Add To Skypeを押すと自分のスカイプに登録して試すことができます。そのほかにも、このページからSlackなど様々なチャットサービスに登録ができます。
それでは良いBot生活を!
この先
LUISとの連携機能もあったりするので、それを使えば、より自然な文章を解析することができるようになります。