DialogflowでWebhook

 DialogflowWebhookを利用するサンプルプログラムを書いてみた。参考にしたサイトは「Dialogflow入門」。ただし、このサイトは3年前(2018年1月)に書かれたもので、現在ではDialogflowも随分進化しているし、肝心のWebhookの実装もない(JSON形式のインターフェースは書かれているが、内容が古いので使えない)ので、同じ題材で内容を一新してここにまとめてみた。

関連サイト

話を進める前に関連するサイトをまとめておく。

Dialogflowは、大規模で有料のDiagflow CXと小規模で無料でも使えるDialogflow ESの2つのサービスがある。今回使うのは無料版のESで、ドキュメントを見るにはDialogflowドキュメントのメニューから「Dialogflow ESのドキュメント」を選択する。左のメニューから[コンセプト]→[フルフィルメント]→[Webhookサービス]と進む。

ホテル予約エージェントの作成

ここから参考サイト「Dialogflow入門」に従ってホテル予約のエージェントを作成する。

ホテル予約エージェントbooking

画面構成は、項目をグループ化するなど幾分変わっているが、だいたい対応が付くので、参考サイトのとおりに入力した。

Entities

ホテル名を表す「building」entityと「予約」「キャンセル」など要求を表す「Request」entityを定義する。


Intents

左メニューの[Intents]の右にある+をクリックしてIntentsを作成する。Intent nameは「booking」、Training phrases(参考サイトにはUser saysと書いてある)には「明日の福岡を予約」と入力する。

すると、入力内容を基に自動的にEntitiesが検出されて[Action and parameters]に一覧表示される。ここで、興味深いのはTraining phrasesに入力した「明日の」をシステム組み込みEntity「@sys.date」と検知し、パラメタ名(PARAMETER NAME)を「date」としている点である。

ついで、参考サイトにあるようにdateとbuildingを必須(REQUIRED)に設定してプロンプトを入力する。これは良くできた仕組みで、入力時点で忘れていたら督促してくれるので大変助かる。

次に参考サイトではResponsesの設定に進むが、これは「静的な応答」の設定なので、ここでは省略してWebhookの設定に進む。

Fulfillment

フルフィルメント(Fulfillment)とは、日本語で「履行」「遂行」「実現」とかいう意味であるが、Dialogflowでは「動的な応答」を実現する仕組みのことを言う。

IntentのFulfillmentを有効に設定すると、Dialogflowはあらかじめ定義されたサービスを呼び出して動的な応答メッセージを返すことができる。その方法には外部サービスを呼び出すWebhookを設定する方法とインラインエディタでクラウド上にプログラムコードを打ち込む方法があるが、後者は無料版では利用できない(厳密には無料版で利用できないのではなく、上限を超えると課金される)。

WebhookはGoogle Apps Script (GAS)で作成したウェブアプリケーションが利用できるので、最も手軽に安心してできるのはこの方法だろう。

まず、IntentsのFulfillmentを有効にする。左メニューから[Intents]を選んで、右に表示されるIntent設定画面を一番最後までスクロールさせ、[Fulfillment]グループの[Enable webhook call for this intent]をONにして[SAVE]ボタンをクリックする。

次に左メニューから[Fulfillment]をクリックし、右側に表示された[Webhook]を「ENABLED」に設定し、[URL]にWebhookで呼び出したい外部サービスのURL(のちに説明するGASで作成したWebサービスのURL)を入力して画面最後尾の[SAVE]ボタンをクリックする。


GASによるWebhookの作成

GASプログラム

DialogflowエージェントbookingのFulfillmentで設定するWebhookのウェブアプリケーションをGASを使って開発した。以下がそのソースプログラムである。

/**
 * 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 text = parameters['building'] + 'を' + GST2JST(parameters['date-time'], 'M月d日') + 'に' + parameters['request'] + 'しました。';
  const res = {
    "fulfillmentMessages": [
      {
        "text": {
          "text": [
            text
          ]
        }
      }
    ]
  };
  return res;
}

/**
 * グリニッジ標準時(GST)を日本標準時(JST)に編集する
 * str_gst_date: '2021-02-20T12:00:00+09:00'
 * format: 'MM月dd日'
 */
