不動の鳥の勉強記録

時間があるときに勉強したことをメモします。

DialogflowのFulfillmentのテンプレートコードを読んでみる

今日はDialogflowのFulfillmentのテンプレートコードを上から見ていきたいと思います。
本当は、Actions on Googleのレビューで詰まった点をネタにしたかったのですが、
レビューの応答が一週間音沙汰がありませんでした…

テンプレートのスクリプト関連はGithubにありました。

github.com

テンプレートコードは、functions内にあるので見てみます。
まずいつも最初にみるのはpackage.jsonです。個人的には特にdependenciesを見ています。

■package.jsonを抜粋

  "dependencies": {
    "actions-on-google": "^1.5.x",
    "firebase-admin": "^4.2.1",
    "firebase-functions": "^0.5.7",
    "apiai": "^4.0.3"
  }

firebase上で動かすスクリプトのためfirebaseのモジュールをインストールしていることがわかります。
きっとwebhookを作るときは、actions-on-googleとapiaiをインストールすればいいんでしょうか。

続いて本題のindex.jsです。
先頭の変数宣言などは割愛します。

■index.js 22~34行目

exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
  console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
  console.log('Dialogflow Request body: ' + JSON.stringify(request.body));

  if (request.body.result) {
    processV1Request(request, response);
  } else if (request.body.queryResult) {
    processV2Request(request, response);
  } else {
    console.log('Invalid Request');
    return response.status(400).end('Invalid Webhook Request (expecting v1 or v2 webhook request)');
  }
});

if文処理で、requestをみて、processV1Request, processV2Requestの関数をそれぞれ呼び出しています。
Dialogflow APIでは2つwebhookの種類があるようで、下記リファレンスページがありました。

■参考:V1 & V2 comparison  |  Dialogflow

こちらによると、ざっくりDialogflowAPIのV2はBETA版、V1が安定版のようです。
Dialogflowから送られてくる形式が、V1かV2かでリクエストのどこにデータがあるのかが変わるので、
このような分岐をしているのだと思います。
2018/02/25時点では、Dialogflowから特に指定せずFulfillmentを設定すると、V1が起動する?ようです。
全部読むと記事が長くなるので、V1のところだけにしたいと思います。

■index.js 40~46行目(processV1Request内)

  let action = request.body.result.action; // https://dialogflow.com/docs/actions-and-parameters
  let parameters = request.body.result.parameters; // https://dialogflow.com/docs/actions-and-parameters
  let inputContexts = request.body.result.contexts; // https://dialogflow.com/docs/contexts
  let requestSource = (request.body.originalRequest) ? request.body.originalRequest.source : undefined;

  const googleAssistantRequest = 'google'; // Constant to identify Google Assistant requests
  const app = new DialogflowApp({request: request, response: response});

ここはお決まりの変数宣言みたいに見えるので、そっとしておきます。

重要なのは次のactionHandlersみたいです。
この中にはDialogflowのIntentに記載されたAction nameと一致するイベントが実行されるようです。
例えばIntentにActions nameを「AAA」と記載すると、actionHandlersのなかの「AAA」のイベントが呼び出され、
イベント内の処理が実行されるみたいです。

■index.js 50~58行目 (processV1Request内, actionHandler内)

    // The default welcome intent has been matched, welcome the user (https://dialogflow.com/docs/events#default_welcome_intent)
    'input.welcome': () => {
      // Use the Actions on Google lib to respond to Google requests; for other requests use JSON
      if (requestSource === googleAssistantRequest) {
        sendGoogleResponse('Hello, Welcome to my Dialogflow agent!'); // Send simple response to user
      } else {
        sendResponse('Hello, Welcome to my Dialogflow agent!'); // Send simple response to user
      }
    },

Default Welcome Intentから来た時にマッチするイベントのようです。
GoogleAssistantから来たリクエストなのか、他から来たリクエストなのかでif分岐しているみたいです。

■index.js 59~67行目 (processV1Request内, actionHandler内)

    // The default fallback intent has been matched, try to recover (https://dialogflow.com/docs/intents#fallback_intents)
    'input.unknown': () => {
      // Use the Actions on Google lib to respond to Google requests; for other requests use JSON
      if (requestSource === googleAssistantRequest) {
        sendGoogleResponse('I\'m having trouble, can you try that again?'); // Send simple response to user
      } else {
        sendResponse('I\'m having trouble, can you try that again?'); // Send simple response to user
      }
    },

