diff --git a/nanoclaude/services/backend_registry.py b/nanoclaude/services/backend_registry.py index ed3a36e..25b218a 100644 --- a/nanoclaude/services/backend_registry.py +++ b/nanoclaude/services/backend_registry.py @@ -26,7 +26,9 @@ CLASSIFIER_PROMPT = """\ - calendar.create_confirmed() — pending_draft가 있을 때, 사용자가 "확인/예/yes" 한 경우만 사용 - email.search(query, days) — 메일 검색. days 기본 7 - email.read(uid) — 메일 본문 조회 -- document.search(query) — 문서 검색 +- document.ask(query) — 문서 기반 AI 답변. 법령/규정/기술 내용을 물어볼 때 사용. 시간 걸림 +- document.search_full(query) — 문서 고급 검색 (리랭킹 포함). 문서 목록이 필요할 때 사용 +- document.search(query) — 문서 간단 검색 - document.read(doc_id) — 문서 조회 - infra.status(host) — 서버 Docker 컨테이너 상태. host: gpu (기본), nas-company - infra.health(service) — 서비스 헬스체크. service 생략 시 전체 체크. 개별: document-server, mlx, ollama-gpu @@ -51,7 +53,10 @@ JSON 형식: - "이번주 일정" → tools: calendar.search(이번주 월~일) - "내일 3시 회의" → tools: calendar.create_draft(...) - "최근 메일" → tools: email.search("", 7) -- "문서 찾아줘" → tools: document.search(...) +- "문서 찾아줘" → tools: document.search_full(...) +- "산업안전보건법 제6장 내용이 뭐야" → tools: document.ask("산업안전보건법 제6장") +- "ASME Section VIII 관련 자료" → tools: document.search_full("ASME Section VIII") +- "위험성평가 절차 알려줘" → tools: document.ask("위험성평가 절차") - "GPU 서버 상태" → tools: infra.status("gpu") - "서비스 살아있어?" → tools: infra.health() - "디스크 용량" → tools: infra.disk() diff --git a/nanoclaude/services/worker.py b/nanoclaude/services/worker.py index 93cfd2f..bf3a58a 100644 --- a/nanoclaude/services/worker.py +++ b/nanoclaude/services/worker.py @@ -183,18 +183,36 @@ def _pre_route(message: str) -> dict | None: # 문서 키워드 — 질문형/탐색형 점수 기반 분기 doc_entry = any(k in msg for k in ["문서", "도큐먼트", "자료", "파일"]) doc_action = any(k in msg for k in ["찾아", "검색", "확인", "알려", "설명", "뭐야"]) - if doc_entry and doc_action: + + # 도메인 키워드 — "문서 찾아줘" 없이도 전문 분야 질문이면 자동 라우팅 + domain_keywords = [ + "산업안전", "산안법", "안전보건", "위험성평가", "중대재해", + "asme", "압력용기", "배관", "용접", "기계안전", + "화학물질", "유해물질", "msds", "ghs", + "법령", "시행령", "시행규칙", "고시", "규정", + "산업안전기사", "건설안전", "전기안전", + ] + domain_hit = any(k in msg.lower() for k in domain_keywords) + + if (doc_entry and doc_action) or domain_hit: query = msg - for rm in ["문서", "도큐먼트", "자료", "파일", "찾아줘", "찾아", "검색", "확인", "해줘", "줘", "좀"]: - query = query.replace(rm, "") + # 명시적 문서 키워드가 있으면 제거 + if doc_entry: + for rm in ["문서", "도큐먼트", "자료", "파일", "찾아줘", "찾아", "검색", "확인", "해줘", "줘", "좀"]: + query = query.replace(rm, "") query = query.strip() if query: # 질문형 강신호 - ask_signals = ["알려줘", "설명해", "차이", "비교", "절차", "요건", "무엇", "왜", "어떻게", "뭐야", "뭔가"] + ask_signals = ["알려줘", "설명해", "차이", "비교", "절차", "요건", "무엇", "왜", "어떻게", "뭐야", "뭔가", "뭐지", "내용"] # 탐색형 강신호 search_signals = ["찾아", "검색", "목록", "최근", "업로드", "리스트"] ask_score = sum(1 for s in ask_signals if s in msg) search_score = sum(1 for s in search_signals if s in msg) + + # 도메인 키워드만으로 진입한 경우 질문형으로 간주 (+1 보너스) + if domain_hit and not doc_entry: + ask_score += 1 + # 초기 운영 가드: ask는 강신호 2개 이상일 때만 if ask_score >= 2 and ask_score > search_score: operation = "ask" @@ -357,6 +375,11 @@ async def run(job: Job) -> None: await state_stream.push(job.id, "result", {"content": response}) await conversation_store.add(user_id, "assistant", response) else: + # 문서 도구 호출 시 안내 문구 + if tool_name == "document" and job.callback == "synology": + notice = "서고를 확인하는 중입니다..." if operation == "ask" else "문서를 검색하는 중입니다..." + await send_to_synology(notice, raw=True) + # 일반 도구 실행 (document.ask는 긴 timeout) timeout = DOCUMENT_ASK_TIMEOUT if (tool_name == "document" and operation == "ask") else TOOL_TIMEOUT try: