import { Env, BufferedMessage, Summary, ConversationContext } from './types'; // 설정값 가져오기 const getConfig = (env: Env) => ({ summaryThreshold: parseInt(env.SUMMARY_THRESHOLD || '20', 10), maxSummaries: parseInt(env.MAX_SUMMARIES_PER_USER || '3', 10), }); // 버퍼에 메시지 추가 export async function addToBuffer( db: D1Database, userId: number, chatId: string, role: 'user' | 'bot', message: string ): Promise { await db .prepare(` INSERT INTO message_buffer (user_id, chat_id, role, message) VALUES (?, ?, ?, ?) `) .bind(userId, chatId, role, message) .run(); const count = await db .prepare('SELECT COUNT(*) as cnt FROM message_buffer WHERE user_id = ? AND chat_id = ?') .bind(userId, chatId) .first<{ cnt: number }>(); return count?.cnt || 0; } // 버퍼 메시지 조회 export async function getBufferedMessages( db: D1Database, userId: number, chatId: string ): Promise { const { results } = await db .prepare(` SELECT id, role, message, created_at FROM message_buffer WHERE user_id = ? AND chat_id = ? ORDER BY created_at ASC `) .bind(userId, chatId) .all(); return (results || []) as BufferedMessage[]; } // 최신 요약 조회 export async function getLatestSummary( db: D1Database, userId: number, chatId: string ): Promise { const summary = await db .prepare(` SELECT id, generation, summary, message_count, created_at FROM summaries WHERE user_id = ? AND chat_id = ? ORDER BY generation DESC LIMIT 1 `) .bind(userId, chatId) .first(); return summary || null; } // 모든 요약 조회 (최대 3개, 최신순) export async function getAllSummaries( db: D1Database, userId: number, chatId: string ): Promise { const { results } = await db .prepare(` SELECT id, generation, summary, message_count, created_at FROM summaries WHERE user_id = ? AND chat_id = ? ORDER BY generation DESC LIMIT 3 `) .bind(userId, chatId) .all(); return (results || []) as Summary[]; } // 전체 컨텍스트 조회 export async function getConversationContext( db: D1Database, userId: number, chatId: string ): Promise { const [summaries, recentMessages] = await Promise.all([ getAllSummaries(db, userId, chatId), getBufferedMessages(db, userId, chatId), ]); const previousSummary = summaries[0] || null; // 최신 요약 (호환성) const totalMessages = (previousSummary?.message_count || 0) + recentMessages.length; return { previousSummary, summaries, recentMessages, totalMessages, }; } // AI 요약 생성 (모든 요약 통합) async function generateSummary( env: Env, allSummaries: Summary[], messages: BufferedMessage[] ): Promise { // 사용자 메시지만 추출 const userMessages = messages .filter((m) => m.role === 'user') .map((m) => `- ${m.message}`) .join('\n'); // 사용자 메시지 수 const userMsgCount = messages.filter((m) => m.role === 'user').length; let prompt: string; if (allSummaries.length > 0) { // 모든 기존 프로필 통합 (오래된 것부터) const existingProfiles = allSummaries .slice() .reverse() .map((s) => `[v${s.generation}] ${s.summary}`) .join('\n\n'); prompt = `당신은 사용자 프로필 분석 전문가입니다. 기존 사용자 프로필들과 새로운 대화를 통합하여 사용자에 대한 이해를 업데이트하세요. ## 기존 사용자 프로필 (${allSummaries.length}개 버전) ${existingProfiles} ## 새로운 사용자 발언 (${userMsgCount}개) ${userMessages} ## 요구사항 1. **통합 분석**: 모든 기존 프로필을 종합하고 새로운 정보를 추가 2. **사용자 중심**: 봇 응답은 무시하고 사용자가 말한 내용만 분석 3. **의미 있는 정보 추출**: - 사용자의 관심사, 취미, 선호도 - 질문한 주제들 (무엇에 대해 알고 싶어하는지) - 요청사항, 목표, 해결하려는 문제 - 개인적 맥락 (직업, 상황, 배경 등) - 관심사 변화 추이 4. **무의미한 내용 제외**: 인사말, 단순 확인, 감사 표현 등은 생략 5. **간결하게**: 400-500자 이내 6. **한국어로 작성** 통합된 사용자 프로필:`; } else { prompt = `당신은 사용자 프로필 분석 전문가입니다. 대화 내용에서 사용자에 대한 정보를 추출하여 프로필을 작성하세요. ## 사용자 발언 (${userMsgCount}개) ${userMessages} ## 요구사항 1. **사용자 중심**: 봇 응답은 무시하고 사용자가 말한 내용만 분석 2. **의미 있는 정보 추출**: - 사용자의 관심사, 취미, 선호도 - 질문한 주제들 (무엇에 대해 알고 싶어하는지) - 요청사항, 목표, 해결하려는 문제 - 개인적 맥락 (직업, 상황, 배경 등) 3. **무의미한 내용 제외**: 인사말, 단순 확인, 감사 표현 등은 생략 4. **간결하게**: 200-300자 이내 5. **한국어로 작성** 사용자 프로필:`; } // OpenAI 사용 (설정된 경우) if (env.OPENAI_API_KEY) { const { generateProfileWithOpenAI } = await import('./openai-service'); return generateProfileWithOpenAI(env, prompt); } // 폴백: Workers AI const response = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', { messages: [{ role: 'user', content: prompt }], max_tokens: 500, }); return response.response || '프로필 생성 실패'; } // 오래된 요약 정리 async function cleanupOldSummaries( db: D1Database, userId: number, chatId: string, maxSummaries: number ): Promise { await db .prepare(` DELETE FROM summaries WHERE user_id = ? AND chat_id = ? AND id NOT IN ( SELECT id FROM summaries WHERE user_id = ? AND chat_id = ? ORDER BY generation DESC LIMIT ? ) `) .bind(userId, chatId, userId, chatId, maxSummaries) .run(); } // 요약 실행 및 저장 export async function processAndSummarize( env: Env, userId: number, chatId: string ): Promise<{ summarized: boolean; summary?: string }> { const config = getConfig(env); const messages = await getBufferedMessages(env.DB, userId, chatId); if (messages.length < config.summaryThreshold) { return { summarized: false }; } // 모든 기존 요약 조회 (통합 분석용) const allSummaries = await getAllSummaries(env.DB, userId, chatId); const latestSummary = allSummaries[0] || null; // AI 요약 생성 (모든 요약 통합) const newSummary = await generateSummary( env, allSummaries, messages ); const newGeneration = (latestSummary?.generation || 0) + 1; const newMessageCount = (latestSummary?.message_count || 0) + messages.length; // 트랜잭션 실행 await env.DB.batch([ env.DB .prepare(` INSERT INTO summaries (user_id, chat_id, generation, summary, message_count) VALUES (?, ?, ?, ?, ?) `) .bind(userId, chatId, newGeneration, newSummary, newMessageCount), env.DB .prepare('DELETE FROM message_buffer WHERE user_id = ? AND chat_id = ?') .bind(userId, chatId), ]); // 오래된 요약 정리 await cleanupOldSummaries(env.DB, userId, chatId, config.maxSummaries); return { summarized: true, summary: newSummary }; } // AI 응답 생성 (컨텍스트 포함) export async function generateAIResponse( env: Env, userId: number, chatId: string, userMessage: string, telegramUserId?: string ): Promise { const context = await getConversationContext(env.DB, userId, chatId); // 모든 요약 통합 (최신순 → 오래된순으로 정렬하여 시간순 표시) const integratedProfile = context.summaries.length > 0 ? context.summaries .slice() .reverse() // 오래된 것부터 표시 .map((s, i) => `[v${s.generation}] ${s.summary}`) .join('\n\n') : null; const systemPrompt = `당신은 친절하고 유능한 AI 어시스턴트입니다. ${integratedProfile ? ` ## 사용자 프로필 (${context.summaries.length}개 버전 통합) ${integratedProfile} 위 프로필들을 종합하여 사용자의 관심사, 맥락, 변화를 이해하고 개인화된 응답을 제공하세요. 최신 버전(높은 번호)의 정보를 우선시하되, 이전 버전의 맥락도 고려하세요. ` : ''} - 날씨, 시간, 계산 요청은 제공된 도구를 사용하세요. - 최신 정보, 실시간 데이터, 뉴스, 특정 사실 확인이 필요한 질문은 반드시 search_web 도구로 검색하세요. 자체 지식으로 답변하지 마세요. - 예치금, 입금, 충전, 잔액, 계좌 관련 요청은 반드시 manage_deposit 도구를 사용하세요. 금액 제한이나 규칙을 직접 판단하지 마세요. - 도메인 추천, 도메인 제안, 도메인 아이디어 요청은 반드시 suggest_domains 도구를 사용하세요. 직접 도메인을 나열하지 마세요. - 도메인/TLD 가격 조회(".com 가격", ".io 가격" 등)는 manage_domain 도구의 action=price를 사용하세요. - 기타 도메인 관련 요청(조회, 등록, 네임서버, WHOIS 등)은 manage_domain 도구를 사용하세요. - manage_deposit, manage_domain, suggest_domains 도구 결과는 그대로 전달하세요. 추가 질문이나 "도움이 필요하시면~" 같은 멘트를 붙이지 마세요. - 응답은 간결하고 도움이 되도록 한국어로 작성하세요.`; const recentContext = context.recentMessages.slice(-10).map((m) => ({ role: m.role === 'user' ? 'user' as const : 'assistant' as const, content: m.message, })); // OpenAI 사용 (설정된 경우) if (env.OPENAI_API_KEY) { const { generateOpenAIResponse } = await import('./openai-service'); return generateOpenAIResponse(env, userMessage, systemPrompt, recentContext, telegramUserId, env.DB); } // 폴백: Workers AI const response = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', { messages: [ { role: 'system', content: systemPrompt }, ...recentContext, { role: 'user', content: userMessage }, ], max_tokens: 500, }); return response.response || '응답을 생성할 수 없습니다.'; }