Replace pattern-based agent routing with AI intent classification
Instead of regex patterns to detect user intent, use a lightweight AI call (classifyIntent) that categorizes messages into troubleshoot, onboarding, billing, asset, or general. This catches ambiguous expressions like "example.com 좀 봐주세요" that patterns would miss. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,18 +6,12 @@
|
|||||||
|
|
||||||
import { sendMessage, sendMessageWithKeyboard, sendChatAction } from '../../telegram';
|
import { sendMessage, sendMessageWithKeyboard, sendChatAction } from '../../telegram';
|
||||||
import { checkRateLimit } from '../../security';
|
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 { OnboardingAgent } from '../../agents/onboarding-agent';
|
||||||
import { TroubleshootAgent } from '../../agents/troubleshoot-agent';
|
import { TroubleshootAgent } from '../../agents/troubleshoot-agent';
|
||||||
import { AssetAgent } from '../../agents/asset-agent';
|
import { AssetAgent } from '../../agents/asset-agent';
|
||||||
import { BillingAgent } from '../../agents/billing-agent';
|
import { BillingAgent } from '../../agents/billing-agent';
|
||||||
import { getMessage } from '../../i18n';
|
import { getMessage } from '../../i18n';
|
||||||
import {
|
|
||||||
ONBOARDING_PATTERNS,
|
|
||||||
TROUBLESHOOT_PATTERNS,
|
|
||||||
ASSET_PATTERNS,
|
|
||||||
BILLING_PATTERNS,
|
|
||||||
} from '../../utils/patterns';
|
|
||||||
import { selectToolsForMessage, executeTool } from '../../tools';
|
import { selectToolsForMessage, executeTool } from '../../tools';
|
||||||
import { createLogger } from '../../utils/logger';
|
import { createLogger } from '../../utils/logger';
|
||||||
import { getOpenAIUrl } from '../../utils/api-urls';
|
import { getOpenAIUrl } from '../../utils/api-urls';
|
||||||
@@ -73,7 +67,8 @@ export async function handleMessage(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 4. Route to active agent session first
|
// 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) {
|
if (agentResult) {
|
||||||
const { cleanText, sessionEnded } = cleanSessionMarkers(agentResult.response);
|
const { cleanText, sessionEnded } = cleanSessionMarkers(agentResult.response);
|
||||||
|
|
||||||
@@ -97,36 +92,32 @@ export async function handleMessage(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Detect intent for new session creation
|
// 5. AI-based intent classification → agent routing
|
||||||
let response: string | null = null;
|
const intent = await classifyIntent(env, text);
|
||||||
let sessionType: string | null = null;
|
logger.info('의도 분류 결과', { intent, telegramUserId });
|
||||||
|
|
||||||
if (ONBOARDING_PATTERNS.test(text)) {
|
const agentMap: Record<string, { agent: RegisterableAgent; type: string }> = {
|
||||||
response = await onboardingAgent.processConsultation(env.DB, telegramUserId, text, env);
|
onboarding: { agent: onboardingAgent, type: 'onboarding' },
|
||||||
sessionType = 'onboarding';
|
troubleshoot: { agent: troubleshootAgent, type: 'troubleshoot' },
|
||||||
} else if (TROUBLESHOOT_PATTERNS.test(text)) {
|
billing: { agent: billingAgent, type: 'billing' },
|
||||||
response = await troubleshootAgent.processConsultation(env.DB, telegramUserId, text, env);
|
asset: { agent: assetAgent, type: 'asset' },
|
||||||
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';
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
const { cleanText, sessionEnded } = cleanSessionMarkers(response);
|
||||||
await storeConversation(env.DB, user.id, text, cleanText, requestId);
|
await storeConversation(env.DB, user.id, text, cleanText, requestId);
|
||||||
await sendMessage(env.BOT_TOKEN, chatId, cleanText);
|
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);
|
await promptFeedback(env, chatId, lang, sessionType);
|
||||||
}
|
}
|
||||||
return;
|
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);
|
const aiResponse = await handleGeneralAI(env, user, text, telegramUserId);
|
||||||
await storeConversation(env.DB, user.id, text, aiResponse, requestId);
|
await storeConversation(env.DB, user.id, text, aiResponse, requestId);
|
||||||
await sendMessage(env.BOT_TOKEN, chatId, aiResponse);
|
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<string | null> {
|
||||||
|
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(
|
async function handleGeneralAI(
|
||||||
env: Env,
|
env: Env,
|
||||||
user: User,
|
user: User,
|
||||||
|
|||||||
Reference in New Issue
Block a user