こんにちは、イシイです。

音声対話形式でGoogleカレンダーで管理している社内会議室の施設予約を行うアプリケーションの開発をしてみるべく、前回のエントリーでは、Dialogflowを使って対話から会議室予約に必要な要素を得るところまでを作り、その動作をシミュレータで確認するところまで行ってみましたので、今回はその続きです。

Googleアシスタントからの呼び出し名称の変更

前回シミュレータで動作確認をしてみたときは、Googleアシスタントからの呼び出しが「テスト用アプリ」となっていましたので、ここを自分で設定した呼び出し名から呼び出せるように変更してみます。

シミュレータを動かしたときに開いたActions on Googleのページの左サイドメニューから「Invocation」のページを開きます。

thumb_1

呼び出し名称を「Invocation name」に設定しますが、Googleアシスタント側から一意の名前でなければいけないため、すでに登録されている呼び出し名を登録しようとするとエラーとなり、使用できません。ここでは「会議室予約」という呼び出し名を登録しようとしましたが、すでに登録されているため別の名前をつけることにします。

thumb_2

今回は「ドクターマルキュー」という呼び出し名を登録してみました。また、「Invocation」のページには「Directory title」という項目もあります。これは、ユーザーがGoogleアシスタントの拡張としてどんなものがあるのかを検索したときに表記されるものになるようです。ここでは「Invocation name」と同じものを設定しておき、右上のSAVEを行っておきます。

thumb_3

一度Actions on Googleのページを閉じ、シミュレータを起動したときの流れと同じように、Dialogflowの「Integrations」ページから「TEST」をクリックして再度Actions on Googleを開きます。

thumb_4

シミュレータが起動したら、先程登録した「Invocation name」の通り「ドクターマルキューにつないで」を試してみます。

thumb_5

これで、設定した呼び出し名から呼び出すことができるようになりました。

シミュレータからの返答文言の変更

次に、会議室予約手続き後のシミュレータからの返答で、「かしこまりました。2018-07-01の14:00:00から1時、A2会議室を予約しました。」となっていて、利用時間の部分が「1時間」ではなく「1時」と返してきてしてまっていた部分の対応をしてみます。

現在はText responseのところに「かしこまりました。$dateの$start-timeから$use-time、$room-typeを予約しました。」と登録しているので、これを「かしこまりました。$dateの$start-timeから$use-time間、$room-typeを予約しました。」というように、利用時間の$use-timeの後に「間」をつけることで対応ができます。

thumb_6

変更後、シミュレータで動かして確認してみます。

thumb_7

これで、利用時間のところの返答部分の対応ができました。ただし、後述のフルフィルメントを使用する場合にはレスポンス部分はフルフィルメント側で対応してしまうので、この部分については最終的には不要となりますが、前回の積み残しとして試してみました。

外部連携するための処理の実装方法

今までの工程では、ユーザからの呼び出しを受け、ユーザーとの対話から会議室予約に必要な要素を取得し、適切な返答を行うところまでを行ってきましたが、実際にカレンダー登録を行うにはこれらの取得した要素を元に、Google Calendar APIを使ってカレンダー登録を行う処理を加える必要があります。この処理部分の支援機能が「フルフィルメント」になります。

フルフィルメント

フルフィルメントは、Actions on Googleによると、

自然言語を理解し、ユーザーの入力から必要なデータを解析するという難解で手間のかかる処理の大半は、Dialogflow が行います。 ただし、ユーザーとの会話では一般的に、ビジネス ロジックを実行して、ユーザーに返答し、要求された処理を完了する必要があります。 これには、Dialogflow Webhook を実装する Web サービス(フルフィルメントと呼ばれる)を使用します。Dialogflow Webhook は、Dialogflow を使ってアプリをアシスタントに統合する方法を定義する、JSON ベースのプロトコルです。

とあります。フルフィルメントでは、対話処理をしてくれるDialogflowの補完機能として独自に処理を加えることによって、パラメータの値を取得して何かをしたり、外部連携などの実装を行える部分になります。このフルフィルメント機能の実装部分については、Actions on GoogleのNode.jsクライアントライブラリを使用することが推奨されています。

フルフィルメントを試してみる

