diff --git a/src/agents/deposit-agent.ts b/src/agents/deposit-agent.ts index a1afc2c..0386b5c 100644 --- a/src/agents/deposit-agent.ts +++ b/src/agents/deposit-agent.ts @@ -17,7 +17,9 @@ import { createLogger } from '../utils/logger'; import { executeWithOptimisticLock, OptimisticLockError } from '../utils/optimistic-lock'; import { TRANSACTION_STATUS, TRANSACTION_TYPE } from '../constants'; -import type { Env, ManageDepositArgs, DepositFunctionResult, DepositSession, DepositSessionStatus } from '../types'; +import type { Env, ManageDepositArgs, DepositFunctionResult, DepositSession, DepositSessionStatus, OpenAIToolCall, OpenAIMessage, OpenAIAPIResponse } from '../types'; +import { SessionManager } from '../utils/session-manager'; +import { getSessionConfig } from '../constants/agent-config'; const logger = createLogger('deposit-agent'); @@ -25,176 +27,8 @@ const MIN_DEPOSIT_AMOUNT = 1000; // 1,000원 const MAX_DEPOSIT_AMOUNT = 100_000_000; // 1억원 const DEFAULT_HISTORY_LIMIT = 10; -// D1 Session Management -const DEPOSIT_SESSION_TTL = 30 * 60 * 1000; // 30분 (입금 작업은 빠르게 처리) -const MAX_MESSAGES = 10; // 세션당 최대 메시지 수 - -/** - * D1에서 입금 세션 조회 - * - * @param db - D1 Database - * @param userId - Telegram User ID - * @returns DepositSession 또는 null (세션 없거나 만료) - */ -export async function getDepositSession( - db: D1Database, - userId: string -): Promise { - try { - const now = Date.now(); - const result = await db.prepare( - 'SELECT * FROM deposit_sessions WHERE user_id = ? AND expires_at > ?' - ).bind(userId, now).first<{ - user_id: string; - status: string; - collected_info: string | null; - messages: string | null; - created_at: number; - updated_at: number; - expires_at: number; - }>(); - - if (!result) { - logger.info('입금 세션 없음', { userId }); - return null; - } - - const session: DepositSession = { - user_id: result.user_id, - status: result.status as DepositSessionStatus, - collected_info: result.collected_info ? JSON.parse(result.collected_info) : {}, - messages: result.messages ? JSON.parse(result.messages) : [], - created_at: result.created_at, - updated_at: result.updated_at, - expires_at: result.expires_at, - }; - - logger.info('입금 세션 조회 성공', { userId, status: session.status }); - return session; - } catch (error) { - logger.error('입금 세션 조회 실패', error as Error, { userId }); - return null; - } -} - -/** - * 입금 세션 저장 (생성 또는 업데이트) - * - * @param db - D1 Database - * @param session - DepositSession - */ -export async function saveDepositSession( - db: D1Database, - session: DepositSession -): Promise { - try { - const now = Date.now(); - const expiresAt = now + DEPOSIT_SESSION_TTL; - - await db.prepare(` - INSERT INTO deposit_sessions - (user_id, status, collected_info, messages, created_at, updated_at, expires_at) - VALUES (?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(user_id) DO UPDATE SET - status = excluded.status, - collected_info = excluded.collected_info, - messages = excluded.messages, - updated_at = excluded.updated_at, - expires_at = excluded.expires_at - `).bind( - session.user_id, - session.status, - JSON.stringify(session.collected_info || {}), - JSON.stringify(session.messages || []), - session.created_at || now, - now, - expiresAt - ).run(); - - logger.info('입금 세션 저장 성공', { userId: session.user_id, status: session.status }); - } catch (error) { - logger.error('입금 세션 저장 실패', error as Error, { userId: session.user_id }); - throw error; - } -} - -/** - * 입금 세션 삭제 - * - * @param db - D1 Database - * @param userId - Telegram User ID - */ -export async function deleteDepositSession( - db: D1Database, - userId: string -): Promise { - try { - await db.prepare('DELETE FROM deposit_sessions WHERE user_id = ?') - .bind(userId) - .run(); - logger.info('입금 세션 삭제 성공', { userId }); - } catch (error) { - logger.error('입금 세션 삭제 실패', error as Error, { userId }); - throw error; - } -} - -/** - * 새 입금 세션 생성 - * - * @param userId - Telegram User ID - * @param status - 세션 상태 - * @returns 새로운 DepositSession 객체 - */ -export function createDepositSession( - userId: string, - status: DepositSessionStatus = 'collecting_amount' -): DepositSession { - const now = Date.now(); - return { - user_id: userId, - status, - collected_info: {}, - messages: [], - created_at: now, - updated_at: now, - expires_at: now + DEPOSIT_SESSION_TTL, - }; -} - -/** - * 세션 만료 여부 확인 - * - * @param session - DepositSession - * @returns true if expired, false otherwise - */ -export function isSessionExpired(session: DepositSession): boolean { - return session.expires_at < Date.now(); -} - -/** - * 세션에 메시지 추가 - * - * @param session - DepositSession - * @param role - 메시지 역할 ('user' | 'assistant') - * @param content - 메시지 내용 - */ -export function addMessageToSession( - session: DepositSession, - role: 'user' | 'assistant', - content: string -): void { - session.messages.push({ role, content }); - - // 최대 메시지 수 제한 - if (session.messages.length > MAX_MESSAGES) { - session.messages = session.messages.slice(-MAX_MESSAGES); - logger.warn('세션 메시지 최대 개수 초과, 오래된 메시지 제거', { - userId: session.user_id, - maxMessages: MAX_MESSAGES, - }); - } -} +// Session manager instance +const sessionManager = new SessionManager(getSessionConfig('deposit')); /** * 입금 세션 존재 여부 확인 (라우팅용) @@ -204,8 +38,7 @@ export function addMessageToSession( * @returns true if active session exists, false otherwise */ export async function hasDepositSession(db: D1Database, userId: string): Promise { - const session = await getDepositSession(db, userId); - return session !== null && !isSessionExpired(session); + return await sessionManager.has(db, userId); } export interface DepositContext { @@ -286,28 +119,6 @@ const DEPOSIT_TOOLS = [ } ]; -// OpenAI API response types -interface OpenAIToolCall { - id: string; - type: 'function'; - function: { - name: string; - arguments: string; - }; -} - -interface OpenAIMessage { - role: 'assistant'; - content: string | null; - tool_calls?: OpenAIToolCall[]; -} - -interface OpenAIAPIResponse { - choices: Array<{ - message: OpenAIMessage; - finish_reason: string; - }>; -} /** * Deposit Expert AI 호출 (Function Calling 지원) @@ -523,15 +334,15 @@ export async function processDepositConsultation( try { // 1. Check for existing session - let session = await getDepositSession(db, userId); + let session = await sessionManager.get(db, userId); // 2. Create new session if none exists if (!session) { - session = createDepositSession(userId, 'collecting_amount'); + session = sessionManager.create(userId, 'collecting_amount'); } // 3. Add user message to session - addMessageToSession(session, 'user', userMessage); + sessionManager.addMessage(session, 'user', userMessage); // 4. Call AI to get response and possible tool calls const aiResult = await callDepositExpertAI(session, userMessage, env); @@ -589,15 +400,15 @@ export async function processDepositConsultation( // 9. Handle __SESSION_END__ - session complete if (finalResponse.includes('__SESSION_END__')) { logger.info('입금 상담 세션 종료', { userId }); - await deleteDepositSession(db, userId); + await sessionManager.delete(db, userId); finalResponse = finalResponse.replace('__SESSION_END__', '').trim(); return finalResponse; } // 10. Add assistant response to session and save - addMessageToSession(session, 'assistant', finalResponse); + sessionManager.addMessage(session, 'assistant', finalResponse); session.updated_at = Date.now(); - await saveDepositSession(db, session); + await sessionManager.save(db, session); logger.info('입금 상담 완료', { userId, diff --git a/src/agents/domain-agent.ts b/src/agents/domain-agent.ts index 28a662e..99272af 100644 --- a/src/agents/domain-agent.ts +++ b/src/agents/domain-agent.ts @@ -11,183 +11,13 @@ import type { Env, DomainSession, DomainSessionStatus } from '../types'; import { createLogger } from '../utils/logger'; import { executeDomainAction, executeSuggestDomains } from '../tools/domain-tool'; +import { DomainSessionManager } from '../utils/session-manager'; +import { getSessionConfig } from '../constants/agent-config'; const logger = createLogger('domain-agent'); -// D1 Session Management -const DOMAIN_SESSION_TTL = 60 * 60 * 1000; // 1시간 (도메인 작업은 시간이 더 필요) -const MAX_MESSAGES = 20; // 세션당 최대 메시지 수 - -/** - * D1에서 도메인 세션 조회 - * - * @param db - D1 Database - * @param userId - Telegram User ID - * @returns DomainSession 또는 null (세션 없거나 만료) - */ -export async function getDomainSession( - db: D1Database, - userId: string -): Promise { - try { - const now = Date.now(); - const result = await db.prepare( - 'SELECT * FROM domain_sessions WHERE user_id = ? AND expires_at > ?' - ).bind(userId, now).first<{ - user_id: string; - status: string; - collected_info: string | null; - target_domain: string | null; - messages: string | null; - created_at: number; - updated_at: number; - expires_at: number; - }>(); - - if (!result) { - logger.info('도메인 세션 없음', { userId }); - return null; - } - - const session: DomainSession = { - user_id: result.user_id, - status: result.status as DomainSessionStatus, - collected_info: result.collected_info ? JSON.parse(result.collected_info) : {}, - target_domain: result.target_domain || undefined, - messages: result.messages ? JSON.parse(result.messages) : [], - created_at: result.created_at, - updated_at: result.updated_at, - expires_at: result.expires_at, - }; - - logger.info('도메인 세션 조회 성공', { userId, status: session.status }); - return session; - } catch (error) { - logger.error('도메인 세션 조회 실패', error as Error, { userId }); - return null; - } -} - -/** - * 도메인 세션 저장 (생성 또는 업데이트) - * - * @param db - D1 Database - * @param session - DomainSession - */ -export async function saveDomainSession( - db: D1Database, - session: DomainSession -): Promise { - try { - const now = Date.now(); - const expiresAt = now + DOMAIN_SESSION_TTL; - - await db.prepare(` - INSERT INTO domain_sessions - (user_id, status, collected_info, target_domain, messages, created_at, updated_at, expires_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(user_id) DO UPDATE SET - status = excluded.status, - collected_info = excluded.collected_info, - target_domain = excluded.target_domain, - messages = excluded.messages, - updated_at = excluded.updated_at, - expires_at = excluded.expires_at - `).bind( - session.user_id, - session.status, - JSON.stringify(session.collected_info || {}), - session.target_domain || null, - JSON.stringify(session.messages || []), - session.created_at || now, - now, - expiresAt - ).run(); - - logger.info('도메인 세션 저장 성공', { userId: session.user_id, status: session.status }); - } catch (error) { - logger.error('도메인 세션 저장 실패', error as Error, { userId: session.user_id }); - throw error; - } -} - -/** - * 도메인 세션 삭제 - * - * @param db - D1 Database - * @param userId - Telegram User ID - */ -export async function deleteDomainSession( - db: D1Database, - userId: string -): Promise { - try { - await db.prepare('DELETE FROM domain_sessions WHERE user_id = ?') - .bind(userId) - .run(); - logger.info('도메인 세션 삭제 성공', { userId }); - } catch (error) { - logger.error('도메인 세션 삭제 실패', error as Error, { userId }); - throw error; - } -} - -/** - * 새 도메인 세션 생성 - * - * @param userId - Telegram User ID - * @param status - 세션 상태 - * @returns 새로운 DomainSession 객체 - */ -export function createDomainSession( - userId: string, - status: DomainSessionStatus = 'gathering' -): DomainSession { - const now = Date.now(); - return { - user_id: userId, - status, - collected_info: {}, - messages: [], - created_at: now, - updated_at: now, - expires_at: now + DOMAIN_SESSION_TTL, - }; -} - -/** - * 세션 만료 여부 확인 - * - * @param session - DomainSession - * @returns true if expired, false otherwise - */ -export function isSessionExpired(session: DomainSession): boolean { - return session.expires_at < Date.now(); -} - -/** - * 세션에 메시지 추가 - * - * @param session - DomainSession - * @param role - 메시지 역할 ('user' | 'assistant') - * @param content - 메시지 내용 - */ -export function addMessageToSession( - session: DomainSession, - role: 'user' | 'assistant', - content: string -): void { - session.messages.push({ role, content }); - - // 최대 메시지 수 제한 - if (session.messages.length > MAX_MESSAGES) { - session.messages = session.messages.slice(-MAX_MESSAGES); - logger.warn('세션 메시지 최대 개수 초과, 오래된 메시지 제거', { - userId: session.user_id, - maxMessages: MAX_MESSAGES, - }); - } -} +// Session manager instance +const sessionManager = new DomainSessionManager(getSessionConfig('domain')); // Domain Expert System Prompt const DOMAIN_EXPERT_PROMPT = `당신은 10년 경력의 도메인 컨설턴트입니다. @@ -306,28 +136,8 @@ const DOMAIN_TOOLS = [ } ]; -// OpenAI API response types -interface OpenAIToolCall { - id: string; - type: 'function'; - function: { - name: string; - arguments: string; - }; -} - -interface OpenAIMessage { - role: 'assistant'; - content: string | null; - tool_calls?: OpenAIToolCall[]; -} - -interface OpenAIAPIResponse { - choices: Array<{ - message: OpenAIMessage; - finish_reason: string; - }>; -} +// Import OpenAI types from centralized types +import type { OpenAIToolCall, OpenAIMessage, OpenAIAPIResponse } from '../types'; /** * Domain Expert AI 호출 (Function Calling 지원) @@ -598,16 +408,16 @@ export async function processDomainConsultation( try { // 1. Check for existing session - let session = await getDomainSession(db, userId); + let session = await sessionManager.get(db, userId); // 2. Create new session if none exists and message seems domain-related // (For first call, we always try to process - AI will return __PASSTHROUGH__ if not relevant) if (!session) { - session = createDomainSession(userId, 'gathering'); + session = sessionManager.create(userId, 'gathering'); } // 3. Add user message to session - addMessageToSession(session, 'user', userMessage); + sessionManager.addMessage(session, 'user', userMessage); // 4. Call AI to get response and possible tool calls const aiResult = await callDomainExpertAI(session, userMessage, env); @@ -648,15 +458,15 @@ export async function processDomainConsultation( // 8. Handle __SESSION_END__ - session complete if (finalResponse.includes('__SESSION_END__')) { logger.info('도메인 상담 세션 종료', { userId }); - await deleteDomainSession(db, userId); + await sessionManager.delete(db, userId); finalResponse = finalResponse.replace('__SESSION_END__', '').trim(); return finalResponse; } // 9. Add assistant response to session and save - addMessageToSession(session, 'assistant', finalResponse); + sessionManager.addMessage(session, 'assistant', finalResponse); session.updated_at = Date.now(); - await saveDomainSession(db, session); + await sessionManager.save(db, session); logger.info('도메인 상담 완료', { userId, @@ -680,6 +490,5 @@ export async function processDomainConsultation( * @returns true if active session exists, false otherwise */ export async function hasDomainSession(db: D1Database, userId: string): Promise { - const session = await getDomainSession(db, userId); - return session !== null && !isSessionExpired(session); + return await sessionManager.has(db, userId); } diff --git a/src/agents/server-agent.ts b/src/agents/server-agent.ts index 8174453..b826a84 100644 --- a/src/agents/server-agent.ts +++ b/src/agents/server-agent.ts @@ -15,74 +15,18 @@ * 4. Expected: Order confirmation */ -import type { Env, ServerSession, ServerSessionStatus, BandwidthInfo, RecommendResponse } from '../types'; +import type { Env, ServerSession, ServerSessionStatus, BandwidthInfo, RecommendResponse, OpenAIToolCall, OpenAIMessage, OpenAIAPIResponse } from '../types'; import { createLogger } from '../utils/logger'; import { executeSearchWeb, executeLookupDocs } from '../tools/search-tool'; import { formatTrafficInfo } from '../utils/formatters'; import { SERVER_CONSULTATION_STATUS, LANGUAGE_CODE } from '../constants'; +import { ServerSessionManager } from '../utils/session-manager'; +import { getSessionConfig } from '../constants/agent-config'; const logger = createLogger('server-agent'); -// D1 Session Management -const SERVER_SESSION_TTL_MS = 60 * 60 * 1000; // 1시간 - -/** - * 새 서버 세션 생성 - * - * @param userId - Telegram User ID - * @param status - 세션 상태 - * @returns 새로운 ServerSession 객체 - */ -export function createServerSession( - userId: string, - status: ServerSessionStatus = 'gathering' -): ServerSession { - const now = Date.now(); - return { - user_id: userId, - status, - collected_info: {}, - messages: [], - created_at: now, - updated_at: now, - expires_at: now + SERVER_SESSION_TTL_MS, - }; -} - -/** - * 세션 만료 여부 확인 - * - * @param session - ServerSession - * @returns true if expired, false otherwise - */ -export function isSessionExpired(session: ServerSession): boolean { - return session.expires_at < Date.now(); -} - -/** - * 세션에 메시지 추가 - * - * @param session - ServerSession - * @param role - 메시지 역할 ('user' | 'assistant') - * @param content - 메시지 내용 - */ -export function addMessageToSession( - session: ServerSession, - role: 'user' | 'assistant', - content: string -): void { - session.messages.push({ role, content }); - - // 최대 메시지 수 제한 (20개) - const MAX_MESSAGES = 20; - if (session.messages.length > MAX_MESSAGES) { - session.messages = session.messages.slice(-MAX_MESSAGES); - logger.warn('세션 메시지 최대 개수 초과, 오래된 메시지 제거', { - userId: session.user_id, - maxMessages: MAX_MESSAGES, - }); - } -} +// Session manager instance +const sessionManager = new ServerSessionManager(getSessionConfig('server')); /** * 서버 세션 존재 여부 확인 (라우팅용) @@ -92,122 +36,7 @@ export function addMessageToSession( * @returns true if active session exists, false otherwise */ export async function hasServerSession(db: D1Database, userId: string): Promise { - const session = await getServerSession(db, userId); - return session !== null && !isSessionExpired(session); -} - -/** - * D1에서 서버 세션 조회 - * - * @param db - D1 Database - * @param userId - Telegram User ID - * @returns ServerSession 또는 null (세션 없거나 만료) - */ -export async function getServerSession( - db: D1Database, - userId: string -): Promise { - try { - const now = Date.now(); - const result = await db.prepare( - 'SELECT * FROM server_sessions WHERE user_id = ? AND expires_at > ?' - ).bind(userId, now).first<{ - user_id: string; - status: string; - collected_info: string | null; - last_recommendation: string | null; - messages: string | null; - created_at: number; - updated_at: number; - expires_at: number; - }>(); - - if (!result) { - logger.info('세션 없음', { userId }); - return null; - } - - const session: ServerSession = { - user_id: result.user_id, - status: result.status as ServerSessionStatus, - collected_info: result.collected_info ? JSON.parse(result.collected_info) : {}, - last_recommendation: result.last_recommendation ? JSON.parse(result.last_recommendation) : undefined, - messages: result.messages ? JSON.parse(result.messages) : [], - created_at: result.created_at, - updated_at: result.updated_at, - expires_at: result.expires_at, - }; - - logger.info('세션 조회 성공', { userId, status: session.status, hasLastRecommendation: !!session.last_recommendation }); - return session; - } catch (error) { - logger.error('세션 조회 실패', error as Error, { userId }); - return null; - } -} - -/** - * 서버 세션 저장 (생성 또는 업데이트) - * - * @param db - D1 Database - * @param session - ServerSession - */ -export async function saveServerSession( - db: D1Database, - session: ServerSession -): Promise { - try { - const now = Date.now(); - const expiresAt = now + SERVER_SESSION_TTL_MS; - - await db.prepare(` - INSERT INTO server_sessions - (user_id, status, collected_info, last_recommendation, messages, created_at, updated_at, expires_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(user_id) DO UPDATE SET - status = excluded.status, - collected_info = excluded.collected_info, - last_recommendation = excluded.last_recommendation, - messages = excluded.messages, - updated_at = excluded.updated_at, - expires_at = excluded.expires_at - `).bind( - session.user_id, - session.status, - JSON.stringify(session.collected_info || {}), - session.last_recommendation ? JSON.stringify(session.last_recommendation) : null, - JSON.stringify(session.messages || []), - session.created_at || now, - now, - expiresAt - ).run(); - - logger.info('세션 저장 성공', { userId: session.user_id, status: session.status }); - } catch (error) { - logger.error('세션 저장 실패', error as Error, { userId: session.user_id }); - throw error; - } -} - -/** - * 서버 세션 삭제 - * - * @param db - D1 Database - * @param userId - Telegram User ID - */ -export async function deleteServerSession( - db: D1Database, - userId: string -): Promise { - try { - await db.prepare('DELETE FROM server_sessions WHERE user_id = ?') - .bind(userId) - .run(); - logger.info('세션 삭제 성공', { userId }); - } catch (error) { - logger.error('세션 삭제 실패', error as Error, { userId }); - throw error; - } + return await sessionManager.has(db, userId); } /** @@ -516,28 +345,6 @@ function inferExpectedUsers(scale: string, techStack?: string[]): number { return 10; // Default to personal } -// OpenAI API 응답 타입 -interface OpenAIToolCall { - id: string; - type: 'function'; - function: { - name: string; - arguments: string; - }; -} - -interface OpenAIMessage { - role: 'assistant'; - content: string | null; - tool_calls?: OpenAIToolCall[]; -} - -interface OpenAIAPIResponse { - choices: Array<{ - message: OpenAIMessage; - finish_reason: string; - }>; -} /** * Server Expert AI 호출 (Function Calling 지원) @@ -778,11 +585,11 @@ export async function processServerConsultation( try { // 1. Check for existing session - let session = await getServerSession(db, userId); + let session = await sessionManager.get(db, userId); // 2. Create new session if none exists if (!session) { - session = createServerSession(userId, 'gathering'); + session = sessionManager.create(userId, 'gathering'); } // ordering 상태에서 "신청" 외 메시지 입력 시 세션 정리 @@ -790,7 +597,7 @@ export async function processServerConsultation( // "신청"은 message-handler에서 처리, 여기까지 오면 다른 메시지임 const orderConfirmKey = `server_order_confirm:${session.user_id}`; await env.SESSION_KV?.delete(orderConfirmKey); - await deleteServerSession(db, session.user_id); + await sessionManager.delete(db, session.user_id); logger.info('주문 확인 세션 취소 (다른 메시지 입력)', { userId: session.user_id }); return '__PASSTHROUGH__'; // 일반 대화로 전환 @@ -800,7 +607,7 @@ export async function processServerConsultation( // "취소", "다시", "처음", "리셋", "초기화" 등 if (/^(취소|다시|처음|리셋|초기화)/.test(userMessage.trim()) || /취소할[게래]|다시\s*시작|처음부터/.test(userMessage)) { - await deleteServerSession(db, session.user_id); + await sessionManager.delete(db, session.user_id); logger.info('사용자 요청으로 상담 취소', { userId: session.user_id, previousStatus: session.status, @@ -811,14 +618,14 @@ export async function processServerConsultation( // "서버 추천" 키워드로 새로 시작 요청 (기존 세션 리셋) if (/서버\s*추천/.test(userMessage)) { - await deleteServerSession(db, session.user_id); + await sessionManager.delete(db, session.user_id); logger.info('서버 추천 키워드로 세션 리셋', { userId: session.user_id, previousStatus: session.status }); // 새 세션 생성하고 시작 메시지 반환 - const newSession = createServerSession(session.user_id, 'gathering'); - await saveServerSession(db, newSession); + const newSession = sessionManager.create(session.user_id, 'gathering'); + await sessionManager.save(db, newSession); return '안녕하세요! 서버 추천을 도와드리겠습니다. 😊\n\n어떤 서비스를 운영하실 건가요?\n\n1. 웹 서비스 (SaaS, 랜딩페이지)\n2. 모바일 앱 백엔드\n3. AI/ML 서비스 (챗봇, 모델 서빙)\n4. 게임 서버\n5. Discord/Telegram 봇\n6. 자동화 서버 (n8n, 크롤링)\n7. 미디어 스트리밍\n8. 개발/테스트 환경\n9. 데이터베이스 서버\n10. 기타 (직접 입력)\n\n번호나 용도를 말씀해주세요!'; } @@ -836,7 +643,7 @@ export async function processServerConsultation( // 명확히 다른 기능 요청인 경우 세션 종료하고 일반 처리로 전환 const unrelatedPatterns = /기억|날씨|계산|검색|도메인|입금|충전|잔액|시간|문서/; if (unrelatedPatterns.test(userMessage)) { - await deleteServerSession(db, session.user_id); + await sessionManager.delete(db, session.user_id); logger.info('무관한 요청으로 세션 자동 종료', { userId: session.user_id, message: userMessage.slice(0, 30) @@ -867,7 +674,7 @@ export async function processServerConsultation( // Mark session as ordering session.status = 'ordering'; - await saveServerSession(db, session); + await sessionManager.save(db, session); // 주문 확인 세션 저장 (텍스트 기반 확인) const orderConfirmKey = `server_order_confirm:${session.user_id}`; @@ -942,7 +749,7 @@ export async function processServerConsultation( // Mark session as recommending session.status = SERVER_CONSULTATION_STATUS.RECOMMENDING; - await saveServerSession(db, session); + await sessionManager.save(db, session); // 1. Call recommendation API (추천 먼저 받기) logger.info('추천 API 호출', { collectedInfo: session.collected_info }); @@ -1064,7 +871,7 @@ export async function processServerConsultation( // Mark session as selecting (사용자 선택 대기) session.status = SERVER_CONSULTATION_STATUS.SELECTING; - await saveServerSession(db, session); + await sessionManager.save(db, session); // 4. 추천 결과 + AI 검토 코멘트 (검토 코멘트는 마지막에) // __DIRECT__ 마커가 앞에 와야 제대로 처리됨 @@ -1072,14 +879,14 @@ export async function processServerConsultation( } else { // 추천 결과 없음 - 세션 삭제 session.status = SERVER_CONSULTATION_STATUS.COMPLETED; - await deleteServerSession(db, session.user_id); + await sessionManager.delete(db, session.user_id); return `${aiResult.message}\n\n조건에 맞는 서버를 찾지 못했습니다.`; } } else { // Continue gathering information session.status = SERVER_CONSULTATION_STATUS.GATHERING; - await saveServerSession(db, session); + await sessionManager.save(db, session); return aiResult.message; } @@ -1088,7 +895,7 @@ export async function processServerConsultation( // Clean up session on error (if exists) try { - await deleteServerSession(db, userId); + await sessionManager.delete(db, userId); } catch (deleteError) { logger.error('세션 삭제 실패 (무시)', deleteError as Error, { userId }); } diff --git a/src/agents/troubleshoot-agent.ts b/src/agents/troubleshoot-agent.ts index cf47ab3..b894eac 100644 --- a/src/agents/troubleshoot-agent.ts +++ b/src/agents/troubleshoot-agent.ts @@ -14,182 +14,16 @@ * 4. Expected: Session deleted */ -import type { Env, TroubleshootSession, TroubleshootSessionStatus } from '../types'; +import type { Env, TroubleshootSession, TroubleshootSessionStatus, OpenAIToolCall, OpenAIMessage, OpenAIAPIResponse } from '../types'; import { createLogger } from '../utils/logger'; import { executeSearchWeb, executeLookupDocs } from '../tools/search-tool'; +import { SessionManager } from '../utils/session-manager'; +import { getSessionConfig } from '../constants/agent-config'; const logger = createLogger('troubleshoot-agent'); -// D1 Session Management -const TROUBLESHOOT_SESSION_TTL_MS = 60 * 60 * 1000; // 1시간 -const MAX_MESSAGES = 20; // 세션당 최대 메시지 수 - -/** - * D1에서 트러블슈팅 세션 조회 - * - * @param db - D1 Database - * @param userId - Telegram User ID - * @returns TroubleshootSession 또는 null (세션 없거나 만료) - */ -export async function getTroubleshootSession( - db: D1Database, - userId: string -): Promise { - try { - const now = Date.now(); - const result = await db.prepare( - 'SELECT * FROM troubleshoot_sessions WHERE user_id = ? AND expires_at > ?' - ).bind(userId, now).first<{ - user_id: string; - status: string; - collected_info: string | null; - messages: string | null; - created_at: number; - updated_at: number; - expires_at: number; - }>(); - - if (!result) { - logger.info('트러블슈팅 세션 없음', { userId }); - return null; - } - - const session: TroubleshootSession = { - user_id: result.user_id, - status: result.status as TroubleshootSessionStatus, - collected_info: result.collected_info ? JSON.parse(result.collected_info) : {}, - messages: result.messages ? JSON.parse(result.messages) : [], - created_at: result.created_at, - updated_at: result.updated_at, - expires_at: result.expires_at, - }; - - logger.info('트러블슈팅 세션 조회 성공', { userId, status: session.status }); - return session; - } catch (error) { - logger.error('트러블슈팅 세션 조회 실패', error as Error, { userId }); - return null; - } -} - -/** - * 트러블슈팅 세션 저장 (생성 또는 업데이트) - * - * @param db - D1 Database - * @param session - TroubleshootSession - */ -export async function saveTroubleshootSession( - db: D1Database, - session: TroubleshootSession -): Promise { - try { - const now = Date.now(); - const expiresAt = now + TROUBLESHOOT_SESSION_TTL_MS; - - await db.prepare(` - INSERT INTO troubleshoot_sessions - (user_id, status, collected_info, messages, created_at, updated_at, expires_at) - VALUES (?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(user_id) DO UPDATE SET - status = excluded.status, - collected_info = excluded.collected_info, - messages = excluded.messages, - updated_at = excluded.updated_at, - expires_at = excluded.expires_at - `).bind( - session.user_id, - session.status, - JSON.stringify(session.collected_info || {}), - JSON.stringify(session.messages || []), - session.created_at || now, - now, - expiresAt - ).run(); - - logger.info('트러블슈팅 세션 저장 성공', { userId: session.user_id, status: session.status }); - } catch (error) { - logger.error('트러블슈팅 세션 저장 실패', error as Error, { userId: session.user_id }); - throw error; - } -} - -/** - * 트러블슈팅 세션 삭제 - * - * @param db - D1 Database - * @param userId - Telegram User ID - */ -export async function deleteTroubleshootSession( - db: D1Database, - userId: string -): Promise { - try { - await db.prepare('DELETE FROM troubleshoot_sessions WHERE user_id = ?') - .bind(userId) - .run(); - logger.info('트러블슈팅 세션 삭제 성공', { userId }); - } catch (error) { - logger.error('트러블슈팅 세션 삭제 실패', error as Error, { userId }); - throw error; - } -} - -/** - * 새 트러블슈팅 세션 생성 - * - * @param userId - Telegram User ID - * @param status - 세션 상태 - * @returns 새로운 TroubleshootSession 객체 - */ -export function createTroubleshootSession( - userId: string, - status: TroubleshootSessionStatus = 'gathering' -): TroubleshootSession { - const now = Date.now(); - return { - user_id: userId, - status, - collected_info: {}, - messages: [], - created_at: now, - updated_at: now, - expires_at: now + TROUBLESHOOT_SESSION_TTL_MS, - }; -} - -/** - * 세션 만료 여부 확인 - * - * @param session - TroubleshootSession - * @returns true if expired, false otherwise - */ -export function isSessionExpired(session: TroubleshootSession): boolean { - return session.expires_at < Date.now(); -} - -/** - * 세션에 메시지 추가 - * - * @param session - TroubleshootSession - * @param role - 메시지 역할 ('user' | 'assistant') - * @param content - 메시지 내용 - */ -export function addMessageToSession( - session: TroubleshootSession, - role: 'user' | 'assistant', - content: string -): void { - session.messages.push({ role, content }); - - // 최대 메시지 수 제한 - if (session.messages.length > MAX_MESSAGES) { - session.messages = session.messages.slice(-MAX_MESSAGES); - logger.warn('세션 메시지 최대 개수 초과, 오래된 메시지 제거', { - userId: session.user_id, - maxMessages: MAX_MESSAGES, - }); - } -} +// Session manager instance +const sessionManager = new SessionManager(getSessionConfig('troubleshoot')); /** * 트러블슈팅 세션 존재 여부 확인 (라우팅용) @@ -199,8 +33,7 @@ export function addMessageToSession( * @returns true if active session exists, false otherwise */ export async function hasTroubleshootSession(db: D1Database, userId: string): Promise { - const session = await getTroubleshootSession(db, userId); - return session !== null && !isSessionExpired(session); + return await sessionManager.has(db, userId); } // Troubleshoot Expert System Prompt @@ -325,28 +158,6 @@ async function executeTroubleshootTool( } } -// OpenAI API 응답 타입 -interface OpenAIToolCall { - id: string; - type: 'function'; - function: { - name: string; - arguments: string; - }; -} - -interface OpenAIMessage { - role: 'assistant'; - content: string | null; - tool_calls?: OpenAIToolCall[]; -} - -interface OpenAIAPIResponse { - choices: Array<{ - message: OpenAIMessage; - finish_reason: string; - }>; -} /** * Troubleshoot Expert AI 호출 (Function Calling 지원) @@ -503,15 +314,15 @@ export async function processTroubleshootConsultation( try { // 1. Check for existing session - let session = await getTroubleshootSession(db, userId); + let session = await sessionManager.get(db, userId); // 2. Create new session if none exists if (!session) { - session = createTroubleshootSession(userId, 'gathering'); + session = sessionManager.create(userId, 'gathering'); } // 3. Add user message to session - addMessageToSession(session, 'user', userMessage); + sessionManager.addMessage(session, 'user', userMessage); // 4. Call AI to get response and possible tool calls const aiResult = await callTroubleshootExpertAI(session, userMessage, env); @@ -526,12 +337,12 @@ export async function processTroubleshootConsultation( // 6. Handle __SESSION_END__ - session complete if (aiResult.response.includes('[세션 종료]')) { logger.info('트러블슈팅 상담 세션 종료', { userId }); - await deleteTroubleshootSession(db, userId); + await sessionManager.delete(db, userId); return aiResult.response.replace('[세션 종료]', '').trim(); } // 7. Add assistant response to session and save - addMessageToSession(session, 'assistant', aiResult.response); + sessionManager.addMessage(session, 'assistant', aiResult.response); // Update session status based on response content (simple heuristic) if (aiResult.response.includes('원인') || aiResult.response.includes('분석')) { @@ -541,7 +352,7 @@ export async function processTroubleshootConsultation( } session.updated_at = Date.now(); - await saveTroubleshootSession(db, session); + await sessionManager.save(db, session); logger.info('트러블슈팅 상담 완료', { userId,