はじめに
これはDialogflowを初めて触るための記事です。
Dialogflow関連の記事を見ていても全体図が見えてこなかったので、ここでは全体図を紹介することに専念しています。
#全体像
##やたらと多い登場人物
Google Home関連の話を聞いていて最初に躓いたのがココ。
Google HomeがいたりGoogle on Actionがあったり、DialogflowがいたりFirebaseがいたりと、とにかくやたら登場人物が多い。
##Google Home
いわゆるスマートスピーカーで音声の入出力を取り扱う。
OK Googleや ねぇGoogleでマイクをONにして、受け取った音声をGoogle Assistantに流す。
##Google Assistant
受け取った音声を解析してニュースや音楽などを流す。
また、場合によりActions on Googleのサービスを起動する。
##Actions on Google
Google Assistantから呼び出せるサービスのこと。
既存のアプリをActions on Googleに対応することでGoogle Assistantを経由して自社サービスやアプリなどを操作出来る様になる。
##Dialogflow
言語解析エンジン
音声入力で問題となりやすい自然言語の解析を簡単にやってくれる機能。
これを挟むことで言語解析のややこしいところを大幅に削減できる。
今回はここを紹介します。
##Server
Dialogflowから送られてきた解析済みのコマンドをServerで処理する。
ここでよくFirebaseが紹介されるけれど、手っ取り早く無料でServerを立ち上げられるというだけの話で特にサーバーの縛りはなくて、Herokuでもいいし、実業務では多くの場合、既に稼働しているサーバーに接続する形になるはず。
Dialogflowとの間はシンプルなJSONでのやり取りとなるため、既存サーバーと直接つなぐには既存サーバー側に対応したAPIを用意する必要がある。
APIが用意されていないサービスと繋ぐために、スクレイピングや変換するサーバーを立てることもある。
##複雑さのメリット
###実はそこまで難しくない
という感じで、Web実装やネイティブ実装に比べて、いろんな登場人物が出てくる世界なわけですが、開発する目線で言うと、実際に作らないといけないのはActions on Googleの部分だけなので、それほど難しく考える必要はありません。
とすると、Dialogflowはプログラムフリーですし、Serverはシンプルに定形のJsonを受け取って、(必要な処理を行い)、定形のJsonを返すだけです。
###Google Assistantを使うメリット
このような複雑な構造になっているのには理由があって、それぞれの階層で切り分けることで柔軟性を増すことができます。
Google Assistantを使うメリットは、音声入力に対応するのはもちろんのこと、これによりGoogle Assitantに対応した様々な端末にサービスを対応させることができます。
Actions on Googleに対応すると、Google Homeだけでなく、Androidスマートフォンや(Google Assistantをインストールした)iPhoneにも自動的に対応することになる。
現在のところActions on Googleが呼び出されるのはこの3つからのようですが、Google Assistant自体はAndroidWearやAndroid Auto,Android TVなどにも搭載されているため、これらへも将来的に対応してくれるかもしれません。
###Dialogflowが別れているメリット
DialogflowはGoogle Assistantの一部ではなく、別のサービスとして別れています。
実はDialogflowにとってGoogle Assistantは提携するサービスの1つにすぎず、Facebook botやLine bot、あるいは独自のアプリやWebサイトなど、様々なものと連携することができます。
これにより、DialogflowのAgentを作ることで、Google Homeのみならず、AndroidやiPhone、SNS botなどに一度に対応することが出来るというわけです。
###Dialogflowを使うメリット
Dialogflowを使うことで言語解析を容易に行うことができます。
GUIやCUIに比べて音声入力は表現の幅が広く予測不可能な言葉で操作されるという特徴があります。
例えばホテルを予約したい場合、ユーザーは
- 火曜日の宿を予約して
- 明日の宿を予約
- 部屋を取って
- ホテルに泊まりたい
など、様々な言い方をしてくる可能性がありますが、Dialogflowを使うことでこれらの異なる言い回しをまとめたり、宿泊日のような予約に必要な情報が抜け落ちていた場合に追加で質問をすることができます。
##重要な3つのキーワード
DialogflowのAgentを作る上で重要なキーワードが3つあります。
###Intent
Android アプリ開発をしている人であれば、IntentというとActivityやServiceを起動する仕組みを思い浮かべるかもしれませんが、Dialogflowにおいては「ユーザーがやりたい事全体」を指すものになります。
Android開発者にとって紛らわしい名前がついているのはAlexa Skilの名称に合わせてきたのが理由かと思います。
###Entity
ユーザーから抽出したいキーワードがEntityです。
Entityには日付や住所と言った標準でDialogflowが抽出・解析できるSystem Entityに加えて、開発者が独自にDeveloper Entityを追加することもできます。
###FulfilIntent
解析した結果をサーバーに渡す処理がFulfilIntentです。サーバーとのやり取りはHTTPSが使用され内容はJSONのPUSHとレスポンスです。
FulfilIntentは必須ではなく、場合によってはDialogflow内に記載したIntentのResponseだけで完結することもあるかもしれませんが、基本的には、ユーザーのやりたい事(Intent)からキーワード(Entity)を抜き取ってサーバーに渡す(FulfilIntent)がDialogflowの流れとなります。
#DialogflowでAgentを作ってみる。
##今回作るもの
今回は仮想的なホテル予約エージェントを作ってみます。
このホテルチェーンは福岡タウンホテルと、東京タウンホテルと、北海道シティホテルの3店舗を持っていると設定します。
また、各ホテルはレストランが有り、希望者はレストランの予約も可能とします。
#Agentを作る
##新規Agentの作り方
Agentを作るにはDialogflow.comにアクセスして、SIGN UP FOR FREEを選びます。
途中Agentを作るGogleアカウントや権限の許可、利用規約への承諾などを聞かれるのでチェックしていきます。
ウィザードを進めていくと、Agent nameなどを入力する画面が表示されます。
Agent nameにはAgent名(開発者が分かる名前でOK)を入力します。
ADD SAMPLE DATAにはサンプルデータが必要な場合にどのようなサンプルを取得するかを選びます。
残念なことにサンプルは英語しか用意されておらず、DEFAULT LANGUAGEでEnglish以外を選択するとサンプルを選ぶことができません。
DEFAULT LANGUAGEには標準言語を入力します。このDEFAULT LANGUAGEは後から変更できないので注意が必要です。Dialogflowは多言語対応したサービスを受け付けていますが、連携するサービスによってはDEFAULT LANGUAGEが無条件で呼び出されます。
GOOGLE PROJECTには、このAgentを動かすGoogle Cloud Platformのプロジェクトを選びます。特に理由がない場合はCreate a new Google projectのままとしてください。
DEFAULT TIME ZONEには標準的なタイムゾーンを入力します。
必要項目を入力したらCREATEをクリックします。
Googleのサービスでは入力後非同期に保存されるサービスが多いですが、Dialogflowでは各画面において明示的な保存作業が必要で、それらを行わない場合、変更内容がリセットされるので注意してください。
#Entityを作る
##building entityの追加
左のメニュー(表示されていないときはハンバーガーメニューをクリックして表示)からEntityの+をクリックしてホテル名を入力していきます。
Define synonyms(別名)にチェックが入っていることを確認し、Entity nameにbuildingと入力し、Click here to edit entityをクリックしてホテル名を入力します。
Enter reference valueに名前を入力し、Enter synonymには別名を入力します。
Synonymを入力することでユーザーが様々な言い回しをした場合でも、それらを同じEnter reference valueにまとめることができます。
Define synonymsにチェックが入っていない場合synonymを追加することはできません。
Allow automated expansionにチェックが入っていると自動的に似たようなSynonymを追加していってくれます。
今回は福岡タウンホテルには、synonymとして福岡タウン、福岡を追加し
東京タウンホテルにはsynonymとして東京タウン、東京を
北海道シティホテルにはsynonymとして、北海道シティ、北海道、シティホテル、シティを追加します。
##Request Entityの追加
再度Entityの+をクリックしてユーザーが予約を行いたいのかキャンセルを行いたいのかなど、ユーザーが行いたいアクションをEntityとして追加しておきます。
今回は予約とキャンセルを追加します。
#Intentを作る
左のメニューからIntentの+をクリックしてユーザーが予約を行うIntentを追加します。
##画面の説明
Intentの項目はEntityに比べてとても複雑で特に最初はどこに何をいれて良いのかわからない状態になっています。
各項目は次のようになっています。
非常にややこしいです。
種類ごとにまとめると上のようになります。
Intent Name
Intent Nameには開発時に識別できるような名前を設定します。
Contexts
一番最上部にあるContextsですが、実はオプション条件となっていて、最初は意識する必要がありません。
ここは、他のIntentが行われた後のみに動作するIntentを指定することができます。
メニューを開くと input contextとoutput contextを入力するフィールドが表示されます。
このIntentが実行されるとoutput contextがContextsに追加されます。
input contextが指定されているIntentは該当のContextが追加されている時にしか呼び出されません。
すなわちContextsを指定することで特定状況にあるときにしか反応しないIntentを追加できるという訳。
User says
どういう時にこのIntentが呼び出されるかを指定します。
Events
Botが呼び出された時やFulfil IntentのレスポンスでEventが渡された時など、ユーザーの呼びかけに寄らないシステム側で発生したEventに応じてIntentを発動させます。
Action name
サーバーに送られるアクション名(固定)です。
可変のパラメーターを追加できます。
Parameters
ユーザーの会話から抽出したEntityをパラメータとしてサーバーに送ることができます。
通常はUser saysの内容を元に自動で追加されますが、うまく取得できない場合などに手動で追加・修正することも可能です。
Response
ユーザーへの回答を返すことができます。
簡単な答えを返すだけなら、Default Responseで回答が可能です。
##Intentの入力
それでは実際に入力してみましょう。
Intent nameをbookingとして、
User saysに
「明日の福岡を予約」
と入力します。
入力内容を元に自動的にEntityが検出されParameterとして設定されました。
日付と建物名は予約で必須のためActionのParametersでREQUIREDにチェックをいれておきます。
Define promptsをクリックして、ユーザーに必須のパラメータが入力されていない場合にどのように質問するかを入力します。
dateは何日を予約しますか? buildingには福岡タウンホテル、東京タウンホテル、北海道シティホテルのどちらを予約しますか?と入力します。
ココまで来たらSAVEをクリックしてIntentを試します。
##Try it now
画面左側のTry it nowを使用することで、ユーザーが喋ったことに対してAgentがどのように返すかを試すことができます。
早速24日の北海道を予約
と入力してみましょう。。
Responseとして**2017-12-24 の 北海道シティホテル を予約しました。**が返されました。
入力した内容はIntentのUser saysに入力した内容と違いますが、Dialogflowがもっとも近いIntentと認識して呼び出していること、Entityのsynonymがまとめられていること、日付が年月を指定していなくても、現在日付を元に補完されている事などが見て取れます。
次にパラメータが足りない例を試してみましょう。
東京を予約
と入力してみます。
今度はパラメーターのPROMPTSに入力した
何日を予約しますか?
が返されました。
次に1日
と入力すると
2018-01-01 の 東京タウンホテル を予約しました。
と不足していたパラメーターが埋められてレスポンスが返されたのが確認できます。
このようにUserSaysを1例入力するだけで強力に似た言葉をDialogflowが見つけ出していることがわかります。
#FulfilIntentを使う
上記のAgentでは固定で予約完了を返していましたが、実際にはサーバーと連携する必要があります。
このための仕組みとしてFulfilIntentを使用します。
##2種類のFulfilIntent
FulfilIntentにはWebhookとInline Editorの二種類のFulfilIntentがあります。
Webhookは指定したURLに対してHttpsのPostでJson形式のテキストを送り、結果をJsonで受け取ります。
Inline EditorはDialog flow内のテキストエディタを使って簡易的なNode.jsの処理を記載することができます。サーバーを立ち上げたりする手間が必要ないのでInline Editorはスクレイピング処理を行いたいだけの場合やテスト時などにも便利です。
##Webhookを使う
Webhook を使うにはメニューからFulfilIntentをクリックし、WebhookのEnabledにチェック
URLにPOST先のURL、BASIC AUTHはベーシック認証のための情報、HEADERSにはHTTPSのヘッダーで必要な項目があれば記載し、DOMAINSはEnable webhook for all domainsを選びます。
何故かDomainsは特定のドメインを指定できるとかではなくEnableとDisableの二択で実際的にはEnableを選ぶ以外意味がないという状態になっています。
認証としてベーシック認証しかないのは心もとない感じがしますが、このPostはGoogleHomeからではなくGoogleのサーバーから送られてくるので送信元のアドレスを限定するなどして、追加の保護を加えるのが良いかと思います。
次にメニューからIntentを選び、先程作ったbooking Intentを選択すると、Responseの一番下にUse webhookというチェックボックスが追加されているので、ここにチェックを入れます。
もう一つのUse webhook for slot fillingは後続するIntentのためにPost先のサーバーでパラメータを埋めたい場合にチェックします。
指定のアドレスへどのようなPostデータが送られるかは Try it nowでSHOW JSONをクリックすることで確認できます。
###渡ってくるJson
重要なところを抜粋すると次のようになります。
V1の場合
{
"result": {
"resolvedQuery": "24日に福岡タウンホテルを予約",
"action": "reserve",
"parameters": {
"request": "予約",
"building": "福岡タウンホテル",
"date": "2017-12-24T03:00:00Z"
},
}
node.jsで取得する場合は次のようになりますね
functions.https.onRequest((req, res) => {
res.setHeader('Content-Type', 'application/json');
var output;
var action = req.body.result.action;
var resolvedQuery = req.body.result.resolvedQuery;
var parameters = req.body.result.parameters;
var request = parameters['request'];
var building =parameters['building'];
var date = parameters['date'];
V2の場合
{
"queryResult": {
"queryText": "24日に福岡タウンホテルを予約",
"parameters": {
"request": "予約",
"building": "福岡タウンホテル",
"date": "2017-12-24T03:00:00Z"
},
}
}
node.jsのコード
functions.https.onRequest((req, res) => {
res.setHeader('Content-Type', 'application/json');
var output;
var action = req.body.result.action;
var resolvedQuery = req.body.result.resolvedQuery;
var parameters = req.body.queryResult.parameters;
var request = parameters['request'];
var building =parameters['building'];
var date = parameters['date'];
###データの返し方
データには画像を添付したりURLを渡したりする機能がありますが、最低限以下の2つが必要です。
{
"speech": "予約を承りました。",
"displayText": "予約を承りました。",
}
node.jsなら次のように返せますね
functions.https.onRequest((req, res) => {
res.send(
JSON.stringify({
'speech': '予約を承りました。',
'displayText': '予約を承りました。'});
##Inline Editorの使い方
Inline Editorを使うにはFulfilIntentでInline Editorにチェックを入れます。Inline Editorを使うかWebhookを使うかはどちらか一方のみで両方を使い分けるということはできないので注意してください。
RequestとResponseはWebhookでnode.jsを使う場合と同一です。
#対話的Intent
##対話型Intentとは
Intentの作成でパラメータをRequiredにすることで、ユーザーに必要な情報を問い合わせして対話形式に必要項目をいれられることを確認しました。
今回は、より高度に特定のIntentを経由しないと呼ばれない継続処理用のIntentを作ってみます。
対話的にIntentを受け取るには大きく分けてFollowup IntentとContextの2種類があります。
##Followup Intent
###Followup Intentを作る
まずは、手軽に実装できるFollowup Intentを作ってみます。
Followup Intentは特定のIntentが発行された直後にのみ受け取る事ができるIntentを作成できます。
例えば、予約を行った直後に取り消したい場合を考えてみます。
メニューからIntentを選び、bookingにカーソルを合わせると Add-followup intentという隠しメニューが表示されるので、そこをクリックします。
キャンセル処理を追加したいのでcancelをクリックします。
booking - cancelが追加されます。
開いてみると User saysに最初からキャンセルの文言が入っていることがわかります。
今回は標準で入っているCancelを選びましたが、Customを選んでUser saysを手入力することで好きな言葉に反応するFollowup−Intentを作成可能です。
###Followup Intentを試す。
Try it nowでFollowup Intentを試します。
最初にホテルを予約して
次にキャンセルして というと booking - cancelが動いていることがわかります。
次にReset contextsをクリックして、もう一度キャンセルして と言うと今度は先程のbooking - cancelが呼ばれなくなっていることがわかります。
このようにFollowup Intentが親のIntentが呼び出された後でのみ呼び出されていることがわかります。
Followup IntentもFulfilIntentにしてサーバーで処理させることができます。送られてくるJsonには元のIntentの情報も含まれているため、その情報を使ってキャンセル処理を実装することができます。
##Contextを使う
###Contextを作る
Followup Intentより柔軟な対話を作る方法としてContextを使うことができます。
例えばホテル予約後にレストランも予約できる仕組みを作ってみます。
reserveインテントを開いてContextsのAdd output contextの横にreservedと記載します。
既に作った記憶がない booking-followupというoutput contextが追加されていますが、これについては後述します。
次にメニューからIntentの+を選び、FoodReserve Intentを作ります。
Intentsにfood-reserved intentをいれて、input Contextにreservedを指定します。
User saysにこのIntentが呼び出される条件として食事も予約を入れておきます。
###Contextを試す
Try it nowで標準状態では食事も予約と入力してもIntentが呼び出されないことを確認します。
次にホテルを予約したあとで、食事も予約と入力し、今度はfood-reservedが呼び出されることを確認します。
##Followup-IntentとContextの違い。
おやっと思われるかもしれませんが、実は先程の例だと、Folloup-Intentを使用しても実現することができるため、あえてContextを使用する必要はありません。
実のところFollowup-Intentを作成するのも裏で自動的にContextが作成されています。ReservedのOutputContextにいつの間にか入っていたbooking-followupというContextがそれです。
手動でContextを使う理由としては、好きなIntentで好きなContextを追加できるため、Followup-Intentのツリー構造より柔軟な構造に出来るということがあります。
例えば、宿泊客だけでなく、休憩客にも、レストランを予約させたいとなった場合に、休憩客用のIntentのOutputContextにReservedというContextを設定すると、宿泊用のIntentでも休憩用のIntentのどちらでもfood-reservedが呼び出せる状態になります。
また、ContextはFulfilIntentによりサーバーからも設定できるため、予約処理時はContextを有効にして、キャンセル時は無効にするといったことも可能です。
##Contextのライフサイクル
Output Contextを入力する時に数字が入っていたのに気がついたでしょうか?
これはContextのライフサイクルを表しています。
該当のContextが始まった場合、以降IntentごとにContextのライフサイクルは1ずつ減っていき、0になった時点でContextが終了します。
後続のIntentで同名のContextが指定されていた場合、後続のIntentのライフサイクルが上書きされます。
ライフサイクルに0を指定することで指定のContextを終了させることも可能です。
Intentsからbooking - cancelを選び、booking-followupとreservedのライフサイクルを0としてみましょう。
Try it nowを使用してホテルを予約しCONTEXTSがbooking-followupとreservedになっていることを確認します。
これらはそれぞれライフサイクルが2と5に設定されています。
キャンセルしてというと、Contextがリセットされていることを確認します。
#より複雑なEntityを作る
##Dev Composite
ホテルにおいて2人部屋を予約のように、人数を指定して予約をしたいということがあります。
しかしながら、現在のところSystemEntityでは人数を取り扱うEntityがありません。
かと言ってCustomEntityに1人、2人、3人と指定していくのはちょっと避けたいですよね
こういうときのためには <数字>人
と指定された場合のEntityを追加します。
Entitiesの+を追加して新しいEntityを指定
Entity名をnumber-of-menとし
Define sysnonymsのチェックを外し
@sys.number:{プロパティ名}人や@sys.number:{プロパティ名}名を追加します。
次に、booking intentに移動して、User saysに、明日の福岡タウンホテルを3名で予約と人数付きで入力します。
パラメータで人数が取得出来る用になったことが確認できます。
このようにDialogFlowを使うことで複雑な言語解析をGUI上でシンプルに実装することができます。