Googleアシスタントとしりとりしてみる

二児のパパエンジニアです。

Google Homeのアプリ作って家族とエンジョイみたいな記事が最近多いので、そこに乗っかってみよう（+スキルアップ）な感じで嫁と相談してみました。

自分：「どういうの作ると子供って喜ぶと思う？」
嫁：「しりとり。最近やってる」

おおぅ、確かにそうだけど、、、しりとりって地味に難しくない？
まあ、まずはやってみようと、ちょっとやってみましたの記事です。

ちなみにまだGoogle Homeは持ってないのでGoogleアシスタントでのテストまでになります。
買ってみてどうなるかは、また後日。

# 構成
- Googleアシスタント
- Dialogflow
- Firebase
- Actions On Google

GoogleアシスタントのアプリなのでDialogflowはほぼ必須として。（そこは自前むずいですよね）
それだけでしりとりの処理は無理やろと、バックエンドの処理はActions On Googleで。
あとサーバーサイドは参考記事も豊富でディプロイしやすそうなFirebaseを使うことにしました。

# 処理の流れ
1. アプリを呼び出す
2. ユーザー名を決める
3. Googleアシスタントからしりとりをはじめる
4. ユーザーがしりとりを返す
5. (繰り返し)
6. 「ン」がついたら終了
7. 勝敗を宣言して、もう一回やるか確認

今回のところは、Googleアシスタントからの開始で固定しました。
じゃんけんするわけにもいかないし（別にやってもいいんだけど趣旨ぶれる）、会話のとっかかりは音声インターフェースの難しい部分ですよね。

あと、実は今回の処理には、しりとりの辞書っていうとってもめんどうなものが必要なのですが、そこは別で記事起こしたいと思います。
とりあえず概要だけ言うと、適当なものが思う浮かばなかったのでWikipediaのタイトルからピックアップした辞書をPythonで適当に作成しました。

# プロジェクトの準備
まずは、プロジェクトを作ります。