function GST2JST(str_gst_date, format = 'yyyy/MM/dd HH:hh:mm') {
  const date = new Date(str_gst_date);
  return Utilities.formatDate(date, 'JST', format);
}

Webhook リクエスト

Dialogflowから送られてきたリクエストは、POSTメソッドのエンドポイント関数doPostの引数eの属性postDataのメソッドgetDataAsString()によって、以下のJSON形式のデータで取得できる。

{
  "responseId": "cbf27aca-3e69-4cc9-aa48-18a5ccbc830e-10fc9d68",
  "queryResult": {
    "queryText": "明日の福岡を予約",
    "parameters": {
      "date-time": "2021-02-21T12:00:00+09:00",
      "request": "予約",
      "building": "福岡タウンホテル"
    },
    "allRequiredParamsPresent": true,
    "fulfillmentMessages": [
      {
        "text": {
          "text": [
            "福岡タウンホテルを2月21日に予約しました。"
          ]
        }
      }
    ],
    "intent": {
      "name": "projects/samplehotelagent-glwx/locations/asia-northeast1/agent/intents/b5374aa1-466d-47fc-ac4b-23274897201e",
      "displayName": "booking"
    },
    "intentDetectionConfidence": 1,
    "diagnosticInfo": {
      "webhook_latency_ms": 997
    },
    "languageCode": "ja"
  },
  "webhookStatus": {
    "message": "Webhook execution successful"
  },
  "agentId": "ae655b4c-60b2-4746-9ebe-83c3a5563fd7"
}

これは、Dialogflowコンソールで右上の[Try it now]に「明日の福岡を予約」と入力したときに、右下の[DIAGNOSTIC INFO]をクリックするとポップアップで表示できる。


Webhook レスポンス

Webhookからのレスポンスには様々なものがある(詳細はドキュメント参照)。

テキスト返信

{
  "fulfillmentMessages": [
    {
      "text": {
        "text": [
          "福岡タウンホテルを2月21日に予約しました。"
        ]
      }
    }
  ]
}

カスタム ペイロードのレスポン

上記のテキスト返信でLINEへテキストメッセージを送信できるが、ほとんどの LINE メッセージ タイプはカスタム ペイロード レスポンスで送信できる。以下はテキストメッセージを送信するフォーマットである。

{
  "fulfillmentMessages": [
    // テキストメッセージ
    {
      "payload": {
        "line": {
          "type": "text",
          "text": text,
        }
      },
      "platform": "LINE"
    },
  ]
};

以下はスタンプメッセージを送信するフォーマットである。

{
  "fulfillmentMessages": [
    // スタンプメッセージ
    {
      "payload": {
        "line": {
          "type": "sticker",
          "stickerId": "52002735",
          "packageId": "11537"
        }
      },
      "platform": "LINE"
    },
  ]
};

これによって下図のようなスタンプが表示される。なお、stickerIdやpackageIdについてはスタンプリストを参照のこと。

図7.スタンプメッセージ

 

以下は画像メッセージを送信するフォーマットである。

{
  "fulfillmentMessages": [
    // 画像メッセージ
    {
      "payload": {
        "line": {
          "type": "image",
          "originalContentUrl": "https://1.bp.blogspot.com/.../totoro.png",
          "previewImageUrl": "https://1.bp.blogspot.com/.../totoro.png"
        }
      },
      "platform": "LINE"
    },
  ]
};

これによって下図のような画像が表示される。

図8.画像メッセージ

 

以下はテンプレートメッセージ(ボタン)を送信するフォーマットである。

