電車の運行情報を教えてくれるAssistant対応アプリを作ってみた

[Actions on Google Advent Calendar 2017](https://qiita.com/advent-calendar/2017/aog) の初日を担当させていただきます。今年もいよいよ始まりましたね。
ちなみに、私は [会社のアドベントカレンダー](https://qiita.com/advent-calendar/2017/rakus) の人数枠からあぶれてしまい、アドベントカレンダー難民になりかけてこちらに参加させて頂いております。こちらで全力を尽くしますのでよろしくお願いします。クリスマスまで楽しみましょう。

----

# はじめに

Google Home Miniを手に入れてせっかくなので何か作ってみようと思い、出かける前に電車が動いているかどうかを聞いたら答えてくれるアプリを作ってみようと考えました。例えば「阪急電車は動いてる？」と聞いたら阪急電車の運行情報のサイトを自動でチェックして答えてくれるようにします。

# 構成

現状、Google Assistant対応アプリを作るにはいくつかのプラットフォームと連携させる作り方が一般的です。今回は以下のような構成で実現してみます。通常はDialogflowからのWebhookをFirebaseで処理する流れが一般的ですが、Firebaseの無料枠は外部サイトにアクセスできないので今回はHerokuを選択してみました。

<img width="844" alt="スクリーンショット 2017-11-19 16.03.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/3324/0451e701-5de6-0d71-965b-68f387503148.png">

それぞれのプラットフォームについてはブログにまとめましたので参考にしてください。

[Google Assistantアプリ開発まとめ - radioc@?](http://radiocat.hatenablog.com/entry/2017/11/18/000000)

## Action on Google

まずはAction on Googleにプロジェクトを作成します。「Build and add actions to your app」で「Dialogflow」をBUILDします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/3324/e65dd73d-615d-8a93-0de7-73dacdc78d69.png)

他にも色々設定項目がありますが適宜設定します（後から変更も可能）。Actionを公開しないのであれば全ての項目を入力しなくてもひとまずTEST DRAFTしてシュミレーターなどを試すことができます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/3324/45e20553-d3da-6cc5-03cf-ad830b6b205c.png)

## Dialogflow

### Intents

`Aと言われたらBと応答する` の設定を行います。CREATE INTENTして
User saysに `Aと言われたら` の部分を登録します。「阪急電車」の部分は別の電車にも置き換えられるように `Train` という動的パラメータにしました。 

![image.png](https://qiita-image-store.s3.amazonaws.com/0/3324/1fca7aaa-bc0d-2d3a-e50c-e0da04c150ec.png)

パラメータをActionに登録します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/3324/24cdeabd-17e8-2e05-eead-979c985dc183.png)

Responseには応答する言葉を設定しますが、今回はHeroku上でスクリプトを動かして取得した運行情報を応答させたいのでここでは設定しません。かわりにHerokuにWebhookするようにFulfillmentで `Use webhook` をチェックします。Google Assistantでは応答したら会話を終了させる場合、 `End conversation` をチェックします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/3324/86c9be01-ca3e-268d-f571-c0585f800691.png)

### Entities

パラメータTrainにどのような言葉が使われるかを設定します。CREATE ENTITIYしてTrainの内容を設定します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/3324/5497a02b-a7e4-524e-6339-e9208630564b.png)

### Fulfillment

WebhookをENABLEDにしてリクエストを送るHerokuのURLを設定します。WebhookのURLは存在してレスポンスを返せればなんでも良いので自分のサイトにリクエストを送って注文を受け付けたりすることも可能になります。なお、設定できるURLはhttpsのみです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/3324/26de6617-7ba1-41a9-77a2-5e20788a64a1.png)

# Heroku

前述の通り、Webhookのリクエストを処理してJSON形式のレスポンスを返せばHerokuである必要はなくどの言語で実装しても良いわけですが、今回はNode.js＋Expressで実装したのでNode.jsのコードを例に書きます。また、以下のモジュールも使いました。

```json:package.json
  "dependencies": {
    "util": "0.10.3", // Dialogflowのリクエストの詳細をログに出すために使用
    "express": "4.15.2",  // アプリケーションサーバ
    "cheerio-httpcli": "^0.7.2",   // 運行情報取得用のスクレイピングモジュール
    "body-parser": "^1.18.2"    // POSTの処理
  }
```

## Response

最低限 `speech` と `displayText` に応答内容をセットして返せばアクションとして機能します。前者は音声応答、後者はスマホの画面に表示する内容です。

```js
  var sendResponse = function(response, word){
      //Dialogflowへ`speech`と`displayText`の情報を返す
      response.setHeader("Content-Type", "application/json");
      response.send(
        JSON.stringify({
          "speech": word , "displayText": word
        })
      );
  };
```

## Request Parameter

DialogflowのIntentsに設定したEntityのパラメータは以下のようにリクエストのJSON内にセットされて送られてきます。

```js
{
  // 略
  "result": {
    // 略
    "parameters": {
      "Train": "hankyu"
    },
    // 略
```

