Dialogflowで応答文の一部にwebhookの結果を設定する方法

[ボットアドベントカレンダー](https://qiita.com/advent-calendar/2017/bot)１日目です。このカレンダーには、チャットボットに限らず、株取引、Wiki更新、仮想世界の様々な場所で活躍するボットを作った／作ろうとしている方のエントリを歓迎します。ボット関連のサービスのちょっとしたネタでも構いません。後半まだまだ空きがありますので、是非。

さて１日目は、Dialogflow(旧api.ai)のwebhook(外部API)連携について書きます。

> 本記事は、Fulfillmentについてある程度ご存知であることを前提とした記事になっています。そもそもFulfillmentについての日本語情報が少ない中恐縮ですが、ご了承ください。
> 
> 要は、([認証](https://dialogflow.com/docs/fulfillment#authentication)、)[リクエスト](https://dialogflow.com/docs/fulfillment#request)を受け取り、[正常レスポンス](https://dialogflow.com/docs/fulfillment#response)や[異常レスポンス](https://dialogflow.com/docs/fulfillment#errors)を返すようなWeb APIを、Googleのサーバから見える位置に置いて、DialogflowのFulfillmentメニューでURL(や認証情報)を指定して`ENABLED`スライドを動かし、利用したいIntentで`Use webhook`にチェックを入れれば、利用可能になります。
>
> 一応[公式にチュートリアルがありますが](https://dialogflow.com/docs/how-tos/getting-started-fulfillment)、話を難しくしている感があります。仕様に沿ったAPIを自分で作ってサーバーレス系のクラウドサービスに置く方が簡単かもしれません。ちなみに私は[Azure Function](https://azure.microsoft.com/ja-jp/services/functions/)の「HttpTrigger - C#」をF1プランで使っています。Dialogflowのwebhookとして利用できる簡単な関数(run.csx)のサンプルを[Gistに置きました](https://gist.github.com/Makopo/6e2171443517778405a8fb5d9ce8ab33)。

## やりたいこと

Dialogflowで[Fulfillment](https://dialogflow.com/docs/fulfillment)を普通に設定すると、webhookからのレスポンス本文の`speech`に書いてある内容がそのまま表示されてしまいます。例えば、以下をwebhookから返すとしましょう。

```json
{"speech":"大吉だよ！"}
```

ボットは「大吉だよ！」と応答します。

![fixedspeech.png](https://qiita-image-store.s3.amazonaws.com/0/10290/679c0f07-81e7-1347-cea6-6971b55fa22f.png)

「大吉」の部分は仕方ないにせよ、ボット様の気分(ランダム)によって、多少は表現をアレンジしてみたいものです。しかし、`speech`に設定する方法だと、webhookから返ってきたものがそのまま応答になります。結局、応答文に関する処理をwebhookに盛り込むことになってしまい、何のためにDialogflowを使っているのかわからなくなってきます。webhookには、外部APIとのパイプ役に徹してもらいたいものです。

そこで思いつくのは、Responseセクションに変数を書いておいたら、そこにwebhookから何か設定してもらえるんじゃないか、ということでしょう。「大吉」だけwebhookに返してもらって、あとはDialogFlowがランダムに文章を選択して返してくれないかなー、という期待です。

![want.png](https://qiita-image-store.s3.amazonaws.com/0/10290/0aec0df4-703a-d828-7752-f815dbdfd24e.png)

> 上の画面キャプチャは、「イメージ」です。実際にはこのような設定を行いませんので、ご注意ください。

## 実現方法

webhookからのレスポンスに`followupEvent`を入れて、新しいイベントを起こします。すると、そのイベント名で待ち受けている別のIntentに結果が送られます。そのIntentに、変数を定義しておきます。その変数に、webhookからイベントの引数として送られた内容をアサインします。Responseに変数のプレースホルダを含めた文章を設定しておくことで、プレースホルダの部分が変数の内容(=イベントの引数で受け取った内容)で置換され、ユーザに送り返されます。

![flow.png](https://qiita-image-store.s3.amazonaws.com/0/10290/c2503e01-c927-8af7-5f91-078cc73ab343.png)


> 同じIntentがユーザの発言とイベントの両方を受け付けることはできないため、イベントを受ける専用のIntentが必要となっています。

### webhook返却後の処理を行うIntentの作成

新しいIntentを作成します。名前は元のIntentと全然関係ないもので構いませんが、元の名称の末尾に`.Response`などとつけた名前にしておくと、Dialogflow画面上での管理がしやすくなると思います。

`User says`は空欄のままとします。`Events`という文字をクリックすると入力欄が開きます。そこに、後でイベント名として使う文字列を設定します。`Action`はボットのクライアントから使わない限りは空欄で結構です。

![contexts.png](https://qiita-image-store.s3.amazonaws.com/0/10290/6fa6b5c6-d925-8641-2ecc-02a51540f9d7.png)

ここでは、`unsei`という名前の変数を作って、そこに「大吉」などとwebhookで入れてもらうようにするための設定を行います。

![val.png](https://qiita-image-store.s3.amazonaws.com/0/10290/ecf4393a-5e39-5123-e7bd-f2f118f83c9f.png)

`PARAMETER NAME`には`unsei`、`ENTITY`には`@sys.any`、`VALUE`には`#イベント名.unsei`と入力します。`REQUIRED`や`IS LIST`はチェックを入れないままで結構です。

そして、`Response`の部分に、応答文を入れます。これは「やりたいこと」でお見せしたのと**ほぼ**同じです。「`$` + `PARAMETER NAME`で入力した文字列」の部分が、webhookの結果で置換されることになります。

![スクリーンショット 2017-11-25 0.31.41.png](https://qiita-image-store.s3.amazonaws.com/0/10290/024d94a8-1d1a-573f-1e4d-55c2db3059ca.png)

**`Fulfillment`の`Use webhook`にチェックを入れる必要はありません**。この点は、元のIntentと異なりますので、注意してください。

### Webhookスクリプト／プログラムの変更

レスポンス本文が、以下のような感じになるように、webhookを修正して保存します。

```json
{  
   "followupEvent":{  
      "name":"LOCAL_WEBHOOK_RECEIVED",
      "data":{  
         "unsei":"大吉"
      }
   }
}
```

Azure Functionで実装した例を[Gistに置いておきます](https://gist.github.com/Makopo/fb47c840f0eaf2289a037c5e993f8423)。

## 試してみる

それでは、右側のチャット欄でテストしてみましょう。

![1.png](https://qiita-image-store.s3.amazonaws.com/0/10290/dfc45a9d-f0e3-27ed-fd81-cfafc4806afd.png)

![2.png](https://qiita-image-store.s3.amazonaws.com/0/10290/cfda0ebc-837f-de4e-a3bd-412a11253da6.png)

![3.png](https://qiita-image-store.s3.amazonaws.com/0/10290/7f4e3b3d-6bf7-e8a8-d45f-184f1615eb78.png)

適当にランダムに返答が来るようになりました。



## 補足：`Use webhook for slot-filling`とは？

`Use webhook`の隣に`Use webhook for slot-filling`というオプションがあります。いかにもこの目的のためのオプションのように見えますし、このオプションを使ったやり方でもできることはあります。しかし、コンテキストが残存してしまって変なことになる可能性が高く、[公式フォーラムの議論](https://discuss.api.ai/t/use-webhook-for-slot-filling-question/3598)の中で多くの人が`followUpEvent`を使うことを勧めています。このため、本記事ではこのオプションを使わずに`followUpEvent`を使う方法を紹介しました。