Alexaで最短のインタラクションで自由発話を受け取る
過去にもこういうニーズはあって、
ダイアログモデルを使うってのがまあ今のところのベターなやり方なのだと思う。
AMAZON.BookとかAMAZON.Cityとかのビルトインスロットで拾うっていうやり方もあるにはあるけど、精度的にどうなの?ってのと、それはスロットの使い方としていいの?ってところがややもにょる。
上記の2番目を見ると、
- スキルを起動。LaunchRequestで発話が行われる。
- 「オウム返しして」と発話するとRepeatIntentに入る
- RepeatIntentはmessageスロットが必須になっているので、ダイアログモデルでこれを収集する。
という流れになっていて、一旦はインテントに入るための発話が必要になる。イメージ的にはこういう感じ
U: アレクサ、オウム返しを開いて
A: オウム返しスキルです。なんでもオウム返しします。オウム返ししてと言ってみてください。?
U: オウム返しして (※ここでダイアログモデルのインテントに入れるようにする)
A: ではなにか言ってみてください。
U: 〇〇〇
A: 〇〇〇
ただこの場合は少しインタラクションの手間が発生してしまう。できればこうやりたい。
U: アレクサ、オウム返しを開いて
A: オウム返しです。なんでもオウム返しします。なにか言ってみてください。
U: 〇〇〇
A: 〇〇〇
これをやろうと思うと、インテントでAMAZON.SearchQueryを含めたサンプル発話を用意しないといけないが、御存知の通りSearchQueryはスロット単体では使えずキャリアフレーズが必要になってしまう。
ということで強制的にLaunchRequestでからダイアログモデルに入るようにしてみる。
対話モデルの設定は上記の1つ目の記事に従って作成すればOK。JSONだとこんな感じになる。
{
"interactionModel": {
"languageModel": {
"invocationName": "オウム返し",
"intents": [
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
},
{
"name": "AMAZON.StopIntent",
"samples": []
},
{
"name": "AMAZON.NavigateHomeIntent",
"samples": []
},
{
"name": "CaptureAllIntent",
"slots": [
{
"name": "user_utterance",
"type": "AMAZON.SearchQuery",
"samples": [
"{user_utterance}"
]
}
],
"samples": []
},
{
"name": "AMAZON.FallbackIntent",
"samples": []
},
{
"name": "dummyIntent",
"slots": [],
"samples": [
"ダミー"
]
}
],
"types": []
},
"dialog": {
"intents": [
{
"name": "CaptureAllIntent",
"confirmationRequired": false,
"prompts": {},
"slots": [
{
"name": "user_utterance",
"type": "AMAZON.SearchQuery",
"confirmationRequired": false,
"elicitationRequired": true,
"prompts": {
"elicitation": "Elicit.Slot.XXXXXXXXXX.XXXXXXXXXX"
}
}
]
}
],
"delegationStrategy": "ALWAYS"
},
"prompts": [
{
"id": "Elicit.Slot.XXXXXXXXXX.XXXXXXXXXX",
"variations": [
{
"type": "PlainText",
"value": "なにか言ってみてください。"
}
]
}
]
}
}
CaptureAllIntentってのがそれ。CaptureAllIntentにはサンプル発話を設定していないがwarningにはなるけどerrorにはならないので問題ないのではないかと思っている。
でバックエンド側でダイアログモデルのステータスをいじってやる。今回はAlexa-hostedでPythonのHelloWorldをベースにしている。
この辺を追加
from ask_sdk_model.dialog import ElicitSlotDirective
from ask_sdk_model import (Intent , IntentConfirmationStatus, Slot, SlotConfirmationStatus)
LaunchRequest
class LaunchRequestHandler(AbstractRequestHandler):
"""Handler for Skill Launch."""
def can_handle(self, handler_input):
# type: (HandlerInput) -> bool
return ask_utils.is_request_type("LaunchRequest")(handler_input)
def handle(self, handler_input):
# type: (HandlerInput) -> Response
speak_output = "オウム返しです。なんでもオウム返しします。なにか言ってみてください。"
directive = ElicitSlotDirective(
slot_to_elicit="user_utterance",
updated_intent = Intent(
name = "CaptureAllIntent",
confirmation_status = IntentConfirmationStatus.NONE,
slots ={
"user_utterance": Slot(name= "user_utterance", value = "", confirmation_status = SlotConfirmationStatus.NONE)
}
)
)
return (
handler_input.response_builder
.speak(speak_output)
.ask("なにか言ってみてください。")
.add_directive(directive)
.response
)
Dialogインターフェースを使うとダイアログモデルの状態をバックエンド側で制御できるのだけど、ダイアログモデルのやり取り中にインテントやスロットを書き換えたりすることができる。
これを使ってLaunchRequestの中から別のインテント、ここではCaptureAllIntentHandlerのダイアログモデルをトリガーする。
で実際にダイアログモデルで発話を受け取るCaptureAllIntentHandlerでスロット値を収集して発話させ、さらにLaunchRequestと全く同じディレクティブを返して、ダイアログモデルの状態をずっと継続させるようにする。
class CaptureAllIntentHandler(AbstractRequestHandler):
"""Handler for Caprture ALL Intent."""
def can_handle(self, handler_input):
# type: (HandlerInput) -> bool
return ask_utils.is_intent_name("CaptureAllIntent")(handler_input)
def handle(self, handler_input):
# type: (HandlerInput) -> Response
user_input = handler_input.request_envelope.request.intent.slots["user_utterance"].value
speak_output = user_input
directive = ElicitSlotDirective(
slot_to_elicit="user_utterance",
updated_intent = Intent(
name = "CaptureAllIntent",
confirmation_status = IntentConfirmationStatus.NONE,
slots ={
"user_utterance": Slot(name= "user_utterance", value = "", confirmation_status = SlotConfirmationStatus.NONE)
}
)
)
return (
handler_input.response_builder
.speak(speak_output)
.ask("なにか言ってみてください。")
.add_directive(directive)
.response
)
これを使うと、例えば、AlexaからGPTにまるっと発話内容を渡してレスポンスを返す、というようなことができる。
こんな感じ
import openai
(snip)
def generate_response(user_input, api_key):
prompt = (f"You are AI chatbot. you are very kind, polite, and creative. You will always answer shortly and easily to understand.\n\nThe following is the user question.\nUser: {user_input}\nBot:")
response = openai.Completion.create(
engine="text-davinci-003",
prompt=prompt,
temperature=0.9,
max_tokens=500,
n = 1,
stop=['\n'],
api_key=api_key
)
message = response.choices[0].text.strip()
return message
(snip)
class CaptureAllIntentHandler(AbstractRequestHandler):
"""Handler for Caprture ALL Intent."""
def can_handle(self, handler_input):
# type: (HandlerInput) -> bool
return ask_utils.is_intent_name("CaptureAllIntent")(handler_input)
def handle(self, handler_input):
# type: (HandlerInput) -> Response
user_input = handler_input.request_envelope.request.intent.slots["user_utterance"].value
speak_output = generate_response(user_input, OPENAI_API_KEY)
directive = ElicitSlotDirective(
slot_to_elicit="user_utterance",
updated_intent = Intent(
name = "CaptureAllIntent",
confirmation_status = IntentConfirmationStatus.NONE,
slots ={
"user_utterance": Slot(name= "user_utterance", value = "", confirmation_status = SlotConfirmationStatus.NONE)
}
)
)
return (
handler_input.response_builder
.speak(speak_output)
.ask("なにか言ってみてください。")
.add_directive(directive)
.response
)
実際に動かしてみた。
回答の内容はともかく、自由な入力を受け取れている。
会話履歴を記録しておいてプロンプトに含めるようにすれば、コンテキストを維持して会話を繋げることができるはず。
LangChainのMemoryとかでも良さそうだけど、レスポンス時間には注意する必要がある。