プロトでお会計

 2018年にマイナビ出版から刊行された「スマートスピーカー × AIプログラミング 自分で作る人工知能」のChapter 3で取り扱われているGoogle Homeのオリジナルアクション「プロトでお会計」を実装してみた。

2年前の本なので(現在はDialogflowは公式にGoogle Assistantと統合されていないので)特にActionsコンソールとDialogflowの連携などの画面まわりがかなり変わっているが試行錯誤を重ねてやってみた。

また、本ではWebhookをPython(flask-assistantというパッケージを使って)で開発しているが(サーバはRaspberry Piでngrok使用)、ここではGAS(Google Apps Script)を使用した。

Actionsコンソール

下図はActionsコンソールの[Overview]→[Quick setup]→[Decide how your Action is invoked]画面でDisplay nameを設定しているところである。

図1.Display nameの設定

この設定によって「OKグーグル プロトでお会計につないで」と呼びかけると、ここで作ったActionが呼び出される。

Dialogflow

下図はDialogflowでIntent「calcIntent」を作成している画面である。

 

図2.DialogflowのIntentの設定画面
トレーニングフレーズとして「赤いりんご4個と青いりんご3個ください」と入力して、「赤いりんご」「4個」「青いりんご」「3個」をそれぞれ「@list_of_red」「@number_1」「@list_of_green」「@number_2」という名前のEntityに割り当てている。ここで、「@list_of_red」「@list_of_green」は自作のEntityで、「@number_1」と「@number_2」はシステム組み込みEntity「@sys.number」を利用している。下記のコードは「@list_of_red」をExportしたJSONデータである。

list_of_red.json

{
  "id": "99cba08e-a1d4-41b0-9fd8-6af8836584ee",
  "name": "list_of_red",
  "isOverridable": true,
  "isEnum": false,
  "isRegexp": false,
  "automatedExpansion": false,
  "allowFuzzyExtraction": false
}

list_of_red_entries_ja.json

[
  {
    "value": "赤りんご",
    "synonyms": [
      "赤りんご",
      "あかりんご",
      "あか",
      "赤",
      "赤いの",
      "あかいの",
      "赤いほう",
      "あかいほう"
    ]
  }
]

このようにDialogflowには設定内容をJSON形式のデータでExport/Importする機能がある。

図3.Dialogflow Export and Import画面

左側のメニューから歯車をクリックして中央の画面に表示されるタブメニューで[Export and Import]をクリックするとZIPファイルでダウンロードでき(EXPORT AS ZIP)、展開するとIntentやEntityが各々フォルダに格納されたファイルが得られる(下図)。

図4.ダウンロードしたZIPファイルを展開

GASによるWebhook作成

下図はGASでWebhookを作成しているところである。

図5.「プロトでお会計」のWebhook
以下にプログラムを示す。

/**
 * POSTメソッド
 */
function doPost(e) {
  const data = e.postData.getDataAsString();
  const req = JSON.parse(data);
  const res = handleWebhook(req);
  return ContentService.createTextOutput(JSON.stringify(res));
}

/**
 * Webhookリクエストハンドラ
 * Dialogflowから送信されてきた HTTPS POST Webhook リクエストを処理する関数
 * https://cloud.google.com/dialogflow/es/docs/fulfillment-webhook?hl=ja
 */
function handleWebhook(req) {
  const parameters = req.queryResult.parameters;
  const intent = req.queryResult.intent.displayName;
  if (intent == 'Default Welcome Intent') {
    return greet_and_start();
  } else if (intent == 'calcIntent') {
    return calc(parameters);
  } else {
    return ask('何を言っているのかわかりません。');
  }
}

/**
 * 挨拶と開始
 */
function greet_and_start() {
  return ask('いらっしゃい、赤りんごと青りんごがおすすめですよ。');
}

const red_price = 90;
const green_price = 120;
function checkout(x, y) {
  return x * red_price + y * green_price;
}

/**
 * お会計
 */
function calc(p) {
  if(!p.list_of_red) {
    const checkout_all = p.number_2 * green_price;
    return tell(p.list_of_green + p.number_2 + '個で' + checkout_all + '円になります。'); 
  } else if(!p.list_of_green) {
    const checkout_all = p.number_1 * red_price;
    return tell(p.list_of_red + p.number_1 + '個で' + checkout_all + '円になります。'); 
  } else {
    const checkout_all = checkout(p.number_1, p.number_2);
    return tell(p.list_of_red + p.number_1 + '個と' + p.list_of_green + p.number_2 + '個で' + checkout_all + '円になります。'); 
  }
}