{
  "fulfillmentMessages": [
    // テンプレートメッセージ(ボタン)
    {
      "payload": {
        "line": {
          "type": "template",
          "altText": "テンプレートメッセージ(ボタン)",
          "template": {
            "type": "buttons",
            "thumbnailImageUrl": "https://1.bp.blogspot.com/.../totoro.png",
            "imageAspectRatio": "rectangle",
            "imageSize": "contain",
            "imageBackgroundColor": "#FFFFFF",
            "title": "メニュー",
            "text": "選択してください。",
            "defaultAction": {
              "type": "uri",
              "label": "詳しく見る",
              "uri": "https://semi2020kumw.blogspot.com/"
            },
            "actions": [
              {
                "type": "postback",
                "label": "購入",
                "data": "action=buy&itemid=123"
              },
              {
                "type": "message",
                "label": "カートに入れる",
                "text": "1週間後に北海道を予約して"
              },
              {
                "type": "uri",
                "label": "詳しく見る",
                "uri": "https://semi2020kumw.blogspot.com/"
              }
            ]
          }
        }
      },
      "platform": "LINE"
    },
  ]
};

下図はテンプレートのボタンメッセージを送信したLINE画面である。

図9.テンプレート(ボタン)メッセージ

ここで[詳細表示]ボタンをクリックすると、uriアクションに設定されているURLへ分岐して次のような画面が表示される。

図10.uriアクションで分岐したWebサイト

また、[カートに入れる]ボタンをクリックすると、メッセージアクションが設定されているテキスト「1週間後に北海道を予約して」というメッセージが入力される。なお、[購入]ボタンをクリックすると、ポストバックアクションが発生するが、DialogflowのWebhookにはメッセージがポストされないようなので、これは使えない。したがって、ボタンに割り当てることのできるアクションはメッセージアクションとURIアクションくらいである。

以下はテンプレートメッセージ(カルーセルテンプレート)を送信するフォーマットである。

{
  "fulfillmentMessages": [
    // テンプレートメッセージ(カルーセルテンプレート)
    {
      "payload": {
        "line": {
          "type": "template",
          "altText": "テンプレートメッセージ(カルーセルテンプレート)",
          "template": {
            "type": "carousel",
            "columns": [
              {
                "thumbnailImageUrl": "https://1.bp.blogspot.com/.../totoro.png",
                "imageBackgroundColor": "#FFFFFF",
                "title": "メニュー",
                "text": "カルーセルテンプレートのサンプル",
                "defaultAction": {
                  "type": "uri",
                  "label": "詳細を表示",
                  "uri": "https://semi2020kumw.blogspot.com/"
                },
                "actions": [
                  {
                    "type": "postback",
                    "label": "購入",
                    "data": "action=buy&itemid=111"
                  },
                  {
                    "type": "postback",
                    "label": "カートに追加",
                    "data": "action=add&itemid=111"
                  },
                  {
                    "type": "uri",
                    "label": "詳細を表示",
                    "uri": "https://semi2020kumw.blogspot.com/"
                  }
                ]
              },
              {
                "thumbnailImageUrl": "https://1.bp.blogspot.com/.../totoro.png",
                "imageBackgroundColor": "#000000",
                "title": "メニュー2",
                "text": "これは2つ目のカルーセルです",
                "defaultAction": {
                  "type": "uri",
                  "label": "詳細を表示2",
                  "uri": "https://semi2020kumw.blogspot.com/"
                },
                "actions": [
                  {
                    "type": "postback",
                    "label": "購入2",
                    "data": "action=buy&itemid=222"
                  },
                  {
                    "type": "postback",
                    "label": "カートに追加2",
                    "data": "action=add&itemid=222"
                  },
                  {
                    "type": "uri",
                    "label": "詳細を表示2",
                    "uri": "https://semi2020kumw.blogspot.com/"
                  }
                ]
              }
            ],
            "imageAspectRatio": "rectangle",
            "imageSize": "contain"
          }
        }
      },
      "platform": "LINE"
    },
  ]
};

これによって下図のようなカルーセル画面が表示される。

図11.カルーセル画面

なお、fulfillmentMessages配列に複数のメッセージ、例えばテキストメッセージとスタンプメッセージを配列要素に設定すると、図8のように複数のメッセージを送信できる。

{
  "fulfillmentMessages": [
    // テキストメッセージ
    {
      "payload": {
        "line": {
          "type": "text",
          "text": text,
        }
      },
      "platform": "LINE"
    },
    // スタンプメッセージ
    {
      "payload": {
        "line": {
          "type": "sticker",
          "stickerId": "52002735",
          "packageId": "11537"
        }
      },
      "platform": "LINE"
    },
  ]
}

