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 { createLogger } from '../utils/logger';
|
||||||
import { executeWithOptimisticLock, OptimisticLockError } from '../utils/optimistic-lock';
|
import { executeWithOptimisticLock, OptimisticLockError } from '../utils/optimistic-lock';
|
||||||
import { TRANSACTION_STATUS, TRANSACTION_TYPE } from '../constants';
|
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');
|
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 MAX_DEPOSIT_AMOUNT = 100_000_000; // 1억원
|
||||||
const DEFAULT_HISTORY_LIMIT = 10;
|
const DEFAULT_HISTORY_LIMIT = 10;
|
||||||
|
|
||||||
// D1 Session Management
|
// Session manager instance
|
||||||
const DEPOSIT_SESSION_TTL = 30 * 60 * 1000; // 30분 (입금 작업은 빠르게 처리)
|
const sessionManager = new SessionManager<DepositSession>(getSessionConfig('deposit'));
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 입금 세션 존재 여부 확인 (라우팅용)
|
* 입금 세션 존재 여부 확인 (라우팅용)
|
||||||
@@ -204,8 +38,7 @@ export function addMessageToSession(
|
|||||||
* @returns true if active session exists, false otherwise
|
* @returns true if active session exists, false otherwise
|
||||||
*/
|
*/
|
||||||
export async function hasDepositSession(db: D1Database, userId: string): Promise<boolean> {
|
export async function hasDepositSession(db: D1Database, userId: string): Promise<boolean> {
|
||||||
const session = await getDepositSession(db, userId);
|
return await sessionManager.has(db, userId);
|
||||||
return session !== null && !isSessionExpired(session);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DepositContext {
|
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 지원)
|
* Deposit Expert AI 호출 (Function Calling 지원)
|
||||||
@@ -523,15 +334,15 @@ export async function processDepositConsultation(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Check for existing session
|
// 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
|
// 2. Create new session if none exists
|
||||||
if (!session) {
|
if (!session) {
|
||||||
session = createDepositSession(userId, 'collecting_amount');
|
session = sessionManager.create(userId, 'collecting_amount');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Add user message to session
|
// 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
|
// 4. Call AI to get response and possible tool calls
|
||||||
const aiResult = await callDepositExpertAI(session, userMessage, env);
|
const aiResult = await callDepositExpertAI(session, userMessage, env);
|
||||||
@@ -589,15 +400,15 @@ export async function processDepositConsultation(
|
|||||||
// 9. Handle __SESSION_END__ - session complete
|
// 9. Handle __SESSION_END__ - session complete
|
||||||
if (finalResponse.includes('__SESSION_END__')) {
|
if (finalResponse.includes('__SESSION_END__')) {
|
||||||
logger.info('입금 상담 세션 종료', { userId });
|
logger.info('입금 상담 세션 종료', { userId });
|
||||||
await deleteDepositSession(db, userId);
|
await sessionManager.delete(db, userId);
|
||||||
finalResponse = finalResponse.replace('__SESSION_END__', '').trim();
|
finalResponse = finalResponse.replace('__SESSION_END__', '').trim();
|
||||||
return finalResponse;
|
return finalResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10. Add assistant response to session and save
|
// 10. Add assistant response to session and save
|
||||||
addMessageToSession(session, 'assistant', finalResponse);
|
sessionManager.addMessage(session, 'assistant', finalResponse);
|
||||||
session.updated_at = Date.now();
|
session.updated_at = Date.now();
|
||||||
await saveDepositSession(db, session);
|
await sessionManager.save(db, session);
|
||||||
|
|
||||||
logger.info('입금 상담 완료', {
|
logger.info('입금 상담 완료', {
|
||||||
userId,
|
userId,
|
||||||
|
|||||||
@@ -11,183 +11,13 @@
|
|||||||
import type { Env, DomainSession, DomainSessionStatus } from '../types';
|
import type { Env, DomainSession, DomainSessionStatus } from '../types';
|
||||||
import { createLogger } from '../utils/logger';
|
import { createLogger } from '../utils/logger';
|
||||||
import { executeDomainAction, executeSuggestDomains } from '../tools/domain-tool';
|
import { executeDomainAction, executeSuggestDomains } from '../tools/domain-tool';
|
||||||
|
import { DomainSessionManager } from '../utils/session-manager';
|
||||||
|
import { getSessionConfig } from '../constants/agent-config';
|
||||||
|
|
||||||
const logger = createLogger('domain-agent');
|
const logger = createLogger('domain-agent');
|
||||||
|
|
||||||
// D1 Session Management
|
// Session manager instance
|
||||||
const DOMAIN_SESSION_TTL = 60 * 60 * 1000; // 1시간 (도메인 작업은 시간이 더 필요)
|
const sessionManager = new DomainSessionManager(getSessionConfig('domain'));
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Domain Expert System Prompt
|
// Domain Expert System Prompt
|
||||||
const DOMAIN_EXPERT_PROMPT = `당신은 10년 경력의 도메인 컨설턴트입니다.
|
const DOMAIN_EXPERT_PROMPT = `당신은 10년 경력의 도메인 컨설턴트입니다.
|
||||||
@@ -306,28 +136,8 @@ const DOMAIN_TOOLS = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// OpenAI API response types
|
// Import OpenAI types from centralized types
|
||||||
interface OpenAIToolCall {
|
import type { OpenAIToolCall, OpenAIMessage, OpenAIAPIResponse } from '../types';
|
||||||
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;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Domain Expert AI 호출 (Function Calling 지원)
|
* Domain Expert AI 호출 (Function Calling 지원)
|
||||||
@@ -598,16 +408,16 @@ export async function processDomainConsultation(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Check for existing session
|
// 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
|
// 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)
|
// (For first call, we always try to process - AI will return __PASSTHROUGH__ if not relevant)
|
||||||
if (!session) {
|
if (!session) {
|
||||||
session = createDomainSession(userId, 'gathering');
|
session = sessionManager.create(userId, 'gathering');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Add user message to session
|
// 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
|
// 4. Call AI to get response and possible tool calls
|
||||||
const aiResult = await callDomainExpertAI(session, userMessage, env);
|
const aiResult = await callDomainExpertAI(session, userMessage, env);
|
||||||
@@ -648,15 +458,15 @@ export async function processDomainConsultation(
|
|||||||
// 8. Handle __SESSION_END__ - session complete
|
// 8. Handle __SESSION_END__ - session complete
|
||||||
if (finalResponse.includes('__SESSION_END__')) {
|
if (finalResponse.includes('__SESSION_END__')) {
|
||||||
logger.info('도메인 상담 세션 종료', { userId });
|
logger.info('도메인 상담 세션 종료', { userId });
|
||||||
await deleteDomainSession(db, userId);
|
await sessionManager.delete(db, userId);
|
||||||
finalResponse = finalResponse.replace('__SESSION_END__', '').trim();
|
finalResponse = finalResponse.replace('__SESSION_END__', '').trim();
|
||||||
return finalResponse;
|
return finalResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. Add assistant response to session and save
|
// 9. Add assistant response to session and save
|
||||||
addMessageToSession(session, 'assistant', finalResponse);
|
sessionManager.addMessage(session, 'assistant', finalResponse);
|
||||||
session.updated_at = Date.now();
|
session.updated_at = Date.now();
|
||||||
await saveDomainSession(db, session);
|
await sessionManager.save(db, session);
|
||||||
|
|
||||||
logger.info('도메인 상담 완료', {
|
logger.info('도메인 상담 완료', {
|
||||||
userId,
|
userId,
|
||||||
@@ -680,6 +490,5 @@ export async function processDomainConsultation(
|
|||||||
* @returns true if active session exists, false otherwise
|
* @returns true if active session exists, false otherwise
|
||||||
*/
|
*/
|
||||||
export async function hasDomainSession(db: D1Database, userId: string): Promise<boolean> {
|
export async function hasDomainSession(db: D1Database, userId: string): Promise<boolean> {
|
||||||
const session = await getDomainSession(db, userId);
|
return await sessionManager.has(db, userId);
|
||||||
return session !== null && !isSessionExpired(session);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,74 +15,18 @@
|
|||||||
* 4. Expected: Order confirmation
|
* 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 { createLogger } from '../utils/logger';
|
||||||
import { executeSearchWeb, executeLookupDocs } from '../tools/search-tool';
|
import { executeSearchWeb, executeLookupDocs } from '../tools/search-tool';
|
||||||
import { formatTrafficInfo } from '../utils/formatters';
|
import { formatTrafficInfo } from '../utils/formatters';
|
||||||
import { SERVER_CONSULTATION_STATUS, LANGUAGE_CODE } from '../constants';
|
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');
|
const logger = createLogger('server-agent');
|
||||||
|
|
||||||
// D1 Session Management
|
// Session manager instance
|
||||||
const SERVER_SESSION_TTL_MS = 60 * 60 * 1000; // 1시간
|
const sessionManager = new ServerSessionManager(getSessionConfig('server'));
|
||||||
|
|
||||||
/**
|
|
||||||
* 새 서버 세션 생성
|
|
||||||
*
|
|
||||||
* @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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 서버 세션 존재 여부 확인 (라우팅용)
|
* 서버 세션 존재 여부 확인 (라우팅용)
|
||||||
@@ -92,122 +36,7 @@ export function addMessageToSession(
|
|||||||
* @returns true if active session exists, false otherwise
|
* @returns true if active session exists, false otherwise
|
||||||
*/
|
*/
|
||||||
export async function hasServerSession(db: D1Database, userId: string): Promise<boolean> {
|
export async function hasServerSession(db: D1Database, userId: string): Promise<boolean> {
|
||||||
const session = await getServerSession(db, userId);
|
return await sessionManager.has(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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -516,28 +345,6 @@ function inferExpectedUsers(scale: string, techStack?: string[]): number {
|
|||||||
return 10; // Default to personal
|
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 지원)
|
* Server Expert AI 호출 (Function Calling 지원)
|
||||||
@@ -778,11 +585,11 @@ export async function processServerConsultation(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Check for existing session
|
// 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
|
// 2. Create new session if none exists
|
||||||
if (!session) {
|
if (!session) {
|
||||||
session = createServerSession(userId, 'gathering');
|
session = sessionManager.create(userId, 'gathering');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ordering 상태에서 "신청" 외 메시지 입력 시 세션 정리
|
// ordering 상태에서 "신청" 외 메시지 입력 시 세션 정리
|
||||||
@@ -790,7 +597,7 @@ export async function processServerConsultation(
|
|||||||
// "신청"은 message-handler에서 처리, 여기까지 오면 다른 메시지임
|
// "신청"은 message-handler에서 처리, 여기까지 오면 다른 메시지임
|
||||||
const orderConfirmKey = `server_order_confirm:${session.user_id}`;
|
const orderConfirmKey = `server_order_confirm:${session.user_id}`;
|
||||||
await env.SESSION_KV?.delete(orderConfirmKey);
|
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 });
|
logger.info('주문 확인 세션 취소 (다른 메시지 입력)', { userId: session.user_id });
|
||||||
return '__PASSTHROUGH__'; // 일반 대화로 전환
|
return '__PASSTHROUGH__'; // 일반 대화로 전환
|
||||||
@@ -800,7 +607,7 @@ export async function processServerConsultation(
|
|||||||
// "취소", "다시", "처음", "리셋", "초기화" 등
|
// "취소", "다시", "처음", "리셋", "초기화" 등
|
||||||
if (/^(취소|다시|처음|리셋|초기화)/.test(userMessage.trim()) ||
|
if (/^(취소|다시|처음|리셋|초기화)/.test(userMessage.trim()) ||
|
||||||
/취소할[게래]|다시\s*시작|처음부터/.test(userMessage)) {
|
/취소할[게래]|다시\s*시작|처음부터/.test(userMessage)) {
|
||||||
await deleteServerSession(db, session.user_id);
|
await sessionManager.delete(db, session.user_id);
|
||||||
logger.info('사용자 요청으로 상담 취소', {
|
logger.info('사용자 요청으로 상담 취소', {
|
||||||
userId: session.user_id,
|
userId: session.user_id,
|
||||||
previousStatus: session.status,
|
previousStatus: session.status,
|
||||||
@@ -811,14 +618,14 @@ export async function processServerConsultation(
|
|||||||
|
|
||||||
// "서버 추천" 키워드로 새로 시작 요청 (기존 세션 리셋)
|
// "서버 추천" 키워드로 새로 시작 요청 (기존 세션 리셋)
|
||||||
if (/서버\s*추천/.test(userMessage)) {
|
if (/서버\s*추천/.test(userMessage)) {
|
||||||
await deleteServerSession(db, session.user_id);
|
await sessionManager.delete(db, session.user_id);
|
||||||
logger.info('서버 추천 키워드로 세션 리셋', {
|
logger.info('서버 추천 키워드로 세션 리셋', {
|
||||||
userId: session.user_id,
|
userId: session.user_id,
|
||||||
previousStatus: session.status
|
previousStatus: session.status
|
||||||
});
|
});
|
||||||
// 새 세션 생성하고 시작 메시지 반환
|
// 새 세션 생성하고 시작 메시지 반환
|
||||||
const newSession = createServerSession(session.user_id, 'gathering');
|
const newSession = sessionManager.create(session.user_id, 'gathering');
|
||||||
await saveServerSession(db, newSession);
|
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번호나 용도를 말씀해주세요!';
|
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 = /기억|날씨|계산|검색|도메인|입금|충전|잔액|시간|문서/;
|
const unrelatedPatterns = /기억|날씨|계산|검색|도메인|입금|충전|잔액|시간|문서/;
|
||||||
if (unrelatedPatterns.test(userMessage)) {
|
if (unrelatedPatterns.test(userMessage)) {
|
||||||
await deleteServerSession(db, session.user_id);
|
await sessionManager.delete(db, session.user_id);
|
||||||
logger.info('무관한 요청으로 세션 자동 종료', {
|
logger.info('무관한 요청으로 세션 자동 종료', {
|
||||||
userId: session.user_id,
|
userId: session.user_id,
|
||||||
message: userMessage.slice(0, 30)
|
message: userMessage.slice(0, 30)
|
||||||
@@ -867,7 +674,7 @@ export async function processServerConsultation(
|
|||||||
|
|
||||||
// Mark session as ordering
|
// Mark session as ordering
|
||||||
session.status = 'ordering';
|
session.status = 'ordering';
|
||||||
await saveServerSession(db, session);
|
await sessionManager.save(db, session);
|
||||||
|
|
||||||
// 주문 확인 세션 저장 (텍스트 기반 확인)
|
// 주문 확인 세션 저장 (텍스트 기반 확인)
|
||||||
const orderConfirmKey = `server_order_confirm:${session.user_id}`;
|
const orderConfirmKey = `server_order_confirm:${session.user_id}`;
|
||||||
@@ -942,7 +749,7 @@ export async function processServerConsultation(
|
|||||||
|
|
||||||
// Mark session as recommending
|
// Mark session as recommending
|
||||||
session.status = SERVER_CONSULTATION_STATUS.RECOMMENDING;
|
session.status = SERVER_CONSULTATION_STATUS.RECOMMENDING;
|
||||||
await saveServerSession(db, session);
|
await sessionManager.save(db, session);
|
||||||
|
|
||||||
// 1. Call recommendation API (추천 먼저 받기)
|
// 1. Call recommendation API (추천 먼저 받기)
|
||||||
logger.info('추천 API 호출', { collectedInfo: session.collected_info });
|
logger.info('추천 API 호출', { collectedInfo: session.collected_info });
|
||||||
@@ -1064,7 +871,7 @@ export async function processServerConsultation(
|
|||||||
|
|
||||||
// Mark session as selecting (사용자 선택 대기)
|
// Mark session as selecting (사용자 선택 대기)
|
||||||
session.status = SERVER_CONSULTATION_STATUS.SELECTING;
|
session.status = SERVER_CONSULTATION_STATUS.SELECTING;
|
||||||
await saveServerSession(db, session);
|
await sessionManager.save(db, session);
|
||||||
|
|
||||||
// 4. 추천 결과 + AI 검토 코멘트 (검토 코멘트는 마지막에)
|
// 4. 추천 결과 + AI 검토 코멘트 (검토 코멘트는 마지막에)
|
||||||
// __DIRECT__ 마커가 앞에 와야 제대로 처리됨
|
// __DIRECT__ 마커가 앞에 와야 제대로 처리됨
|
||||||
@@ -1072,14 +879,14 @@ export async function processServerConsultation(
|
|||||||
} else {
|
} else {
|
||||||
// 추천 결과 없음 - 세션 삭제
|
// 추천 결과 없음 - 세션 삭제
|
||||||
session.status = SERVER_CONSULTATION_STATUS.COMPLETED;
|
session.status = SERVER_CONSULTATION_STATUS.COMPLETED;
|
||||||
await deleteServerSession(db, session.user_id);
|
await sessionManager.delete(db, session.user_id);
|
||||||
|
|
||||||
return `${aiResult.message}\n\n조건에 맞는 서버를 찾지 못했습니다.`;
|
return `${aiResult.message}\n\n조건에 맞는 서버를 찾지 못했습니다.`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Continue gathering information
|
// Continue gathering information
|
||||||
session.status = SERVER_CONSULTATION_STATUS.GATHERING;
|
session.status = SERVER_CONSULTATION_STATUS.GATHERING;
|
||||||
await saveServerSession(db, session);
|
await sessionManager.save(db, session);
|
||||||
|
|
||||||
return aiResult.message;
|
return aiResult.message;
|
||||||
}
|
}
|
||||||
@@ -1088,7 +895,7 @@ export async function processServerConsultation(
|
|||||||
|
|
||||||
// Clean up session on error (if exists)
|
// Clean up session on error (if exists)
|
||||||
try {
|
try {
|
||||||
await deleteServerSession(db, userId);
|
await sessionManager.delete(db, userId);
|
||||||
} catch (deleteError) {
|
} catch (deleteError) {
|
||||||
logger.error('세션 삭제 실패 (무시)', deleteError as Error, { userId });
|
logger.error('세션 삭제 실패 (무시)', deleteError as Error, { userId });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,182 +14,16 @@
|
|||||||
* 4. Expected: Session deleted
|
* 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 { createLogger } from '../utils/logger';
|
||||||
import { executeSearchWeb, executeLookupDocs } from '../tools/search-tool';
|
import { executeSearchWeb, executeLookupDocs } from '../tools/search-tool';
|
||||||
|
import { SessionManager } from '../utils/session-manager';
|
||||||
|
import { getSessionConfig } from '../constants/agent-config';
|
||||||
|
|
||||||
const logger = createLogger('troubleshoot-agent');
|
const logger = createLogger('troubleshoot-agent');
|
||||||
|
|
||||||
// D1 Session Management
|
// Session manager instance
|
||||||
const TROUBLESHOOT_SESSION_TTL_MS = 60 * 60 * 1000; // 1시간
|
const sessionManager = new SessionManager<TroubleshootSession>(getSessionConfig('troubleshoot'));
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 트러블슈팅 세션 존재 여부 확인 (라우팅용)
|
* 트러블슈팅 세션 존재 여부 확인 (라우팅용)
|
||||||
@@ -199,8 +33,7 @@ export function addMessageToSession(
|
|||||||
* @returns true if active session exists, false otherwise
|
* @returns true if active session exists, false otherwise
|
||||||
*/
|
*/
|
||||||
export async function hasTroubleshootSession(db: D1Database, userId: string): Promise<boolean> {
|
export async function hasTroubleshootSession(db: D1Database, userId: string): Promise<boolean> {
|
||||||
const session = await getTroubleshootSession(db, userId);
|
return await sessionManager.has(db, userId);
|
||||||
return session !== null && !isSessionExpired(session);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Troubleshoot Expert System Prompt
|
// 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 지원)
|
* Troubleshoot Expert AI 호출 (Function Calling 지원)
|
||||||
@@ -503,15 +314,15 @@ export async function processTroubleshootConsultation(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Check for existing session
|
// 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
|
// 2. Create new session if none exists
|
||||||
if (!session) {
|
if (!session) {
|
||||||
session = createTroubleshootSession(userId, 'gathering');
|
session = sessionManager.create(userId, 'gathering');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Add user message to session
|
// 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
|
// 4. Call AI to get response and possible tool calls
|
||||||
const aiResult = await callTroubleshootExpertAI(session, userMessage, env);
|
const aiResult = await callTroubleshootExpertAI(session, userMessage, env);
|
||||||
@@ -526,12 +337,12 @@ export async function processTroubleshootConsultation(
|
|||||||
// 6. Handle __SESSION_END__ - session complete
|
// 6. Handle __SESSION_END__ - session complete
|
||||||
if (aiResult.response.includes('[세션 종료]')) {
|
if (aiResult.response.includes('[세션 종료]')) {
|
||||||
logger.info('트러블슈팅 상담 세션 종료', { userId });
|
logger.info('트러블슈팅 상담 세션 종료', { userId });
|
||||||
await deleteTroubleshootSession(db, userId);
|
await sessionManager.delete(db, userId);
|
||||||
return aiResult.response.replace('[세션 종료]', '').trim();
|
return aiResult.response.replace('[세션 종료]', '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Add assistant response to session and save
|
// 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)
|
// Update session status based on response content (simple heuristic)
|
||||||
if (aiResult.response.includes('원인') || aiResult.response.includes('분석')) {
|
if (aiResult.response.includes('원인') || aiResult.response.includes('분석')) {
|
||||||
@@ -541,7 +352,7 @@ export async function processTroubleshootConsultation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
session.updated_at = Date.now();
|
session.updated_at = Date.now();
|
||||||
await saveTroubleshootSession(db, session);
|
await sessionManager.save(db, session);
|
||||||
|
|
||||||
logger.info('트러블슈팅 상담 완료', {
|
logger.info('트러블슈팅 상담 완료', {
|
||||||
userId,
|
userId,
|
||||||
|
|||||||
Reference in New Issue
Block a user