DialogflowでGoogle Homeの挙動をカスタムする

#目的
先日、YouTube Premium特典でGoogle Nest Miniを無料で頂いちゃいました！
早速ルンバやら電気やら色々連携して楽しんでいたのですが、やはり挙動をカスタムしてみたくなりまして...。
Dialogflowを使えばそこそこ簡単にできそうだったので、備忘録として残します。

注記：DialogflowはちょこちょこWebページのUIが変わるっぽくて、ネット上の記事を漁っていても全然違う画面で大変でした。これを読む頃にはまた変わっているかもしれません！


#やりたいこと
2つのことをやってみたいと思います。

* DialogflowのIntent機能を使って簡単なレスポンスを構築する
こちらに関してはほぼコーディング不要です。

* DialogflowのIntent機能＋Webhookで簡単なレスポンスを構築する
上とやりたいことは同じですが、あえてコードを書きます。これによって外部サービスを呼び出すなど、幅が広がるでしょう。


#前提

* Google Home（またはGoogle Home mini、Google Nest mini、Google Assistantアプリなど）があること
* Google AssistantアプリでVoiceMatchが済んでいること
これが終わっていないと、いくらGoogle Homeに話しかけても「話しかけている人のコンテンツ」であることがわからないのでうまく動きません。
それでもうまく行かない場合は、VoiceMatchをやり直してみると良いかもしれません。

#DialogflowのIntent機能のみで簡単なレスポンスを構築する
##Actions on Googleにアクセスしてプロジェクトを作成
名前の通りですが、カスタムアクションを定義するにはまずは[ここ](https://console.actions.google.com/)にアクセスしましょう。

初回ならログインを求められると思いますが、ココ超重要!です。
使いたいGoogle Homeにログインしているアカウントと同一のアカウントにしましょう。でないと色々破綻します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/9ac0d638-6399-e602-caa8-2d369ff6dba1.png)
早速「New Project」ボタンを押して、プロジェクト名と言語、地域を選択します。

##テンプレを選ぶ
色々とテンプレが用意されていますが、今回は「Conversational」を使ってみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/8c95f344-7ae0-a2d1-a2f1-c20ab0d6a92d.png)

##このアクションのトリガーを決める
下図の画面が出るので、「Decide how your Action is invoked」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/a003d00f-dbbf-ab8f-4506-08baf7d0605b.png)

「Display Name」部分がトリガーのキーワードになります。
下図の例だと、「OK Google、過去ガジェット研究所につないで」がトリガーになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/8d320b60-c006-7d58-73c2-8eefc5e04295.png)

※このDisplay Nameは、Actions directory（公開リポジトリ的な）内で一意である必要があるため、場合によっては既に取られてしまっている可能性があります。まあしょうがないですね。ちなみに今回はActions directoryに公開はしません。審査とかあるっぽいので。

右上の「Save」を押せば設定終了です。

##Actionの作成
続いてActionを作成していきます。
左側の「Actions」から、今回は「Custom Intent」を選び、「Build」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/5f366a6b-d05b-7951-bdf0-de46128f4997.png)

するとDialogflowの画面が立ち上がるので、とりあえず言語を設定してから「Create」ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/07562328-e9eb-15e7-1bce-28a342b2e38d.png)

この類の話だとかなり一般的な話ですが、こういった各種会話サービスは

* ユーザからのリクエストをもらう
* 意図を解釈する
* レスポンスを決める
* レスポンスを返す

必要があります。
今回、ユーザからのリクエストはGoogle Homeが受け取りますのであまりいじりません。
左側の「Intents」では、意図とレスポンスの設定ができるのでIntentsを見ていきます。

##Intentsの設定
「Intents」から「Create Intent」ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/87efd6af-5688-9bf4-95e7-7500020a507a.png)

色々あってややこしいですが、とりあえず今回は2つだけ使います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/6861c728-92f8-12d3-6184-9ed9ca1e9cc1.png)

###Training phrases
まずはTraining phrasesを展開し、「Add Training Phrases」をクリックします。
さてこのTraining phrasesですが、できるだけ正確な言葉で書いておくことで、実際のユーザのリクエストの文言が多少ぶれていたとしても、ある程度ユーザの意図をよしなに汲み取ってくれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/264fc2f7-06db-e13b-d344-628e3169126c.png)

