refactor: apply SessionManager to all agents
- domain-agent: use DomainSessionManager (handles target_domain) - deposit-agent: use SessionManager<DepositSession> - troubleshoot-agent: use SessionManager<TroubleshootSession> - server-agent: use ServerSessionManager (handles last_recommendation) - Remove ~760 lines of duplicated session CRUD code - Use centralized constants from agent-config.ts - Import OpenAI types from types.ts instead of local definitions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<DepositSession | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<DepositSession>(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<boolean> {
|
||||
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,
|
||||
|
||||
@@ -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<DomainSession | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
const session = await getDomainSession(db, userId);
|
||||
return session !== null && !isSessionExpired(session);
|
||||
return await sessionManager.has(db, userId);
|
||||
}
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<ServerSession | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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 });
|
||||
}
|
||||
|
||||
@@ -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<TroubleshootSession | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<TroubleshootSession>(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<boolean> {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user