feat: 이드 도구 확장 — 캘린더/메일/문서 연동 (read-only + 캘린더 생성 확인)
- tools/calendar_tool.py: CalDAV search/today/create_draft/create_confirmed - tools/email_tool.py: IMAP search/read (전송 비활성화) - tools/document_tool.py: Document Server search/read (read-only) - tools/registry.py: 도구 디스패처 + WRITE_OPS 안전장치 + 에러 표준화 - 분류기: "tools" 액션 추가, 도구 목록/파라미터 스키마/규칙 명시 - Worker: tools 분기 + tool timeout 10초 + payload 2000자 제한 - conversation: pending_draft (TTL 5분) + create 확인 플로우 - 현재 시간을 분류기에 전달 (날짜 질문 대응) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
91
nanoclaude/tools/document_tool.py
Normal file
91
nanoclaude/tools/document_tool.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""Document 도구 — Document Server REST API (read-only)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import httpx
|
||||
|
||||
from config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TOOL_NAME = "document"
|
||||
MAX_RESULTS = 5
|
||||
|
||||
|
||||
def _make_result(ok: bool, operation: str, data=None, summary: str = "", error: str | None = None) -> dict:
|
||||
return {"ok": ok, "tool": TOOL_NAME, "operation": operation, "data": data or [], "summary": summary, "error": error}
|
||||
|
||||
|
||||
def _headers() -> dict:
|
||||
return {"Authorization": f"Bearer {settings.document_api_token}"} if settings.document_api_token else {}
|
||||
|
||||
|
||||
async def search(query: str) -> dict:
|
||||
"""문서 하이브리드 검색."""
|
||||
if not settings.document_api_url:
|
||||
return _make_result(False, "search", error="Document Server 설정이 없습니다.")
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.get(
|
||||
f"{settings.document_api_url}/search/",
|
||||
params={"q": query, "mode": "hybrid"},
|
||||
headers=_headers(),
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
return _make_result(False, "search", error=f"API 응답 오류 ({resp.status_code})")
|
||||
|
||||
results = resp.json()
|
||||
if isinstance(results, dict):
|
||||
results = results.get("results", results.get("data", []))
|
||||
|
||||
# top-N 제한
|
||||
results = results[:MAX_RESULTS]
|
||||
|
||||
items = []
|
||||
for doc in results:
|
||||
items.append({
|
||||
"id": doc.get("id", ""),
|
||||
"title": doc.get("title", "(제목 없음)"),
|
||||
"domain": doc.get("domain", ""),
|
||||
"preview": str(doc.get("content", doc.get("snippet", "")))[:200],
|
||||
})
|
||||
|
||||
summary = f"'{query}' 검색 결과 {len(items)}건"
|
||||
return _make_result(True, "search", data=items, summary=summary)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Document search failed")
|
||||
return _make_result(False, "search", error=str(e))
|
||||
|
||||
|
||||
async def read(doc_id: str) -> dict:
|
||||
"""문서 내용 조회."""
|
||||
if not settings.document_api_url:
|
||||
return _make_result(False, "read", error="Document Server 설정이 없습니다.")
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.get(
|
||||
f"{settings.document_api_url}/documents/{doc_id}",
|
||||
headers=_headers(),
|
||||
)
|
||||
if resp.status_code == 404:
|
||||
return _make_result(False, "read", error=f"문서 {doc_id}를 찾을 수 없습니다.")
|
||||
if resp.status_code != 200:
|
||||
return _make_result(False, "read", error=f"API 응답 오류 ({resp.status_code})")
|
||||
|
||||
doc = resp.json()
|
||||
data = {
|
||||
"id": doc.get("id", ""),
|
||||
"title": doc.get("title", ""),
|
||||
"domain": doc.get("domain", ""),
|
||||
"content": str(doc.get("content", doc.get("markdown_content", "")))[:2000],
|
||||
}
|
||||
return _make_result(True, "read", data=data, summary=f"문서: {data['title']}")
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Document read failed")
|
||||
return _make_result(False, "read", error=str(e))
|
||||
Reference in New Issue
Block a user