カスタムペイロードレスポンスクラス

下記はカスタムペイロードレスポンスメッセージを作るクラスである。

/**
 * DialogFlow webhook response class
 */
class Response {
  // コンストラクタ―
  constructor() {
    this.fulfillmentMessages = [];
  }
  // LINEテキストメッセージを追加する
  pushLineText(text) {
    this.pushMessage({
      "type": "text",
      "text": text,
    });
  }
  // LINEスタンプメッセージを追加する
  pushLineSticker(stickerId, packageId) {
    this.pushMessage({
      "type": "sticker",
      "stickerId": stickerId,
      "packageId": packageId
    });
  }
  // LINE画像メッセージを追加する
  pushLineImage(originalContentUrl, previewImageUrl) {
    this.pushMessage({
      "type": "image",
      "originalContentUrl": originalContentUrl,
      "previewImageUrl": previewImageUrl
    });
  }
  // LINEテンプレートメッセージ(ボタン)を追加する
  pushLineTemplateButton(altText, thumbnailImageUrl, title, text, defaultAction, actions) {
    this.pushMessage({
      "type": "template",
      "altText": altText,
      "template": {
        "type": "buttons",
        "thumbnailImageUrl": thumbnailImageUrl,
        "imageAspectRatio": "rectangle",
        "imageSize": "contain",
        "imageBackgroundColor": "#FFFFFF",
        "title": title,
        "text": text,
        "defaultAction": defaultAction,
        "actions": actions
      }
    });
  }
  // LINEテンプレートメッセージ(カルーセルテンプレート)を追加する
  pushLineTemplateCarousel(altText, columns) {
    this.pushMessage({
      "type": "template",
      "altText": altText,
      "template": {
        "type": "carousel",
        "columns": columns,
        "imageAspectRatio": "rectangle",
        "imageSize": "contain"
      }
    });
  }
  // カスタムペイロードメッセージを追加する
  pushMessage(line_message) {
    this.fulfillmentMessages.push({
      "payload": {
        "line": line_message
      },
      "platform": "LINE"
    });
  }
  // レスポンスメッセージを取得する
  getMessage() {
    return {
      "fulfillmentMessages": this.fulfillmentMessages
    }
  }
}

このクラスを使うと以下のように簡単にカスタムペイロードレスポンスメッセージを作ることができる。

  const res = new Response();
  res.pushLineText('こんにちは');
  res.pushLineSticker("52002735","11537");
  const imageUrl = "https://exampl.some.com/image.png";
  res.pushLineImage(imageUrl, imageUrl);

res.getMessage()によってテキスト、スタンプ、そして画像のレスポンスメッセージを得ることができる。

テンプレートメッセージのボタンはdefaultActionとボタンに割り当てるActionを設定する必要があるので、少し複雑になる。以下に例を示す。

  const defaultAction = {
    "type": "uri",
    "label": "詳しく見る",
    "uri": "https://semi2020kumw.blogspot.com/"
  };
  const actions = [];
  actions.push({
    "type": "postback",
    "label": "購入",
    "data": "action=buy&itemid=123"
  });
  actions.push({
    "type": "message",
    "label": "カートに入れる",
    "text": "1週間後に北海道を予約"
  });
  actions.push(defaultAction);
  res.pushLineTemplateButton('テンプレートメッセージ(ボタン)', imageUrl, 'メニュー', '選択してください。', defaultAction, actions);

この例では、[購入]ボタン、[カートに入れる]ボタン、[詳しく見る]ボタンの3つのボタンを作成し、それぞれ、ポストバックアクション、メッセージアクション、uriアクションを割り当てている。なお、ポストバックアクションはDialogflowへ通知されないため、ボタンをタップしても何も起こらない。

テンプレートメッセージのカルーセルはカルーセルに表示するカラム(最大6個)を設定する必要があるが、このカラムが複雑な構造をしている(と言ってもテンプレートボタンとほとんど同じ構造であるが)ので、カルーセルカラムクラスCarouselColumnを作成した。

