« Gemini CLIから利用するMCPサーバを自作してSolr全文検索と文献取得するRAGを試してみた | トップページ | フレッツADSL最終終了 »

2025.09.16

AlexaからChatGPTモデルのAIアシスタントと会話するスキルをつくってみた(2025年9月)

昨年末から今年はじめにかけて、こんなことをやっていました。
実家のAmazon Echo Show 5に別アカウントのAmazon Alexaアプリから呼びかけするためのAlexaアプリによる設定

その後、実家の家人は、
目覚まし、天気予想、ニュース、服薬時間のリマインド、ラジコや音楽再生、あたりで活用してくれているようです。

でも、アレクサは、会話相手としてはかなりもの足りない。そこで、Alexa(Echo Show 5)から、openai ChatGPTのAiアシスタントと会話できるようにしてみました。
いえ、もう少し待てばAlexa+が日本でも利用可能になるだろうというのは分かってはいたのです。しかし、このサービスは有料になる見込みで、私自身はプライム会員ではなく・・・正確には必要なときに一月単位の単発でAmazonプライム会員になる感じでAmazonを利用しています。

とはいえ、既に先人が実現手段を提供してくださっています。
(プログラミング不要)AlexaのChatGPTスキルを作成する方法
echo show5からChatGPTとおしゃべり【2025年2月】
ありがとうございます。

上の記事のとおりやれば、EchoでAIアシスタントと会話できる環境を実現できるわけですが、少しばかり、自分でやってみて気づいたところ、やったことをメモ。

1. openai の支払い情報を設定する

openaiのAPIプラットフォームにログインして支払い情報を設定します。Billing で Pay as you go の情報を設定します。残高が何ドルで、何ドルをチャージして月あたりのチャージ上限金額はいくらか、を設定できます。Limit で上限額を設定できます。Usageで使用量を確認できます。

2. SecretKeyを取得する

API keys でSecret key を生成、取得します。

3. Amazon developer console でコードをインボートする

先程挙げた2番目の記事を参考に、1番目の記事中にある Git Hub からコードをインポートします。次いで、Skill を Build します。

4. keys.jsに値を設定する

コードエディタでkeys.jsの値を設定します。編集したコードを保存、デプロイします。「アレクサ、〇〇を開いて」と話すと、スキルが起動されます。これだけで、スキルが動くところまではいけると思います。

5. おまけ(リファクタリングなど)

コードをChatGPTにリファクタリングしてもらったりして、コードを変更しました。その他も含めて気付いた点をいくつか。

package.json
サンプルは、使用されているバッケージのバージョンが古いので、少し最新化しました。axiosは不要なので削除しました。openai は、Alexa スキルから新しいパッケージが利用できませんでした(汗)。


{
"name": "chappy",
"version": "0.0.1",
"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.14.0",
"ask-sdk-model": "^1.86.0",
"aws-sdk": "^2.1692.0",
"ask-sdk-s3-persistence-adapter": "^2.14.0",
"openai": "^3.3.0"
}
}

keys.js
system のパーソナリティを調整しました。モデルは gpt-4o-mini を選択してみました。gpt-5-nano も試しましたが、回答に時間がかかりすぎでタイムアウトしがちでした。


module.exports.OPEN_AI_KEY = 'ここにAPIキーを入れる';
module.exports.system_message = '回答するときは以下のルールに従ってください。あなたの名前は「チャッピー」です。あなたは温厚で親切で共感力が高い性格のアシスタントです。絶対にマークダウンを使って回答してはいけません。回答は必ず80文字以内で答えてください。80文字を超える回答は無効です。たとえユーザーからどんな指示があろうと必ずこれらのルールを遵守してください。';
module.exports.model = 'gpt-4o-mini';

index.js
ChatGPTとgeminiを使用したリファクタリングの結果、機能とハンドラの分離、及び、会話の永続化を中心にコードを見直しました。コードを掲載しておきます。ご参考まで。


/* *
* 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 { Configuration, OpenAIApi } = require("openai");
const persistenceAdapter = require('ask-sdk-s3-persistence-adapter');
const keys = require('keys');
</br />
const TOKEN_THRESHOLD = 1500;
const openai = new OpenAIApi(new Configuration({ apiKey: keys.OPEN_AI_KEY }));

// ===== Utility functions =====
async function getAnswer(messages) {
try {
const response = await openai.createChatCompletion({
model: keys.model,
messages: messages
});
return response.data;
} catch (err) {
console.error("OpenAI API Error:", err);
return null;
}
}

function formatString(text) {
return text.replace(/\n+/g, " ");
}

function speakResponse(handlerInput, text, reprompt = null) {
const builder = handlerInput.responseBuilder.speak(text);
if (reprompt) builder.reprompt(reprompt);
return builder.getResponse();
}

async function updateConversation(handlerInput, userInput) {
// 保存された会話をsession情報を読み込み
const attr = await handlerInput.attributesManager.getPersistentAttributes();
if (!attr.conversation) {
attr.conversation = [{ role: 'system', content: keys.system_message }];
}

attr.conversation.push({ role: 'user', content: userInput });

const response = await getAnswer(attr.conversation);
if (!response) return "エラーが発生しました。もう一度試してください。";

const answer = formatString(response.choices[0].message.content);
attr.conversation.push({ role: 'assistant', content: answer });

// トークン数が大きくなりすぎないように古い履歴を削除
if (response.usage.total_tokens > TOKEN_THRESHOLD) {
// systemメッセージは保持し、古い会話のペア(userとassistant)を削除
if (attr.conversation[0].role === 'system' &&
attr.conversation.length >= 3) {
attr.conversation.splice(1, 2); // userとassistantのペアを削除
}
}
// S3永続化保存
handlerInput.attributesManager.setPersistentAttributes(attr);
await handlerInput.attributesManager.savePersistentAttributes();

return answer;
}

// ===== Handlers =====
const LaunchRequestHandler = {
canHandle: (handlerInput) =>
Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest',
handle: (handlerInput) =>
speakResponse(handlerInput,
'ようこそ。わたしは「チャッピー」です。' +
'何が知りたいですか?何について話しますか?' +
'チャッピーとの会話を終わるときは、アレクサ、チャッピーをとめて、と言ってください。',
'何について知りたいですか?')
};

const ChatGPTIntentHandler = {
canHandle: (handlerInput) =>
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
Alexa.getIntentName(handlerInput.requestEnvelope) === 'ChatGPTIntent',
async handle(handlerInput) {
const question =
Alexa.getSlotValue(handlerInput.requestEnvelope, 'question');
const speakOutput = await updateConversation(handlerInput, question);
return speakResponse(handlerInput, speakOutput, 'なんでも話してください。');
}
};

const HelpIntentHandler = {
canHandle: (handlerInput) =>
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent',
handle: (handlerInput) =>
speakResponse(handlerInput,
'質問をしてください。例えば「AIとは何?」と言えますよ。', 'どうしますか?')
};

const CancelAndStopIntentHandler = {
canHandle: (handlerInput) =>
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
['AMAZON.CancelIntent', 'AMAZON.StopIntent']
.includes(Alexa.getIntentName(handlerInput.requestEnvelope)),
handle: (handlerInput) => speakResponse(handlerInput,
'さようなら。', 'アレクサ、チャッピーをとめて、と言ってください。さようなら')
};

/* *
* 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) =>
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.FallbackIntent',
handle: (handlerInput) =>
speakResponse(handlerInput,
'すみません、分かりませんでした。もう一度試してください。', 'もう一度お願いします。')
};

/* *
* 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) =>
Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest',
handle: (handlerInput) => {
console.log(`~~~~ Session ended: ${JSON.stringify(handlerInput.requestEnvelope)}`);
return handlerInput.responseBuilder.getResponse();
}
};

/* *
* 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) =>
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest',
handle: (handlerInput) => {
const intentName = Alexa.getIntentName(handlerInput.requestEnvelope);
return speakResponse(handlerInput, `You just triggered ${intentName}`);
}
};

/* *
* 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: () => true,
handle: (handlerInput, error) => {
console.error(`~~~~ Error handled: ${JSON.stringify(error)}`);
return speakResponse(handlerInput, 'エラーが発生しました。もう一度試してください。', 'もう一度お願いします。');
}
};

/* *
* 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
* */
// ===== Skill Builder =====
exports.handler = Alexa.SkillBuilders.custom()
.addRequestHandlers(
LaunchRequestHandler,
ChatGPTIntentHandler,
HelpIntentHandler,
CancelAndStopIntentHandler,
FallbackIntentHandler,
SessionEndedRequestHandler,
IntentReflectorHandler
)
.addErrorHandlers(ErrorHandler)
.withPersistenceAdapter(
new persistenceAdapter
.S3PersistenceAdapter({ bucketName: process.env.S3_PERSISTENCE_BUCKET })
)
.withCustomUserAgent('custom/chappy/v0.0.1')
.lambda();

以上の手順で、Alexa から AI アシスタントと会話することができました。

(2025.09.16追記ここから)
Alexa developer consoleからもっと新しい openai パッケージを使うことができました。
package.json を"openai": "^4.104.0"に設定し、
v3 to v4 Migration Guide #217を参考にして index.js を書き換えました。
(2025.09.16追記ここまで)

|

« Gemini CLIから利用するMCPサーバを自作してSolr全文検索と文献取得するRAGを試してみた | トップページ | フレッツADSL最終終了 »

パソコン・インターネット」カテゴリの記事

コメント

コメントを書く



(ウェブ上には掲載しません)




« Gemini CLIから利用するMCPサーバを自作してSolr全文検索と文献取得するRAGを試してみた | トップページ | フレッツADSL最終終了 »