DialogFlowのEntitiesを組み合わせて、スマートスピーカーへの発話回数を減らす工夫をしてみた


この記事は[スマートスピーカー Advent Calendar 2018](https://qiita.com/advent-calendar/2018/smart-speaker)の20日目の記事となります。


# はじめに

昨年個人的に「育児日記」というGoogle Assistant向けのアプリを開発しました。
その後大きなアップデートは出来ていない状況ですが、
この1年個人的に家庭で使っていく中で得られた知見を今日は共有できればと思います。

「育児日記」アプリの詳細については下記をご覧ください。

- [Google Home向け子供の育児日記アプリを作ってみた〜申請とリジェクトから得られた知見〜
](https://qiita.com/megadreams14/items/98fbf6c5990ef5a42ffa)

# DialogFlowのEntitiesとは

Google Assistantのアプリを開発する時に自然言語の解析などはDialogFlowを利用しているのですが、その中にEntitiesという機能があります。

Entitiesとは、「ユーザからの入力として受け付ける単語」を定義し、
表記ブレであったり同じ意味だけど複数の言い回しがある単語をまとめたりとかが可能となります。


例えば「育児日記」のアプリで利用しているのは、
朝子供の保育園に送っていく前に計測している体温を記録したりする際に
「(子供の名前)の体温は、36.8度です」などと言って記録しているのですが、
たまに「今日のお熱は36.8度です」などと言ってしまうこともあるため
下記のように「体温」や「お熱」「熱」の表現は、「体温」の意味で捉える
というような設定をしております。

<img width="400" src="https://qiita-image-store.s3.amazonaws.com/0/17346/87481949-86b1-b847-a69b-3f5aad60d55d.png">

## Entitiesを複合

育児日記をつける際に基本的には「今したことをその場で記録する」というスタンスですが記録を忘れたときには後から一気に記録したいということもあります。

■ その場で記録する時

例） 起床時間を記録したい(07:00の時)
　・「(子供の名前)が起きた時間を記録して」と発話


■ あとからまとめて記録する時

例) 起床時間(07:00)と朝食の時間(07:30)を夕方(16:00)に記録する場合
　・「(子供の名前)が07:00に起きたと記録して」と発話
　・「(子供の名前)が07:30に朝ごはんを食べたと記録して」と発話

このようにあとからまとめて記録する際には2回発話しないといけないため非常に非効率です。
そこで、Entitiesを組み合わせることで1回の発話で入力が可能なようにしてみます。



目指す記録の形としては、子供の名前を「太郎」としたときに

**「太郎が07:00に起床 07:30に朝ごはん 13:00に寝た 15:00に起きた と記録して」**
と発話すると、朝起きた時間、朝食の時間、お昼寝の開始時間、お昼寝から起きた時間などを1回の発話で全ての項目を登録できる形を目指したいと思います。


## フレーズの定義

複数の記録を入力できるようにするにはまずは発話フレーズのパターン化を行う必要があります。

下記の図をご覧ください。

<img src="https://qiita-image-store.s3.amazonaws.com/0/17346/0839a8a5-d7ca-215d-8682-0357682aaf8d.png">

このように例文をパターンに分けるとまずは大きく3種類の要素に分けることが出来ます。

| 発話 | 太郎が| 07:00に起床 ~ 起きた | と記録して|
|:-:|:-:|:-:|:-:|
|要素|子供の名前|記録項目が複数件集まっている| 固定文字列 |
| 説明| 記録するデータが誰のものなのかを特定するためのフレーズ| 「いつ 何を」の記録項目が複数個集まってフレーズを構成 | 「と記録して」がIntentを識別するための固定文字列 |
| 備考 | | ※ Entitiesで定義 | |


そして、上記のように考えると、Intentでの定義とEntitiesでの定義を下記のように分類することが出来ます。


<img src="https://qiita-image-store.s3.amazonaws.com/0/17346/628bdf26-2dd6-4500-96f2-263b532f4406.png">

上記のように考えると、必要なEntitiesは大きく4つになります。

|| Entitiesの種類 | 概要 |
|:-:|:-:|:--|
|1| 時間Entities | 時間を管理するEntities。<br>基本的にデフォルトのものを利用するで良さそう|
|2| 記録項目Entities | 起床・朝食・昼食・夕食・お昼寝・おやつ・おむつ交換・お風呂・就寝など<br>記録項目にあたる項目をEntitiesとして定義する |
|3| 記録内容Entities | 1と2を組み合わせたEntitiesを定義|
|4| 複数個の集合 | 3 を複数個空白で区切ったEntitiesを定義<br>ここで定義された数のパターンだけ同時入力が可能|

参考までに2~4の項目をDialogFlowの画面で紹介します。

### 2: 記録項目Entities

記録項目の種類を表記の統一も含めて下記のように登録します。

<img width=700 src="https://qiita-image-store.s3.amazonaws.com/0/17346/9aa29788-ad01-87e9-577c-84aae1dfb633.png">

### 3: 記録内容Entities

このEntitiesは、「07:00に起床」というようなフレーズを構成するため下記のように設定を行う。

<img width=700 src="https://qiita-image-store.s3.amazonaws.com/0/17346/24010860-96fa-ea9c-d48c-f09a6550406d.png">

ここでは、他で定義しているEntitiesを利用するため、Define synonymsのチェックを外しておく。

- 「[@sys.time:recordTime]()」
   - @sys.timeはデフォルトで定義されている時間を認識するものを利用
   - :recordTime は、WebHookで呼ばれる際のキー情報となる
- 「[@RecordType:recordType]()」
   - 先程定義した「2: 記録項目Entities」を利用する
   - :recordType は、WebHookで呼ばれる際のキー情報となる

また、ハマりポイントとしては **Entitiesと次の文字列との間は空白2文字開けないとダメ** な点。
英語の場合は1文字あけるだけでよいが、 **日本語の場合は2文字開けないと正常に認識してくれなかった**

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">developer compositeを利用すればEntitiesのグルーピング出来たが、日本語を使う場合は定義の際に空白を2つ入れないと反応してくれないっぽい。<br>英語だと半角1文字で良いらしい。<br>なんだこの仕様…</p>&mdash; めがりょう@介護ITエンジニア組織を目指して (@megadreams14) <a href="https://twitter.com/megadreams14/status/1063435595232436224?ref_src=twsrc%5Etfw">2018年11月16日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

### 4: 複数個の集合

ここまでに「07:00に起床」という単一の記録項目のEntitiesは完成したのでこれらを複数入力できるようなEntitiesを作成します。

<img width=700 src="https://qiita-image-store.s3.amazonaws.com/0/17346/27493280-5ee1-32f2-084e-9f650bee3539.png">

ここでは、先程作成した「3: 記録内容Entities」である「[@RecordSingle]()」を複数個認識可能なEntitiesを定義している。
この例だと、1個だけの場合、2個の場合、3個の場合、4個の場合、5個の場合と最大5つの記録項目を同時に入力できるようなEntitiesの定義を行っている。

なお、ここでも **空白2文字をあけないと認識されない** ので注意

## Intentの定義

さてここまで来てようやくIntentの定義を行う。
最初に考えたフレーズの定義をもう一度見直すと

**「誰が」+「複数個の記録項目」+「と記録して」**

というフレーズで成り立つため、このフレーズに合わせてIntentを定義します。

<img width=700 src="https://qiita-image-store.s3.amazonaws.com/0/17346/e31d37ad-b70e-59f3-5324-b84a44321e48.png">

このように先程定義した複数個の記録項目の部分は例文を入力するだけで自動的に「[@RecordMulti]()」のENTITYに紐づけが行われます。

これで設定は完了です。


## 複数件入力を実施

当初の目標だった
**「太郎が07:00に起床 07:30に朝ごはん 13:00に寝た 15:00に起きた と記録して」**
という発話を行ってみます。

<img width=300 src="https://qiita-image-store.s3.amazonaws.com/0/17346/5cb1e2f4-2385-46eb-3bf3-adddf6ba1080.png">


そして、WebHookで受信できるJSONを見てみると下記のような形です。

```
{
  "name": "太郎",
  "RecordMulti": {
    "recordSingle": [
      {
        "recordType": "起床",
        "recordTime": "2018-12-02T07:00:00+09:00"
      },
      {
        "recordTime": "2018-12-02T07:30:00+09:00",
        "recordType": "朝ごはん"
      },
      {
        "recordTime": "2018-12-02T13:00:00+09:00",
        "recordType": "就寝"
      },
      {
        "recordTime": "2018-12-02T15:00:00+09:00",
        "recordType": "起床"
      }
    ]
  }
}
```

はい、複数件の記録データを取得できましたね。
[@sys.time]()が未来の日付でリクエストが飛んでくるなどまだ対処しなければいけないことはありますが、
Entitiesを組み合わせて利用することで1回の発話で複数の記録を取れるようになり、
発話回数を減らして効率よく記録が行えるようになりました。


# まとめ

こちらの機能を搭載した「育児日記」アプリのアップデートはまだ出来ていないので
現状は個人的に利用している機能に過ぎないのですが、Entitiesを組み合わせることによって、
もっともっと使いやすい会話フローが実現できるようになるとかなり期待しています。

ただし、Entitiesを複雑に組み合わせるとなぜうまく動いているのかがわからなくなったり、
意図しない形で認識されてしまったりバグのもとにもなりうると感じているので、
しっかり設計を行い使っていくことが必要なのかなと感じています。


このあたり他の方がどうされているのかなというのをすごく気になりつつ
知見共有を出来ればと思い記載してみました。


# 参考

- [Developer composite - Dialogflow](https://dialogflow.com/docs/entities/developer-entities#developer_composite)