前回作成した対話部分の「Hearing Intent」にてフルフィルメントを使いたいので、まずは「Intents」ページの「Hearing Intent」の下部のFulfilmentのセクションで、「Enable webhook call for this intent」を有効にしておきます。

thumb_8

Dialogflowの「Fulfillment」のページを開いてみると「Webhook」と「Inline Editor」の2つの項目が見られますので、最初に「Webhook」を「ENABLED」にしてみます。

thumb_9

「Webhook」は、指定したサーバー側にDialogflowからJSON形式でPOSTし、結果をJSON形式で受け取って対話処理を継続させます。そのため、別のサーバーが必要になります。「ENABLED」にした後に表示された「URL」にPOST先のサーバーURLを、POST先のサーバー側へ必要であれば「BASIC AUTH」と「HEADERS」でベーシック認証の情報やヘッダー情報を付与します。「DOMAINS」のところは現在のところプルダウン形式で「Disable webhook for all domians」か「Enable webhook for all domians」しかなく、「Webhook」を使う場合にはEnableの方を選んでおくことになります。なお、「Webhook」の利用にはHTTPSが必須のようです。

次に、「Inline Editor」の方を「ENABLED」にしてみます。すると、下記のようなメッセージが表示されました。

thumb_10

フルフィルメントで提供されている「Webhook」と「Inline Editor」は同時に使用することはできず、どちらか一方しか使えないため、片方をENABLEDにするともう片方がDISABLEDになるようです。では「Inline Editor」の方を見てみます。

thumb_11

「Inline Editor」の項目に(Powered by Cloud Functions for Firebase)とありますが、「Inline Editor」の方では実装部分を記述してデプロイすることで、Firebaseの機能を使ったりすることができ、別のサーバーを用意することなく、簡易的な実装を「Inline Editor」内だけで完結させることができます。また、Node.jsで使用したいモジュールがある場合にはpackage.jsonに追加することができ、デプロイ時にFirebaseの方でインストールしてくれます。

thumb_12

Inline Editorを触ってログ出力を確認してみる

今回はInline Editorを使って進めてみます。
まずは元々入っているコードを変更せず、テキストフィールド下部にある「DEPLOY」を行ってみます。デプロイまで少し時間がかかりますが、下記のようにSuccessfullyが出たらデプロイ完了です。

thumb_13

デプロイが完了したら、テキストフィールド下の「View execution logs in the Firebase console」のリンクからFirebase consoleを開きます。ログ出力が正しくできればここに出力されるはずなので、実際にシミュレータで動かしてログが出るのかを試してみます。

thumb_14

シミュレータを動かした後にFirebase consoleを見てみると、元から入っていたコードに手を加えていないため、ハンドリングあたりのエラーは出てしまってはいますが、ログが出力されていることが確認できます。また、

  console.log('Dialogflow Request body: ' + JSON.stringify(request.body));

でログ出力しているリクエストボディ部分を見てみると、対話において発生したJSONの内容が見られました。

thumb_15

続けて、エラーとなった箇所を改善できるようにコードを少し改変してみます。

'use strict';
 
const functions = require('firebase-functions');
const {WebhookClient} = require('dialogflow-fulfillment');

process.env.DEBUG = 'dialogflow:debug';
 
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
  const agent = new WebhookClient({ request, response });
  console.log('Dialogflow Request body: ' + JSON.stringify(request.body));

  function hearing(agent) {
    console.log('Welcome to hearing function')
    let conv = agent.conv();
    conv.close('Request done.') 
    agent.add(conv);
  }

  let intentMap = new Map();
  intentMap.set('Hearing Intent', hearing);
  agent.handleRequest(intentMap);
});

今回フルフィルメントを使用するようにしたのは「Hearing Intent」のみであるため、functionなどをそれに適した形にし、作成したfunction内に処理として入ってきているかどうかの確認のために「Welcome to hearing function」とログを出力するようにしてみました。それではデプロイ後にシミュレータで動かしてログを確認してみます。

thumb_16

thumb_17

これで、エラーもなくなり、ログ出力されることが確認できました。元のコードのままでデプロイしてシミュレータを実行したときには、「Hearing Intent」のIntentsページで設定したText Responseの内容が出力されていましたが、今回はフルフィルメントで記述した内容「Request done.」でレスポンスが返されています。これはおそらく、元のコードではフルフィルメント側に該当のインテントがなかったために、Dialogflow側のText Responseの内容が出力されていたためと思われます。