以下のように取得します。

```js
  //Dialogflowからのパラメータ取得
  const train = (function (req) {
      if (req.body && req.body.result) {
        return req.body.result.parameters.Train
      }
      return '';
  })(request);
```

## 運行情報の取得

最後にリクエストで取得した `Train` パラメータが阪急電車であれば阪急電車のサイトの運行情報を調べてその内容をレスポンスで返します。

```js
  if (train === 'hankyu') {
    // 阪急の運行情報を調べる
    client.fetch('http://www.hankyu.co.jp/railinfo/', {}, function (err, $, res) {
      var word = $('.all_route > p', '#railinfo').text();
      console.log(word);
      sendResponse(response, word);
    });
  }
```

# 応用：スマホの場合はサイトへのリンクも返したい

スマホのGoogle Assistantから呼び出された場合はせっかくなので実際の運行情報サイトへのリンクも付けて返したいところです。

## スマホかスピーカーかを判定する

まずリクエストがスマホからなのかスピーカーからなのかを特定する必要があります。Dialogflowからのリクエストには `surface.capabilities` というプロパティが含まれており、クライアントのデバイスがスクリーンを持つ場合だけ `actions.capability.SCREEN_OUTPUT` がセットされます。この値をチェックしてスマホからのリクエストを判定します。

```js
const hasScreen = (function (data) {
      if (data.surface && data.surface.capabilities) {
        for (let v of data.surface.capabilities) {
          if (v.name === 'actions.capability.SCREEN_OUTPUT'){
            console.log('ENABLE SCREEN_OUTPUT');
            return true;
          };
        };
      }
      return false;
  })(request.body.originalRequest.data);
```

`surface.capabilities` に関しては別記事にまとめたので良ければそちらも参照ください。

[Google HomeからのリクエストかどうかをWebhookで判別する](https://qiita.com/radiocat/items/91f8066ea25c5a401935)


## Rich Responses

Action on Googleのレスポンス仕様にはリッチなレスポンスを返す仕組みがあります。Basic Cardを使うと以下のようなサイトの情報の一部を表示し、Read moreをタップするとサイトを呼び出すようなレスポンスを作れます。

<img width="410" alt="スクリーンショット 2017-11-13 0.40.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/3324/a242d975-5223-b458-7a23-a63814d8ac1f.png">

Responseを返す時にBasic Card用のJSONを作るだけです。これも詳細は別記事にまとめました。

[DialogflowのWebhookでGoogle AssistantにBasic Cardのレスポンスを返す](https://qiita.com/radiocat/items/8def1d9ba0eb38b8a04e)

# 動かしてみる

## Simulator

Action on Googleのシュミレーターで試してみます。スマホとスピーカーの違いもシュミレーションできます。

![GoA_testapp.png](https://qiita-image-store.s3.amazonaws.com/0/3324/09a630f2-fab2-b9d4-a436-c89622f6fbdc.png)


### スピーカーのシュミレート

実際にGoogle Homeを使って試すこともできますが、わかりやすくシュミレーターで試してキャプチャをとってみました。スピーカーのアイコンと一緒に文字が表示されている部分が実際にはスピーカーから発信される内容です。スクリーンを持たないので文字の情報は表示されません。

<img width="400" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/3324/e39b899d-af09-64e8-dcea-8b3800f50c68.png">

### スマホのシュミレート

スマホの場合はスピーカーのアイコンとスクリーンに表示される文字が表示されています。実際の端末ではスクリーンに表示される文字のほうだけが表示されます。そしてその下にBasic Cardも表示されています（阪急のロゴ画像の表示が少しおかしい気がしますがよくわかりません）。

<img width="400" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/3324/122d701b-e307-4af3-3f0c-c7f0db56e401.png">

実機でも試してみました。ちょうど遅れが発生したようです。おかげで遅れのパターンもテストすることができました。

<img width="400" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/3324/b0d966a0-a6ba-13b8-4527-d752c6ea21bc.png">

# まとめ

Action on GoogleやDialogflow、そして今回であればHerokuなどプラットフォームをまたいで連携させる部分がやや複雑ではありますが、メインのコードの部分はWebhookを扱ったことがあれば技術的には難しくないのではないかと思います。リクエストとレスポンスの仕様さえ把握すれば使いなれたプラットフォームと言語を採用できるのも扱いやすい部分なので、Google Homeを使っていて良いアイデアを思いついたら是非Assistant対応アプリを作ってみてはいかがでしょうか。なお、色々なところで言われていますが、Assistant対応アプリのWebhookの開発には [ngrok](https://ngrok.com/) が超便利ですのでぜひご活用ください。

サンプルコードはGithubに置きました。

https://github.com/radiocat/train-status

----

それでは2日目以降も [Actions on Google Advent Calendar 2017](https://qiita.com/advent-calendar/2017/aog) をお楽しみください。

弊社の [ラクス Advent Calendar 2017](https://qiita.com/advent-calendar/2017/rakus) もよろしければ是非ご覧ください。