/**
 * LINE カルーセルのカラムクラス
 */
class CarouselColumn {
  constructor() {
    this.columns = [];
  }
  // カラムを追加する
  add(thumbnailImageUrl, title, text, defaultAction, actions){
    this.columns.push({
      "thumbnailImageUrl": thumbnailImageUrl,
      "imageBackgroundColor": "#FFFFFF",
      "title": title,
      "text": text,
      "defaultAction": defaultAction,
      "actions": actions
    });
  }
}

これを使うと以下のようにテンプレートカルーセルメッセージを作成できる。

  const cCols = new CarouselColumn();
  cCols.add(imageUrl, 'メニュー1', 'これは1つ目のカラムです', defaultAction, actions);
  cCols.add(imageUrl, 'メニュー2', 'これは2つ目のカラムです', defaultAction, actions);
  res.pushLineTemplateCarousel('テンプレートメッセージ(カルーセルテンプレート)', cCols.columns); 

この例では2つの(ほぼ全く同じ内容の)カラムを持つカルーセルを作っている。

Google アシスタント レスポンス

Google HomeやGoogle Assistantへメッセージを送るには次のフォーマットで行う。

{
  "payload": {
    "google": {
      "expectUserResponse": true,
      "richResponse": {
        "items": [
          {
            "simpleResponse": {
              "textToSpeech": "this is a Google Assistant response"
            }
          }
        ]
      }
    }
  }
}

課題と疑問 

現時点ではここまでわかったが、以下に疑問点と課題を記しておく。 

DialogflowとGoogle Home (Google Assistant)との連携

Dialogflowは独立した「自然言語理解プラットフォーム」である。Google Home (Google Assistant)と連携するにはAction on Googleというものを利用するようだ。その際、Dialogflowで作成したAgentとどうやって紐づけるのだろう。具体的には、どのような発話をトリガーとしてDialogflowで定義したAgentと紐づけるのだろう(→これについては解決済み。「DialogflowとGoogle Assistantの統合」を参照。Actionsコンソールからプロジェクトを作成してDialogflowを用いてビルドする[Build with Dialogflow]という流れになる)。

調べていくうちに、今ではGoogleアシスタントはDialogflowとは統合されていないということがわかった。 その代り、会話アクション(Conversational Actions)というプラットフォームで開発することが推奨されている。ただし、DialogflowのAction on Googleレガシー統合は引き続き機能している。もし、Dialogflowを利用するのであれば、現時点ではこの選択肢が最有望ということか(【重要】Dialogflowコンソールはデフォルトリージョン(USリージョン)以外はメニューにIntegrationが表示されないので注意)。

Webhook URLはIntentごとに設定でない?

Dialogflowコンソール画面ではFulfillmentのWebhook URL設定はIntentの設定とは独立して存在する。つまり、Webhook URLはIntentごとに設定できるのではなく、一つのAgentには1つのWebhook URLしか設定できないのだろうか?

そうなると、あらゆるケースを想定しないといけないのでプログラムが複雑になりそうだし、そもそも、プログラム側でどのようにしてIntentを識別できるのだろう?

と、そう思って見てみたら、Fulfillment requestにはIntent情報があるので、DialogflowはIntentを識別した状態でWebhook URLへパラメタを送信しているようだ(queryResult.intent.displayName)。

AgentとIntentの関係はどうなっているのだろう?

EntityはIntentとは独立した概念?

Dialogflowコンソール画面ではEntitiesもIntentsと独立したメニューになっているので、IntentごとにEntityは作るものではなく、Agentに共通したものになる。

考えてみれば、これは当然のことかもしれないが、Entityを設計する際に慎重に設計しないと混乱が起きる可能性がある。たとえば、今回、buildingというEntityを作ったが、「福岡タウンホテル」のシノニムに「福岡」と設定したため、地名の「福岡」がbuldingと検知されてしまうのではないかと危惧される。そうすると地名を扱うIntentを作ろうと思うとどうなるのだろう?

AgentとIntentの役割はどうなっているのだろう?

0 件のコメント:

コメントを投稿