これで、Inline Editorからのデプロイ、Firebaseでのログ確認などの流れがつかめてきました。

Inline Editorのコードをv1からv2へ変更する

Inline Editorに元々入っていたコードを触ってみましたが、このコードはActions on Google Node.js Client Library Version 1のもののようで、2018年4月16日にVersion 2がリリースされており、v2のものとは少し書き方が違うようです。v1からv2への移行ガイドはこちらで参照できるようですが、GitHub pagesを参考にしてv2のコードで書き換えてみます。

'use strict';

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

const app = dialogflow()

app.intent('Hearing Intent', conv => {
    console.log('Welcome to hearing function')
    conv.close('Request done.')
})

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

こちらのコードに変更後、デプロイしてシミュレータで実行してみます。

thumb_18

thumb_19

v1のときと同じように、フルフィルメントからの返答、ログ出力を行うことができました。

v2に対応したコードでフルフィルメントからのレスポンスを実装する

さらに、フルフィルメント側でもText Responseと同じように対話部分で得た要素を盛り込んだ返答ができるように修正してみます。Actions on Googleの「Build Fulfilment」のページを参考にしながら、処理の実装をしてみたいと思います。

ではコードを変更してみます。なお、日付フォーマット変更用にdate-utilsモジュールを利用していますので、package.jsonの方にもdate-utilsを追加しておきます。

'use strict';

require('date-utils');

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

const app = dialogflow({debug: true});

const HEARING_INTENT = 'Hearing Intent';
const ARGUMENT_DATE      = 'date';
const ARGUMENT_STARTTIME = 'start-time';
const ARGUMENT_USETIME   = 'use-time';
const ARGUMENT_USETIME_PARM = { //{"usage-time":{"amount":2,"unit":"時"}}
  AMOUNT: 'amount',
  UNIT: 'unit',
};
const ARGUMENT_ROOMTYPE  = 'room-type';

app.intent('Hearing Intent', (conv, params)  => {

  console.log('Hearing Intent');
  console.log(params);

  //予約日
  let dDate = new Date(params[ARGUMENT_DATE]);
  let reserveDate = dDate.toFormat('YYYY年MM月DD日');

  //予約開始時間
  let startTime = new Date(params[ARGUMENT_STARTTIME]);
  dDate.setHours(startTime.getHours() + 9); //to JST
  let reserveBeginTime = dDate.toFormat('HH24時MI分');

  //利用時間
  let useTimeAmount = params[ARGUMENT_USETIME][ARGUMENT_USETIME_PARM.AMOUNT];
  let useTimeUnit   = params[ARGUMENT_USETIME][ARGUMENT_USETIME_PARM.UNIT];
  let useTime = useTimeAmount + useTimeUnit;

  //予約終了時間
  let endTime = dDate;

  switch (useTimeUnit) {//@sys.durationのunitに応じて処理をわける。日は無視して時と分のみだけの対応にしておく
    case '時':
      endTime.setHours(dDate.getHours() + Number(useTimeAmount));
      break;
    case '分':
      endTime.setMinutes(dDate.getMinutes() + Number(useTimeAmount));
      break;
    default:  
      endTime.setHours(dDate.getHours() + Number(useTimeAmount));
      break;
  }

  let reserveEndTime = endTime.toFormat('HH24時MI分');

  //予約会議室
  let roomType = params[ARGUMENT_ROOMTYPE];

  conv.close(`${reserveDate}  ${reserveBeginTime} から ${reserveEndTime} までの ${useTime}間、${roomType} を予約しました。`);    
});

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

これで、dialogflowのインテントで設定したパラメータを取得し、レスポンス部分までフルフィルメント側で実装ができました。レスポンス部分については、予約開始時間と利用時間を元に予約終了時間を算出して「2018年07月15日の14時00分から16時00分までの2時間、A2会議室を予約しました。」というような返答を行うようにしてみました。

thumb_20

次回に向けて

実際のカレンダー連携の実装の前の、まずはフルフィルメントを触ってみるところまででかなり長くなってしまったので、続きはまた次回エントリーにて進めてみたいと思います。