feat(nanoclaude): Phase 3 infra intent — 시놀로지 Chat에서 서버 관리

EXAONE classifier에 infra 도구 5개 추가 (status, health, disk, network, models).
infra_tool.py가 infra.core/ 호출 → NanoClaude 반환 형식으로 변환.
"GPU 상태 알려줘" → tools: infra.status("gpu") → 구조화된 결과.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-04-13 13:24:02 +09:00
parent 82ce83b8b7
commit c7e44646fd
3 changed files with 141 additions and 1 deletions
+10
View File
@@ -28,6 +28,11 @@ CLASSIFIER_PROMPT = """\
- email.read(uid) — 메일 본문 조회 - email.read(uid) — 메일 본문 조회
- document.search(query) — 문서 검색 - document.search(query) — 문서 검색
- document.read(doc_id) — 문서 조회 - document.read(doc_id) — 문서 조회
- infra.status(host) — 서버 Docker 컨테이너 상태. host: gpu (기본), nas-company
- infra.health(service) — 서비스 헬스체크. service 생략 시 전체 체크. 개별: document-server, mlx, ollama-gpu
- infra.disk(host) — 디스크 사용률. host 생략 시 gpu+macmini 전체
- infra.network() — Tailscale 네트워크 연결 상태
- infra.models(host) — 모델 목록. host: gpu (Ollama), mlx (맥미니 MLX)
메일 전송은 불가능하다. 메일 보내달라는 요청은 거부하라. 메일 전송은 불가능하다. 메일 보내달라는 요청은 거부하라.
@@ -47,6 +52,11 @@ JSON 형식:
- "내일 3시 회의" → tools: calendar.create_draft(...) - "내일 3시 회의" → tools: calendar.create_draft(...)
- "최근 메일" → tools: email.search("", 7) - "최근 메일" → tools: email.search("", 7)
- "문서 찾아줘" → tools: document.search(...) - "문서 찾아줘" → tools: document.search(...)
- "GPU 서버 상태" → tools: infra.status("gpu")
- "서비스 살아있어?" → tools: infra.health()
- "디스크 용량" → tools: infra.disk()
- "네트워크 상태" → tools: infra.network()
- "모델 뭐 있어?" → tools: infra.models("gpu")
- "안녕" → direct - "안녕" → direct
- "양자역학 설명해줘" → route - "양자역학 설명해줘" → route
+112
View File
@@ -0,0 +1,112 @@
"""Infra tool — NanoClaude wrapper over infra.core/ functions.
Converts infra.core results to NanoClaude tool return format:
{"ok": bool, "tool": "infra", "operation": str, "data": list, "summary": str, "error": str}
"""
from __future__ import annotations
import asyncio
import logging
from infra.core.docker import docker_status
from infra.core.health import service_health, VALID_SERVICES
from infra.core.system import disk_usage
from infra.core.network import tailscale_status
from infra.core.models import ollama_models, mlx_models
logger = logging.getLogger(__name__)
async def status(host: str = "gpu") -> dict:
"""Docker container status overview."""
result = await docker_status(host)
if not result.ok:
return {"ok": False, "tool": "infra", "operation": "status",
"data": [], "summary": "", "error": result.error or "확인 실패"}
data = [{"name": c.name, "status": c.status, "uptime": c.uptime}
for c in result.containers]
return {"ok": True, "tool": "infra", "operation": "status",
"data": data, "summary": result.summary, "error": ""}
async def health(service: str = "") -> dict:
"""Service health check. If no service specified, check all critical ones."""
if service:
services = [service]
else:
services = ["document-server", "mlx", "ollama-gpu"]
results = []
all_ok = True
for svc in services:
r = await service_health(svc)
results.append({
"service": r.service, "status": r.status, "ok": r.ok,
"details": r.details,
})
if not r.ok:
all_ok = False
summary_parts = []
for r in results:
icon = "정상" if r["ok"] else "이상"
summary_parts.append(f"{r['service']}: {icon}")
return {"ok": all_ok, "tool": "infra", "operation": "health",
"data": results, "summary": ", ".join(summary_parts), "error": ""}
async def disk(host: str = "") -> dict:
"""Disk usage. If no host, check gpu + macmini."""
hosts = [host] if host else ["gpu", "macmini"]
all_data = []
warnings = []
for h in hosts:
result = await disk_usage(h)
if not result.ok:
warnings.append(f"{h}: {result.error}")
continue
for fs in result.filesystems:
all_data.append({"host": h, "mount": fs.mount,
"used_pct": fs.used_pct, "used": fs.used, "total": fs.total})
warnings.extend(result.warnings)
summary = ", ".join(f"{d['host']}:{d['mount']} {d['used_pct']}%" for d in all_data[:5])
return {"ok": len(warnings) == 0, "tool": "infra", "operation": "disk",
"data": all_data, "summary": summary,
"error": "; ".join(warnings) if warnings else ""}
async def network() -> dict:
"""Tailscale network status."""
result = await tailscale_status()
if not result.ok:
return {"ok": False, "tool": "infra", "operation": "network",
"data": [], "summary": "", "error": result.error or "확인 실패"}
data = [{"hostname": p.hostname, "ip": p.ip, "status": p.status, "os": p.os}
for p in result.peers]
online = sum(1 for p in result.peers if p.status != "offline")
summary = f"{online}/{len(result.peers)} 온라인"
return {"ok": True, "tool": "infra", "operation": "network",
"data": data, "summary": summary, "error": ""}
async def models(host: str = "gpu") -> dict:
"""Model inventory."""
if host == "mlx" or host == "macmini":
result = await mlx_models()
else:
result = await ollama_models(host)
if not result.ok:
return {"ok": False, "tool": "infra", "operation": "models",
"data": [], "summary": "", "error": result.error or "확인 실패"}
data = [{"id": m.id, "size": m.size} for m in result.models]
summary = f"{result.source} on {result.host}: {len(result.models)}개 모델"
return {"ok": True, "tool": "infra", "operation": "models",
"data": data, "summary": summary, "error": ""}
+19 -1
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
import logging import logging
from tools import calendar_tool, document_tool, email_tool from tools import calendar_tool, document_tool, email_tool, infra_tool
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -13,6 +13,7 @@ ERROR_MESSAGES = {
"calendar": "⚠️ 캘린더 서비스를 사용할 수 없습니다. 잠시 후 다시 시도해주세요.", "calendar": "⚠️ 캘린더 서비스를 사용할 수 없습니다. 잠시 후 다시 시도해주세요.",
"email": "⚠️ 메일 서비스를 사용할 수 없습니다. 잠시 후 다시 시도해주세요.", "email": "⚠️ 메일 서비스를 사용할 수 없습니다. 잠시 후 다시 시도해주세요.",
"document": "⚠️ 문서 서비스를 사용할 수 없습니다. 잠시 후 다시 시도해주세요.", "document": "⚠️ 문서 서비스를 사용할 수 없습니다. 잠시 후 다시 시도해주세요.",
"infra": "⚠️ 인프라 서비스를 확인할 수 없습니다. 잠시 후 다시 시도해주세요.",
} }
# 허용된 operations # 허용된 operations
@@ -20,6 +21,7 @@ ALLOWED_OPS = {
"calendar": {"today", "search", "create_draft", "create_confirmed"}, "calendar": {"today", "search", "create_draft", "create_confirmed"},
"email": {"search", "read"}, "email": {"search", "read"},
"document": {"search", "read"}, "document": {"search", "read"},
"infra": {"status", "health", "disk", "network", "models"},
} }
# payload hard limit # payload hard limit
@@ -43,6 +45,8 @@ async def execute_tool(tool_name: str, operation: str, params: dict) -> dict:
result = await _exec_email(operation, params) result = await _exec_email(operation, params)
elif tool_name == "document": elif tool_name == "document":
result = await _exec_document(operation, params) result = await _exec_document(operation, params)
elif tool_name == "infra":
result = await _exec_infra(operation, params)
else: else:
result = _error(tool_name, operation, "미구현") result = _error(tool_name, operation, "미구현")
@@ -98,5 +102,19 @@ async def _exec_document(operation: str, params: dict) -> dict:
return _error("document", operation, "미구현") return _error("document", operation, "미구현")
async def _exec_infra(operation: str, params: dict) -> dict:
if operation == "status":
return await infra_tool.status(params.get("host", "gpu"))
elif operation == "health":
return await infra_tool.health(params.get("service", ""))
elif operation == "disk":
return await infra_tool.disk(params.get("host", ""))
elif operation == "network":
return await infra_tool.network()
elif operation == "models":
return await infra_tool.models(params.get("host", "gpu"))
return _error("infra", operation, "미구현")
def _error(tool: str, operation: str, msg: str) -> dict: def _error(tool: str, operation: str, msg: str) -> dict:
return {"ok": False, "tool": tool, "operation": operation, "data": [], "summary": "", "error": msg} return {"ok": False, "tool": tool, "operation": operation, "data": [], "summary": "", "error": msg}