Dialogflow V2 の Inline Editor 用コードテンプレート


# Inline Editor 用コードテンプレート

Dialogflow の Fullfillment にある Inline Editor は、Web ブラウザーだけでアプリを作れるのでとても便利です。

ただ、デフォルトで表示されるコードが公式ドキュメント（例えば [Actions on Google Node.js Client Library Version 1 Migration Guide](https://developers.google.com/actions/reference/nodejs/lib-v1-migration)）と違っているのでとっても不便。

そこで、以下のコードをベースに開発するようにしています。[gist](https://gist.github.com/takatama/186f7e500b4a761ed3fc2978699a3ad9)

```javascript
'use strict';

const functions = require('firebase-functions');
const { dialogflow } = require('actions-on-google');

const app = dialogflow();

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

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

### ポイント1
app.intent() には、インテント名（Default Welcome Intent）を使います。V1で使っていたアクション名（input.welcome）ではありません。

### ポイント2
exports.dialogflowFirebaseFulfillment を使います。上記 Migration Guide では exports.factsAboutGoogle になっています。package.jsonでこの関数名を使うように指定してあります。

### 動作させるための事前準備

1. [Dialogflow console](https://console.dialogflow.com) の左ペイン Intents > Default Welcome Intent を選択し、ページ一番下の Fullfillment で Enable webhook call for this intent をチェックして Save ボタンを押下
2. 同じく左ペイン > Fullfillment の Inline Editor を Enable にして、index.js に上記コードをコピーしてから Deploy ボタンを押下

![inline-editor.png](https://qiita-image-store.s3.amazonaws.com/0/6760/13d12651-7a28-e616-fa86-6b1c53b68fbe.png "inline-editor.png")

ちなみに、Inline Editor の package.json は、それぞれのモジュールのバージョンを上げて以下のようにしています。

```javascript
{
  "name": "dialogflowFirebaseFulfillment",
  "description": "This is the default fulfillment for a Dialogflow agents using Cloud Functions for Firebase",
  "version": "0.0.1",
  "private": true,
  "license": "Apache Version 2.0",
  "author": "Google Inc.",
  "engines": {
    "node": "8"
  },
  "scripts": {
    "start": "firebase serve --only functions:dialogflowFirebaseFulfillment",
    "deploy": "firebase deploy --only functions:dialogflowFirebaseFulfillment"
  },
  "dependencies": {
    "actions-on-google": "^2.12.0",
    "firebase-admin": "^8.11.0",
    "firebase-functions": "^3.6.1",
    "dialogflow": "^1.2.0",
    "dialogflow-fulfillment": "^0.6.1"
  }
}
```

### 背景

Dialogflow API が V2 になったタイミングで、Inline Editor に表示されるデフォルトのコードが [dialogflow-fullfillment-nodejs](https://github.com/dialogflow/dialogflow-fulfillment-nodejs/blob/master/samples/quick-start/functions/index.js) に変更されましたが、公式ドキュメントの書き方と合っていません。

そこで [Actions on Google client library for Node.js v2.0.0](
https://medium.com/@silvano.luciani/actions-on-google-client-library-for-node-js-v2-0-0-alpha-c92361167f15) を参考に書き直しました。

# アプリ開発に役立つ機能

上記コードをベースにして、いろいろな機能を使ってみます。

## 注意事項

Google Cloud Console 経由で Cloud Functions の設定を変更できますが、一度でもそれをやると Dialogflow の Inline Editor が使えなくなります！
また、ターミナルで gcloud コマンドを使っても同様に使えなくなります。
一度使えなくなると、ずっと使えないまま（戻せない）ので注意してください。

## Deployに失敗した時のソースコードを参照する

Deployに失敗したソースコードは、Inline Editor に表示されません。
Deployに失敗するのは、ソースコードにJavaScriptの構文エラーや、package.json での定義モレがある場合です。エディター下部にエラーメッセージが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6760/9d2cc8b1-8dfe-42c8-9f53-146316a25268.png)

この時、うっかりエディターとは別のページを開いたり、エディターをリロードしてしまうと、Deployに失敗したソースコードは消えてしまい、Deployが最後に成功した時のソースコードが表示されるようになります。

そんな時は、[Google Cloud Console](https://console.cloud.google.com/) の左上ハンバーガーメニュー＞Cloud Functions を選択。上部メニューからプロジェクトを選択＞dialogflowFirebaseFulfillment を選択。Sourceタブを選ぶと、Deployに失敗したソースコードが表示されます。

ただし、このソースコードは参照するのにとどめてください。ここで操作すると、上述した注意事項の通り、Dialogflow の Inline Editor での編集ができなくなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6760/4e4e53b6-363d-bc37-7c4c-e4ab82f0be09.png)

## パラメーターを参照する
公式ドキュメント: https://developers.google.com/actions/dialogflow/fulfillment#create_functions_to_handle_requests

[分割代入](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) を使ってパラメーターを参照できます。以下の num がパラメーターです。

```javascript
const app = dialogflow();
app.intent('Default Welcome Intent', (conv) => {
    conv.ask('Welcome to number echo! Say a number.');
});
app.intent('Input Number', (conv, {num}) => {
    // extract the num parameter as a local string variable
    conv.close(`You said ${num}`);
});
```

## ユーザーとの「よくある」やりとりをお手軽に作る

公式ドキュメント: https://developers.google.com/actions/assistant/helpers

Helper を使うことで、Intent を作る手間を省けます。Helper に対応する Dialogflow event がそれぞれ存在します。

楽できる反面、できないこともあるので注意が必要です（後述）。

### ユーザーの情報を聞き出す例 (Permission)

Permission クラスと actions_intent_PERMISSION イベントを使って、ユーザーの名前を取得する例を示します。具体的には以下のやりとりを行います。

> GoogleHome: 挨拶をするために、名前が必要です。Googleの情報を利用してもよいでしょうか。
> 
> あなた: はい
> 
> GoogleHome: <あなたの名前>さん、こんにちは。

具体的な手順は次の通り。

1. 新しい Intent を作り、対応する Dialogflow event を受け付けるように設定します。ask_for_permission_confirmation という名前をインテントを作り、Events に actions_intent_PERMISSION を指定し、Fulfillment を enabled にします。

1. Inline Editor に以下のコードを入力して Deploy します。最初に Permission を使えるように require() しておきます。Default Welcome Intent で conv.ask(new Permission()) してユーザーに許可を求めます。

```javascript
'use strict';

const functions = require('firebase-functions');
const { dialogflow, Permission } = require('actions-on-google');

const app = dialogflow();

app.intent('Default Welcome Intent', conv => {
    const options = {
        context: '挨拶をするために',
        // NAME, DEVICE_PRECISE_LOCATION, DEVICE_COARSE_LOCATION から複数指定できる
        permissions: ['NAME']
    };
    conv.ask(new Permission(options));
});

app.intent('ask_for_permission_confirmation', (conv, params, confirmationGranted) => {
    if (confirmationGranted) {
        // NAMEの場合
        const {name} = conv.user;
        if (name) {
            return conv.close(`${name.display}さん、こんにちは`);
        }
        // DEVICE_PRECISE_LOCATIONの場合
        // const {location} = conv.device;
    }
    conv.close(`お名前を伺うことができませんでした。`);
});

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

### できないことの例（Confirmation）

Confirmation クラスと intent_action_CONFIRMATION イベントを使えば、ユーザーに「はい」「いいえ」で答えてもらうことができるようになります。
しかし、BasicCard を使ったビジュアルな返信と組み合わせることができません。
具体的には、conv.ask(new Confirmation()) する前に conv.ask(new BasicCard()) で指定していた返信は無視されます。

そんなときは自分で Yes用/No用の Intent をそれぞれ作る必要があります。Dialogflow 上で Followup Intent を作るとよいでしょう。

回避できないのかな...。

### デバイスの位置情報

Permission クラスと actions_intent_PERMISSION イベントを使います。

```javascript
app.intent('nearby', conv => {
    conv.ask(new Permission({
        context: '付近の鳥を探すために',
        permissions: ['DEVICE_PRECISE_LOCATION']
    }));
    // conv.ask(new Suggestions(['はい', 'いいえ'])); // 適用されない。
});

app.intent('permission_confirm', (conv, params, granted) => {
    if (granted) {
        const {coordinates} = conv.device.location;
        console.log(corrdinates.latitude);
        console.log(corrdinates.longitude);
        ...
       
```

### 困っていること

actions_intent_PLACE の使い方が分からないので、どなたか教えて下さい...。

## データを保存する

公式ドキュメント: https://developers.google.com/actions/assistant/save-data

### アプリが起動して終了するまでの間なら conv.data

conv.data を使います。例えば config に {ver: 1} を保存する場合、

```javascript
conv.data.config = {ver: 1};
```

### 永続的に保存するなら conv.user.storage（ただし条件付）

ユーザーが「Voice Match でアカウントに基づく情報を受け取る」ようになっている（```conv.user.verification```が```VERIFIED```になっている）場合には、conv.user.storage が使えます。

参考：https://developers.google.com/assistant/conversational/save-data#determining_and_handling_user_verification_status

ただし、個人情報を保存する場合には注意が必要です。国によっては、ユーザーの同意が必要です（例えば、EU 一般データ保護規則 GDPR）。また、アプリを公開する時に作成するプライバシーポリシーへの記載も必要になります。

Voice Match が設定されておらず、ユーザーを特定できない（```conv.user.verification```が```GUEST```になっている）場合には conv.user.storage は使えず、常に {} になっているので注意してください。

[Google Home で Voice Match を設定する](https://support.google.com/googlehome/answer/7323910?hl=ja)


```javascript
conv.user.storage.config = {ver: 1};
```

## 音楽を再生する

まるまる一曲やポッドキャストを再生する場合は MediaResponses を使います。それ以外は SSML を使います。

どちらも音楽のURLを指定しますが、Firebase の無料プランだと google 外の URL を呼び出せないので注意してください。

### MediaResponse API を使う

公式ドキュメント: https://developers.google.com/actions/assistant/responses#media_responses

2018/05/06時点で、iOS の Google アシスタントは対応していない。なので、MEDIA_RESPONSE_AUDIO で再生前にチェックを入れている。

MediaObject の前に、必ずテキスト or SSML レスポンスを入れることに注意してください。これを忘れると、

- conv.close の場合、`MalformedResponse at final_response.rich_response: the first element must be a 'simple_response', a 'structured_response' or a 'custom_response'` というエラーが出ます。

- conv.ask の場合、`alformedResponse at expected_inputs[0].input_prompt.rich_initial_prompt: the first element must be a 'simple_response', a 'structured_response' or a 'custom_response'` というエラーが出ます。

大きな画像を表示したい場合は image を、小さな画像は icon を指定します。なお、Image を require すると Inline Editor に `Redifinition of Image` という警告が出るけれど無視しましょう。

```javascript
const { dialogflow, MediaObject, Image, Suggestions } = require('actions-on-google');
...
const play = (conv, name, url, desc, imageUrl, alt) => {
    if (!conv.surface.capabilities.has('actions.capability.MEDIA_RESPONSE_AUDIO')) {
        return conv.close('このデバイスは音楽を再生できません。');
    }
    conv.close(`${name}を再生します。`);
    conv.close(new MediaObject({
        name: name,
        url: url,
        description: desc,
        //icon: new Image({
        image: new Image({
          url: imageUrl,
          alt: name
        })
    }));
};
```

conv.ask(new MediaObject) より conv.close(new MediaObject) がオススメです。 アプリが一度終了した上で、音楽再生がはじまるので、GoogleHome デフォルトのコマンド（音量を変える、再生をストップするなど）が使えるようになるためです。

#### 悩ましい問題

[actions console](https://console.actions.google.com) の Overview > Surface capabilities で、media playback 有りにすれば iOS / Android のアプリ紹介 (actions directory) には表示されなくなるのだけれど、露出が減って利用者が増えないので困る...。

![surface.png](https://qiita-image-store.s3.amazonaws.com/0/6760/e21b973a-0917-d4a9-343c-0bdc1ab0fd19.png "surface.png")


### SSML を使う

公式ドキュメント: https://developers.google.com/actions/reference/ssml 

BGMをフェードアウトさせつつ音声を再生するなど結構複雑なことができます。[actions console](https://console.actions.google.com) の Simulator でお試し再生できるのも便利です。ただし、URL は https でないと再生できないので注意してください。

公式ドキュメントだけだと分かりづらいので、https://medium.com/@silvano.luciani/more-ssml-for-actions-on-google-e365af89e56d を読むことをオススメします。。

サンプルは https://github.com/actions-on-google/dialogflow-ssml-nodejs にある。

なお、SSML で音楽を再生すると処理時間がかかり、GoogleHome からの応答に時間がかかるようになる。だいぶイライラするので注意。

なお、~~2分~~ 4分以上の音楽を SSML を使って再生する場合は、前述した MediaResponses API を使ってください。~~2分~~ 4分以上 GoogleHome をしゃべらせると審査で落とされます。~~120秒~~ 240秒ルール。

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">We&#39;ve increased the maximum duration for audio in an SSML response to 240 seconds. 🎙️🔊<a href="https://twitter.com/hashtag/AoGDevs?src=hash&amp;ref_src=twsrc%5Etfw">#AoGDevs</a>, read all about it at <a href="https://t.co/DY09OU11eg">https://t.co/DY09OU11eg</a></p>&mdash; 𝙻𝚎𝚘𝚗 𝙽𝚒𝚌𝚑𝚘𝚕𝚕𝚜 (@TheLeonNicholls) <a href="https://twitter.com/TheLeonNicholls/status/1155881750561558534?ref_src=twsrc%5Etfw">July 29, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

### Cloud Storage にアップロードした音楽を再生する。

Cloud Storageにアップロードした音楽データを、Publicにせずに、Firebase Functionsあからアクセスするには、署名付きURLを生成します。

https://medium.com/google-developers/using-cloud-storage-for-firebase-for-hosting-rich-media-in-your-actions-a838977eb758

に従って、IAMを使って Firebase Functions が署名付きURLを生成できるよう権限を付与します。

- Google Cloud Console で [IAM > IAM page](https://console.cloud.google.com/iam-admin/iam) へ行き、App Engine default service accountのアカウントのedit icon (🖉)を押す。
- Add another roleを教えて、Service Accounts > Service Account Token Creatorを選択する。

さらに　https://console.developers.google.com/apis/api/iam.googleapis.com も有効にしておきます。

これらを忘れると、次のようなエラーが表示されます。

```console
Error: Identity and Access Management (IAM) API has not been used in project xxxxxxxxxxxx before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/iam.googleapis.com/overview?project=xxxxxxxxxxxx then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.
```

```console
Error: Permission iam.serviceAccounts.signBlob is required to perform this operation on service account projects/xxxxxxxxxxxxxxxxx/serviceAccounts/xxxxxxxxxxxxxxxxx@appspot.gserviceaccount.com.
```

ソースコードは以下ですが、bucketName（<project-id>.appspot.com）を渡している点と、エスケープしている点が異なります。

```javascript
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
const bucket = admin.storage().bucket('<project-id>.appspot.com');
const bucketAccessConfig = {
  action: 'read',
  expires: Date.now() + 1000 * 60 * 60
};

const {dialogflow} = require('actions-on-google');

const app = new dialogflow();

app.intent('ssml response', async conv => {
    const soundFileRef = bucket.file('audio/sound-effect.mp3');
    const [soundUrl] = await soundFileRef.getSignedUrl(bucketAccessConfig);
    const escapedUrl = soundUrl.replace(/&/g, '&amp;')
               .replace(/</g, '&lt;')
               .replace(/>/g, '&gt;')
               .replace(/"/g, '&quot;')
               .replace(/'/g, '&apos;');

    conv.ask(`<speak>I can return some text, then a sound effect.
            <audio src="${escapedUrl}"></audio>
            </speak>`);
});
```

### 「スマートフォンだと音楽が流れない！」に備える

Android（Google アシスタントアプリをインストールした iPhone）の Google アシスタントから、マイクではなく、キーボード入力でアクションを起動すると、音楽が再生されません。

そこで、SimpleResponse を使って、ユーザーに注意喚起します。

```javascript

app.intent('Default Welcome Intent', (conv) => {
  const speech = 'オープニング曲を聞いてアニメを当てるクイズです。遊んでみますか？';
  conv.ask(new SimpleResponse({
    speech: speech,
    text: speech + '(この音声が聞こえていないと音楽が再生されません。マイクアイコンをタップして「はい」と話しかけてください)'
  }));
});
```

## HTTP API / JSON API を呼び出す

以前は[request](https://github.com/request/request)を紹介していたのですが、非推奨になりました。そこで、ネイティブのhttpsとasync/awaitを使います。

参考：[Saying goodbye to Request: one of JavaScript’s oldest npm modules](https://medium.com/@wilmoore/saying-goodbye-to-request-one-of-javascripts-oldest-npm-modules-58f4405db1b7)

なお、Firebase の無料プランだと google 外の URL を呼び出せないので注意してください。

### Inline Editorのindex.jsでHTTP API / JSON APIを呼び出す

```javascript
const https = require('https');
const get = (url) => new Promise((resolve, reject) => {
  https.get(url, (response) => {
    let body = '';
    response.on('data', (chunk) => body += chunk);
    response.on('end', () => resolve(body));
  }).on('error', reject);
});
...
app.intent(<インテント名>, async conv => { // convの前にasyncを記述
  const url = 'https://example.com/hello';
  const responsedJson = await get(url);
  ...
});
```

async/awaitを使わない場合は、必ずgetの返り値（Promise）をreturnしてください。

```javascript
app.intent(<インテント名>, conv => {
  const url = 'https://example.com/hello';
  return get(url).then(responsedJson => {
    ...
  });
});
```

[Intent handler helpers and arguments](https://developers.google.com/actions/reference/nodejs/lib-v1-migration#intent_handler_helpers_and_arguments) より引用: 

> Additionally, async tasks now have built-in direct support in the library. To perform an async task, you must return a Promise to the intent handler.

## 時刻を扱う

Firebase Functions はタイムゾーンが UTC なので、new Date() すると9時間前の時刻になります。

[moment.js](https://momentjs.com) を使って、の locale に conv.user.locale を設定しましょう。

参考:
- [5 Tips when Localizing your Action (adding different languages)](https://medium.com/google-developers/5-tips-when-localizing-your-action-adding-different-languages-b4213cc9573f?linkId=61286202)
- https://developers.google.com/assistant/actions/localization/fulfillment#localization_libraries

```json:packages.json
...
  "dependencies": {
    ...
    "moment": "^2.25.0"
  }
...
```

ミドルウェアで locale を設定します。

```javascript
const app = dialogflow();
const moment = require('moment');
...
app.middleware(conv => {
    moment.locale(conv.user.locale);
});
```

使い方

```javascript
// エープリルフールかどうか？
const isAprilFool = () => {
    const now = moment();
    return now.month() + 1 === 4 && n.date() === 1;
};

//Date#toISOString()で保存した文字列を表示する
const logDate = (isoString) => {
  console.log(moment(isoString, moment.ISO_8601).format('YYYY-MM-DD'));
};
```

## Firebase Functions が障害で使えない場合に備えて Text response を設定しておく

Firebase Functions が障害で使えないことがありました。滅多にあることではないのですが。

念のため、Dialogflow 上で Default Welcome Intent や、Explicit invocation で起動する Intent の Text response に

```
アプリの不具合、もしくは、Google の障害で利用することができません。少し時間をおいて、改めてお試しください。
```

と記述していると、悪い印象を軽減できます。

Dialogflow で Intent の Responses　は Set this intent as end of coversation をチェックしておきます。

## ユーザーを特定する

あくまで日本向けのアクションを想定します。EUユーザーが使う場合には個人情報に関する法律 GDPR に基づいて利用者に同意が必要になるかもしれないのでご注意ください。

2019/05/31 に conv.user.id は廃止になりました。

公式ドキュメント:
匿名ユーザー ID https://developers.google.com/actions/identity/user-info

なので、[npm uuid](https://www.npmjs.com/package/uuid) を使って、以下のように対処します。

(1) uuid を読み込みます。

```javascript:package.json
  ...
  "dependencies": {
    ...
    "uuid": "^3.3.2"
  }
```

(2) ミドルウェアを使って conv.user.storage.userId で保存していた id を conv.user.id に設定します。

```javascript:index.js
const app = dialogflow();
const uuid = require('uuid/v4'); //random

app.middleware(conv => {
  if ('userId' in conv.user.storage) {
    conv.user.id = conv.user.storage.userId;
  } else {
    const userId = uuid();
    conv.user.storage.userId = userId;
    conv.user.id = userId;
  }
});
```

参考：
Actions on Googleでパーソナライズ化 https://qiita.com/1coin178/items/e6fb34d7ba4dd065a44b

## アプリがどう使われているかを Google Analytics で分析する

Google Analytics で Track ID を払い出します。
Web を選択して作ります（Mobile App を選択すると firebase analysis へ誘導されるが、Android、iOS しか選択できず、JavaScript では作れません）。

分析したいところで次の関数を呼び出します。

```javascript
const trackEvent = (conv, category, action, label, value) => {
    const data = {
        v: '1',
        tid: 'UA-XXXXXXXXX-X',
        cid: conv.user.id,
        t: 'event',
        ec: category,
        ea: action
    };
    if (label) {
        data.el = label;
    }
    if (value) {
        data.ev = value;
    }
    request.post('http://www.google-analytics.com/collect', {
        form: data
    });
};
```

- category: イベントのカテゴリー (例: Video)
- action: 対象に対する操作 (例: play)
- label: 必須ではない。説明用の文字列 (例: 再生したビデオのタイトル)
- value: 必須ではない。説明用の値 (例: 再生したビデオの人気度）

なお、cid (クライアントID) ではなく uid (ユーザーID) を使うと、Realtime ではイベントを受信しているのに、次の日に記録に残っていません。GDPR が関係しているのでしょうか？

## すみません。よく聞き取れませんでした (NO_INPUT)

ユーザー入力がない場合に対応するため、コードを準備する必要があります。node.js client library v1 の時は、ask の後に reprompt の内容を渡していましたが、v2 では ```conv.noInputs``` プロパティに設定します。

参考: [Static Reprompts came back to SDK v2](https://medium.com/google-developer-experts/static-reprompts-came-back-to-sdk-v2-86539d8dbdb2)

公式: https://developers.google.com/actions/assistant/reprompts#dynamic_reprompts では違う方法が書いてあるのでご注意ください。

こんな感じのユーティリティを作って使っている...のですが、SSMLを使って音楽を再生する場合にはうまく動作しません。conv.noInputs には new SimpleResponse({text, speech}) も入れられるのですが、speech にいれた SSML がテキストとして扱われているような動作です。

```javascript
const { dialogflow, Suggestions } = require('actions-on-google');
...
const merge = (simpleResponse, preSpeech = '', postSpeech = '') => {
  const START = '<speak>';
  const END = '</speak>';
  let ssml = simpleResponse.textToSpeech.trim();
  ssml = ssml.slice(START.length, ssml.length - END.length);
  return new SimpleResponse({
    text:  preSpeech + simpleResponse.displayText + postSpeech,
    speech: START + preSpeech + ssml + postSpeech + END
  });
};

const ask = (conv, inputPrompt, suggestions = null) => {
  conv.data.inputPrompt = inputPrompt;
  const sorry = 'すみません。よく聞き取れませんでした。';
  if (inputPrompt.displayText) {
    //音楽を使う場合は上手く動作しないので使わない
    conv.noInputs = [
      merge(inputPrompt, sorry, ''),
      merge(inputPrompt, sorry, ''),
      bye
    ];
  } else {
    conv.noInputs = [
      sorry + inputPrompt,
      sorry + inputPrompt,
      bye
    ];
  }  conv.data.suggestions = suggestions;
  if (suggestions) {
    conv.ask(new Suggestions(suggestions));
  }
  return conv.ask(inputPrompt);
};

const repeat = (conv, speech = '') => {
  if (conv.data.suggestions) {
    conv.ask(new Suggestions(conv.data.suggestions));
  }
  const i = conv.data.inputPrompt;
  if (i && i.displayText) {
    return conv.ask(merge(i, speech, ''));
  }
  return conv.ask(speech + i);
};

app.intent('Default Welcome Intent', conv => {
    ...
    ask(conv, 'はい。案内アプリです。やりたいことを教えてください。', ['ミニゲーム', '使い方']);
});

app.intent('repeat', conv => {
    return repeat(conv, 'わかりました。もう一度言いますね。');    
});
```

音楽を使う場合には、Dialogflow で actions_intent_NO_INPUT を Event として拾う Intent を作り（Intenet の名前は何でもいいのですが、ここでは No Input にします）、以下のように対応します。Dialogflow は3回 NO_INPUT が続くと会話を停止します。

```javascript
app.intent('No Input', conv => {
  const sorry = 'よく聞き取れませんでした。繰り返します。';
  return repeat(conv, sorry);
});
```

イベントを上手く拾わないことがあったのですが、Dialogflow の左ペイン Integrations で Google Assistant の設定から Auto-preview changes をオンにしたら動作しました。これが原因かどうかは不明です...。

## Firebase Functions から データベースを使う

Realtime database と、ベータ版の Firestore が使えます。Firestore は検索機能が豊富ですが、読み取り速度に少々難があるためすぐに移行できるとは限らないようです。今後に期待。

### 事前準備 firebase-admin を設定する。

どちらのデータベースを使うにしても、firebase-admin を利用してアクセスします。
まず、package.json を修正して、firebase-admin と firebase-function のバージョンを新しくします。

```json:pacakge.json
{
  ...
  "dependencies": {
    ...
    "firebase-admin": "^6.1.0",
    "firebase-functions": "^2.1.0",
```

次に、index.js に追加します。

```js:index.js
// 追加
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
// Realtime Database を使う場合
var rdb = admin.database();
// Firestore を使う場合
var db = admin.firestore();
```

詳しい使い方は公式ドキュメントをご覧ください。

- Realtime database https://firebase.google.com/docs/database/
- Firestore https://firebase.google.com/docs/firestore/

データの読み書きをするときは、HTTP通信と同様に、intent handler から Promise を return する必要があるので注意してください。

### Realtime Database でデータを読み取る

```js:RealtimeDatabase
app.intent('show-list', conv => {
    const path = '/path/to/data'; // スラッシュからはじまる
    // Promise を return することに注意
    return rdb.ref(path).once('value', snapshot => {
        return conv.ask(dataToSsml(snapshot.val()));
    }, error => {
        console.error(error + ': ' + path);
        return conv.close('データベースを参照できませんでした。終了します。');
    });
});
```
### Firestore でデータを読み取る

```js:Firestore
app.intent('show-list', conv => {
    const path = 'path/to/data'; // 先頭のスラッシュ不要
    // Promise を return することに注意
    return db.doc('path/to/data').get().then(doc => {
        if (!doc.exists) {
            console.error(`no doc for ${path}`);
            return conv.close('データベースを参照できませんでした。終了します。'));
        }
        return conv.ask(dataToSsml(doc.data()));
    }).catch(error => {
        console.error(error + ': ' + path);
        return conv.close('データベースを参照できませんでした。終了します。'));
    });
});
```
## Google Apps Script からデータベースを使う

GAS はスケジュールを指定して実行できるのがとても便利です。cron の代わりになります。

### GAS から Realtime Database を更新する

https://sites.google.com/site/scriptsexamples/new-connectors-to-google-services/firebase を使えば Realtime Database の読み書きが可能になります。

Database Secret を払い出します。払い出したkeyは誰にも見つからないようにしてくださいね。

### GAS から Firestore を更新する

[Firestore for Google Apps Scripts](https://github.com/grahamearley/FirestoreGoogleAppsScript)を使えばFirestore の読み書きが可能になります。

GAS用にサービスアカウントを作ります。払い出したkeyは誰にも見つからないようにしてくださいね。

## スマートスピーカー・スマートディスプレイからスマートフォンに情報を送る

Rich Responseのカルーセルは、Webブラウザーが使える端末でないと利用できない。スマートスピーカーだけでなく、スマートディスプレイもWebブラウザーが使えない。そこで、スマートフォンに情報を送ります。

参考：
- https://developers.google.com/assistant/conversational/surface-capabilities#multi-surface_conversations

スマートフォンに情報を送りたくなったら、```conv.ask(new NewSurface())``` します。
Dialogflow側は```actions_intent_NEW_SURFACE```イベントを受け取るインテントを作っておきます。

```javascript
const { NewSurface, BrowseCarouselItem, BrowseCarousel } = require('actions-on-google');
...
app.intent('Default Welcome Intent', conv => {
  ...
  if(conv.available.surfaces.capabilities.has('actions.capability.WEB_BROWSER')) {    
    const context = `詳しい情報を表示します。`;  // この後に「こちらをスマートフォンに送りますか？」とユーザーに尋ねる
    const notification = `詳しい情報をお届けしました。`; // スマートフォンへの通知タイトル
    const capabilities = ['actions.capability.SCREEN_OUTPUT', 'actions.capability.WEB_BROWSER'];
    return conv.ask(new NewSurface({context, notification, capabilities}));
  }
  ...
}

app.intent('New Surface', (conv, input, newSurface) => {
  // こちらをスマートフォンに送りますか？に対して「はい」と答えた場合
  if (newSurface.status === 'OK') {
    // BrowseCarouselItem, BrowseCarousel を使った表示
    return conv.close(...);
  }
  // 「いいえ」と答えた場合
  conv.close('わかりました。またお会いしましょう。');
});
```

## 画面に表示する文言としゃべる文言を変える

conv#ask(), conv#close() で SimpleResponse を使います。text には画面に表示する文言を、speech にはしゃべる文言（SSML）をそれぞれ指定します。

```javascript
const { SimpleResponse } = require('actions-on-google');
...
conv.ask(new SimpleResponse({ text: '画面に表示する文言', speech: 'しゃべる文言' }));
```

公式:
- https://developers.google.com/assistant/conversational/simple-responses#SimpleResponseSamples


## Dialogflow 上で Followup Intent を作ると、勝手に多言語化してくれます

例えば英語は yes の言い回しがすごくたくさん登録されているので、便利です。

## Contexts を使う

https://actions-on-google.github.io/actions-on-google-nodejs/classes/dialogflow.contextvalues.html

例えば「クイズを続けますか？」と聞いたときに、それに対して「はい」「いいえ」で回答してもらうとき、Context を使うと便利です。

```javascript
conv.ask('クイズを続けますか？');
conv.contexts.set('ask-continue');
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6760/d1e54b7a-7331-757a-4e0a-c0b536003334.png)

Folloup Intent で yes / no を作ると手間が省けます。
Action and parameters を空欄にしておくと、Followup Intent でなくなります。
Fulfillment をチェックするのを忘れないようにしてください。

## BrowseCarouselItem は2つ以上

エラーログが見えないので注意。items が1つの場合は、BasicCardを使ってください。

## 英語でしゃべらせる

通常は指定した言語の指定した声色でしかしゃべらせられませんが、SSMLを使うと「英語」でだけ特別に女性2種類、男性2種類の計4種類の音声を使うことができます。
actions console の audio タブで試すことができます。

```xml
<speak>
  <voice gender="female" variant="1">
    I'm a woman. わたしはおすしがだいすきです。
  </voice>
  <voice gender="female" variant="2">
    I'm also a woman. わたしもおすしがだいすきです。
  </voice>
  <voice gender="male" variant="1">
    I'm a man. わたしはラーメンがすきですね。
  </voice>
  <voice gender="male" variant="2">
    I'm also a man. わたしもラーメンがだいすきです。
  </voice>
</speak>
```

公式ドキュメントにはのっていません。
ひらながなら、片言でしゃべてくれます。英語にすると中国語のような発音になります。面白いですね。

ちなみに、alexa は lang を指定できます。
https://developer.amazon.com/en-US/docs/alexa/custom-skills/speech-synthesis-markup-language-ssml-reference.html#lang

## 古い Action が動作しない時に Node.js のバージョンが古くないか疑う

古い Action が動作しない時に Node.js のバージョンが古くないか疑ってください。
どのバージョンを使っているかは、Cloud Console ( https://console.cloud.google.com ) にアクセスして、左メニューから Cloud Functions を選ぶとわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6760/ce46644e-f6a9-5d4b-98a3-66332f024664.png)

Cloud Functions で使える Node.js の runtime のバージョンは https://cloud.google.com/functions/docs/concepts/nodejs-runtime で確認できます。2021/08/18時点で Node 6 は動作しないため、Action も動かなくなります。

Cloud Functions の runtime を変更するには（Dialogflow の Inline Editor ではなく）Cloud Console　の Inline Editor を使います。dialogflowFirebaseFullfillment をクリックして、更に画面上部の EDIT をクリックします。2ステップで変更しますが、2ステップ目で runtime を指定できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6760/9ffd069f-6185-ae25-6310-278ac94375c6.png)

もし、Cloud Build API が有効化されていない場合は ENABLE API をクリックして、遷移先の画面で有効にしてください。

なお、packages.json では、次の dependencies を指定しました。バージョンが古いと Deploy に失敗します。

```json
   "dependencies": {
    "actions-on-google": "^2.2.0",
    "firebase-admin": "^5.13.1",
    "firebase-functions": "^2.0.2",
    "dialogflow": "^0.6.0",
    "dialogflow-fulfillment": "^0.5.0",
    ...
   }
```

DEPLOYボタンを押して完了です。

なお、Dialogflow Console の Inline Editor から package.json の ```"engines": {"node": "<version>" }``` を修正しても反映されません。

ちなみに、以前は古い runtime の場合にエラーが出ていた（Error happened during Cloud Function Deploymentと表示されてDeployできなかった）のですが、このエラーはでなくなっていました。一応、その時の対処法も以下に掲載しておきます：

昔作ったActionを久しぶりに更新し、Deployボタンを押したのですが、次のエラーが表示されてしまいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6760/1d447750-6866-5f17-af72-85f0712b0dcc.png)

Node.js の runtime が 6 で、deprecated されていたことが原因のようです。

まず、Cloud Console ( https://console.cloud.google.com ) にアクセスして、原因を確認します。プロジェクトを指定して、左のメニューから Cloud Functions を選択します。DETAILS タブを見ると、Cloud Build API を有効にするように注意書きがありました。

> Deployment failure:
> Build failed: Cloud Build API has not been used in project XXXXXXXXXXXX before or it is > disabled. Enable it by visiting https://console.developers.google.com/apis/api/cloudbuild.googleapis.com/overview?project=XXXXXXXXXXXX then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.

上記URLにアクセスして、Cloud Build API を enable します。時間が経ってから、今度は Cloud Console から deploy を試みたところ、runtimeを変更するように指示がありました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6760/78fb9603-1d07-bcd3-0632-5020da00d591.png)

> The request contains invalid arguments "runtime": "Runtime nodejs6 is no longer allowed. This is based on the deprecation of nodejs6 announced in April 2019. Please use the latest nodejs runtime for cloud functions, details provided at https://cloud.google.com/functions/docs/migrating/nodejs-runtimes

そこで、Cloud Console で Node の runtime を直接変更して、deployしたところ、成功しました。Cloud Console を直接編集すると、Dialogflow の Inline Editor からは見えなくなるかな？と思ったのですが、問題なくアクセスできました。

## Actionの命名方法

以下を参照してください。

- https://developers.google.com/assistant/console/policies/general-policies#name_requirements
- https://developers.google.com/assistant/console/languages-locales#japanese
 
## その他、知っていると便利なこと（これから追記する予定）
- XML を処理する