Alexaスキル用のチャットボットを8秒以内に応答させたい
はじめに
ChatGPTと会話できるAlexaスキルを作って遊んでいるのですが、ときどき「スキルがリクエストに正しく応答できませんでした」と言われて止まってしまうことがあります。
これは、Alexaスキルの仕様で、8秒以内に応答しないとエラーになってしまうためです。応答に時間がかかるのは、OpenAIのAPIが原因です。どうにかしたいと思っていたのですが、ひとつ解決方法を思いついたので紹介します。
解決方法の概要
OpenAIのCreate chat completion API(いわゆるChatGPTのAPI)にはstream
というオプションがあります。これをtrue
にすると、レスポンスがストリーミングで送られてきます。完成したメッセージがまとめて送られてくるのではなく、部分的なメッセージの差分が細切れになって送られて送られてくるわけです。エラーになるよりは、途中でもいいからメッセージを組み立てて、8秒以内に応答を返してしまおうというのが、今回のアイデアの骨子です。
実装
ベースとなるソースコードはこちらです。この中のChatBotIntentHandler
を次のように書き換えます。
class ChatBotIntentHandler(AbstractRequestHandler):
"""Handler for ChatBot Intent."""
def can_handle(self, handler_input):
# type: (HandlerInput) -> bool
return ask_utils.is_intent_name("ChatBotIntent")(handler_input)
def handle(self, handler_input):
# type: (HandlerInput) -> Response
# 開始時刻
start_time = time.time()
# OpenAIのAPIキーを設定
openai.api_key = 'your-api-key'
# プロンプトの準備
template = """あなたは音声対話型チャットボットです。以下の制約にしたがって回答してください。
制約:
- ユーザーのメッセージに句読点を補ってから回答します
- 140文字以内を目安に簡潔な短い文章で話します
- 質問の答えがわからない場合は「わかりません」と答えます"""
# メッセージの初期化
messages = [
{
"role": "system",
"content": template
}
]
# セッションからメッセージを取り出す
if "MESSAGES" in handler_input.attributes_manager.session_attributes:
messages = handler_input.attributes_manager.session_attributes["MESSAGES"]
user_input = ask_utils.get_slot_value(handler_input=handler_input, slot_name="user_message")
# ユーザーのメッセージを追加
messages.append({
"role": "user",
"content": user_input if isinstance(user_input, str) else "こんにちは"
})
try:
# Streamingを有効にしてOpenAIのAPIを呼び出す
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=messages,
stream=True
)
message = ""
for chunk in response:
elapsed_time = time.time() - start_time
finish_reason = chunk['choices'][0]['finish_reason']
if finish_reason != 'stop':
message += chunk['choices'][0]['delta']['content']
if elapsed_time > 7.9:
message += "。すみません、タイムアウトしました。"
break
speak_output = message
except Exception as e:
logger.error(f"OpenAI API request failed: {e}")
speak_output = "すみません、エラーが発生しました。しばらく時間をおいてからもう一度お試しください。"
# ChatGPTの回答をメッセージに追加
messages.append({
"role": "assistant",
"content": speak_output
})
# セッションにメッセージを保存
handler_input.attributes_manager.session_attributes["MESSAGES"] = messages
directive = ElicitSlotDirective(
slot_to_elicit="user_message",
updated_intent = Intent(
name = "ChatBotIntent",
confirmation_status = IntentConfirmationStatus.NONE,
slots ={
"user_message": Slot(name= "user_message", value = "", confirmation_status = SlotConfirmationStatus.NONE)
}
)
)
return (
handler_input.response_builder
.speak(speak_output)
.ask("なにか話しかけてください。")
.add_directive(directive)
.response
)
重要なのは次の箇所です。
# Streamingを有効にしてOpenAIのAPIを呼び出す
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=messages,
stream=True
)
message = ""
for chunk in response:
elapsed_time = time.time() - start_time
finish_reason = chunk['choices'][0]['finish_reason']
if finish_reason != 'stop':
message += chunk['choices'][0]['delta']['content']
if elapsed_time > 7.9:
message += "。すみません、タイムアウトしました。"
break
speak_output = message
ポイントを列挙します。
-
elapsed_time
には、Alexaスキルが呼び出されてから経過した時間が格納されます - このfor文は、ストリーミングが終わるか、経過時間が7.9秒を越えるまで繰り返されます
- ストリーミングが途中の場合、
finish_reason
にはnullが格納されます - ストリーミングが最後まで送信されると、
finish_reason
には文字列stop
が格納されます -
message
には、分割されたメッセージを結合して格納します - 経過時間が7.9秒を超えたら、ストリーミングを停止して
message
にタイムアウトのメッセージを追加します
実際に動かした様子はこちらです。
途中で文章がとぎれてタイムアウトしていますが、「スキルがリクエストに正しく応答できませんでした」といわれることはなくなりました。タイムアウトしても「続きをお願いします」といえば、続きを再開してくれます。エラーになると最初からやり直しなので、会話を継続できるのは地味ですが重要な改善といえます。実際、これで利用時のストレスはかなり減りました。
宣伝
Alexaスキルを動かす手順は、こちらの本で詳しく説明しています。
ブラウザで完結するようになっているので、環境構築が不要でお手軽です。ぜひお試しください。面白かったら、感想を書いていただけるととても嬉しいです。
先日書いたFunction callingの記事も、もしよければお読みください。
Discussion