この記事で躓いた、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
}
## 仕組みの説明
本記事のキモはHelloIntent
とHelloIntent.SetValue
という2つのIntentです。
「こんにちは」等あいさつを呼びかけると、まずHelloIntent
が発動してgreeting_context
というcontextに入り、返答としてFulfillmentに処理をリクエストします。HelloIntent
にはgreeting
とgreeting_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に入っている値」だったので、先程HelloIntent
でgreeting
に格納されていた「ユーザーのあいさつの種類」が再び格納されます。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での就業形インターンとして、この記事を作成しました。