import WebSocket from "ws";
import readline from "readline";
import dotenv from "dotenv"; // dotenvをインポート
dotenv.config(); // .envファイルを読み込む
// WebSocketエンドポイントとAPIキー
const url = "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01";
const API_KEY = process.env.OPENAI_API_KEY; // .envからAPIキーを取得
if (!API_KEY) {
console.error("Error: OPENAI_API_KEY is not set in .env file");
process.exit(1); // APIキーが設定されていない場合は終了
}
// リアルタイム状態を管理するクラス
class RealtimeState {
constructor() {
this.response = null; // サーバーからのレスポンス状態を管理
}
updateResponseStatus(responseId, status) {
if (!this.response || this.response.id !== responseId) {
this.response = { id: responseId, status }; // 新しいレスポンスを登録
} else {
this.response.status = status; // 状態を更新
}
console.log(`Response [${responseId}] status updated to: ${status}`);
}
getResponseStatus() {
return this.response ? this.response.status : null;
}
}
// インスタンス作成
const state = new RealtimeState();
// WebSocket接続
const ws = new WebSocket(url, {
headers: {
Authorization: "Bearer " + API_KEY,
"OpenAI-Beta": "realtime=v1",
},
});
// ユーザー入力用のインターフェースを作成
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
// WebSocket接続時の処理
ws.on("open", function open() {
console.log("Connected to server.");
// セッションの更新: 関数の定義を送信
const sessionUpdate = {
type: 'session.update',
session: {
tools: [
{
type: 'function',
name: 'getCurrentWeather',
description: '指定された都市の現在の天気を取得します。',
parameters: {
type: 'object',
properties: {
location: {
type: 'string',
description: '都市名'
}
},
required: ['location']
}
}
],
tool_choice: 'auto'
}
};
ws.send(JSON.stringify(sessionUpdate));
// セッション成立後、3秒待ってからユーザー入力を促す
setTimeout(() => {
console.log("準備ができました。メッセージを入力してください:");
promptUserInput(); // 入力待機を開始
}, 3000); // 待機時間3秒
});
// ResponseをCreateする関数
function createResponse(instructions = null) {
const response = {
type: "response.create",
response: {
modalities: ["text"],
},
};
// 引数としてinstructionsが渡された場合、設定する
if (instructions) {
response.response.instructions = instructions;
}
ws.send(JSON.stringify(response));
console.log(
`Response.create を送信しました: ${
instructions ? `Instructions: ${instructions}` : "No instructions"
}`
);
}
// ユーザー入力を受け取る関数
function promptUserInput() {
rl.question("> ", (userInput) => {
if (userInput.trim().toLowerCase() === "exit") {
console.log("終了します。");
rl.close();
ws.close();
return;
}
// 現在の状態を確認
const status = state.getResponseStatus();
if (status === "done" || status === null) {
console.log("Response is done or status is unknown. Sending conversation item and creating a new response.");
sendUserMessage(userInput); // メッセージ送信
setTimeout(() => createResponse("Continue assisting the user."), 1000); // 新しいInstructions付きのResponseを1秒待機後に作成
} else if (status === "in_progress") {
console.log("Response is in progress. Sending message directly without creating a new response.");
sendUserMessage(userInput); // 状態が in_progress の場合、直接送信
} else {
console.error("Unknown state:", status); // その他の不明な状態(デバッグ用)
}
// 再度入力を促す
promptUserInput();
});
}
// ユーザーのメッセージを送信する関数
function sendUserMessage(userInput) {
const userMessage = {
type: "conversation.item.create",
item: {
type: "message",
role: "user",
content: [
{
type: "input_text",
text: userInput,
},
],
},
};
ws.send(JSON.stringify(userMessage));
console.log("メッセージを送信しました:", userInput);
}
// メッセージ受信時の処理
ws.on("message", function incoming(message) {
const parsedMessage = JSON.parse(message.toString());
// 分岐処理: イベントのtypeによって処理を分ける
switch (parsedMessage.type) {
case "response.created":
const responseId = parsedMessage.response.id;
state.updateResponseStatus(responseId, "in_progress");
break;
case "response.done":
const responseDoneId = parsedMessage.response.id;
state.updateResponseStatus(responseDoneId, "done");
break;
case "response.function_call_arguments.done":
handleFunctionCall(parsedMessage);
break;
default:
console.log("Unhandled event type:", parsedMessage.type);
}
console.log("受信:", JSON.stringify(parsedMessage, null, 2));
});
// Function Call の処理
function handleFunctionCall(parsedMessage) {
const { name, arguments: args, call_id } = parsedMessage;
// 関数名に基づく分岐処理
let result;
switch (name) {
case "getCurrentWeather":
result = getCurrentWeather(args);
break;
default:
console.log("Unhandled function call:");
console.log(`Name: ${name}`);
console.log(`Arguments: ${args}`);
return; // 未対応の関数は処理しない
}
// Tool関数の結果を送信
if (result) {
const event = {
type: "conversation.item.create",
item: {
type: "function_call_output",
call_id: call_id, // 元の call_id をそのまま使用
output: JSON.stringify(result), // 結果をJSON文字列に変換
},
};
ws.send(JSON.stringify(event));
ws.send(JSON.stringify({type: 'response.create'}));
}
}
// Tool関数:getCurrentWeather
function getCurrentWeather(args) {
try {
const parsedArgs = JSON.parse(args); // 引数をパース
const location = parsedArgs.location;
if (!location) {
console.error("Error: Missing location argument in getCurrentWeather call.");
return null; // エラー時は null を返す
}
// ダミーの結果を生成
return {
message: `${location}はとても良い天気です!`
};
} catch (error) {
console.error("Error processing getCurrentWeather dummy function:", error);
return null; // エラー時は null を返す
}
}