コメントを見るかぎり異常時(聞き取れなかったときなど)に実行されるイベントのようです。

■index.js 68~88行目(processV1Request内, actionHandler内)

    // Default handler for unknown or undefined actions
    'default': () => {
      // Use the Actions on Google lib to respond to Google requests; for other requests use JSON
      if (requestSource === googleAssistantRequest) {
        let responseToUser = {
          //googleRichResponse: googleRichResponse, // Optional, uncomment to enable
          //googleOutputContexts: ['weather', 2, { ['city']: 'rome' }], // Optional, uncomment to enable
          speech: 'This message is from Dialogflow\'s Cloud Functions for Firebase editor!', // spoken response
          text: 'This is from Dialogflow\'s Cloud Functions for Firebase editor! :-)' // displayed response
        };
        sendGoogleResponse(responseToUser);
      } else {
        let responseToUser = {
          //data: richResponsesV1, // Optional, uncomment to enable
          //outputContexts: [{'name': 'weather', 'lifespan': 2, 'parameters': {'city': 'Rome'}}], // Optional, uncomment to enable
          speech: 'This message is from Dialogflow\'s Cloud Functions for Firebase editor!', // spoken response
          text: 'This is from Dialogflow\'s Cloud Functions for Firebase editor! :-)' // displayed response
        };
        sendResponse(responseToUser);
      }
    }

一致するイベントがなかった場合に実行される処理を記述するイベントのようです。

■index.js 91~97行目(processV1Request内)

  // If undefined or unknown action use the default handler
  if (!actionHandlers[action]) {
    action = 'default';
  }

  // Run the proper handler function to handle the request from Dialogflow
  actionHandlers[action]();

ここでは、actionHandlerの指定を行っているようです。
action名と一致するものがなかったらdefaultイベントに飛ばし、一致するものがあれば一致するイベントを実行すると…ここも特に変える必要はなさそうです。

■index.js 99~121行目(processV1Request内)

    // Function to send correctly formatted Google Assistant responses to Dialogflow which are then sent to the user
  function sendGoogleResponse (responseToUser) {
    if (typeof responseToUser === 'string') {
      app.ask(responseToUser); // Google Assistant response
    } else {
      // If speech or displayText is defined use it to respond
      let googleResponse = app.buildRichResponse().addSimpleResponse({
        speech: responseToUser.speech || responseToUser.displayText,
        displayText: responseToUser.displayText || responseToUser.speech
      });
      // Optional: Overwrite previous response with rich response
      if (responseToUser.googleRichResponse) {
        googleResponse = responseToUser.googleRichResponse;
      }
      // Optional: add contexts (https://dialogflow.com/docs/contexts)
      if (responseToUser.googleOutputContexts) {
        app.setContext(...responseToUser.googleOutputContexts);
      }

      console.log('Response to Dialogflow (AoG): ' + JSON.stringify(googleResponse));
      app.ask(googleResponse); // Send response to Dialogflow and Google Assistant
    }
  }

レスポンスを返す関数で、GoogleAssistantに返す場合は、app.askでしゃべらせているようです。
文字列以外を返したい場合はRichレスポンスなるものを作成しているようです。
オーディオとか、スタンプ?とか送りたいときはrichレスポンスを送るみたいです。
Assistantにしゃべらせたいだけなら、この関数は特に変更不要そうです。

参考:Responses  |  Actions on Google  |  Google Developers

■index.js 122~144行目(processV1Request内)

  // Function to send correctly formatted responses to Dialogflow which are then sent to the user
  function sendResponse (responseToUser) {
    // if the response is a string send it as a response to the user
    if (typeof responseToUser === 'string') {
      let responseJson = {};
      responseJson.speech = responseToUser; // spoken response
      responseJson.displayText = responseToUser; // displayed response
      response.json(responseJson); // Send response to Dialogflow
    } else {
      // If the response to the user includes rich responses or contexts send them to Dialogflow
      let responseJson = {};
      // If speech or displayText is defined, use it to respond (if one isn't defined use the other's value)
      responseJson.speech = responseToUser.speech || responseToUser.displayText;
      responseJson.displayText = responseToUser.displayText || responseToUser.speech;
      // Optional: add rich messages for integrations (https://dialogflow.com/docs/rich-messages)
      responseJson.data = responseToUser.data;
      // Optional: add contexts (https://dialogflow.com/docs/contexts)
      responseJson.contextOut = responseToUser.outputContexts;

      console.log('Response to Dialogflow: ' + JSON.stringify(responseJson));
      response.json(responseJson); // Send response to Dialogflow
    }
  }

