feat: 키워드 사전 라우팅 — EXAONE 분류기 한계 보완
EXAONE 7.8B가 복잡한 JSON 분류를 안정적으로 못함. 키워드 매칭으로 일정/메일/문서/확인 요청을 사전 감지하여 분류기를 건너뛰고 바로 도구로 라우팅. 날짜 계산(오늘/내일/이번주)도 코드에서 처리. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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", "")
|
||||
|
||||
Reference in New Issue
Block a user