diff --git a/migrations/007_add_troubleshoot_sessions.sql b/migrations/007_add_troubleshoot_sessions.sql new file mode 100644 index 0000000..033b76f --- /dev/null +++ b/migrations/007_add_troubleshoot_sessions.sql @@ -0,0 +1,14 @@ +-- Troubleshoot Agent Sessions (D1) +-- 기존 KV 세션을 D1로 마이그레이션 + +CREATE TABLE IF NOT EXISTS troubleshoot_sessions ( + user_id TEXT PRIMARY KEY, + status TEXT NOT NULL CHECK(status IN ('gathering', 'diagnosing', 'suggesting', 'completed')), + collected_info TEXT, + messages TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + expires_at INTEGER NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_troubleshoot_sessions_expires ON troubleshoot_sessions(expires_at); diff --git a/src/agents/troubleshoot-agent.ts b/src/agents/troubleshoot-agent.ts index 3c7c195..cf47ab3 100644 --- a/src/agents/troubleshoot-agent.ts +++ b/src/agents/troubleshoot-agent.ts @@ -3,7 +3,7 @@ * * 기능: * - 대화형 문제 진단 및 해결 - * - 세션 기반 정보 수집 + * - 세션 기반 정보 수집 (D1) * - 카테고리별 전문 솔루션 제공 * - Brave Search / Context7 도구로 최신 해결책 검색 * @@ -14,78 +14,252 @@ * 4. Expected: Session deleted */ -import type { Env, TroubleshootSession } from '../types'; +import type { Env, TroubleshootSession, TroubleshootSessionStatus } from '../types'; import { createLogger } from '../utils/logger'; import { executeSearchWeb, executeLookupDocs } from '../tools/search-tool'; -import { TROUBLESHOOT_STATUS } from '../constants'; const logger = createLogger('troubleshoot-agent'); -// KV Session Management -const SESSION_TTL = 3600; // 1 hour -const SESSION_KEY_PREFIX = 'troubleshoot_session:'; +// 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( - kv: KVNamespace, + db: D1Database, userId: string ): Promise { try { - const key = `${SESSION_KEY_PREFIX}${userId}`; - logger.info('세션 조회 시도', { userId, key }); - const data = await kv.get(key, 'json'); + 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 (!data) { - logger.info('세션 없음', { userId, key }); + if (!result) { + logger.info('트러블슈팅 세션 없음', { userId }); return null; } - logger.info('세션 조회 성공', { userId, key, status: (data as TroubleshootSession).status }); - return data as TroubleshootSession; + 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, key: `${SESSION_KEY_PREFIX}${userId}` }); + logger.error('트러블슈팅 세션 조회 실패', error as Error, { userId }); return null; } } +/** + * 트러블슈팅 세션 저장 (생성 또는 업데이트) + * + * @param db - D1 Database + * @param session - TroubleshootSession + */ export async function saveTroubleshootSession( - kv: KVNamespace, - userId: string, + db: D1Database, session: TroubleshootSession ): Promise { try { - const key = `${SESSION_KEY_PREFIX}${userId}`; - session.updatedAt = Date.now(); + const now = Date.now(); + const expiresAt = now + TROUBLESHOOT_SESSION_TTL_MS; - const sessionData = JSON.stringify(session); - logger.info('세션 저장 시도', { userId, key, status: session.status, dataLength: sessionData.length }); + 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(); - await kv.put(key, sessionData, { - expirationTtl: SESSION_TTL, - }); - - logger.info('세션 저장 성공', { userId, key, status: session.status }); + logger.info('트러블슈팅 세션 저장 성공', { userId: session.user_id, status: session.status }); } catch (error) { - logger.error('세션 저장 실패', error as Error, { userId, key: `${SESSION_KEY_PREFIX}${userId}` }); + logger.error('트러블슈팅 세션 저장 실패', error as Error, { userId: session.user_id }); throw error; } } +/** + * 트러블슈팅 세션 삭제 + * + * @param db - D1 Database + * @param userId - Telegram User ID + */ export async function deleteTroubleshootSession( - kv: KVNamespace, + db: D1Database, userId: string ): Promise { try { - const key = `${SESSION_KEY_PREFIX}${userId}`; - await kv.delete(key); - logger.info('세션 삭제 성공', { userId }); + await db.prepare('DELETE FROM troubleshoot_sessions WHERE user_id = ?') + .bind(userId) + .run(); + logger.info('트러블슈팅 세션 삭제 성공', { userId }); } catch (error) { - logger.error('세션 삭제 실패', error as Error, { userId }); + logger.error('트러블슈팅 세션 삭제 실패', error as Error, { userId }); throw error; } } -// Troubleshoot Expert AI Tools -const troubleshootTools = [ +/** + * 새 트러블슈팅 세션 생성 + * + * @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, + }); + } +} + +/** + * 트러블슈팅 세션 존재 여부 확인 (라우팅용) + * + * @param db - D1 Database + * @param userId - Telegram User ID + * @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); +} + +// Troubleshoot Expert System Prompt +const TROUBLESHOOT_EXPERT_PROMPT = `당신은 20년 경력의 시니어 DevOps/SRE 엔지니어입니다. + +## 전문성 (20년 경력) +- Linux/Windows Server 관리 마스터 (Apache, Nginx, IIS 등) +- 네트워크 트러블슈팅 전문가 (DNS, SSL, 방화벽, CDN) +- 애플리케이션 배포 및 디버깅 경험 수천 건 +- 모든 주요 기술 스택 경험 (Node.js, Python, PHP, Java, Ruby, Go 등) +- 데이터베이스 성능 튜닝 (MySQL, PostgreSQL, MongoDB, Redis) +- 컨테이너/오케스트레이션 (Docker, Kubernetes) +- 클라우드 인프라 마스터 (Anvil 전문) + +## 성격 +- 차분하고 체계적인 문제 해결 +- 비기술자도 이해하기 쉽게 설명 +- 단계별로 명확한 지시 +- 긴급 상황에서도 침착함 유지 +- 따뜻하고 격려하는 어조 + +## 금지 사항 (절대 위반 금지) +- AWS, GCP, Azure, DigitalOcean, Vultr, Linode 등 다른 클라우드 서비스 추천 금지 +- 경쟁사 서비스 언급 금지 +- 우리 서비스(Anvil)만 언급 + +## 문제 카테고리 +1. **Server/Infrastructure**: 서버 느림, 502/503 에러, 메모리 부족, CPU 과부하, 디스크 풀 +2. **Domain/DNS**: 도메인 연결 안됨, SSL 인증서 오류, DNS 전파 지연, 네임서버 문제 +3. **Code/Deploy**: 배포 실패, 빌드 에러, 의존성 충돌, 환경변수 누락 +4. **Network**: 연결 끊김, 타임아웃, CORS 오류, 방화벽 차단 +5. **Database**: 쿼리 느림, 연결 풀 고갈, 데드락, 인덱스 누락 + +## 도구 사용 가이드 (적극 활용) +- 에러 메시지, Stack trace 언급 시 → **반드시** search_solution 호출 +- 특정 프레임워크/라이브러리 문제 → lookup_docs 호출하여 공식 가이드 확인 +- 도구 결과를 자연스럽게 해결책에 포함 (예: "공식 문서에 따르면...", "최근 Stack Overflow 답변을 보니...") +- 검색 쿼리는 영문으로 (더 많은 결과) + +## 대화 흐름 +1. **문제 청취**: 사용자 증상 경청, 카테고리 자동 분류 +2. **정보 수집**: 1-2개 질문으로 환경/에러 메시지 확인 +3. **진단**: 수집된 정보 기반 원인 분석 (도구 활용) +4. **해결**: 단계별 명확한 솔루션 제시 (명령어 포함) +5. **확인**: 해결 여부 확인, 필요시 추가 지원 + +## 핵심 규칙 (반드시 준수) +- 에러 메시지가 명확하면 즉시 진단/해결 제시 +- 정보가 애매하면 최대 2개 질문 +- 해결책은 구체적이고 실행 가능한 명령어/코드 포함 +- 해결 후 "해결되셨나요?" 확인 +- 해결 안되면 추가 조치 또는 상위 엔지니어 에스컬레이션 제안 + +## 특수 지시 +- 트러블슈팅과 무관한 메시지가 들어오면 반드시 "__PASSTHROUGH__"만 응답 +- 문제 해결이 완료되면 "__SESSION_END__"를 응답 끝에 추가`; + +// Troubleshoot Tools for Function Calling +const TROUBLESHOOT_TOOLS = [ { type: 'function' as const, function: { @@ -174,12 +348,19 @@ interface OpenAIAPIResponse { }>; } -// OpenAI 호출 (트러블슈팅 전문가 AI with Function Calling) +/** + * Troubleshoot Expert AI 호출 (Function Calling 지원) + * + * @param session - TroubleshootSession + * @param userMessage - 사용자 메시지 + * @param env - Environment + * @returns AI 응답 및 tool_calls (있을 경우) + */ async function callTroubleshootExpertAI( - env: Env, session: TroubleshootSession, - userMessage: string -): Promise<{ action: 'question' | 'diagnose' | 'solve'; message: string; collectedInfo: TroubleshootSession['collectedInfo'] }> { + userMessage: string, + env: Env +): Promise<{ response: string; toolCalls?: Array<{ name: string; arguments: Record }> }> { if (!env.OPENAI_API_KEY) { throw new Error('OPENAI_API_KEY not configured'); } @@ -192,81 +373,19 @@ async function callTroubleshootExpertAI( content: m.content, })); - const systemPrompt = `당신은 20년 경력의 시니어 DevOps/SRE 엔지니어입니다. - -## 전문성 (20년 경력) -- Linux/Windows Server 관리 마스터 (Apache, Nginx, IIS 등) -- 네트워크 트러블슈팅 전문가 (DNS, SSL, 방화벽, CDN) -- 애플리케이션 배포 및 디버깅 경험 수천 건 -- 모든 주요 기술 스택 경험 (Node.js, Python, PHP, Java, Ruby, Go 등) -- 데이터베이스 성능 튜닝 (MySQL, PostgreSQL, MongoDB, Redis) -- 컨테이너/오케스트레이션 (Docker, Kubernetes) -- 클라우드 인프라 마스터 (Anvil 전문) - -## 성격 -- 차분하고 체계적인 문제 해결 -- 비기술자도 이해하기 쉽게 설명 -- 단계별로 명확한 지시 -- 긴급 상황에서도 침착함 유지 -- 따뜻하고 격려하는 어조 - -## 금지 사항 (절대 위반 금지) -- AWS, GCP, Azure, DigitalOcean, Vultr, Linode 등 다른 클라우드 서비스 추천 금지 -- 경쟁사 서비스 언급 금지 -- 우리 서비스(Anvil)만 언급 - -## 문제 카테고리 -1. **Server/Infrastructure**: 서버 느림, 502/503 에러, 메모리 부족, CPU 과부하, 디스크 풀 -2. **Domain/DNS**: 도메인 연결 안됨, SSL 인증서 오류, DNS 전파 지연, 네임서버 문제 -3. **Code/Deploy**: 배포 실패, 빌드 에러, 의존성 충돌, 환경변수 누락 -4. **Network**: 연결 끊김, 타임아웃, CORS 오류, 방화벽 차단 -5. **Database**: 쿼리 느림, 연결 풀 고갈, 데드락, 인덱스 누락 - -## 도구 사용 가이드 (적극 활용) -- 에러 메시지, Stack trace 언급 시 → **반드시** search_solution 호출 -- 특정 프레임워크/라이브러리 문제 → lookup_docs 호출하여 공식 가이드 확인 -- 도구 결과를 자연스럽게 해결책에 포함 (예: "공식 문서에 따르면...", "최근 Stack Overflow 답변을 보니...") -- 검색 쿼리는 영문으로 (더 많은 결과) - -## 대화 흐름 -1. **문제 청취**: 사용자 증상 경청, 카테고리 자동 분류 -2. **정보 수집**: 1-2개 질문으로 환경/에러 메시지 확인 -3. **진단**: 수집된 정보 기반 원인 분석 (도구 활용) -4. **해결**: 단계별 명확한 솔루션 제시 (명령어 포함) -5. **확인**: 해결 여부 확인, 필요시 추가 지원 - -## 핵심 규칙 (반드시 준수) -- 에러 메시지가 명확하면 즉시 action="diagnose" 또는 "solve"로 진단/해결 제시 -- 정보가 애매하면 action="question"으로 최대 2개 질문 -- 해결책은 구체적이고 실행 가능한 명령어/코드 포함 -- 해결 후 "해결되셨나요?" 확인 -- 해결 안되면 추가 조치 또는 상위 엔지니어 에스컬레이션 제안 + const systemPrompt = `${TROUBLESHOOT_EXPERT_PROMPT} ## 현재 수집된 정보 -${JSON.stringify(session.collectedInfo, null, 2)} - -## 응답 형식 (반드시 JSON만 반환, 다른 텍스트 절대 금지) -{ - "action": "question" | "diagnose" | "solve", - "message": "사용자에게 보여줄 메시지 (도구 결과 자연스럽게 포함)", - "collectedInfo": { - "category": "카테고리 (자동 분류)", - "symptoms": "증상 요약", - "environment": "환경 정보 (OS, 프레임워크 등)", - "errorMessage": "에러 메시지 (있는 경우)" - } -} - -action 선택 기준: -- "question": 정보가 부족하여 추가 질문 필요 (최대 2회) -- "diagnose": 정보 충분, 원인 분석 제시 -- "solve": 즉시 해결책 제시 가능 - -중요: 20년 경험으로 일반적인 문제는 즉시 solve 가능합니다.`; +${JSON.stringify(session.collected_info, null, 2)}`; try { - // Messages array that we'll build up with tool results - const messages: Array<{ role: string; content: string | null; tool_calls?: OpenAIToolCall[]; tool_call_id?: string; name?: string }> = [ + const messages: Array<{ + role: string; + content: string | null; + tool_calls?: OpenAIToolCall[]; + tool_call_id?: string; + name?: string + }> = [ { role: 'system', content: systemPrompt }, ...conversationHistory, { role: 'user', content: userMessage }, @@ -280,9 +399,8 @@ action 선택 기준: const requestBody = { model: 'gpt-4o-mini', messages, - tools: troubleshootTools, + tools: TROUBLESHOOT_TOOLS, tool_choice: 'auto', - response_format: { type: 'json_object' }, max_tokens: 1500, temperature: 0.5, }; @@ -336,39 +454,28 @@ action 선택 기준: continue; } - // No tool calls - parse the final response + // No tool calls - return final response const aiResponse = assistantMessage.content || ''; - logger.info('AI 응답', { response: aiResponse.slice(0, 200), toolCallCount }); + logger.info('AI 응답', { response: aiResponse.slice(0, 200) }); - // JSON 파싱 (마크다운 코드 블록 제거) - const jsonMatch = aiResponse.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/) || - aiResponse.match(/(\{[\s\S]*\})/); - - if (!jsonMatch) { - logger.error('JSON 파싱 실패', new Error('No JSON found'), { response: aiResponse }); - throw new Error('AI 응답 형식 오류'); + // Check for special markers + if (aiResponse.includes('__PASSTHROUGH__')) { + return { response: '__PASSTHROUGH__' }; } - const parsed = JSON.parse(jsonMatch[1]); - - // Validate response structure - if (!parsed.action || !parsed.message) { - throw new Error('Invalid AI response structure'); - } + // Check for session end marker + const sessionEnd = aiResponse.includes('__SESSION_END__'); + const cleanResponse = aiResponse.replace('__SESSION_END__', '').trim(); return { - action: parsed.action, - message: parsed.message, - collectedInfo: parsed.collectedInfo || session.collectedInfo, + response: sessionEnd ? `${cleanResponse}\n\n[세션 종료]` : cleanResponse, }; } - // Max tool calls reached, force a solve + // Max tool calls reached logger.warn('최대 도구 호출 횟수 도달', { toolCallCount }); return { - action: 'solve', - message: '수집한 정보를 바탕으로 해결책을 제시해드리겠습니다.', - collectedInfo: session.collectedInfo, + response: '수집한 정보를 바탕으로 해결책을 제시해드리겠습니다.', }; } catch (error) { logger.error('Troubleshoot Expert AI 호출 실패', error as Error); @@ -376,82 +483,76 @@ action 선택 기준: } } -// Main troubleshooting processing -export async function processTroubleshoot( +/** + * 트러블슈팅 상담 처리 (메인 함수) + * + * @param db - D1 Database + * @param userId - Telegram User ID + * @param userMessage - 사용자 메시지 + * @param env - Environment + * @returns AI 응답 메시지 + */ +export async function processTroubleshootConsultation( + db: D1Database, + userId: string, userMessage: string, - session: TroubleshootSession, env: Env ): Promise { + const startTime = Date.now(); + logger.info('트러블슈팅 상담 시작', { userId, message: userMessage.substring(0, 100) }); + try { - logger.info('트러블슈팅 처리 시작', { - userId: session.telegramUserId, - message: userMessage.slice(0, 50), - status: session.status - }); + // 1. Check for existing session + let session = await getTroubleshootSession(db, userId); - // 취소 키워드 처리 (모든 상태에서 작동) - if (/^(취소|다시|처음|리셋|초기화)/.test(userMessage.trim()) || - /취소할[게래]|다시\s*시작|처음부터/.test(userMessage)) { - await deleteTroubleshootSession(env.SESSION_KV, session.telegramUserId); - logger.info('사용자 요청으로 트러블슈팅 취소', { - userId: session.telegramUserId, - previousStatus: session.status, - trigger: userMessage.slice(0, 20) - }); - return '트러블슈팅이 취소되었습니다. 다시 시작하려면 문제를 말씀해주세요.'; + // 2. Create new session if none exists + if (!session) { + session = createTroubleshootSession(userId, 'gathering'); } - // 해결 완료 키워드 - if (/해결[됐됨했함]|고마워|감사|끝|완료/.test(userMessage)) { - await deleteTroubleshootSession(env.SESSION_KV, session.telegramUserId); - logger.info('문제 해결 완료', { - userId: session.telegramUserId, - category: session.collectedInfo.category - }); - return '✅ 문제가 해결되어 다행입니다! 앞으로도 언제든 도움이 필요하시면 말씀해주세요. 😊'; - } + // 3. Add user message to session + addMessageToSession(session, 'user', userMessage); - // 상담과 무관한 키워드 감지 (passthrough) - const unrelatedPatterns = /서버\s*추천|날씨|계산|도메인\s*추천|입금|충전|잔액|기억|저장/; - if (unrelatedPatterns.test(userMessage)) { - await deleteTroubleshootSession(env.SESSION_KV, session.telegramUserId); - logger.info('무관한 요청으로 세션 자동 종료', { - userId: session.telegramUserId, - message: userMessage.slice(0, 30) - }); + // 4. Call AI to get response and possible tool calls + const aiResult = await callTroubleshootExpertAI(session, userMessage, env); + + // 5. Handle __PASSTHROUGH__ - not troubleshoot related + if (aiResult.response === '__PASSTHROUGH__' || aiResult.response.includes('__PASSTHROUGH__')) { + logger.info('트러블슈팅 상담 패스스루', { userId }); + // Don't save session if passthrough return '__PASSTHROUGH__'; } - // Add user message to history - session.messages.push({ role: 'user', content: userMessage }); - - // Call Troubleshoot Expert AI - const aiResult = await callTroubleshootExpertAI(env, session, userMessage); - - // Update collected info - session.collectedInfo = { ...session.collectedInfo, ...aiResult.collectedInfo }; - - // Add AI response to history - session.messages.push({ role: 'assistant', content: aiResult.message }); - - // Update session status based on action - if (aiResult.action === 'diagnose') { - session.status = TROUBLESHOOT_STATUS.DIAGNOSING; - } else if (aiResult.action === 'solve') { - session.status = TROUBLESHOOT_STATUS.SOLVING; - } else { - session.status = TROUBLESHOOT_STATUS.GATHERING; + // 6. Handle __SESSION_END__ - session complete + if (aiResult.response.includes('[세션 종료]')) { + logger.info('트러블슈팅 상담 세션 종료', { userId }); + await deleteTroubleshootSession(db, userId); + return aiResult.response.replace('[세션 종료]', '').trim(); } - await saveTroubleshootSession(env.SESSION_KV, session.telegramUserId, session); + // 7. Add assistant response to session and save + addMessageToSession(session, 'assistant', aiResult.response); + + // Update session status based on response content (simple heuristic) + if (aiResult.response.includes('원인') || aiResult.response.includes('분석')) { + session.status = 'diagnosing'; + } else if (aiResult.response.includes('해결') || aiResult.response.includes('방법')) { + session.status = 'suggesting'; + } + + session.updated_at = Date.now(); + await saveTroubleshootSession(db, session); + + logger.info('트러블슈팅 상담 완료', { + userId, + duration: Date.now() - startTime, + status: session.status + }); + + return aiResult.response; - return aiResult.message; } catch (error) { - logger.error('트러블슈팅 처리 실패', error as Error, { userId: session.telegramUserId }); - - // Clean up session on error - await deleteTroubleshootSession(env.SESSION_KV, session.telegramUserId); - - return '죄송합니다. 트러블슈팅 중 오류가 발생했습니다.\n다시 시도하려면 문제를 말씀해주세요.'; + logger.error('트러블슈팅 상담 오류', error as Error, { userId }); + return '죄송합니다. 트러블슈팅 상담 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'; } } diff --git a/src/openai-service.ts b/src/openai-service.ts index 30b6d66..49dae1a 100644 --- a/src/openai-service.ts +++ b/src/openai-service.ts @@ -7,7 +7,7 @@ import { metrics } from './utils/metrics'; import { getOpenAIUrl } from './utils/api-urls'; import { ERROR_MESSAGES } from './constants/messages'; import { getServerSession, processServerConsultation } from './agents/server-agent'; -import { getTroubleshootSession, processTroubleshoot } from './agents/troubleshoot-agent'; +import { processTroubleshootConsultation, hasTroubleshootSession } from './agents/troubleshoot-agent'; import { processDomainConsultation, hasDomainSession } from './agents/domain-agent'; import { processDepositConsultation, hasDepositSession } from './agents/deposit-agent'; import { sendMessage } from './telegram'; @@ -246,18 +246,17 @@ export async function generateOpenAIResponse( // Check if troubleshoot session is active try { - const troubleshootSession = await getTroubleshootSession(env.SESSION_KV, telegramUserId); + const hasTroubleshootSess = await hasTroubleshootSession(env.DB, telegramUserId); - if (troubleshootSession && troubleshootSession.status !== 'completed') { - logger.info('Active troubleshoot session detected, routing to troubleshoot', { - userId: telegramUserId, - status: troubleshootSession.status + if (hasTroubleshootSess) { + logger.info('트러블슈팅 세션 감지, 트러블슈팅 에이전트로 라우팅', { + userId: telegramUserId }); - const result = await processTroubleshoot(userMessage, troubleshootSession, env); + const troubleshootResponse = await processTroubleshootConsultation(env.DB, telegramUserId, userMessage, env); // PASSTHROUGH: 무관한 메시지는 일반 처리로 전환 - if (result !== '__PASSTHROUGH__') { - return result; + if (troubleshootResponse !== '__PASSTHROUGH__') { + return troubleshootResponse; } // Continue to normal flow below } diff --git a/src/tools/troubleshoot-tool.ts b/src/tools/troubleshoot-tool.ts index 8849501..0720e5a 100644 --- a/src/tools/troubleshoot-tool.ts +++ b/src/tools/troubleshoot-tool.ts @@ -31,37 +31,30 @@ export async function executeManageTroubleshoot( logger.info('트러블슈팅 도구 호출', { action, userId: telegramUserId }); - if (!env?.SESSION_KV || !telegramUserId) { + if (!env?.DB || !telegramUserId) { return '🚫 트러블슈팅 기능을 사용할 수 없습니다.'; } - const { getTroubleshootSession, saveTroubleshootSession, deleteTroubleshootSession } = await import('../agents/troubleshoot-agent'); + const { getTroubleshootSession, createTroubleshootSession, saveTroubleshootSession, deleteTroubleshootSession } = await import('../agents/troubleshoot-agent'); if (action === 'cancel') { - await deleteTroubleshootSession(env.SESSION_KV, telegramUserId); + await deleteTroubleshootSession(env.DB, telegramUserId); return '✅ 트러블슈팅 세션이 취소되었습니다.'; } // action === 'start' - const existingSession = await getTroubleshootSession(env.SESSION_KV, telegramUserId); + const existingSession = await getTroubleshootSession(env.DB, telegramUserId); if (existingSession && existingSession.status !== 'completed') { return '이미 진행 중인 트러블슈팅 세션이 있습니다. 계속 진행해주세요.\n\n현재까지 파악된 정보:\n' + - (existingSession.collectedInfo.category ? `• 분류: ${existingSession.collectedInfo.category}\n` : '') + - (existingSession.collectedInfo.symptoms ? `• 증상: ${existingSession.collectedInfo.symptoms}\n` : ''); + (existingSession.collected_info.category ? `• 분류: ${existingSession.collected_info.category}\n` : '') + + (existingSession.collected_info.symptoms ? `• 증상: ${existingSession.collected_info.symptoms}\n` : ''); } // Create new session - const newSession = { - telegramUserId, - status: 'gathering' as const, - collectedInfo: {}, - messages: [], - createdAt: Date.now(), - updatedAt: Date.now(), - }; + const newSession = createTroubleshootSession(telegramUserId, 'gathering'); - await saveTroubleshootSession(env.SESSION_KV, telegramUserId, newSession); + await saveTroubleshootSession(env.DB, newSession); logger.info('트러블슈팅 세션 시작', { userId: telegramUserId }); diff --git a/src/types.ts b/src/types.ts index 749e9e7..3e74c15 100644 --- a/src/types.ts +++ b/src/types.ts @@ -283,19 +283,27 @@ export interface ServerSession { }; } -// Troubleshooting Session +// Troubleshooting Session Status +export type TroubleshootSessionStatus = + | 'gathering' // 정보 수집 중 + | 'diagnosing' // 원인 분석 중 + | 'suggesting' // 해결책 제시 중 + | 'completed'; // 완료 + +// Troubleshooting Session (D1) export interface TroubleshootSession { - telegramUserId: string; - status: 'gathering' | 'diagnosing' | 'solving' | 'completed'; - collectedInfo: { + user_id: string; + status: TroubleshootSessionStatus; + collected_info: { category?: string; // Server/Infrastructure, Domain/DNS, Code/Deploy, Network, Database symptoms?: string; // 증상 요약 environment?: string; // OS, 프레임워크, 버전 등 errorMessage?: string; // 에러 메시지 }; messages: Array<{ role: 'user' | 'assistant'; content: string }>; - createdAt: number; - updatedAt: number; + created_at: number; + updated_at: number; + expires_at: number; } // Domain Agent Session Status