diff --git a/src/routes/handlers/message-handler.ts b/src/routes/handlers/message-handler.ts index 27b3584..94c7e29 100644 --- a/src/routes/handlers/message-handler.ts +++ b/src/routes/handlers/message-handler.ts @@ -6,18 +6,12 @@ import { sendMessage, sendMessageWithKeyboard, sendChatAction } from '../../telegram'; import { checkRateLimit } from '../../security'; -import { registerAgent, routeToActiveAgent } from '../../agents/agent-registry'; +import { registerAgent, routeToActiveAgent, type RegisterableAgent } from '../../agents/agent-registry'; import { OnboardingAgent } from '../../agents/onboarding-agent'; import { TroubleshootAgent } from '../../agents/troubleshoot-agent'; import { AssetAgent } from '../../agents/asset-agent'; import { BillingAgent } from '../../agents/billing-agent'; import { getMessage } from '../../i18n'; -import { - ONBOARDING_PATTERNS, - TROUBLESHOOT_PATTERNS, - ASSET_PATTERNS, - BILLING_PATTERNS, -} from '../../utils/patterns'; import { selectToolsForMessage, executeTool } from '../../tools'; import { createLogger } from '../../utils/logger'; import { getOpenAIUrl } from '../../utils/api-urls'; @@ -73,7 +67,8 @@ export async function handleMessage( try { // 4. Route to active agent session first - const agentResult = await routeToActiveAgent(env.DB, telegramUserId, text, env); + const meta = { chatId, messageId: message.message_id }; + const agentResult = await routeToActiveAgent(env.DB, telegramUserId, text, env, meta); if (agentResult) { const { cleanText, sessionEnded } = cleanSessionMarkers(agentResult.response); @@ -97,36 +92,32 @@ export async function handleMessage( return; } - // 5. Detect intent for new session creation - let response: string | null = null; - let sessionType: string | null = null; + // 5. AI-based intent classification → agent routing + const intent = await classifyIntent(env, text); + logger.info('의도 분류 결과', { intent, telegramUserId }); - if (ONBOARDING_PATTERNS.test(text)) { - response = await onboardingAgent.processConsultation(env.DB, telegramUserId, text, env); - sessionType = 'onboarding'; - } else if (TROUBLESHOOT_PATTERNS.test(text)) { - response = await troubleshootAgent.processConsultation(env.DB, telegramUserId, text, env); - sessionType = 'troubleshoot'; - } else if (BILLING_PATTERNS.test(text)) { - response = await billingAgent.processConsultation(env.DB, telegramUserId, text, env); - sessionType = 'billing'; - } else if (ASSET_PATTERNS.test(text)) { - response = await assetAgent.processConsultation(env.DB, telegramUserId, text, env); - sessionType = 'asset'; - } + const agentMap: Record = { + onboarding: { agent: onboardingAgent, type: 'onboarding' }, + troubleshoot: { agent: troubleshootAgent, type: 'troubleshoot' }, + billing: { agent: billingAgent, type: 'billing' }, + asset: { agent: assetAgent, type: 'asset' }, + }; - if (response) { + if (intent && intent in agentMap) { + const { agent, type: sessionType } = agentMap[intent]; + const response = await agent.processConsultation(env.DB, telegramUserId, text, env, meta); const { cleanText, sessionEnded } = cleanSessionMarkers(response); await storeConversation(env.DB, user.id, text, cleanText, requestId); await sendMessage(env.BOT_TOKEN, chatId, cleanText); - // Only prompt feedback for multi-round agents - if (sessionEnded && (sessionType === 'troubleshoot' || sessionType === 'onboarding')) { + + const isMultiRound = sessionType === 'troubleshoot' || sessionType === 'onboarding'; + if (sessionEnded && isMultiRound) { await promptFeedback(env, chatId, lang, sessionType); } return; } - // 6. General AI fallback (no matching agent) + // 6. General AI fallback (intent = 'general' or classification failed) const aiResponse = await handleGeneralAI(env, user, text, telegramUserId); await storeConversation(env.DB, user.id, text, aiResponse, requestId); await sendMessage(env.BOT_TOKEN, chatId, aiResponse); @@ -182,6 +173,61 @@ async function getOrCreateUser( } } +const VALID_INTENTS = ['troubleshoot', 'onboarding', 'billing', 'asset', 'general'] as const; + +async function classifyIntent(env: Env, text: string): Promise { + if (!env.OPENAI_API_KEY) return null; + + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + + try { + const response = await fetch(getOpenAIUrl(env), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${env.OPENAI_API_KEY}`, + }, + signal: controller.signal, + body: JSON.stringify({ + model: AI_CONFIG.model, + messages: [ + { + role: 'system', + content: `사용자 메시지의 의도를 분류하세요. 반드시 아래 중 하나만 응답하세요: +- troubleshoot: 기술 문제, 접속 불가, 오류, 장애, 느림, 차단, 네트워크 문제, 도메인/서버/서비스 문제 해결 +- onboarding: 신규 가입, 서비스 소개, 요금/플랜 문의, 처음 이용 +- billing: 입금, 충전, 잔액, 결제, 환불, 요금 관련 +- asset: 자산 현황, 내 서버/도메인 목록, 보유 서비스 조회 +- general: 위 어느 것에도 해당하지 않는 일반 질문이나 인사 + +한 단어만 응답하세요.`, + }, + { role: 'user', content: text }, + ], + max_tokens: 10, + temperature: 0, + }), + }); + + if (!response.ok) return null; + + const data = (await response.json()) as OpenAIAPIResponse; + const raw = data.choices[0]?.message?.content?.trim().toLowerCase(); + if (raw && VALID_INTENTS.includes(raw as typeof VALID_INTENTS[number])) { + return raw === 'general' ? null : raw; + } + return null; + } finally { + clearTimeout(timeoutId); + } + } catch (error) { + logger.error('Intent classification failed', error as Error); + return null; + } +} + async function handleGeneralAI( env: Env, user: User,