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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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": ""}
|
||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user