GoogleHome
actionsongoogle
dialogflow

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

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

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

ただ、デフォルトで表示されるコードが公式ドキュメント(例えば Actions on Google Node.js Client Library Version 1 Migration Guide)と違っているので不便。

そこで、以下のコードをベースにするのがオススメ。gist

'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)を使うことに注意。アクション名(input.welcome)ではない。

ポイント2

exports.dialogflowFirebaseFulfillment を使うことに注意。上記 Migration Guide では exports.factsAboutGoogle になっている。

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

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

inline-editor.png

ちなみに、Inline Editor の package.json は以下の通り。

{
  "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": "~6.0"
  },
  "scripts": {
    "start": "firebase serve --only functions:dialogflowFirebaseFulfillment",
    "deploy": "firebase deploy --only functions:dialogflowFirebaseFulfillment"
  },
  "dependencies": {
    "actions-on-google": "2.0.0-alpha.4",
    "firebase-admin": "^4.2.1",
    "firebase-functions": "^0.5.7",
    "dialogflow": "^0.1.0",
    "dialogflow-fulfillment": "0.3.0-beta.3"
  }
}

背景

Dialogflow API が V2 になったタイミングで、Inline Editor に表示されるデフォルトのコードが dialogflow-fullfillment-nodejs に変更されたのだけれど、公式ドキュメントの書き方と合っていない。

そこで Actions on Google client library for Node.js v2.0.0 を参考に書き直した。

アプリ開発に役立つ機能

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

注意事項

Google Cloud Console 経由で Cloud Functions の設定を変更することができるのだけれど、一度でもそれをやると Dialogflow の Inline Editor が使えなくなる。ターミナルで gcloud コマンドを使っても使えなくなる。一度使えなくなると、ずっと使えないまま(戻せない)ので注意。

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

公式ドキュメント: 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 にする。

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

'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 をそれぞれ作る必要がある。

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

デバイスの位置情報

Permission クラスと actions_intent_PERMISSION イベントを使う。

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} を保存する場合、

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

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

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

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

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

Google Home で Voice Match を設定する

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

音楽を再生する

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

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 という警告が出るけれど無視する。

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 の Overview > Surface capabilities で、media playback 有りにすれば iOS / Android のアプリ紹介 (actions directory) には表示されなくなるのだけれど、露出が減って利用者が増えないので困る...。

surface.png

SSML を使う

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

BGMをフェードアウトさせつつ音声を再生するなど結構複雑なことができる。actions console の Simulator でお試し再生できるのも便利。ただし、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分以上の音楽を再生した場合は、MediaResponse API を使うと良い。

HTTP API を呼び出す

request を使うと楽。ただし、intent handler から Promise を return する必要があるので注意(後述)。

また、Firebase の無料プランだと google 外の URL を呼び出せないので注意。

Inline Editor の package.json に request を追加

{
  ...
  "dependencies": {
    ...
    "request": "^2.88.0",
  }
}

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

const request = require('request');

const get = (url) => new Promise((resolve, reject) => {
    request.get({
        url: url,
        json: true
    }, (error, res, json) => {
        if (error) {
            return reject(error);
        }
        resolve(json);
    });    
});

...

app.intent(<インテント名>, conv => {
    const url = 'https://example.com/hello';

    // 必ず return すること!
    return get(url).then(responsedJson => {
        // responsedJson に対する処理
    })
});

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 なので、日本時間を知るには9時間足せば良い。ただ、タイムゾーンを JST に設定しているわけではないので注意。

const now = () => {
    const d = new Date();
    d.setTime(d.getTime() + 9 * 60 * 60 * 1000);
    return d;
};

// エープリルフールかどうか?
const isAprilFool = () => {
    const n = now();
    return n.getMonth() + 1 === 4 && n.getDate() === 1;
};

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 をチェックしておく。

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

Google Analytics で Track ID を払い出す。
Web を選択して作る(Mobile App を選択すると firebase analysis へ誘導されるが、Android、iOS しか選択できず、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)

ユーザー入力がない場合に対応するため、Intent とコードを準備する必要がある(node.js client library v1 の時は、ask の後に reprompt の内容を渡せばよかったんだけど、使えなくなった)。

公式: https://developers.google.com/actions/assistant/reprompts#dynamic_reprompts

やっているのは、Normal Intent の方。

  1. actions_intent_NO_INPUT イベントを受け取る Intent を作って、Fulfillment を呼び出す。
  2. 呼び出された Fulfillment で処理する。

この時、Fulfillment のコード例が Best Practices に掲載されているのだけれど、間違って書いてある疑惑。

Best Practices: https://developers.google.com/actions/assistant/best-practices#let_users_replay_information

node.js client library v2 だと、conv.ask の第2引数に、ユーザー入力がない場合の返信を返す機能が削られている。

nodejs v2: https://actions-on-google.github.io/actions-on-google-nodejs/classes/dialogflow.dialogflowconversation.html#ask

なので、こんな感じのユーティリティを作って使っている:

const { dialogflow, Suggestions } = require('actions-on-google');
...
const ask = (conv, inputPrompt, suggestions = null) => {
    conv.data.inputPrompt = inputPrompt;
    conv.data.suggestions = suggestions;
    if (suggestions) {
        conv.ask(inputPrompt);
        return conv.ask(new Suggestions(suggestions));
    }
    return conv.ask(inputPrompt);
};

const repeat = (conv, speech = '') => {
    if (conv.data.suggestions) {
        conv.ask(conv.data.inputPrompt);
        return conv.ask(new Suggestions(conv.data.suggestions));
    }
    return conv.ask(`${speech}${conv.data.inputPrompt}`);    
};

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

app.intent('no_input', conv => {
    return repeat(conv, 'すみません。 よく聞き取れませんでした。');    
});

その他、知っていると便利なこと(これから追記する予定)

  • XML を処理する
  • Firebase Realtime Database を使う
    • Google Apps Script を使って定期的に更新する