たとえば今回は「お腹が空いた」としていますが、「お腹すいた」や「お腹ペコペコ」などでも同じIntentに誘導されます。
入力したらEnterキーを押します。

###Response
続いてResponseを展開し、「Add Response」をクリックします。
複数入力しておくと、どれかをランダムに返す仕様です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/dad40ee1-6382-8155-d8d7-61c3c2748b4b.png)


##名前を決めて保存＆簡易テスト
上部で名前を決め、「Save」ボタンで保存します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/cc8ce5f7-2481-4a00-c942-e5966bd3c4fc.png)

一応Dialogflow単体でもテストができます。
画面右部の入力欄にテキストを入力すると、レスポンスが帰ってきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/d29c0c98-d207-9483-c53a-93a8eaa38ecd.png)

##Integrationsの設定
ここでは、各種サービスとDialogflowの連携が設定できます。
左部のIntegrationsからGoogle Assistantの「Integration settings」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/a0268aee-bf7f-4acf-1205-cba102b0486e.png)

するとこんな画面が出てくるので、Add Intentをクリックして先程作ったIntentを選びましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/833e5945-f15a-41f6-40bc-943ea262d86f.png)

終わったらCloseボタンで閉じます。

##テスト
テスト方法はいくつかありますが、Google Assistantアプリを使うか、Google Home本体を使います。
アプリまたはHomeが、上記Intentを作成したアカウントと同一であれば自動的に接続は済んでいます。
うまく行かない場合はアプリやHomeの再起動、またはVoiceMatchをやり直してください（私も何度かハマりました）。

アプリ、Homeいずれの場合でもこんな感じのやり取りになります。
ユーザ：「（OK Google）、過去ガジェット研究所につないで」
アプリまたはHome：「わかりました。過去ガジェット研究所のテストバージョンです。こんにちは！」
ユーザ：「お腹ペコペコ」
アプリまたはHome：「おじいちゃんご飯は昨日食べたでしょ」

ちなみにこのままだとずっと過去ガジェット研究所対話している状態になり、普段のGoogle Assistantが使えなくなってしまいますが、「キャンセル」と言えば会話から抜け出せます。この退出用キーワードもカスタム可能です。

#DialogflowのIntent機能＋Webhookで簡単なレスポンスを構築する
では、自分でコードを書いてカスタムしてみましょう。これで外部サービスのWeb APIを呼び出すなど、幅が広がるはずです。
ちなみに外部ネットワークへの通信についてはGoogle Cloud Platformの料金体系上、課金が発生するはずです。

##Fulfillmentの設定
コードは、サーバレスでおなじみのCloud Functions on Firebaseで成り立っています。
画面上の簡易エディタもあるのでそこそこ便利です。

さて、先程のDialogflow画面のFulfillmentを選び、2番目の「Disabled」をクリックして有効化しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/44d0b9ed-eb35-8e36-89af-541d8a1d0a0f.png)

このページでやるなら言語環境はnode.jsになります。
ちなみにWebhookはココでやる必要はなくて、例えばCloud Functions本家を使ったりLambdaを使ったりすることは可能ですが、とりあえず手っ取り早いのでココを使います。

###コードのカスタマイズとデプロイ
色々と書いてありますが、大事なのはほんの数行です。
下図はサンプルそのままですが、一部/* */で囲んだところを変更しています。

