From f72eef6e3119db75dd140e1ba61c3766c2e4c724 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Mon, 6 Apr 2026 13:46:07 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=82=AC?= =?UTF-8?q?=EC=A0=84=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20=E2=80=94=20EXAONE=20?= =?UTF-8?q?=EB=B6=84=EB=A5=98=EA=B8=B0=20=ED=95=9C=EA=B3=84=20=EB=B3=B4?= =?UTF-8?q?=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EXAONE 7.8B가 복잡한 JSON 분류를 안정적으로 못함. 키워드 매칭으로 일정/메일/문서/확인 요청을 사전 감지하여 분류기를 건너뛰고 바로 도구로 라우팅. 날짜 계산(오늘/내일/이번주)도 코드에서 처리. Co-Authored-By: Claude Opus 4.6 (1M context) --- nanoclaude/services/worker.py | 106 +++++++++++++++++++++++++++------- 1 file changed, 85 insertions(+), 21 deletions(-) diff --git a/nanoclaude/services/worker.py b/nanoclaude/services/worker.py index d73e986..119782c 100644 --- a/nanoclaude/services/worker.py +++ b/nanoclaude/services/worker.py @@ -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", "")