1. [Firebaseコンソール](https://console.firebase.google.com/)の`プロジェクトを追加`で、適当に追加
2. [Actions On Googleコンソール](https://console.actions.google.com/)の`Add/import project`で作成したプロジェクトを選択


# Dialogflow
Dialogflowの使い方は[公式](https://developers.google.com/actions/dialogflow/first-app?hl=ja)読むのが一番わかりやすいと思います。

Dialogflowは、人間の会話そのものをプログラムレスでインターフェースに変換してくれます。
ややこしい処理を考えなければ、Dialogflow単体でアプリが完成してしまうぐらい強力です。

以下の部分を実装してみます。
> 1. アプリを呼び出す
> 2. ユーザー名を決める

## Dialogflowの準備
Actions On Googleコンソールの、ActionsからDialogflowを選択すると、Dialogflowコンソールに飛びます。

![dialogflow.png](https://qiita-image-store.s3.amazonaws.com/0/7690/c9875bc9-9b0b-be9f-ca30-4b03fa1512a0.png)

![console.png](https://qiita-image-store.s3.amazonaws.com/0/7690/e3f6b553-3076-069a-35ee-067d210bdba7.png)

Dialogflowで必要な設定は以下になります。
1. Intents
2. Fulfillment

## Intentsの作成
Intentsは必要な会話の定義や設定、曖昧な会話の処理を決定します。
今回必要な会話は以下としました。

- Welcome（アプリを呼び出す）
- YourName（ユーザー名を決める）
- Shirotori（しりとり）
- Again（再開）
- Fallback（無意味な会話）
- End（終了）

WelcomeとEndとFallbackは省略します。
他の記事を参考にしてください。

YourNameとShiritoriを作ってみます。
まずはYourNameから。

Dialogflowコンソールの左メニューから`Intents`の`+`を選択し、追加画面に移動します。

まずはアプリ呼び出しのきっかけになる言葉を設定します。
`Training phrases`に入力します。
（ここ最初戸惑ったのですが、どうも`User Says`と言われていたものが`Training phrases`と変わってようです。注意です）

![yourname2.png](https://qiita-image-store.s3.amazonaws.com/0/7690/8492b95c-2b76-984d-8c46-8ae8d2bdaf69.png)

名前を決めるので、こんな感じでしょうか。〇〇の部分はそれぞれなので曖昧な表現にしておきます。
色が変わってますが、これは後で説明します。

次に`Action and parameters`を設定します。

![yourname4.png](https://qiita-image-store.s3.amazonaws.com/0/7690/e9241388-7c39-3427-a8f2-f938a2c5d770.png)

このアクション名とパラメータはActions On Googleで受け取れるデータを定義します。
ENTITYは型のようなもので、会話から切り出した部分を指定の形式として処理します。
今回はぶっちゃけなんでもいいので`@sys.any`にしてます。(よくわかってないですが、`Any`型みたいなもんでしょう)
VALUEは会話の切り出し部分の位置の定義です。

ここで`Training phrases`に戻ります。
さっきの`〇〇`の部分をダブルクリックすると上記で設定した定義が参照できます。

![yourname3.png](https://qiita-image-store.s3.amazonaws.com/0/7690/dd294dc2-92d4-693b-c9c0-50c6ab4053ab.png)

設定すると色が変わります。
これで、実際には「私は太郎です」と言おうが「私は花子です」と言おうが、DialogflowはこのIntentsを呼び出してくれて、`〇〇`にあたる部分をパラメータとして抜き出してくれます。

次に、この会話をきっかけに次の会話へつなげていきたいので、`Contexts`を設定します。

![yourname.png](https://qiita-image-store.s3.amazonaws.com/0/7690/cf7757d8-25a2-20e2-559e-23d52a338bca.png)

`Contexts`を設定することで、会話に連続性が生まれます。
`output context`に設定したデータは次以降の会話に引き渡されるので、それを判定して会話の分岐判断を行えます。

すでにWelcomeのIntentsで`output intents`を設定してあります。
ですので、今回の言葉をいきなり喋ってもアプリは反応しないようにすることができます。

最後にこのIntentsをActions On Googleで処理できるように、`Fulfillment`をONにします。

![yourname5.png](https://qiita-image-store.s3.amazonaws.com/0/7690/ebf68f7f-b73c-397a-cce6-4d5567b4ce4b.png)

`Fulfillment`はこのIntentsの処理をwebhookと繋げてくれる設定です。
実際に繋げるURLは別で設定します。

Shiritoriもほぼ同じ内容になります。

`Contexts`ではYourNameで設定したものを引き渡したいので、そのまま繋げます。

![shiritori.png](https://qiita-image-store.s3.amazonaws.com/0/7690/bef93a43-7b84-81d6-763a-0a14e491b416.png)

`Action and parameters`もしりとりの単語を受け取るだけなので、シンプルに設定します。

![shiritori2.png](https://qiita-image-store.s3.amazonaws.com/0/7690/c6bd440a-9c34-ffb2-95b2-9ecc7d4414df.png)

`Training phrases`は少し注意で、しりとりなので話した全体が候補となるので、適当な単語を入力して全体をパラメータにしてしまいます。

![shiritori3.png](https://qiita-image-store.s3.amazonaws.com/0/7690/7610484d-ee55-2ecc-13d7-eac67db61957.png)

あとは`Fulfillment`をONにして終了です。

## Fulfillmentの設定
webhookとして処理するURLを設定します。
左メニューの`Fulfillment`を選択し、`Webhook`の`ENABLED`にし、URLを入力します。

![fulfillment.png](https://qiita-image-store.s3.amazonaws.com/0/7690/c8d3eebe-7751-5049-3f42-d8f8fe89ca8d.png)

が、まだFirebase側を作成してないのでURLも何もないので、ここは一旦終了です。

# Firebase
次は、Webhook側の処理を書いていきます。
別に何のサーバー、何の言語でも使ってもいいんですが、環境が整っているFirebaseを使うことにします。
無料でほとんど制限なく豊富な機能も、DBも使えます。素晴らしいです。

一点、他のサービスを使う場合、Google以外のドメインにアクセスするとエラーとなるらしいので、そこだけ注意してください。

使用する機能は以下となります。

- Firebase Functions
- Firebase Realtime Database

## Firebase Functionsの準備
Firebase Functionsは、今のところnode.jsでのみ開発できます。

Firebase Functionsのデプロイは非常に簡単で、npmをインストールしてCLI経由でアップロードできます。

まずは以下でインストール。

```console
$ npm install -g firebase-tools
```

成功したら`firebase`コマンドが使えるようになります。
ログインします。

```console
$ firebase login
```

Webページでログインしていいか？と聞いてくるので、許可しましょう。WebページにリダイレクトしてGoogleアカウントのログインをします。

成功したら、環境の準備です。
適用にフォルダ作って、初期化します。

```console
$ firebase init functions
```

紐づけるプロジェクトを聞かれるので、今回作成したものにしましょう。
あと、TypeScriptかJavaScriptかも聞かれます。自分の環境に適したものを選びましょう。

全部終わったら`functions`フォルダができます。
そこが開発環境となるので、そこに移動して開発をはじめます。

## しりとり処理の開発
必要なファイルは`packege.json`と`index.js`です。
紆余曲折色々あったのですが、最終的に以下のようなコードになりました。
npmは必要なものをインストールしましょう。

```javascript:index.js
// モジュール
process.env.DEBUG = 'actions-on-google:*';
const { DialogflowApp } = require('actions-on-google');
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const Promise = require('promise');
const kuromoji = require('kuromoji');

// Firebase Realtime Database
admin.initializeApp(functions.config().firebase);
const db = admin.database();

// DBの取得
function getDB (name) {
  return new Promise(function (resolve, reject) {
    db.ref('/' + name).once('value', function (snapshot) {
      resolve(snapshot.val());
    });
  });
}

// DBの更新
function setDB (name, value) {
  db.ref('/' + name).set(value);
}

// 全DBデータの取得
function getAllDB () {
  return Promise.all([getDB('dict'), getDB('player_results'), getDB('now_player'), getDB('used_word')]);
}

// 読み仮名取得
function yomigana (word) {
  return new Promise(function (resolve, reject) {
    const builder = kuromoji.builder({dicPath: 'node_modules/kuromoji/dict/'});
    builder.build(function (err, tokenizer) {
      if (err != null) {
        reject(err);
        return;
      }
      let yomi = '';
      let path = tokenizer.tokenize(word);
      path.forEach(function (v, i, a) {
        yomi += v['reading'] ? v['reading'] : '';
      });
      resolve(yomi || word);
    });
  });
}

// 単語をランダムで取得
function wordRandom (initialKey, shiritoriDict) {
  const wordDict = shiritoriDict[initialKey];
  const wordLength = Object.keys(wordDict).length;
  const wordKey = Object.keys(wordDict)[Math.floor(Math.random() * wordLength)];
  return wordDict[wordKey];
}

// 最初の単語をランダムで取得
function firstWordRandom (shiritoriDict) {
  const initalKeyLength = Object.keys(shiritoriDict).length;
  const initialKey = Object.keys(shiritoriDict)[Math.floor(Math.random() * initalKeyLength)];

  let word = '';
  do {
    word = wordRandom(initialKey, shiritoriDict);
  } while (word['good'] === false);

  return word;
}

// 次の単語をランダムで取得
function nextWordRandom (last, usedWordList, shiritoriDict) {
  let newWord = {};
  let result = null;

  do {
    // 次の文字
    do {
      newWord = wordRandom(last, shiritoriDict);
    } while (usedWordList.indexOf(newWord['word']) !== -1);

    // 使用済み
    usedWordList.push(newWord['yomigana']);
    setDB('used_word', usedWordList);

    // 最後の文字判定
    const newLast = newWord['yomigana'].slice(-1);
    result = validLastWord(newLast);
  } while (result === 'no_word' || result === 'no_kana');

  return newWord;
}

// 最初の文字判定
function validPlayerWord (word, usedWordList) {
  // 異常
  if (!usedWordList) {
    return 'no_word';
  }
  // 一文字はだめ
  if (usedWordList.length === 0) {
    return 'one_word';
  }
  // 前回と違う言葉は除外
  if (usedWordList[usedWordList.length - 1].slice(-1) !== word) {
    return 'mismatch_word';
  }

  // 成功
  return null;
}

// 最後の文字判定
function validLastWord (word) {
  // 空は終わり
  if (!word) {
    console.log('isLoseWord: no word, ' + word);
    return 'no_word';
  }

  // ンは終わり
  if (word === 'ン') {
    console.log('isLoseWord: end word, ' + word);
    return 'lose_word';
  }

  // 許可する仮名
  const shiritoriKana = [
    'ア', 'イ', 'ウ', 'エ', 'オ',
    'カ', 'キ', 'ク', 'ケ', 'コ',
    'ガ', 'ギ', 'グ', 'ゲ', 'ゴ',
    'サ', 'シ', 'ス', 'セ', 'ソ',
    'ザ', 'ジ', 'ズ', 'ゼ', 'ゾ',
    'タ', 'チ', 'ツ', 'テ', 'ト',
    'ダ', 'ヂ', 'ヅ', 'デ', 'ド',
    'ナ', 'ニ', 'ヌ', 'ネ', 'ノ',
    'ハ', 'ヒ', 'フ', 'ヘ', 'ホ',
    'バ', 'ビ', 'ブ', 'ベ', 'ボ',
    'パ', 'ピ', 'プ', 'ペ', 'ポ',
    'マ', 'ミ', 'ム', 'メ', 'モ',
    'ヤ', 'ユ', 'ヨ',
    'ラ', 'リ', 'ル', 'レ', 'ロ',
    'ワ', 'ン'
  ];

  // カタカナ以外は終わり
  if (shiritoriKana.indexOf(word) === -1) {
    console.log('isLoseWord: no kana word, ' + word);
    return 'no_kana';
  }

  // 成功
  return null;
}

// 戦果記録
function writeResult (win, nowPlayer, playerResults) {
  console.log('result: ' + nowPlayer['name'] + 'の' + (win ? '勝ち' : '負け'));

  let result = playerResults[nowPlayer['name']];
  if (!result) {
    result = {'win': 0, 'lose': 0, 'started_at': nowPlayer['started_at'], 'last_played_at': nowPlayer['started_at']};
  }

  result['win'] += win ? 1 : 0;
  result['lose'] += win ? 0 : 1;
  result['last_played_at'] = nowPlayer['started_at'];

  playerResults[nowPlayer['name']] = result;
  setDB('player_results', playerResults);
}

// メイン処理(Actins)
exports.mainAction = functions.https.onRequest((request, response) => {
  // Dialogflowのシーケンス
  const app = new DialogflowApp({request, response});
  console.log('Request headers: ' + JSON.stringify(request.headers));
  console.log('Request body: ' + JSON.stringify(request.body));

  // 開始
  function yournameResponseHandler (app) {
    getAllDB()
    .then(function (data) {
      const shiritoriDict = data[0];
      const playerResults = data[1];

      // 相手の名前
      const player = request.body.result.parameters.yourname;
      console.log(player);

      // プレイヤーの決定
      setDB('now_player', {name: player, started_at: (new Date()).getTime()});

      let message = '';

      // 過去の戦績
      const result = playerResults[player];
      if (result) {
        console.log(player + 'の成績は' + result['win'] + '勝' + result['lose'] + '敗');
        message += player + 'の成績は' + result['win'] + '勝' + result['lose'] + '敗でしたね。';
      } else {
        console.log(player + 'とは初めて');
        message += player + 'とは初めて会いましたね。';
      }

      // 最初のしりとり
      console.log('shiritori start!');
      message += 'さあ、しりとりをはじめましょう。私から行きますよ。';

      // 最初の言葉を取得
      const word = firstWordRandom(shiritoriDict);

      console.log('first word: tori-peは「' + word['word'] + '(' + word['yomigana'] + ')」と言った');
      message += '最初の言葉は、' + word['yomigana'] + 'です。' + word['yomigana'].slice(-1) + 'でお願いします。';

      // 使用済み
      setDB('used_word', [word['yomigana']]);

      // 返事する
      app.ask(message);
    });
  }

  // しりとり
  function shiritoriResponseHandler (app) {
    getAllDB()
      .then(function (data) {
        const shiritoriDict = data[0];
        const playerResults = data[1];
        const nowPlayer = data[2];
        const usedWordList = data[3];

        // 相手の言葉
        const word = request.body.result.parameters.yourword;
        let message = '';

        // 読み仮名
        yomigana(word)
          .then(function (yomi) {
            message = yomi + 'ですね。';

            // 最初の文字判定
            const first = yomi.slice(0, 1);
            console.log('first: ' + first);
            let result = validPlayerWord(first, usedWordList);
            if (result !== null) {
              switch (result) {
                case 'no_word':
                  message += '変ですね。消えませんでした。もう一度お願いします。';
                  break;
                case 'one_word':
                  message += '一文字は禁止です。もう一度お願いします。';
                  break;
                case 'mismatch_word':
                  message += 'はじめの言葉が違いますよ。もう一度お願いします。';
                  break;
              }
              // 返事する
              app.ask(message);
              return Promise.reject(new Error('error_first'));
            }

            console.log('next word: ' + 'プレイヤー' + 'は「' + word + '(' + yomi + ')」と言った');

            // 使用済み
            usedWordList.push(yomi);
            setDB('used_word', usedWordList);

            // 最後の文字判定
            let last = yomi.slice(-1);
            result = validLastWord(last);
            if (result !== null) {
              // 正常系以外
              switch (result) {
                case 'no_word':
                  message += 'その言葉は知りません。もう一度お願いします。';
                  break;

                case 'no_kana':
                  message += 'その言葉は知りません。もう一度お願いします。';
                  break;

                case 'lose_word':
                  message += 'ンがつきましたよ！あなたの負けです！';

                  writeResult(false, nowPlayer, playerResults);
                  const result = playerResults[nowPlayer['name']];
                  if (result) {
                    console.log(nowPlayer['name'] + 'の成績は' + result['win'] + '勝' + result['lose'] + '敗');
                    message += nowPlayer['name'] + 'の成績は' + result['win'] + '勝' + result['lose'] + '敗でしたね。';
                  }

                  console.log('shiritori end!');
                  message += 'もう一度チャレンジしますか？';
                  break;
              }

              // 返事する
              app.ask(message);
              return Promise.reject(new Error('error_last'));
            }

            message += '私の番です。';

            // 次の単語を取得
            let newWord = nextWordRandom(last, usedWordList, shiritoriDict);
            message += newWord['yomigana'] + 'です。' + newWord['yomigana'].slice(-1) + 'でお願いします。';

            // 最終文字判定
            last = newWord['yomigana'].slice(-1);
            result = validLastWord(last);
            if (result != null) {
              // 正常系以外
              message += 'あ！ンがついてしまいました！あなたの勝ちです！';

              writeResult(true, nowPlayer, playerResults);
              const result = playerResults[nowPlayer['name']];
              if (result) {
                console.log(nowPlayer['name'] + 'の成績は' + result['win'] + '勝' + result['lose'] + '敗');
                message += nowPlayer['name'] + 'の成績は' + result['win'] + '勝' + result['lose'] + '敗でしたね。';
              }

              console.log('shiritori end!');
              message += 'もう一度チャレンジしますか？';

              // 返事する
              app.ask(message);
              return Promise.reject(new Error('error_last_own'));
            }

            // 返事する
            app.ask(message);
          })
          .catch(function (error) {
            console.log(error);
          });
      });
  }

  // もう一回
  function againResponseHandler (app) {
    getAllDB()
    .then(function (data) {
      const shiritoriDict = data[0];
      const nowPlayer = data[2];

      // 相手の名前
      console.log(nowPlayer);

      let message = '';

      // 最初のしりとり
      console.log('shiritori start!');
      message += 'わかりました、もう一度やりましょう。私から行きますよ。';

      // 最初の言葉を取得
      const word = firstWordRandom(shiritoriDict);

      console.log('first word: tori-peは「' + word['word'] + '(' + word['yomigana'] + ')」と言った');
      message += '最初の言葉は、' + word['yomigana'] + 'です。' + word['yomigana'].slice(-1) + 'でお願いします。';

      // 使用済み
      setDB('used_word', [word['yomigana']]);

      // 返事する
      app.ask(message);
    });
  }

  // 受け答えに対応
  const actionMap = new Map();
  actionMap.set('action.yourname', yournameResponseHandler);
  actionMap.set('action.shiritori', shiritoriResponseHandler);
  actionMap.set('action.again', againResponseHandler);
  app.handleRequest(actionMap);
});
```

基本的には以下の流れです。
1. Dialogflowで定義したaction毎に処理をする
2. 受け取ったパラメータを処理、加工する
3. Dialogflowを使って返答する

### Dialogflowのaction
以下の部分です。

```javascript
const actionMap = new Map();
actionMap.set('action.yourname', yournameResponseHandler);
actionMap.set('action.shiritori', shiritoriResponseHandler);
actionMap.set('action.again', againResponseHandler);
app.handleRequest(actionMap);
```

定義した名称をキーとして、ハンドラーを登録します。
これだけで勝手に呼び出されます。

### Dialogflowのパラメータ
Dialogflowから受け取った言葉は、
`request.body.result.parameters.yourname`
のようにパラメータとして受け取れます。

問題は受け取った言葉がそのままだと良い感じに日本語化されている（つまり漢字とかになってる）ので、読み仮名がわからんということです。
頭が良すぎるのも考えものですよね。読み仮名だけ欲しいってのに。

しょうがないので、[kuromoji](https://www.npmjs.com/package/kuromoji)を使うことにしました。
あまりnode.jsに明るくない身なので、無邪気に`mecab`とか普通に使えると思い込んでて大分ハマりました。
ここら辺から、[この方の記事](https://techblog.gmo-ap.jp/2018/01/19/「本気のしりとり」google-homeアプリを作りました%E3%80%82/)に助けられて、色々参考にしています。

### Dialogflowで返答
[公式](https://developers.google.com/actions/reference/nodejs/DialogflowApp)を参考にしてください。
普通に返答するのは`ask`です。

最初Googleが用意してくれているActions on Googleのサンプルコードの[ここ](https://github.com/actions-on-google/dialogflow-webhook-template-nodejs)を参考に書き始めたのですが、ここでは`tell`が使われてました。
これが罠で、`tell`だと会話を終わらせてしまいます。
使ったら**アプリが終了してしまいます**。わけわからずに大分悩みました。
続けて会話を処理する場合は`ask`を使いましょう。

## Firebase Realtime Database
しりとりの辞書とか、処理中の連続的なデータの格納としては、Firebase Realtime Databaseを使いました。
概要としては、辞書形式のデータ構造がそのまま読み書きできる、程度に把握しておけばいいでしょう。
ほとんどローカルストレージ的な扱いができるので、そこは非常に便利です。

データはコンソールから簡単に追加、削除できます。
`Database` - `データ` の `Jsonをインポート`で追加、各項目の`x`ボタンで削除できます。

ここで事前に用意しておいたしりとりの辞書をおもむろにインポートします。

データ構造としては以下のような感じで作ってます。

![database.png](https://qiita-image-store.s3.amazonaws.com/0/7690/3a843467-c0ac-5c32-0788-07baf1c202a6.png)

`good`は見切れてますが`true`が入ってます。ンで終わるかどうかをセットしてます。
なんか色々構想あったんですが特にまだ使えていません。
`yomigana`はローカルで解釈済みの読み仮名です。

### Functionsでのデータのアクセス
node.jsでRealtime Databaseのデータにアクセスするのは非常に簡単ですが、注意が必要です。

#### 追加・変更
Realtime Databaseのデータはパスで表現するので、ルートパスからの階層でアクセスします。
`now_player`というデータがルート直下にある場合は以下のように書くことで値の変更を直接表せます。
まだ値がない場合は勝手に追加されます。

```javascript
db.ref('/now_player').set(`hoge`);
```

#### 取得
取得も同じです。

```javascript
db.ref('/now_player').once('value', function (snapshot) {
    console.log(snapshot.val())
});
```

直接取得するのではなく、`snapshot.val()`なとこに気をつける以外は楽に書けます。
が、非同期で処理される部分は注意です。

これはPromise使って`async await`やな。と途中までコード書いたのですが、いざデプロイで罠が発覚。

Firebase Functionsのnode.jsのバージョンが`v6.11.5`なのです。

https://www.infoq.com/jp/news/2017/03/node-76-async-await
> Node 7.6、async/awaitをデフォルトでサポート

ということで、使えないのです。
（node.jsシロートが気持ち良く書いたのに、、、）

最新のnodeコーダーのみなさんは注意してください。私は全部書き直しました。

## Firebase Functionsのデプロイ
準備ができたのでデプロイします。以下を実行します。

```console
$ firebase deploy --only functions
```

デプロイの前のに自動的に`eslint`でチェックが走ります。問題あればエラーになるので直して再実行しましょう。

必要であれば、`.eslintrc.json`を修正して良いのですが、上記既出のようにnode.jsのバージョンが古いのでそこは気をつけましょう。
自分のコードが悪くてエラーが出てるんじゃくて、バージョンが古いのでエラーが出てる場合もありますよ。

問題なければ、`Deploy complete!`になって終了します。

### Firebase FunctionsのURL
コンソールの`Functions` - `ダッシュボード`にあります。

![url.png](https://qiita-image-store.s3.amazonaws.com/0/7690/98a22d2f-a5a2-7367-d307-d83684e57f9b.png)

そのURLをDialogflowのFulfillmentに戻って設定してください。

以上で、基本的な部分は終了です。お疲れ様でした。

# Googleアシスタントのテスト
Dialogflowに戻ります。
左のメニューの`Integrations`をクリックし、`Google Assistant`をクリックします。

![integr.png](https://qiita-image-store.s3.amazonaws.com/0/7690/92b24273-367e-8050-acc5-3803fb4199f6.png)

表示されたダイアログで`TEST`をクリックします。

![test.png](https://qiita-image-store.s3.amazonaws.com/0/7690/59eb3926-38dc-54bb-258c-b516e8e5680f.png)

Actions On Googleのページに遷移します。

![simu.png](https://qiita-image-store.s3.amazonaws.com/0/7690/2b74c212-e6b7-e7ec-e379-ae7ae8cd9c45.png)

ここはシミュレータのページです。
真ん中のモバイルっぽい表示の下に入力ボックスがあります。ここに入力してテストします。
今作ったアプリをテストしたい場合は`テスト用アプリにつないで`と入力すれば、Welcomeインテントが呼び出された状態からはじまります。

あとは、会話を進行してテストしてください。
何か問題があるとすぐ`テストアプリが退出しました`となるので、都度Firebase Functionsのログ画面で状況を確認してデバッグしましょう。

また、この状態まで行くと実際のGoogleアシスタントでも呼び出せるようになります。
Googleアシスタントで、「テスト用アプリにつないで」と話しかけてください。

以上で全てです。

# 実際の動画
以下が実際のテスト動画です。

[![テストビデオ](http://img.youtube.com/vi/GmiYJK2bCF0/0.jpg)](http://www.youtube.com/watch?v=GmiYJK2bCF0)

アプリのタイトルは「とりっぺ」にしました。
「しり**とり**」「ウィッキ**ペ**ディア」みたいなのです。
適当です。

やってみた結果としては、反応が思いの外遅いです。
デカい辞書を毎回読み直してるのとかあるのでしょうが、普通にDialogflowががんばってるんでしょうか。
あと認識率がめちゃくちゃ悪いのはGoogleアシスタントでしょうね。
受け答えも意味不明になるので、改良の余地ありです。

地味に、しりとりやろうとしても全然単語が出てこなくて衝撃でした。
完全に頭悪くなってるわ、自分。
最後のイランとかもはやギャグでしかない。

# 今後やること
- 細かい受け答えの調整
- Google Homeを買って子供とやらせてみる
- 辞書を改良する

本当に子供が使ってくれるようになるか微妙ですが、もうちょっと考えます。
以上です。