refactor: migrate troubleshoot-agent from KV to D1

- Create troubleshoot_sessions table in D1
- Replace KV session storage with D1
- Unify field names to snake_case (user_id, collected_info, created_at, updated_at, expires_at)
- Add __PASSTHROUGH__/__SESSION_END__ marker support
- Change handler signature to match domain/deposit pattern
- Extract system prompt to constant TROUBLESHOOT_EXPERT_PROMPT
- Add hasTroubleshootSession() for routing
- Update openai-service.ts to use new D1-based functions
- Update troubleshoot-tool.ts to use D1 instead of KV
- Add TroubleshootSessionStatus type

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-02-05 10:44:10 +09:00
parent 2bd9bc4c2b
commit 5d8150e67c
5 changed files with 340 additions and 225 deletions

View File

@@ -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);

View File

@@ -3,7 +3,7 @@
* *
* 기능: * 기능:
* - 대화형 문제 진단 및 해결 * - 대화형 문제 진단 및 해결
* - 세션 기반 정보 수집 * - 세션 기반 정보 수집 (D1)
* - 카테고리별 전문 솔루션 제공 * - 카테고리별 전문 솔루션 제공
* - Brave Search / Context7 도구로 최신 해결책 검색 * - Brave Search / Context7 도구로 최신 해결책 검색
* *
@@ -14,78 +14,252 @@
* 4. Expected: Session deleted * 4. Expected: Session deleted
*/ */
import type { Env, TroubleshootSession } from '../types'; import type { Env, TroubleshootSession, TroubleshootSessionStatus } from '../types';
import { createLogger } from '../utils/logger'; import { createLogger } from '../utils/logger';
import { executeSearchWeb, executeLookupDocs } from '../tools/search-tool'; import { executeSearchWeb, executeLookupDocs } from '../tools/search-tool';
import { TROUBLESHOOT_STATUS } from '../constants';
const logger = createLogger('troubleshoot-agent'); const logger = createLogger('troubleshoot-agent');
// KV Session Management // D1 Session Management
const SESSION_TTL = 3600; // 1 hour const TROUBLESHOOT_SESSION_TTL_MS = 60 * 60 * 1000; // 1시간
const SESSION_KEY_PREFIX = 'troubleshoot_session:'; const MAX_MESSAGES = 20; // 세션당 최대 메시지 수
/**
* D1에서 트러블슈팅 세션 조회
*
* @param db - D1 Database
* @param userId - Telegram User ID
* @returns TroubleshootSession 또는 null (세션 없거나 만료)
*/
export async function getTroubleshootSession( export async function getTroubleshootSession(
kv: KVNamespace, db: D1Database,
userId: string userId: string
): Promise<TroubleshootSession | null> { ): Promise<TroubleshootSession | null> {
try { try {
const key = `${SESSION_KEY_PREFIX}${userId}`; const now = Date.now();
logger.info('세션 조회 시도', { userId, key }); const result = await db.prepare(
const data = await kv.get(key, 'json'); '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) { if (!result) {
logger.info('세션 없음', { userId, key }); logger.info('트러블슈팅 세션 없음', { userId });
return null; return null;
} }
logger.info('세션 조회 성공', { userId, key, status: (data as TroubleshootSession).status }); const session: TroubleshootSession = {
return data as 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) { } catch (error) {
logger.error('세션 조회 실패', error as Error, { userId, key: `${SESSION_KEY_PREFIX}${userId}` }); logger.error('트러블슈팅 세션 조회 실패', error as Error, { userId });
return null; return null;
} }
} }
/**
* 트러블슈팅 세션 저장 (생성 또는 업데이트)
*
* @param db - D1 Database
* @param session - TroubleshootSession
*/
export async function saveTroubleshootSession( export async function saveTroubleshootSession(
kv: KVNamespace, db: D1Database,
userId: string,
session: TroubleshootSession session: TroubleshootSession
): Promise<void> { ): Promise<void> {
try { try {
const key = `${SESSION_KEY_PREFIX}${userId}`; const now = Date.now();
session.updatedAt = Date.now(); const expiresAt = now + TROUBLESHOOT_SESSION_TTL_MS;
const sessionData = JSON.stringify(session); await db.prepare(`
logger.info('세션 저장 시도', { userId, key, status: session.status, dataLength: sessionData.length }); 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, { logger.info('트러블슈팅 세션 저장 성공', { userId: session.user_id, status: session.status });
expirationTtl: SESSION_TTL,
});
logger.info('세션 저장 성공', { userId, key, status: session.status });
} catch (error) { } catch (error) {
logger.error('세션 저장 실패', error as Error, { userId, key: `${SESSION_KEY_PREFIX}${userId}` }); logger.error('트러블슈팅 세션 저장 실패', error as Error, { userId: session.user_id });
throw error; throw error;
} }
} }
/**
* 트러블슈팅 세션 삭제
*
* @param db - D1 Database
* @param userId - Telegram User ID
*/
export async function deleteTroubleshootSession( export async function deleteTroubleshootSession(
kv: KVNamespace, db: D1Database,
userId: string userId: string
): Promise<void> { ): Promise<void> {
try { try {
const key = `${SESSION_KEY_PREFIX}${userId}`; await db.prepare('DELETE FROM troubleshoot_sessions WHERE user_id = ?')
await kv.delete(key); .bind(userId)
logger.info('세션 삭제 성공', { userId }); .run();
logger.info('트러블슈팅 세션 삭제 성공', { userId });
} catch (error) { } catch (error) {
logger.error('세션 삭제 실패', error as Error, { userId }); logger.error('트러블슈팅 세션 삭제 실패', error as Error, { userId });
throw error; 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<boolean> {
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, type: 'function' as const,
function: { 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( async function callTroubleshootExpertAI(
env: Env,
session: TroubleshootSession, session: TroubleshootSession,
userMessage: string userMessage: string,
): Promise<{ action: 'question' | 'diagnose' | 'solve'; message: string; collectedInfo: TroubleshootSession['collectedInfo'] }> { env: Env
): Promise<{ response: string; toolCalls?: Array<{ name: string; arguments: Record<string, unknown> }> }> {
if (!env.OPENAI_API_KEY) { if (!env.OPENAI_API_KEY) {
throw new Error('OPENAI_API_KEY not configured'); throw new Error('OPENAI_API_KEY not configured');
} }
@@ -192,81 +373,19 @@ async function callTroubleshootExpertAI(
content: m.content, content: m.content,
})); }));
const systemPrompt = `당신은 20년 경력의 시니어 DevOps/SRE 엔지니어입니다. const systemPrompt = `${TROUBLESHOOT_EXPERT_PROMPT}
## 전문성 (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개 질문
- 해결책은 구체적이고 실행 가능한 명령어/코드 포함
- 해결 후 "해결되셨나요?" 확인
- 해결 안되면 추가 조치 또는 상위 엔지니어 에스컬레이션 제안
## 현재 수집된 정보 ## 현재 수집된 정보
${JSON.stringify(session.collectedInfo, null, 2)} ${JSON.stringify(session.collected_info, null, 2)}`;
## 응답 형식 (반드시 JSON만 반환, 다른 텍스트 절대 금지)
{
"action": "question" | "diagnose" | "solve",
"message": "사용자에게 보여줄 메시지 (도구 결과 자연스럽게 포함)",
"collectedInfo": {
"category": "카테고리 (자동 분류)",
"symptoms": "증상 요약",
"environment": "환경 정보 (OS, 프레임워크 등)",
"errorMessage": "에러 메시지 (있는 경우)"
}
}
action 선택 기준:
- "question": 정보가 부족하여 추가 질문 필요 (최대 2회)
- "diagnose": 정보 충분, 원인 분석 제시
- "solve": 즉시 해결책 제시 가능
중요: 20년 경험으로 일반적인 문제는 즉시 solve 가능합니다.`;
try { try {
// Messages array that we'll build up with tool results const messages: Array<{
const messages: Array<{ role: string; content: string | null; tool_calls?: OpenAIToolCall[]; tool_call_id?: string; name?: string }> = [ role: string;
content: string | null;
tool_calls?: OpenAIToolCall[];
tool_call_id?: string;
name?: string
}> = [
{ role: 'system', content: systemPrompt }, { role: 'system', content: systemPrompt },
...conversationHistory, ...conversationHistory,
{ role: 'user', content: userMessage }, { role: 'user', content: userMessage },
@@ -280,9 +399,8 @@ action 선택 기준:
const requestBody = { const requestBody = {
model: 'gpt-4o-mini', model: 'gpt-4o-mini',
messages, messages,
tools: troubleshootTools, tools: TROUBLESHOOT_TOOLS,
tool_choice: 'auto', tool_choice: 'auto',
response_format: { type: 'json_object' },
max_tokens: 1500, max_tokens: 1500,
temperature: 0.5, temperature: 0.5,
}; };
@@ -336,39 +454,28 @@ action 선택 기준:
continue; continue;
} }
// No tool calls - parse the final response // No tool calls - return final response
const aiResponse = assistantMessage.content || ''; const aiResponse = assistantMessage.content || '';
logger.info('AI 응답', { response: aiResponse.slice(0, 200), toolCallCount }); logger.info('AI 응답', { response: aiResponse.slice(0, 200) });
// JSON 파싱 (마크다운 코드 블록 제거) // Check for special markers
const jsonMatch = aiResponse.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/) || if (aiResponse.includes('__PASSTHROUGH__')) {
aiResponse.match(/(\{[\s\S]*\})/); return { response: '__PASSTHROUGH__' };
if (!jsonMatch) {
logger.error('JSON 파싱 실패', new Error('No JSON found'), { response: aiResponse });
throw new Error('AI 응답 형식 오류');
} }
const parsed = JSON.parse(jsonMatch[1]); // Check for session end marker
const sessionEnd = aiResponse.includes('__SESSION_END__');
// Validate response structure const cleanResponse = aiResponse.replace('__SESSION_END__', '').trim();
if (!parsed.action || !parsed.message) {
throw new Error('Invalid AI response structure');
}
return { return {
action: parsed.action, response: sessionEnd ? `${cleanResponse}\n\n[세션 종료]` : cleanResponse,
message: parsed.message,
collectedInfo: parsed.collectedInfo || session.collectedInfo,
}; };
} }
// Max tool calls reached, force a solve // Max tool calls reached
logger.warn('최대 도구 호출 횟수 도달', { toolCallCount }); logger.warn('최대 도구 호출 횟수 도달', { toolCallCount });
return { return {
action: 'solve', response: '수집한 정보를 바탕으로 해결책을 제시해드리겠습니다.',
message: '수집한 정보를 바탕으로 해결책을 제시해드리겠습니다.',
collectedInfo: session.collectedInfo,
}; };
} catch (error) { } catch (error) {
logger.error('Troubleshoot Expert AI 호출 실패', error as 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, userMessage: string,
session: TroubleshootSession,
env: Env env: Env
): Promise<string> { ): Promise<string> {
const startTime = Date.now();
logger.info('트러블슈팅 상담 시작', { userId, message: userMessage.substring(0, 100) });
try { try {
logger.info('트러블슈팅 처리 시작', { // 1. Check for existing session
userId: session.telegramUserId, let session = await getTroubleshootSession(db, userId);
message: userMessage.slice(0, 50),
status: session.status
});
// 취소 키워드 처리 (모든 상태에서 작동) // 2. Create new session if none exists
if (/^(취소|다시|처음|리셋|초기화)/.test(userMessage.trim()) || if (!session) {
/취소할[게래]|다시\s*시작|처음부터/.test(userMessage)) { session = createTroubleshootSession(userId, 'gathering');
await deleteTroubleshootSession(env.SESSION_KV, session.telegramUserId);
logger.info('사용자 요청으로 트러블슈팅 취소', {
userId: session.telegramUserId,
previousStatus: session.status,
trigger: userMessage.slice(0, 20)
});
return '트러블슈팅이 취소되었습니다. 다시 시작하려면 문제를 말씀해주세요.';
} }
// 해결 완료 키워드 // 3. Add user message to session
if (/해결[됐됨했함]|고마워|감사|끝|완료/.test(userMessage)) { addMessageToSession(session, 'user', userMessage);
await deleteTroubleshootSession(env.SESSION_KV, session.telegramUserId);
logger.info('문제 해결 완료', {
userId: session.telegramUserId,
category: session.collectedInfo.category
});
return '✅ 문제가 해결되어 다행입니다! 앞으로도 언제든 도움이 필요하시면 말씀해주세요. 😊';
}
// 상담과 무관한 키워드 감지 (passthrough) // 4. Call AI to get response and possible tool calls
const unrelatedPatterns = /서버\s*추천|날씨|계산|도메인\s*추천|입금|충전|잔액|기억|저장/; const aiResult = await callTroubleshootExpertAI(session, userMessage, env);
if (unrelatedPatterns.test(userMessage)) {
await deleteTroubleshootSession(env.SESSION_KV, session.telegramUserId); // 5. Handle __PASSTHROUGH__ - not troubleshoot related
logger.info('무관한 요청으로 세션 자동 종료', { if (aiResult.response === '__PASSTHROUGH__' || aiResult.response.includes('__PASSTHROUGH__')) {
userId: session.telegramUserId, logger.info('트러블슈팅 상담 패스스루', { userId });
message: userMessage.slice(0, 30) // Don't save session if passthrough
});
return '__PASSTHROUGH__'; return '__PASSTHROUGH__';
} }
// Add user message to history // 6. Handle __SESSION_END__ - session complete
session.messages.push({ role: 'user', content: userMessage }); if (aiResult.response.includes('[세션 종료]')) {
logger.info('트러블슈팅 상담 세션 종료', { userId });
// Call Troubleshoot Expert AI await deleteTroubleshootSession(db, userId);
const aiResult = await callTroubleshootExpertAI(env, session, userMessage); return aiResult.response.replace('[세션 종료]', '').trim();
// 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;
} }
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) { } catch (error) {
logger.error('트러블슈팅 처리 실패', error as Error, { userId: session.telegramUserId }); logger.error('트러블슈팅 상담 오류', error as Error, { userId });
return '죄송합니다. 트러블슈팅 상담 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.';
// Clean up session on error
await deleteTroubleshootSession(env.SESSION_KV, session.telegramUserId);
return '죄송합니다. 트러블슈팅 중 오류가 발생했습니다.\n다시 시도하려면 문제를 말씀해주세요.';
} }
} }

View File

@@ -7,7 +7,7 @@ import { metrics } from './utils/metrics';
import { getOpenAIUrl } from './utils/api-urls'; import { getOpenAIUrl } from './utils/api-urls';
import { ERROR_MESSAGES } from './constants/messages'; import { ERROR_MESSAGES } from './constants/messages';
import { getServerSession, processServerConsultation } from './agents/server-agent'; 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 { processDomainConsultation, hasDomainSession } from './agents/domain-agent';
import { processDepositConsultation, hasDepositSession } from './agents/deposit-agent'; import { processDepositConsultation, hasDepositSession } from './agents/deposit-agent';
import { sendMessage } from './telegram'; import { sendMessage } from './telegram';
@@ -246,18 +246,17 @@ export async function generateOpenAIResponse(
// Check if troubleshoot session is active // Check if troubleshoot session is active
try { try {
const troubleshootSession = await getTroubleshootSession(env.SESSION_KV, telegramUserId); const hasTroubleshootSess = await hasTroubleshootSession(env.DB, telegramUserId);
if (troubleshootSession && troubleshootSession.status !== 'completed') { if (hasTroubleshootSess) {
logger.info('Active troubleshoot session detected, routing to troubleshoot', { logger.info('트러블슈팅 세션 감지, 트러블슈팅 에이전트로 라우팅', {
userId: telegramUserId, userId: telegramUserId
status: troubleshootSession.status
}); });
const result = await processTroubleshoot(userMessage, troubleshootSession, env); const troubleshootResponse = await processTroubleshootConsultation(env.DB, telegramUserId, userMessage, env);
// PASSTHROUGH: 무관한 메시지는 일반 처리로 전환 // PASSTHROUGH: 무관한 메시지는 일반 처리로 전환
if (result !== '__PASSTHROUGH__') { if (troubleshootResponse !== '__PASSTHROUGH__') {
return result; return troubleshootResponse;
} }
// Continue to normal flow below // Continue to normal flow below
} }

View File

@@ -31,37 +31,30 @@ export async function executeManageTroubleshoot(
logger.info('트러블슈팅 도구 호출', { action, userId: telegramUserId }); logger.info('트러블슈팅 도구 호출', { action, userId: telegramUserId });
if (!env?.SESSION_KV || !telegramUserId) { if (!env?.DB || !telegramUserId) {
return '🚫 트러블슈팅 기능을 사용할 수 없습니다.'; return '🚫 트러블슈팅 기능을 사용할 수 없습니다.';
} }
const { getTroubleshootSession, saveTroubleshootSession, deleteTroubleshootSession } = await import('../agents/troubleshoot-agent'); const { getTroubleshootSession, createTroubleshootSession, saveTroubleshootSession, deleteTroubleshootSession } = await import('../agents/troubleshoot-agent');
if (action === 'cancel') { if (action === 'cancel') {
await deleteTroubleshootSession(env.SESSION_KV, telegramUserId); await deleteTroubleshootSession(env.DB, telegramUserId);
return '✅ 트러블슈팅 세션이 취소되었습니다.'; return '✅ 트러블슈팅 세션이 취소되었습니다.';
} }
// action === 'start' // action === 'start'
const existingSession = await getTroubleshootSession(env.SESSION_KV, telegramUserId); const existingSession = await getTroubleshootSession(env.DB, telegramUserId);
if (existingSession && existingSession.status !== 'completed') { if (existingSession && existingSession.status !== 'completed') {
return '이미 진행 중인 트러블슈팅 세션이 있습니다. 계속 진행해주세요.\n\n현재까지 파악된 정보:\n' + return '이미 진행 중인 트러블슈팅 세션이 있습니다. 계속 진행해주세요.\n\n현재까지 파악된 정보:\n' +
(existingSession.collectedInfo.category ? `• 분류: ${existingSession.collectedInfo.category}\n` : '') + (existingSession.collected_info.category ? `• 분류: ${existingSession.collected_info.category}\n` : '') +
(existingSession.collectedInfo.symptoms ? `• 증상: ${existingSession.collectedInfo.symptoms}\n` : ''); (existingSession.collected_info.symptoms ? `• 증상: ${existingSession.collected_info.symptoms}\n` : '');
} }
// Create new session // Create new session
const newSession = { const newSession = createTroubleshootSession(telegramUserId, 'gathering');
telegramUserId,
status: 'gathering' as const,
collectedInfo: {},
messages: [],
createdAt: Date.now(),
updatedAt: Date.now(),
};
await saveTroubleshootSession(env.SESSION_KV, telegramUserId, newSession); await saveTroubleshootSession(env.DB, newSession);
logger.info('트러블슈팅 세션 시작', { userId: telegramUserId }); logger.info('트러블슈팅 세션 시작', { userId: telegramUserId });

View File

@@ -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 { export interface TroubleshootSession {
telegramUserId: string; user_id: string;
status: 'gathering' | 'diagnosing' | 'solving' | 'completed'; status: TroubleshootSessionStatus;
collectedInfo: { collected_info: {
category?: string; // Server/Infrastructure, Domain/DNS, Code/Deploy, Network, Database category?: string; // Server/Infrastructure, Domain/DNS, Code/Deploy, Network, Database
symptoms?: string; // 증상 요약 symptoms?: string; // 증상 요약
environment?: string; // OS, 프레임워크, 버전 등 environment?: string; // OS, 프레임워크, 버전 등
errorMessage?: string; // 에러 메시지 errorMessage?: string; // 에러 메시지
}; };
messages: Array<{ role: 'user' | 'assistant'; content: string }>; messages: Array<{ role: 'user' | 'assistant'; content: string }>;
createdAt: number; created_at: number;
updatedAt: number; updated_at: number;
expires_at: number;
} }
// Domain Agent Session Status // Domain Agent Session Status