🦔

ChatGPT API(gpt-3.5-turbo)に問い合わせるAlexa hosted スキルの開発中コード

2023/03/12に公開2

概要

いわゆるオウム返しスキルレベルの、単純にChatGPT API(gpt-3.5-turbo)にリクエストを投げるスキルですが、何日考えてもエラーが解消されず申請手続き出来ないので途中経過を公開します。

参考にした動画

Connect ChatGPT-3 with an Alexa skill
https://www.youtube.com/watch?v=jxmQZwkB3Hs&lc=UgyKYLP1-uQh_OuBSzJ4AaABAg.9n3rbVs6SfG9n4B1JAlE8_

一時停止してメモ帳に書き出しました。ソースコードはメール問い合わせしないと教えてもらえないようなので、あきらめました。

Alexa hostedスキル

テンプレートのHello worldスキルを改造していきました。

インテント部分の設定

JSONエディター

{
    "interactionModel": {
        "languageModel": {
            "invocationName": "サイバーイルカ",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                },
                {
                    "name": "AskChatgptIntent",
                    "slots": [
                        {
                            "name": "question",
                            "type": "AMAZON.SearchQuery",
                            "samples": [
                                "{question}"
                            ]
                        }
                    ],
                    "samples": [
                        "{question} を教えて",
                        "{question} は何ですか",
                        "{question} とは何ですか",
                        "{question} について教えて",
                        "{question} を調べて"
                    ]
                },
                {
                    "name": "HelloWorldIntent",
                    "slots": [],
                    "samples": [
                        "おはようございます",
                        "こんにちは",
                        "はーい"
                    ]
                }
            ],
            "types": []
        },
        "dialog": {
            "intents": [
                {
                    "name": "AskChatgptIntent",
                    "confirmationRequired": false,
                    "prompts": {},
                    "slots": [
                        {
                            "name": "question",
                            "type": "AMAZON.SearchQuery",
                            "confirmationRequired": false,
                            "elicitationRequired": true,
                            "prompts": {
                                "elicitation": "Elicit.Slot.970891513553.193435702448"
                            }
                        }
                    ]
                }
            ],
            "delegationStrategy": "ALWAYS"
        },
        "prompts": [
            {
                "id": "Elicit.Slot.970891513553.193435702448",
                "variations": [
                    {
                        "type": "PlainText",
                        "value": "Chat G P Tに質問したいことを話しかけてください。"
                    }
                ]
            }
        ]
    }
}

コードエディタ部分

追加したのは、keys.jsだけです。

keys.js

module.exports.OPEN_AI_KEY = '取得したパスワードを貼り付ける。';

openaiを読み込むための設定がここで出来ます。
以下を追加しました。※ カンマに注意
,
"i18next": "^15.1.3",
"openai": "^3.2.1"

package.json

{
  "name": "hello-world",
  "version": "1.2.0",
  "description": "alexa utility for quickly building skills",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Amazon Alexa",
  "license": "Apache License",
  "dependencies": {
    "ask-sdk-core": "^2.7.0",
    "ask-sdk-model": "^1.19.0",
    "aws-sdk": "^2.326.0",
    "i18next": "^15.1.3",
    "openai": "^3.2.1"
  }
}

index.jsでは、const openai = new OpenAIApi(configuration);までの部分と
AskChatgptIntentHandlerの部分がポイントになります。

index.js


/* *
 * This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK (v2).
 * Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management,
 * session persistence, api calls, and more.
 * */
const Alexa = require('ask-sdk-core');
const title = 'サイバーイルカ';


const { Configuration, OpenAIApi } = require("openai");
const keys = require('./keys');
const configuration = new Configuration({
    apiKey: keys.OPEN_AI_KEY
});
const openai = new OpenAIApi(configuration);


const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    handle(handlerInput) {
        const speakOutput = title + 'が起動されました。Chat G P T に何を聞きたいですか。';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .withSimpleCard(
                title, 
                speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

const HelloWorldIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'HelloWorldIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'こんにちは。Chat G P T に質問したいことを話しかけてください。';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .withSimpleCard(
                title, 
                speakOutput)
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};

const AskChatgptIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AskChatgptIntent';
    },
    async handle(handlerInput) {
        const question = Alexa.getSlotValue(handlerInput.requestEnvelope, 'question');
        
        console.log(question);

        const completion = await openai.createChatCompletion({
            model: "gpt-3.5-turbo",
            messages: [{role: "user", content: question}],
            stream: true,
          });

        console.log(completion);
        
        console.log(completion.data.choices[0].message.content);
        // console.log(completion.data.choices[0].message);

        const speakOutput = completion.data.choices[0].message.content;
        // const speakOutput = completion.data.choices[0].message;
        // const speakOutput = handlerInput.t('HELLO_MSG');

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .withSimpleCard(
                title, 
                speakOutput)
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};

const HelpIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'ヘルプと言われましたね。このスキルはChat G P T APIに接続して、返答を返すスキルです。質問を続けてください。';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .withSimpleCard(
                title, 
                speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

const CancelAndStopIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && (Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent'
                || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent');
    },
    handle(handlerInput) {
        const speakOutput = '終了します。ご利用ありがとうございました。';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .withSimpleCard(
                title, 
                speakOutput)
            .getResponse();
    }
};
/* *
 * FallbackIntent triggers when a customer says something that doesn’t map to any intents in your skill
 * It must also be defined in the language model (if the locale supports it)
 * This handler can be safely added but will be ingnored in locales that do not support it yet 
 * */
const FallbackIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.FallbackIntent';
    },
    handle(handlerInput) {
        const speakOutput = '申し訳ございません。もう一度おっしゃっていただけませんか。';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .withSimpleCard(
                title, 
                speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};
/* *
 * SessionEndedRequest notifies that a session was ended. This handler will be triggered when a currently open 
 * session is closed for one of the following reasons: 1) The user says "exit" or "quit". 2) The user does not 
 * respond or says something that does not match an intent defined in your voice model. 3) An error occurs 
 * */
const SessionEndedRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest';
    },
    handle(handlerInput) {
        console.log(`~~~~ Session ended: ${JSON.stringify(handlerInput.requestEnvelope)}`);
        // Any cleanup logic goes here.
        return handlerInput.responseBuilder.getResponse(); // notice we send an empty response
    }
};
/* *
 * The intent reflector is used for interaction model testing and debugging.
 * It will simply repeat the intent the user said. You can create custom handlers for your intents 
 * by defining them above, then also adding them to the request handler chain below 
 * */
const IntentReflectorHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest';
    },
    handle(handlerInput) {
        const intentName = Alexa.getIntentName(handlerInput.requestEnvelope);
        const speakOutput = `申し訳ございません。システムエラーです。You just triggered ${intentName}`;

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .withSimpleCard(
                title, 
                speakOutput)
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};
/**
 * Generic error handling to capture any syntax or routing errors. If you receive an error
 * stating the request handler chain is not found, you have not implemented a handler for
 * the intent being invoked or included it in the skill builder below 
 * */
const ErrorHandler = {
    canHandle() {
        return true;
    },
    handle(handlerInput, error) {
        const speakOutput = '申し訳ございません。エラーが発生しました。もう一度、質問していただけませんか。';
        console.log(`~~~~ Error handled: ${JSON.stringify(error)}`);

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

/**
 * This handler acts as the entry point for your skill, routing all request and response
 * payloads to the handlers above. Make sure any new handlers or interceptors you've
 * defined are included below. The order matters - they're processed top to bottom 
 * */
exports.handler = Alexa.SkillBuilders.custom()
    .addRequestHandlers(
        LaunchRequestHandler,
        AskChatgptIntentHandler,
        HelloWorldIntentHandler,
        HelpIntentHandler,
        CancelAndStopIntentHandler,
        FallbackIntentHandler,
        SessionEndedRequestHandler,
        IntentReflectorHandler)
    .addErrorHandlers(
        ErrorHandler)
    .withCustomUserAgent('sample/hello-world/v1.2')
    .lambda();

エラー内容

作成中の「サイバーイルカ」スキルですが、返答が「スキルがリクエストに正しく応答できませんでした」になります。
CloudWatchを見ると、Task timed out after 8.01 seconds、そして"message": "An exception occurred while dispatching the request to the skill."と出力されています。

https://twitter.com/allforbigfire/status/1634578486089367552

同様の英語版プログラムでは、alexa developer consoleのテスト画面ですが、エラーなくChatGPTからの返答が出力されています。

その為、日本語の場合だけなのかChatGPT APIへ問い合わせできているが返答が8秒超えているため、Alexaがエラー処理としているのではないかと、思っております。




ホスト地域が3か所から選べます。
米国東部(バージニア北部)
EU(アイルランド)
米国西部(オレゴン)

日本語の場合のレスポンス速度が原因の場合、ホスト地域についても検証が必要かもしれません。

スマートスピーカーAmazon AlexaとChatGPT APIとの組み合わせたAlexaスキルを開発されたい方の一助になれれば幸いです。

Discussion

allforbigfireallforbigfire

Pythonで開発された方がコードを公開されていました。

alexa_chatgpt > アレクサとChatGPT使って雑談
https://github.com/ShunjiHashimoto/alexa_chatgpt

Lambda の開始方法
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/getting-started.html

PythonでAlexa Hello World
https://qiita.com/toshimin/items/e4d9bc3380db4d171f0e

How to invoke OpenAI APIs from AWS Lambda functions
https://thedeveloperspace.com/how-to-invoke-openai-apis-from-aws-lambda-functions/

私は、簡単なのでAlexa-hosted Skillsでいいかと思ったのですが、こちらの方法でも試してみます。

allforbigfireallforbigfire

Alexa Community(英語)の方で相談したら、次のようなアドバイスを頂きました。
https://alexacommunity.slack.com/ssb/redirect

①Alexa hosted スキルは8秒制限(公式ドキュメントに載っているかな?)があるから、バックエンドを AWS Lambda に切り替える。
②ChatGPT API の応答が返されるまでの空き時間を会話(「ただいま問い合わせ中です。続けますか?」)で気にならないようにする。
③英語で ChatGPT API をクエリする場合の処理時間を確認します。