GoogleHome (Googleアシスタント) で「フィードバックを送信」を作ってみる


GoogleHome のアプリ (アクション) に「フィードバックを送信」する機能を追加して、利用者の生の声 (といっても録音した音声ではなくて音声認識した結果のテキスト) を受け取ります。

Dialogflow の Follow-up intent を使ういい練習になるので、よかったら試してみてください。[Dialogflow の Inline Editor](https://qiita.com/takatama/items/c8146faf9f04fc6b0698)を使って、Webブラウザーだけで開発します。

頂いたフィードバックはせっかくなので、Google Apps Script (GAS) を使って Googleスプレッドシートに記録し、さらにメールで通知します。

なお、Cloud Functions から GAS を呼び出すところは、Firebase の有料プランが必要になるのでご注意ください。Google内の呼び出しなんだから無料プランでいいのにな...。

# 想定する利用シーン
- ユーザー「フィードバックを送信。」
- GoogleHome「わかりました。ご意見やご感想を教えてください。」
- ユーザー「いい感じですよ。」
- GoogleHome「いただいたご意見は「いい感じですよ」でよろしいでしょうか？」
- ユーザー「はい。」
- GoogleHome「わかりました。フィードバックをありがとうございました。」

# 「フィードバックを送信」ログ出力編

まずは頂いたフィードバックをログに出力するところまで作ってみます。

## 1. Dialogflow で Intent を作る

合計で5つの Intent を作ります。利用シーンに出てくる会話との対応も書いておきます。

```
- start_feedback (ユーザー「フィードバックを送信。」を処理)
  - start_feedback - custom (ユーザー「いい感じですよ。」を処理)
    - start_feedback - custom - yes (ユーザー「はい。」を処理)
    - start_feedback - custom - no
    - start_feedback - custom - cancel
```

### start_feedback

まず、ユーザーが「フィードバックを送信」と話しかけた時の処理をする Intent (start_feedback) を作ります。

- Intent名: start_feedback
- Training phrases: フィードバックを送信
- Fulfillment: Enable webhook call for this intent

![スクリーンショット 2018-08-29 20.12.00.png](https://qiita-image-store.s3.amazonaws.com/0/6760/0b398296-1909-50e8-c338-c193ca08cd13.png "スクリーンショット 2018-08-29 20.12.00.png")

![スクリーンショット 2018-08-29 20.12.15.png](https://qiita-image-store.s3.amazonaws.com/0/6760/b3337859-5a82-3731-6808-ca78f3ab0227.png "スクリーンショット 2018-08-29 20.12.15.png")

### start_feedback - custom

次に、ユーザーからのフィードバックを受け取るインテントを、start_feedback の Follow-up intent として作ります。Follow-up intent は、ある Intent に続く会話を処理するのに便利な intent です。

まず左ペインで Intents を選んで Intent 一覧を表示し、start_feedback の上にマウスオーバーすると「Add follow-up intent」と表示されるのでクリックし、「custom」を選択します。

![Kobito.zDqwZl.png](https://qiita-image-store.s3.amazonaws.com/0/6760/9c9c2c0a-e11b-484f-fccd-6292537b9763.png "Kobito.zDqwZl.png")

![Kobito.M8Fbru.png](https://qiita-image-store.s3.amazonaws.com/0/6760/2751872f-b147-8e1a-84a3-ce6589ac2991.png "Kobito.M8Fbru.png")

すると、start_feedback の下に、start_feedback - custom が追加されます。

![Kobito.NNc6cJ.png](https://qiita-image-store.s3.amazonaws.com/0/6760/f84eb048-c72e-6a90-8761-72dd98142453.png "Kobito.NNc6cJ.png")

Folloup-intent なので、Input context に start_feedback-followup が入っています。

次に Training phrases を設定します。ユーザーが話す全ての内容をフィードバックとして認識できるようにします。まず、Training phrases に (何でもいいですがとりあえず) 「フィードバックの内容」と書きます。

![Kobito.bZryCW.png](https://qiita-image-store.s3.amazonaws.com/0/6760/c2b488a0-6fb5-59d6-6e34-9d18630430b9.png "Kobito.bZryCW.png")

次に、書いたテキスト全体をマウスでハイライトして、@sys.anyを選びます。

![Kobito.XLYJFH.png](https://qiita-image-store.s3.amazonaws.com/0/6760/df1f61c1-70b0-3343-d4fc-159d129912f7.png "Kobito.XLYJFH.png")

警告が出るんですが、今回はユーザーの話したこと丸ごと認識したいので、OK を押します。

![Kobito.kRONSW.png](https://qiita-image-store.s3.amazonaws.com/0/6760/02d653ec-a762-90a7-d7fe-9aec63ff48e4.png "Kobito.kRONSW.png")

次に、PARAMETER NAME を any から feedback に変えておきます。

![Kobito.CzlPop.png](https://qiita-image-store.s3.amazonaws.com/0/6760/1d171405-0676-333c-5ca2-4b0518ccf2da.png "Kobito.CzlPop.png")

最後に、Fulfillment の Enable webhook call for this intent を有効にして SAVE します。

- Intent名: start_feedback - custom
- Training phrases: feedback@sys.any
- Fulfillment: Enable webhook call for this intent

### start_feedback - custom - yes / no / cancel

GoogleHome はユーザーからもらったフィードバックの内容を繰り返し、それが正しいかを確認します。OKの場合の intent を、start_feedback - custom の Follow-up intent として作ります。

先ほどと同じように、左ペインから Intents を選択し、start_feedback - custom の上にマウスオーバーして Add follow-up intent をクリックします。次に yes を選択します。

出来上がった start_feedback - custom - yes をクリックして内容を確認します。

Training phrases は最初から入ってるので、Fulfillment の　Enable webhook call だけ有効にして　SAVE します。

同様にして、no, cancel も作り、それぞれ Fulfillment の Enable webhook call を忘れずに有効にしして SAVE します。

これで5つの Intent ができました。

![スクリーンショット 2018-08-29 20.52.03.png](https://qiita-image-store.s3.amazonaws.com/0/6760/4e85558a-d639-c937-7984-c8c478ab8e5f.png "スクリーンショット 2018-08-29 20.52.03.png")

くどいようですが、どの Intent も Fulfillment の　Enable webhook call は有効にしてくださいね。

![スクリーンショット 2018-08-29 20.12.15.png](https://qiita-image-store.s3.amazonaws.com/0/6760/b3337859-5a82-3731-6808-ca78f3ab0227.png "スクリーンショット 2018-08-29 20.12.15.png")

## 2. Dialogflow の Fulfillment > Inline Editor で index.js にコードを追加

[Dialogflow V2 の Inline Editor 用コードテンプレート](https://qiita.com/takatama/items/c8146faf9f04fc6b0698)に従って、index.js にコードを追加します。

```javascript
'use strict';

const functions = require('firebase-functions');
// Suggestions を追加しておく
const { dialogflow, Suggestions } = require('actions-on-google');

const app = dialogflow();

app.intent('Default Welcome Intent', conv => {
    ...
});

app.intent('start_feedback', conv => {
    return conv.ask('わかりました。ご意見やご感想を教えてください。');
});

app.intent('start_feedback - custom', (conv, {feedback}) => {
    // 会話データとしてフィードバックを保存しておく
    conv.data.feedback = feedback;
    conv.ask(`いただいたご意見は「${feedback}」でよろしいでしょうか？`);
    return conv.ask(new Suggestions(['はい', 'いいえ']));
});

app.intent('start_feedback - custom - yes', conv => {
    console.log(`feedback: ${conv.data.feedback}`);
    return conv.close('わかりました。フィードバックをありがとうございました。');
});

const quitWithoutFeedback = conv => conv.close('わかりました。フィードバックは送信しません。');

app.intent('start_feedback - custom - no', conv => {
    return quitWithoutFeedback(conv);
});

app.intent('start_feedback - custom - cancel', conv => {
    return quitWithoutFeedback(conv);
});

exports.dialogflowFirebaseFulfillment = functions.https.onRequest(app);
```

うまく動くか試してみましょう。

「<アプリ名>を使ってフィードバックを送信」(アプリ名を設定していない時は「テスト用アプリを使ってフィードバックを送信」) で試せます。

利用シーンに従って会話をし、ログにフィードバックが出力されていれば成功です。


# フィードバックを Googleスプレッドシートに記録し、さらにメールで送信する

ログに出力するだけでは見落としてしまうので、Google Apps Script (GAS) を使って改善します。

参考: [Google SpreadSheet にデータを追加、更新、削除する Web API をサクッと作る](https://qiita.com/takatama/items/e5cb83012d14c0094a79)

## 3. Firebase を有料プランにする

Cloud Functions から GAS を呼び出すところは、Firebase の有料プランが必要になります。ログに以下のような警告が出ているので気がつけます。

```
Error: getaddrinfo ENOTFOUND script.google.com script.google.com:443
```

```
Billing account not configured. External network is not accessible and quotas are severely limited. Configure billing account to remove these restrictions
```

なお、有料プラン、特に青天井の Blaze プランにすると使い過ぎが気になります。[
使用量と請求をモニタリングする](https://firebase.google.com/docs/firestore/usage?hl=ja)を参考にして、気がつけるようにしておきましょう。

## 4. Google スプレッドシートを作る

参考記事に従ってスプレッドシートを作り、Script Editorでコードを書き、Webアプリとして公開して、URLをメモしておきます。

2列目の1行目は feedback としておきます。

## 5. Firebase から HTTP POST できるように package.json を変更する。

Webアプリにフィードバックを渡すために、npm の [request](https://www.npmjs.com/package/request) モジュールを追加します。

Dialogflow の左ペイン Fulfillment > Inline Editor で、package.json タブをクリックします。
dependencies に request を追加して DEPLOY します。

![スクリーンショット 2018-08-29 21.55.26.png](https://qiita-image-store.s3.amazonaws.com/0/6760/fca4f7a9-4d59-22ee-0e18-06a04fa5a9ef.png "スクリーンショット 2018-08-29 21.55.26.png")

```javascript
  "dependencies": {
    ...
    "request": "^2.88.0"
  }
```

## 6. Dialogflow の Fulfillment > Inline Editor で index.js にコードを追加

HTTP API を呼び出す時は、Promise を作って intent handler から return しないと動作しません。

参考: https://developers.google.com/actions/reference/nodejs/lib-v1-migration#intent_handler_helpers_and_arguments

そこで、post 用のユーティリティ関数を作っておきます。

```javascript
// 追加
const request = require('request');

// 追加
const post = (url, form) => {
    return new Promise((resolve, reject) => {
        request.post({
            url: url,
            form: form
        }, function (err, res, body) {
            if (err) {
                console.error(err);
                return reject(err);
            }
            resolve(body);
        });
    });
};

// 修正
app.intent('start_feedback - custom - yes', conv => {
    console.log(`feedback: ${conv.data.feedback}`);
    // Googleスプレッドシート Web アプリの URL
    const url = 'https://script.google.com/macros/s/XXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXX_XXXXXXXX-XXXXXXXXX/exec';
    // Promise を return する
    return post(url, conv.data).then((body) => {
        console.log(body); // デバッグ用
        return conv.close('わかりました。フィードバックをありがとうございました。');
    }).catch(err => conv.close(`エラーです。${err}`));
});

...
```

なお、デバッグ用のログ出力はうまくいっている時は以下のように何も返却されないのが正しいです。

```html
<!DOCTYPE html><html><head><link rel="shortcut icon" href="//ssl.gstatic.com/docs/script/images/favicon.ico"><title>Error</title><style type="text/css">body {background-color: #fff; margin: 0; padding: 0;}.errorMessage {font-family: Arial,sans-serif; font-size: 12pt; font-weight: bold; line-height: 150%; padding-top: 25px;}</style></head><body style="margin:20px"><div><img alt="Google Apps Script" src="//ssl.gstatic.com/docs/script/images/logo.png"></div><div style="text-align:center;font-family:monospace;margin:50px auto 0;max-width:600px">The script completed but did not return anything.</div></body></html>
```

## 7. メールで通知

スプレッドシートにフィードバックが追加されたら、メールで通知します。Google スプレッドシートの Web アプリを以下のように修正します。

```javascript
function send(body) {
  // メールアドレス, 件名, 本文
  MailApp.sendEmail('<送信先メールアドレス>', 'フィードバック', body);
}

// 修正
function appendRow(sheet, parameter) {
  parameter['uuid'] = Utilities.getUuid();
  var row = createRow(sheet, parameter);
  sheet.appendRow(row);
  send(row.join('\n'));
}
```

修正が終わったら、Web アプリとして公開 (Publish > Deploy as web app...) しますが、Project version を New にすることをお忘れなく。

これで、ありがたいフィードバックを受け付けて、かつ、見落とさないようになりました！

# 「フィードバックを送信」を作った背景

とあるアクションで、聞き取れなかった発話をエラーログに出力し、随時コードを修正してました。あるユーザーさんがそのことに気がつき、追加機能の要望がエラーログに残るよう、何回かに分けて発話してくれていたのです。創造的な使い方に驚きました。

Actions directory にユーザーがコメントを残せるようになる前の話です。