Dialogflowで会話中に値を保持する方法

　[この記事](https://qiita.com/running1017/items/86b2c8f6c490c9c4e5bf)で躓いた、Dialogflowで会話中に値を保持する方法の記録です。

# contextとは
　contextは日本語で文脈という意味で、Dialogflowでは特定のcontext中にしか発動しないIntentを設定することで、より自然な会話を実現することができます。Alexaスキルにおけるステートのような感じです。詳しくは[公式ドキュメント](https://dialogflow.com/docs/contexts)や"[API.AIのコンテキストを使ってChatOps環境を作る](http://tokibito.hatenablog.com/entry/2017/06/12/190024)"を参照してください。

# 会話中に値を保持する例
>アプリ「あなたの好きな食べ物はなんですか？」
ユーザー「僕の好きな食べ物は春巻きだよ」
アプリ「あなたの好きな色はなんですか？」
ユーザー「僕の好きな色は緑だよ」
アプリ「あなたの好きな食べ物は春巻きで、好きな色は緑ですね！」

（`{好きな食べ物:春巻き, 好きな色:緑}`という値を会話中に保持しなければ実現できない）

# 本題
　Dialogflowで実現しましたが、かなりややこしいです。Alexaのようにもっとシンプルに実現する方法はないんですかね。

## アプリの構成
仕組みを説明する前にデモアプリの構成を示します。[この記事](https://qiita.com/running1017/items/86b2c8f6c490c9c4e5bf)の方法に従ってDialogflowとAWS Lambdaで作成しました。

### 仕様
ユーザーは「おはよう」「こんにちは」「こんばんは」「いってきます」「ただいま」の中から一つをGoogle Assistantに話しかけることができて、適切な返事とそのあいさつが何回目かを教えてくれる。
>会話例:
ユーザー「いってきます」
アプリ「いってらっしゃい！１回目ですね」
ユーザー「ただいま」
アプリ「おかえりなさい！１回目ですね」
ユーザー「いってきます」
アプリ「いってらっしゃい！２回目ですね」
ユーザー「ただいま」
アプリ「おかえりなさい！２回目ですね」
ユーザー「さようなら」
アプリ「さようなら、また会いましょう！」

会話中に下のようなJSONデータを保持して、それに応じてアプリの返答を変化させたい

```json
{
 "おはよう": 0,
 "こんにちは": 0,
 "こんばんは": 0,
 "いってきます": 2,
 "ただいま": 2
}
```

### Entity
あいさつを区別するgreeting entityは以下の設定です。
![スクリーンショット 2018-02-26 14.05.30.png](https://qiita-image-store.s3.amazonaws.com/0/230715/cb024732-1a5b-6b4c-bd8d-31836968b64e.png)

### Intent
ユーザーの発話を認識するIntentは以下の通りです。

Default Welcome Intent
> <img src=https://qiita-image-store.s3.amazonaws.com/0/230715/880601ca-9cca-a9da-70fa-90ddc131f0c5.png width=50%>

<br>
HelloIntent
><img src=https://qiita-image-store.s3.amazonaws.com/0/230715/a76ecd7c-e7ec-a652-fbc5-aa6e86577a13.png width=50%>

<br>
HelloIntent.SetValue
><img src=https://qiita-image-store.s3.amazonaws.com/0/230715/22f70760-9709-1b35-c812-342a2ebf8fcb.png width=50%>

<br>
EndIntent
><img src=https://qiita-image-store.s3.amazonaws.com/0/230715/ebc93bf8-d431-26ff-9567-cdfe86f74174.png width=50%>

### Lambda
Fulfillment先のAWS Lambda関数のコードです。

```python
import json


# メインハンドラ
def lambda_handler(event, context):
    # eventからデータ取り出し
    greeting_data = event['result']['parameters']['greeting_data']
    greeting = event['result']['parameters']['greeting']
    intent = event['result']['metadata']['intentName']
    
    # あいさつデータ
    greetings = [
        "おはよう",
        "こんにちは",
        "こんばんは",
        "いってきます",
        "ただいま"
    ]
    reply = dict(zip(greetings, [
        "おはようございます！",
        "こんにちは！",
        "こんばんは！",
        "いってらっしゃい！",
        "おかえりなさい！"
    ]))
    
    # 初回の呼び出し時はgreeting_data == ""になっているのでgreeting_counterの初期化
    # ２回目以降は文字列化されたgreeting_dataをJSONとして読み込み
    if greeting_data == "":
        greeting_counter = dict(zip(greetings, [0]*len(greetings)))
    else:
        greeting_counter = json.loads(greeting_data)
    
    # HelloIntentのときはgreeting_counterのgreetingに対応する値を+1して
    # 文字列化しつつfollowupEventで返す
    if intent == 'HelloIntent':
        greeting_counter[greeting] += 1
        return {
            'followupEvent': {
                'name': 'GREETING_EVENT',
                'data': {
                    'greeting_data': json.dumps(greeting_counter)
                }
            }
        }
    # HelloIntent.SetValueのときはgreeting_counterから何回目か読み取ってアプリの返答文を返す
    elif intent == 'HelloIntent.SetValue':
        speech_text = reply[greeting] + "{}回目ですね。".format(greeting_counter[greeting])
        return {
            'speech': speech_text,
            'displayText': speech_text
        }
```

##　仕組みの説明
　本記事のキモは`HelloIntent`と`HelloIntent.SetValue`という２つのIntentです。
　「こんにちは」等あいさつを呼びかけると、まず`HelloIntent`が発動して`greeting_context`というcontextに入り、返答としてFulfillmentに処理をリクエストします。`HelloIntent`には`greeting`と`greeting_data`という２つのparameterがあって、`greeting`のvalueは`$greeting`となっているのでユーザーが発したあいさつの種類が格納されます。`greeting_data`のvalueには`#greeting_context.greeting_data`という見慣れない値が格納されています。これは「`greeting_context`というcontext中で存在している`greeting_data`という名前のparameterに入っている値」という意味で、初めて`HelloIntent`を呼び出したときはまだそのような値は存在していないので、初回に限りこのvalueは`""`(空文字列)になります。`HelloIntent`に対するレスポンスはLambdaのコードより

```json
{
    "followupEvent": {
        "name": "GREETING_EVENT",
        "data": {
            "greeting_data": "{\"おはよう\":0,\"こんにちは\":0,\"こんばんは\":0,\"いってきます\":0,\"ただいま\":0}"
        }
    }
}
```

という形になっています。レスポンスに`"followupEvent"`があると、Dialogflowでは`"speech"`の値がもしあっても無視され、ここではIntentの発生イベントにユーザーの発話ではなく`GREETING_EVENT`が設定されている`HelloIntent.SetValue`が発動します。
　`HelloIntent.SetValue`にあるparameterは、`HelloIntent`のparameterと名前は同じですがvalueが異なります。`greeting`のvalueは`#greeting_context.greeting`で、これは「`greeting_context`というcontext中で存在している`greeting`という名前のparameterに入っている値」だったので、先程`HelloIntent`で`greeting`に格納されていた「ユーザーのあいさつの種類」が再び格納されます。`greeting_data`のvalueには`#GREETING_EVENT.greeting_data`というこれまた見慣れない値が格納されています。これは「`HelloIntent.SetValue`を発動させた`GREETING_EVENT`の`"data"`にある`"greeting_data"`の値」という意味で、この場合は上記の`HelloIntent`に対するレスポンスに含まれていたJSONを文字列化した値が格納されます。
　「おはよう」「こんにちは」等に対応するparameterをそれぞれ用意してもいいのですが、わざわざJSONを文字列化しているのは次の2つの理由によるものです。

- parameterの数が`greeting_data`の１個で済む
- 整数値を格納したつもりでも桁を詰めた実数値に変換された上で文字列になってしまう(`123456789`という整数を格納したつもりが`"1.23E8"`という文字列になる)ので、はじめから(`"123456789"`という)文字列にしておきたい

　`HelloIntent.SetValue`に対するレスポンスは以下のような形になっています。

```json
{
    "speech": "こんにちは!1回目ですね。",
    "displayText": "こんにちは!1回目ですね。"
}
```

このレスポンスを受け取ったDialogflowは、ここでやっと`"speech"`の値を返答文としてユーザーに返事してくれました。

　全体的な流れは下図の感じです。
![flow image.001.jpeg](https://qiita-image-store.s3.amazonaws.com/0/230715/fbfd19e2-19ee-b915-9d99-eb9e46bb0fd0.jpeg)

# まとめ
　Intentを２つ用意することで会話中に値を保持させることができました。AlexaだとやりとりするJSONのattributeというキーに任意のデータを放り込んで同じことができて、インテントを２つ用意したり文字列化を気にしたりしなくてもいいので、その点は楽でした。

# 補足
　今回は[株式会社SmartHacks](https://smarthacks.jp/)での就業形インターンとして、この記事を作成しました。