GoogleAssistant以外にレスポンスを返す時の関数のようです。
しゃべらせる相手はわからないので、単純にJSONで処理結果を返しているようです。
ここも特に変更必要なさそうですね。

■index.js 147~167行目

// Construct rich response for Google Assistant (v1 requests only)
const app = new DialogflowApp();
const googleRichResponse = app.buildRichResponse()
  .addSimpleResponse('This is the first simple response for Google Assistant')
  .addSuggestions(
    ['Suggestion Chip', 'Another Suggestion Chip'])
    // Create a basic card and add it to the rich response
  .addBasicCard(app.buildBasicCard(`This is a basic card.  Text in a
 basic card can include "quotes" and most other unicode characters
 including emoji 📱.  Basic cards also support some markdown
 formatting like *emphasis* or _italics_, **strong** or __bold__,
 and ***bold itallic*** or ___strong emphasis___ as well as other things
 like line  \nbreaks`) // Note the two spaces before '\n' required for a
                        // line break to be rendered in the card
    .setSubtitle('This is a subtitle')
    .setTitle('Title: this is a title')
    .addButton('This is a button', 'https://assistant.google.com/')
    .setImage('https://developers.google.com/actions/images/badges/XPM_BADGING_GoogleAssistant_VER.png',
      'Image alternate text'))
  .addSimpleResponse({ speech: 'This is another simple response',
    displayText: 'This is the another simple response 💁' });

Richレスポンスの指定をしているようです。
Richレスポンスはまた今度詳しく見たいと思います・・・

■index.js 169~209行目

// Construct rich response for Google Assistant (v1 requests only)
const app = new DialogflowApp();
const googleRichResponse = app.buildRichResponse()
  .addSimpleResponse('This is the first simple response for Google Assistant')
  .addSuggestions(
    ['Suggestion Chip', 'Another Suggestion Chip'])
    // Create a basic card and add it to the rich response
  .addBasicCard(app.buildBasicCard(`This is a basic card.  Text in a
 basic card can include "quotes" and most other unicode characters
 including emoji 📱.  Basic cards also support some markdown
 formatting like *emphasis* or _italics_, **strong** or __bold__,
 and ***bold itallic*** or ___strong emphasis___ as well as other things
 like line  \nbreaks`) // Note the two spaces before '\n' required for a
                        // line break to be rendered in the card
    .setSubtitle('This is a subtitle')
    .setTitle('Title: this is a title')
    .addButton('This is a button', 'https://assistant.google.com/')
    .setImage('https://developers.google.com/actions/images/badges/XPM_BADGING_GoogleAssistant_VER.png',
      'Image alternate text'))
  .addSimpleResponse({ speech: 'This is another simple response',
    displayText: 'This is the another simple response 💁' });

// Rich responses for Slack and Facebook for v1 webhook requests
const richResponsesV1 = {
  'slack': {
    'text': 'This is a text response for Slack.',
    'attachments': [
      {
        'title': 'Title: this is a title',
        'title_link': 'https://assistant.google.com/',
        'text': 'This is an attachment.  Text in attachments can include \'quotes\' and most other unicode characters including emoji 📱.  Attachments also upport line\nbreaks.',
        'image_url': 'https://developers.google.com/actions/images/badges/XPM_BADGING_GoogleAssistant_VER.png',
        'fallback': 'This is a fallback.'
      }
    ]
  },
  'facebook': {
    'attachment': {
      'type': 'template',
      'payload': {
        'template_type': 'generic',
        'elements': [
          {
            'title': 'Title: this is a title',
            'image_url': 'https://developers.google.com/actions/images/badges/XPM_BADGING_GoogleAssistant_VER.png',
            'subtitle': 'This is a subtitle',
            'default_action': {
              'type': 'web_url',
              'url': 'https://assistant.google.com/'
            },
            'buttons': [
              {
                'type': 'web_url',
                'url': 'https://assistant.google.com/',
                'title': 'This is a button'
              }
            ]
          }
        ]
      }
    }
  }
};

同じくRichレスポンスの指定を行っているようです。
SlackとFacebookへ何か返す時はきっと何かが起きるんでしょうか…

いったん以上が大体のコードになります。

■まとめ
まとめると、Googleにしゃべらせるだけなら、actionHandler内にイベントとロジックを書き、
DialogflowのIntentから実行したいイベントを指定するだけでよさそうです。

間違ってたら指摘お願いいたします。

以上。