/**
 * レスポンス送信後もセッションが続く
 */
function ask(speech) {
  return _speech(speech, true);
}

/**
 * レスポンス送信後にセッションを終了
 */
function tell(speech) {
  return _speech(speech, false);
}

/**
 * レスポンスJSON編集
 */
function _speech(speech, expectUserResponse) {
  return {
    // テキスト レスポンス
    "fulfillmentMessages": [
      {
        "text": {
          "text": [
            speech
          ]
        }
      }
    ],
    // Google アシスタント レスポンス
    "payload": {
      "google": {
        "expectUserResponse": expectUserResponse,
        "richResponse": {
          "items": [
            {
              "simpleResponse": {
                "textToSpeech": speech
              }
            }
          ]
        }
      }
    }
  };
}

POSTメソッドのリクエストはdoPost関数へ送られる。ここでJSON形式のデータをオブジェクトに復元して、handleWebhook関数へ送る。

handleWebhook関数はパラメタとIntentを各々変数parametersとintentに取り出して、intentの値に応じた処理を行う。

まず、intentがDefault Welcome Intentの場合は関数greet_and_startを呼び出して、挨拶メッセージ(ここでは「いらっしゃい、赤りんごと青りんごがおすすめですよ。」)を返す。なお、関数greet_and_startでは、このメッセージを返すために関数askを使っている。関数askは引数expectUserResponseをtrueに設定して_speech関数を呼び出している。_speech関数は所定のJSONフォーマットのresponseメッセージを組み立てる関数である。expectUserResponseをtrueに設定した場合、Google Assistantは引き続きユーザからのリクエストに備えて待機する。

次にintentがcalcIntentである場合、パラメタparametersを引数にして関数calcを呼び出す。関数calcはパラメタから赤りんごと青りんごの個数を取り出して各々の単価をかけて合計金額を計算してメッセージを返す。その際、セッションを終了させるために関数tellを使う。関数tellは引数expectUserResponseをfalseに設定して関数_speechを利用する。これでGoogle Assistantはレスポンスをユーザに送出した後、Actionを終了する。

リクエストメッセージ

ユーザが「赤りんご6個と青りんご7個ください」と言ったときのリクエストメッセージを以下に示す。

{
  "responseId": "3810cc94-0f6c-4a16-b01f-73b9df7c54d7-d8906a81",
  "queryResult": {
    "queryText": "赤りんご6個と青りんご7個ください",
    "parameters": {
      "number_2": 7,
      "number_1": 6,
      "list_of_red": "赤りんご",
      "list_of_green": "青りんご"
    },
    "allRequiredParamsPresent": true,
    "fulfillmentText": "赤りんご が 6 個と 青りんご が 7 個 欲しいのですね",
    "fulfillmentMessages": [
      {
        "text": {
          "text": [
            "赤りんご が 6 個と 青りんご が 7 個 欲しいのですね"
          ]
        }
      }
    ],
    "outputContexts": [
      {
        "name": "projects/proto-calc-d218d/agent/sessions/bd0b0d46-0711-b684-6e41-a3367e62c7df/contexts/__system_counters__",
        "parameters": {
          "no-input": 0,
          "no-match": 0,
          "number_2": 7,
          "number_2.original": "7",
          "number_1": 6,
          "number_1.original": "6",
          "list_of_red": "赤りんご",
          "list_of_red.original": "赤りんご",
          "list_of_green": "青りんご",
          "list_of_green.original": "青りんご"
        }
      }
    ],
    "intent": {
      "name": "projects/proto-calc-d218d/agent/intents/757d80da-4a69-4edb-8095-7385d301bfb5",
      "displayName": "calcIntent"
    },
    "intentDetectionConfidence": 0.57537895,
    "languageCode": "ja"
  },
  "originalDetectIntentRequest": {
    "source": "DIALOGFLOW_CONSOLE",
    "payload": {}
  },
  "session": "projects/proto-calc-d218d/agent/sessions/bd0b0d46-0711-b684-6e41-a3367e62c7df"
}

0 件のコメント:

コメントを投稿