```javascript
// See https://github.com/dialogflow/dialogflow-fulfillment-nodejs
// for Dialogflow fulfillment library docs, samples, and to report issues
'use strict';
 
const functions = require('firebase-functions');
const {WebhookClient} = require('dialogflow-fulfillment');
const {Card, Suggestion} = require('dialogflow-fulfillment');
 
process.env.DEBUG = 'dialogflow:debug'; // enables lib debugging statements
 
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
  const agent = new WebhookClient({ request, response });
  console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
  console.log('Dialogflow Request body: ' + JSON.stringify(request.body));
 
  function welcome(agent) {
    agent.add(`Welcome to my agent!`);
  }
 
  function fallback(agent) {
    agent.add(`I didn't understand`);
    agent.add(`I'm sorry, can you try again?`);
  }

  // // Uncomment and edit to make your own intent handler
  // // uncomment `intentMap.set('your intent name here', yourFunctionHandler);`
  // // below to get this function to be run when a Dialogflow intent is matched
  // function yourFunctionHandler(agent) {
  //   agent.add(`This message is from Dialogflow's Cloud Functions for Firebase editor!`);
  //   agent.add(new Card({
  //       title: `Title: this is a card title`,
  //       imageUrl: 'https://developers.google.com/actions/images/badges/XPM_BADGING_GoogleAssistant_VER.png',
  //       text: `This is the body text of a card.  You can even use line\n  breaks and emoji! 💁`,
  //       buttonText: 'This is a button',
  //       buttonUrl: 'https://assistant.google.com/'
  //     })
  //   );
  //   agent.add(new Suggestion(`Quick Reply`));
  //   agent.add(new Suggestion(`Suggestion`));
  //   agent.setContext({ name: 'weather', lifespan: 2, parameters: { city: 'Rome' }});
  // }

  // // Uncomment and edit to make your own Google Assistant intent handler
  // // uncomment `intentMap.set('your intent name here', googleAssistantHandler);`
  // // below to get this function to be run when a Dialogflow intent is matched
  // function googleAssistantHandler(agent) {
  //   let conv = agent.conv(); // Get Actions on Google library conv instance
  //   conv.ask('Hello from the Actions on Google client library!') // Use Actions on Google library
  //   agent.add(conv); // Add Actions on Google library responses to your agent's response
  // }
  // // See https://github.com/dialogflow/fulfillment-actions-library-nodejs
  // // for a complete Dialogflow fulfillment library Actions on Google client library v2 integration sample

  /* 追加 腹減った系男子が応答する内容を定義する */
  function hungryBoy(agent){
    agent.add('うるせえよ　うるうるせえよ　うるせえよ')
  }
  /* 追加ここまで */ 

  // Run the proper function handler based on the matched Dialogflow intent name
  let intentMap = new Map();
  intentMap.set('Default Welcome Intent', welcome);
  intentMap.set('Default Fallback Intent', fallback);
  // intentMap.set('your intent name here', yourFunctionHandler);
  // intentMap.set('your intent name here', googleAssistantHandler);

  /* 追加 intentMap.set('先の手順で作成したIntentの名前', 動かしたい関数の名前)*/
  intentMap.set('腹減った系男子', hungryBoy);
  /* 追加ここまで */ 
  agent.handleRequest(intentMap);
});
```

ということで以下を変更しています。

* 関数1つの追加
* Intentと関数を対応付ける1文を追加

変更が終わったら「Deploy」ボタンでデプロイしましょう。初回は2-3分かかると思います。

##Intentsの設定
再びIntentsの設定に戻ります。
左部のIntentsボタンから、先に作っておいたIntentをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/52ef95c6-b4e8-1c1c-3f82-1ecfe4beafd8.png)

まずは、デフォルトのレスポンスを消してしまいましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/e396ea3e-eeb1-c016-ddce-d870eb807bf3.png)

次に、「Fulfillment」から「Enable Fulfillment」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/41415abb-a4c5-a0e1-1aee-78878c210b2a.png)

最後に、「Enable webhook call for this intent」をONにしたら完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495652/a3834d14-e1cc-890b-96a4-de4a23246367.png)

Saveをクリックして終わりにしましょう。

##テスト
先程と同じ手順で、Google HomeまたはGoogle Assistantアプリから挙動を確認します。

今回はやりとりはこんな感じになります。
ユーザ：「（OK Google）、過去ガジェット研究所につないで」
アプリまたはHome：「わかりました。過去ガジェット研究所のテストバージョンです。こんにちは！」
ユーザ：「お腹ペコペコ」
アプリまたはHome：「うるせえよ　うるうるせえよ　うるせえよ」

#まとめ
Google Home単体で既に十分な機能を発揮してくれていますが、やはりデベロッパーたるものカスタマイズがしたくなるものです。
Dialogflowを使えばローコーディングでレスポンスを構築できますし、自分でコードを書くこともできます。
あとはEntityの設定やパラメータの受け取りなど、応用を利かせることも可能です。
こちらはそのうちまた記事を書きたいと思います。