feat(nanoclaude): 도메인 키워드 자동 라우팅 + 서고 확인 안내 문구

- _pre_route: 산업안전/ASME/법령 등 도메인 키워드로 "문서 찾아줘" 없이도
  자동으로 document tool 라우팅 (도메인 진입 시 ask +1 보너스)
- EXAONE classifier: document.ask/search_full 도구 + 예시 추가
- worker: document tool 호출 전 "서고를 확인하는 중입니다" 안내 문구 전송

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-04-15 15:06:14 +09:00
parent 637df9df2e
commit 7dbf37a63f
2 changed files with 34 additions and 6 deletions
+7 -2
View File
@@ -26,7 +26,9 @@ CLASSIFIER_PROMPT = """\
- calendar.create_confirmed() — pending_draft가 있을 때, 사용자가 "확인/예/yes" 한 경우만 사용 - calendar.create_confirmed() — pending_draft가 있을 때, 사용자가 "확인/예/yes" 한 경우만 사용
- email.search(query, days) — 메일 검색. days 기본 7 - email.search(query, days) — 메일 검색. days 기본 7
- email.read(uid) — 메일 본문 조회 - email.read(uid) — 메일 본문 조회
- document.search(query) — 문서 검색 - document.ask(query) — 문서 기반 AI 답변. 법령/규정/기술 내용을 물어볼 때 사용. 시간 걸림
- document.search_full(query) — 문서 고급 검색 (리랭킹 포함). 문서 목록이 필요할 때 사용
- document.search(query) — 문서 간단 검색
- document.read(doc_id) — 문서 조회 - document.read(doc_id) — 문서 조회
- infra.status(host) — 서버 Docker 컨테이너 상태. host: gpu (기본), nas-company - infra.status(host) — 서버 Docker 컨테이너 상태. host: gpu (기본), nas-company
- infra.health(service) — 서비스 헬스체크. service 생략 시 전체 체크. 개별: document-server, mlx, ollama-gpu - infra.health(service) — 서비스 헬스체크. service 생략 시 전체 체크. 개별: document-server, mlx, ollama-gpu
@@ -51,7 +53,10 @@ JSON 형식:
- "이번주 일정" → tools: calendar.search(이번주 월~일) - "이번주 일정" → tools: calendar.search(이번주 월~일)
- "내일 3시 회의" → tools: calendar.create_draft(...) - "내일 3시 회의" → tools: calendar.create_draft(...)
- "최근 메일" → tools: email.search("", 7) - "최근 메일" → 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") - "GPU 서버 상태" → tools: infra.status("gpu")
- "서비스 살아있어?" → tools: infra.health() - "서비스 살아있어?" → tools: infra.health()
- "디스크 용량" → tools: infra.disk() - "디스크 용량" → tools: infra.disk()
+27 -4
View File
@@ -183,18 +183,36 @@ def _pre_route(message: str) -> dict | None:
# 문서 키워드 — 질문형/탐색형 점수 기반 분기 # 문서 키워드 — 질문형/탐색형 점수 기반 분기
doc_entry = any(k in msg for k in ["문서", "도큐먼트", "자료", "파일"]) doc_entry = any(k in msg for k in ["문서", "도큐먼트", "자료", "파일"])
doc_action = 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 query = msg
for rm in ["문서", "도큐먼트", "자료", "파일", "찾아줘", "찾아", "검색", "확인", "해줘", "", ""]: # 명시적 문서 키워드가 있으면 제거
query = query.replace(rm, "") if doc_entry:
for rm in ["문서", "도큐먼트", "자료", "파일", "찾아줘", "찾아", "검색", "확인", "해줘", "", ""]:
query = query.replace(rm, "")
query = query.strip() query = query.strip()
if query: if query:
# 질문형 강신호 # 질문형 강신호
ask_signals = ["알려줘", "설명해", "차이", "비교", "절차", "요건", "무엇", "", "어떻게", "뭐야", "뭔가"] ask_signals = ["알려줘", "설명해", "차이", "비교", "절차", "요건", "무엇", "", "어떻게", "뭐야", "뭔가", "뭐지", "내용"]
# 탐색형 강신호 # 탐색형 강신호
search_signals = ["찾아", "검색", "목록", "최근", "업로드", "리스트"] search_signals = ["찾아", "검색", "목록", "최근", "업로드", "리스트"]
ask_score = sum(1 for s in ask_signals if s in msg) 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) 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개 이상일 때만 # 초기 운영 가드: ask는 강신호 2개 이상일 때만
if ask_score >= 2 and ask_score > search_score: if ask_score >= 2 and ask_score > search_score:
operation = "ask" operation = "ask"
@@ -357,6 +375,11 @@ async def run(job: Job) -> None:
await state_stream.push(job.id, "result", {"content": response}) await state_stream.push(job.id, "result", {"content": response})
await conversation_store.add(user_id, "assistant", response) await conversation_store.add(user_id, "assistant", response)
else: else:
# 문서 도구 호출 시 안내 문구
if tool_name == "document" and job.callback == "synology":
notice = "서고를 확인하는 중입니다..." if operation == "ask" else "문서를 검색하는 중입니다..."
await send_to_synology(notice, raw=True)
# 일반 도구 실행 (document.ask는 긴 timeout) # 일반 도구 실행 (document.ask는 긴 timeout)
timeout = DOCUMENT_ASK_TIMEOUT if (tool_name == "document" and operation == "ask") else TOOL_TIMEOUT timeout = DOCUMENT_ASK_TIMEOUT if (tool_name == "document" and operation == "ask") else TOOL_TIMEOUT
try: try: