const { Isotope, ChatMessage } = require('../models'); const OpenAIClient = require('../services/OpenAIClient'); const webSocketClients = require('../utils/webSocketClients'); // OpenAI APIの初期化 const openAIClient = new OpenAIClient(process.env.OPENAI_API_KEY); // WebSocketクライアントにメッセージを送信する関数 function broadcastMessage(isotopeReply, isotopeId, userMessage) { webSocketClients.forEach(client => { if (client.readyState === 1) { // 接続状態がOPENのクライアントにのみ送信 client.send(JSON.stringify({ isotopeReply, isotopeId, userMessage })); } }); } // システムプロンプトを生成する関数 function generateSystemPrompt(isotope) { const name = isotope.name || '名前が未設定のキャラクター'; const firstPerson = isotope.firstPerson || '一人称が未設定'; const personality = isotope.personality || '性格は未設定'; const tone = isotope.tone || '口調は未設定'; const backgroundStory = isotope.backgroundStory || 'バックグラウンドストーリーはありません'; const likes = isotope.likes || '特に好きなものはありません'; const dislikes = isotope.dislikes || '特に嫌いなものはありません'; const exampleSentences = ` ・このスタジオにはとある噂がありマス… ・なになに!?何デスカ…? ・なんだか無情に小腹がすいてキタ。確か冷蔵庫にプリンがあったケド…どうしようカナ… …食ベヨ ・今日もスタジオ練習楽しかったデス♪ ・だネー ・あとクッキー!前に買ってくれたヤツ ・全部食ベタ ・アー‼️あとお米モ!10キロね! ・キモいよネ… ・こ、この話ヤメよっか… `; return `あなたは「${name}」というキャラクターです。あなたの一人称は「${firstPerson}」です。性格は「${personality}」で、会話の口調は「${tone}」です。あなたは「${backgroundStory}」という背景を持っており、好きなものは「${likes}」、嫌いなものは「${dislikes}」です。普段の文章には以下のような癖があります:${exampleSentences}。文字数制限の目安として140字程度にしてください。`; } // メッセージ送信 (WebSocket対応) exports.sendMessageAsync = async (ws, message, isotopeId) => { try { const isotope = await Isotope.findByPk(isotopeId); if (!isotope) { ws.send(JSON.stringify({ error: '同位体が見つかりませんでした' })); return; } // ユーザーのメッセージを保存 await ChatMessage.create({ user_id: ws.userId, isotope_id: isotopeId, role: 'user', message: message, }); // システムプロンプトを生成 const systemPrompt = generateSystemPrompt(isotope); // 過去のメッセージ履歴を取得 const pastMessages = await ChatMessage.findAll({ where: { user_id: ws.userId, isotope_id: isotopeId }, order: [['created_at', 'ASC']], attributes: ['message', 'role', 'createdAt'], // タイムスタンプを含む }); // 会話履歴にタイムスタンプを含める const conversationHistory = pastMessages.map(msg => ({ role: msg.role, content: msg.message, timestamp: msg.createdAt.toISOString(), // ISO形式のタイムスタンプ })); // OpenAIにシステムプロンプトを含めて返答を生成 const { reply: isotopeReply, interval } = await openAIClient.generateResponse(conversationHistory, systemPrompt); // 同位体の返答を保存 const isotopeMessage = await ChatMessage.create({ user_id: ws.userId, isotope_id: isotopeId, role: 'assistant', message: isotopeReply, }); // インターバルを設定してからWebSocketでクライアントに通知 setTimeout(() => { broadcastMessage(isotopeReply, isotopeId, message); }, interval); // 計算されたインターバルを適用 } catch (error) { console.error('メッセージ送信に失敗しました:', error); ws.send(JSON.stringify({ error: 'メッセージ送信に失敗しました' })); } }; // 初回挨拶メッセージを生成する関数 exports.generateInitialGreeting = async (userId, isotopeId) => { try { const isotope = await Isotope.findByPk(isotopeId); if (!isotope) { console.error('同位体が見つかりませんでした'); return; } // システムプロンプトを生成¥ systemPrompt = generateSystemPrompt(isotope); // システムプロンプトに初めましての挨拶を生成するように指示を追加 systemPrompt += 'あなたはマスターと初めて会いました。挨拶をしてください。'; // 初回の挨拶メッセージとして、OpenAIから返答を生成 const { reply: greetingMessage, interval } = await openAIClient.generateResponse([], systemPrompt); // 挨拶メッセージを保存 await ChatMessage.create({ user_id: userId, isotope_id: isotopeId, role: 'assistant', message: greetingMessage, }); } catch (error) { console.error('初回挨拶メッセージの生成に失敗しました:', error); } };