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


 この記事で躓いた、Dialogflowで会話中に値を保持する方法の記録です。

contextとは

 contextは日本語で文脈という意味で、Dialogflowでは特定のcontext中にしか発動しないIntentを設定することで、より自然な会話を実現することができます。Alexaスキルにおけるステートのような感じです。詳しくは公式ドキュメントや"API.AIのコンテキストを使ってChatOps環境を作る"を参照してください。

会話中に値を保持する例

アプリ「あなたの好きな食べ物はなんですか?」
ユーザー「僕の好きな食べ物は春巻きだよ」
アプリ「あなたの好きな色はなんですか?」
ユーザー「僕の好きな色は緑だよ」
アプリ「あなたの好きな食べ物は春巻きで、好きな色は緑ですね!」

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

本題

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

アプリの構成

仕組みを説明する前にデモアプリの構成を示します。この記事の方法に従ってDialogflowとAWS Lambdaで作成しました。

仕様

ユーザーは「おはよう」「こんにちは」「こんばんは」「いってきます」「ただいま」の中から一つをGoogle Assistantに話しかけることができて、適切な返事とそのあいさつが何回目かを教えてくれる。

会話例:
ユーザー「いってきます」
アプリ「いってらっしゃい!1回目ですね」
ユーザー「ただいま」
アプリ「おかえりなさい!1回目ですね」
ユーザー「いってきます」
アプリ「いってらっしゃい!2回目ですね」
ユーザー「ただいま」
アプリ「おかえりなさい!2回目ですね」
ユーザー「さようなら」
アプリ「さようなら、また会いましょう!」

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

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

Entity

あいさつを区別するgreeting entityは以下の設定です。

Intent

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

Default Welcome Intent



HelloIntent



HelloIntent.SetValue



EndIntent

Lambda

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

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の初期化
    # 2回目以降は文字列化された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
        }

 仕組みの説明

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

{
    "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に入っている値」だったので、先程HelloIntentgreetingに格納されていた「ユーザーのあいさつの種類」が再び格納されます。greeting_dataのvalueには#GREETING_EVENT.greeting_dataというこれまた見慣れない値が格納されています。これは「HelloIntent.SetValueを発動させたGREETING_EVENT"data"にある"greeting_data"の値」という意味で、この場合は上記のHelloIntentに対するレスポンスに含まれていたJSONを文字列化した値が格納されます。
 「おはよう」「こんにちは」等に対応するparameterをそれぞれ用意してもいいのですが、わざわざJSONを文字列化しているのは次の2つの理由によるものです。

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

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

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

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

 全体的な流れは下図の感じです。

まとめ

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

補足

 今回は株式会社SmartHacksでの就業形インターンとして、この記事を作成しました。