🔥

Alexaスキル用のチャットボットを8秒以内に応答させたい

2023/08/12に公開

はじめに

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スキルを動かす手順は、こちらの本で詳しく説明しています。

https://www.amazon.co.jp/dp/B0CCTMGRR2

ブラウザで完結するようになっているので、環境構築が不要でお手軽です。ぜひお試しください。面白かったら、感想を書いていただけるととても嬉しいです。

先日書いたFunction callingの記事も、もしよければお読みください。

Discussion