feat: 키워드 사전 라우팅 — EXAONE 분류기 한계 보완

EXAONE 7.8B가 복잡한 JSON 분류를 안정적으로 못함.
키워드 매칭으로 일정/메일/문서/확인 요청을 사전 감지하여
분류기를 건너뛰고 바로 도구로 라우팅.
날짜 계산(오늘/내일/이번주)도 코드에서 처리.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-04-06 13:46:07 +09:00
parent e786307a07
commit f72eef6e31

View File

@@ -95,8 +95,63 @@ async def _send_callback(job: Job, text: str) -> None:
await send_to_synology(text)
def _pre_route(message: str) -> dict | None:
"""키워드 기반 사전 라우팅. EXAONE 7.8B 분류기 보완."""
from datetime import datetime, timedelta
msg = message.lower().strip()
now = datetime.now()
# 캘린더 키워드
cal_keywords = ["일정", "캘린더", "스케줄", "약속", "미팅", "회의"]
if any(k in msg for k in cal_keywords):
# 생성 요청
if any(k in msg for k in ["잡아", "만들", "등록", "추가", "넣어"]):
return None # EXAONE이 파라미터 추출해야 함
# 오늘
if "오늘" in msg:
return {"action": "tools", "tool": "calendar", "operation": "today", "params": {}}
# 내일
if "내일" in msg:
tmr = (now + timedelta(days=1)).strftime("%Y-%m-%d")
return {"action": "tools", "tool": "calendar", "operation": "search", "params": {"date_from": tmr, "date_to": tmr}}
# 이번주
if "이번" in msg and ("" in msg or "week" in msg):
monday = now - timedelta(days=now.weekday())
sunday = monday + timedelta(days=6)
return {"action": "tools", "tool": "calendar", "operation": "search", "params": {"date_from": monday.strftime("%Y-%m-%d"), "date_to": sunday.strftime("%Y-%m-%d")}}
# 기본: 오늘
return {"action": "tools", "tool": "calendar", "operation": "today", "params": {}}
# 메일 키워드
if any(k in msg for k in ["메일", "이메일", "mail", "편지"]):
query = ""
days = 7
if "오늘" in msg:
days = 1
return {"action": "tools", "tool": "email", "operation": "search", "params": {"query": query, "days": days}}
# 문서 키워드
if any(k in msg for k in ["문서", "도큐먼트", "자료", "파일"]) and any(k in msg for k in ["찾아", "검색", "확인"]):
# 검색어 추출: 키워드 제거 후 남은 텍스트
query = msg
for rm in ["문서", "도큐먼트", "자료", "파일", "찾아줘", "찾아", "검색", "확인", "해줘", "", ""]:
query = query.replace(rm, "")
query = query.strip()
if query:
return {"action": "tools", "tool": "document", "operation": "search", "params": {"query": query}}
# pending_draft 확인 응답
if msg in ("확인", "", "yes", "ㅇㅇ", "", "", "좋아", "ok"):
return {"action": "tools", "tool": "calendar", "operation": "create_confirmed", "params": {}}
if msg in ("취소", "아니", "no", "ㄴㄴ"):
return {"action": "direct", "response": "알겠어, 취소했어!", "prompt": ""}
return None
async def run(job: Job) -> None:
"""EXAONE 분류 → direct/route/clarify 분기."""
"""사전 라우팅 → EXAONE 분류 → direct/route/clarify/tools 분기."""
start_time = time()
user_id = job.callback_meta.get("user_id", "api")
classify_model = None
@@ -118,29 +173,38 @@ async def run(job: Job) -> None:
classify_model = backend_registry.classifier.model
# --- 대화 이력 + 현재 시간 포함하여 분류 요청 ---
from datetime import datetime
now_str = datetime.now().strftime("%Y-%m-%d %H:%M (%A)")
history = conversation_store.format_for_prompt(user_id)
classify_input = f"[현재 시간]\n{now_str}\n\n"
if history:
classify_input += f"[대화 이력]\n{history}\n\n"
classify_input += f"[현재 메시지]\n{job.message}"
# --- 사전 라우팅 (키워드 기반, EXAONE 스킵) ---
pre = _pre_route(job.message)
classify_latency = 0
await state_stream.push(job.id, "processing", {"message": "메시지를 분석하고 있습니다..."})
classify_start = time()
if pre:
classification = pre
logger.info("Job %s pre-routed: %s.%s", job.id, pre.get("tool", ""), pre.get("operation", pre.get("action", "")))
else:
# --- EXAONE 분류기 호출 ---
from datetime import datetime
now_str = datetime.now().strftime("%Y-%m-%d %H:%M (%A)")
history = conversation_store.format_for_prompt(user_id)
classify_input = f"[현재 시간]\n{now_str}\n\n"
if history:
classify_input += f"[대화 이력]\n{history}\n\n"
classify_input += f"[현재 메시지]\n{job.message}"
try:
raw_result = await _complete_with_heartbeat(
backend_registry.classifier, classify_input, job.id,
beat_msg="메시지를 분석하고 있습니다..."
)
except Exception:
logger.warning("Classification failed for job %s, falling back to direct", job.id)
raw_result = ""
await state_stream.push(job.id, "processing", {"message": "메시지를 분석하고 있습니다..."})
classify_start = time()
try:
raw_result = await _complete_with_heartbeat(
backend_registry.classifier, classify_input, job.id,
beat_msg="메시지를 분석하고 있습니다..."
)
except Exception:
logger.warning("Classification failed for job %s, falling back to direct", job.id)
raw_result = ""
classify_latency = (time() - classify_start) * 1000
classification = _parse_classification(raw_result)
classify_latency = (time() - classify_start) * 1000
classification = _parse_classification(raw_result)
action = classification.get("action", "direct")
response_text = classification.get("response", "")
route_prompt = classification.get("prompt", "")