다크 모드
Tool 시스템
truloop-assistant의 pydantic-ai 도구 시스템, Deferred Tool 패턴, JSON-RPC 2.0 프로토콜을 설명합니다.
도구 시스템 개요
등록된 도구 목록
회원 Agent 도구 (all_toolsets, 9개)
| 도구 파일 | 실행 위치 | 기능 | 주요 함수 |
|---|---|---|---|
datetime_tools.py | 서버 | 요일 조회, 현재 시각 (타임존 변환) | get_day_of_week, get_current_datetime |
user_tools.py | 서버 | 사용자 프로필, 사용자 검색 (연락처/username/룹), EID 일괄 조회 | get_user_profile, search_users, get_user_eids |
knowledge_tools.py | 서버 | RAG 기반 지식 검색 (Notion 문서) | search_knowledge_base |
memory_tools.py | 서버 | Supermemory 장기 기억 저장/검색/삭제 | search_memories, save_memory, forget_memory |
places_tools.py | 서버 | Google Maps 장소 검색 (비동기 thread pool) | search_places |
relay_tools.py | 서버 | 사용자 간 메시지 릴레이 (Agent 기반 알림 생성) | send_message_to_user |
event_tools.py | 하이브리드 | 일정 CRUD (서버) + 캘린더 조회 (Deferred). create_event, update_event에서 availability_deadline 파라미터 지원 | get_events, create_event, invite_to_event 등 |
interaction_tools.py | 클라이언트 | 확인 다이얼로그 (OS 권한 요청 포함 가능) | confirm_interaction |
permission_tools.py | 클라이언트 | OS 권한 상태 확인 (요청 없이 조회만) | get_permission_status |
비회원 Agent 도구 (guest_toolset, 1개)
주의
미구현: 비회원 Agent 도구는 코드 scaffolding으로 존재하지만, 비회원 Agent 자체가 미구현이므로 프로덕션에서 사용되지 않습니다.
| 도구 파일 | 실행 위치 | 기능 | 주요 함수 |
|---|---|---|---|
guest_tools.py | 서버 | 가용 시간 수집, 일정 조회, 초대 목록 | update_guest_availability, get_event_details, list_invited_events |
알림 안전 도구 (notification_safe_toolsets)
Agent 기반 알림 생성 시 사용하는 안전한 도구 세트. 이벤트 생성/초대 도구를 제외하여 재귀 API 호출 방지:
| 포함 | 제외 (사유) |
|---|---|
datetime_toolset | event_toolset (재귀 이벤트 생성 방지) |
knowledge_toolset | user_toolset (의도치 않은 조회 방지) |
interaction_toolset | relay_toolset (의도치 않은 메시지 발송 방지) |
permission_toolset | |
places_toolset | |
memory_toolset |
공통
| 파일 | 역할 |
|---|---|
validation.py | 도구 입력 검증 유틸리티 (가용 날짜 형식 검증 등) |
즉시 실행 도구
서버에서 바로 실행되는 도구입니다. FunctionToolset 인스턴스에 @toolset.tool 데코레이터로 등록합니다.
python
from pydantic import Field
from pydantic_ai import FunctionToolset, RunContext
from truloop_assistant.agent.deps import AgentDeps
datetime_toolset = FunctionToolset[AgentDeps]()
@datetime_toolset.tool
async def get_day_of_week(
ctx: RunContext[AgentDeps],
date: str | None = Field(default=None, description="ISO format (YYYY-MM-DD)"),
) -> DayOfWeekResult:
"""특정 날짜의 요일을 반환합니다."""
...도구 설계 원칙:
FunctionToolset[AgentDeps]인스턴스를 모듈 레벨에서 생성하고@toolset.tool로 등록- 단일 책임 (하나의 도구 = 하나의 기능)
- 서술적 docstring (Agent가 도구 설명을 읽어 호출 결정)
Field(description=...)으로 파라미터 설명 제공- 성공 시 Pydantic BaseModel 반환, 실패 시
str에러 메시지 반환 (Union 패턴) - API 오류 시
ModelRetry예외로 Agent에게 재시도 기회 제공
Deferred Tool 시스템
개념
Deferred Tool은 서버가 아닌 클라이언트 디바이스에서 실행되는 도구입니다. OS 권한이 필요한 작업(캘린더, 위치 등)을 처리하기 위한 Human-in-the-Loop 패턴입니다.
구현 원리
Deferred Tool은 CallDeferred 예외를 발생시켜 Agent 실행을 일시 중단합니다:
python
from pydantic import Field
from pydantic_ai import FunctionToolset, RunContext
from pydantic_ai.exceptions import CallDeferred
from truloop_assistant.agent.deps import AgentDeps
permission_toolset = FunctionToolset[AgentDeps]()
@permission_toolset.tool
async def get_permission_status(
ctx: RunContext[AgentDeps],
notification_message: str = Field(
description="권한 상태 확인 중 표시할 메시지",
),
permission: str = Field(
description="확인할 OS 권한 (예: 'calendar.read')",
),
) -> str:
"""OS 권한 상태를 확인합니다 (요청 없이 조회만)."""
raise CallDeferred() # Agent 실행 중단, 클라이언트에 위임Agent는 DeferredToolRequests를 반환하고, 서버는 이를 JSON-RPC 요청으로 변환하여 Sendbird를 통해 클라이언트에 전달합니다.
정보
notification_message 파라미터는 클라이언트가 Deferred Tool 실행 중 사용자에게 표시하는 대기 메시지입니다. JSON-RPC params에는 포함되지 않고 별도로 처리됩니다.
JSON-RPC 2.0 프로토콜
클라이언트-서버 간 Deferred Tool 통신은 JSON-RPC 2.0 표준을 따릅니다.
요청 (서버 → 클라이언트)
Sendbird 메시지로 전달됩니다 (custom_type: "agent-rpc"):
json
{
"jsonrpc": "2.0",
"id": "call_abc123",
"method": "calendar.getEvents",
"params": {
"startDate": "2026-03-09T00:00:00+09:00",
"endDate": "2026-03-15T23:59:59+09:00"
}
}성공 응답 (클라이언트 → 서버)
json
{
"jsonrpc": "2.0",
"id": "call_abc123",
"result": {
"events": [
{
"id": "event_1",
"title": "팀 미팅",
"start": { "dateTime": "2026-03-10T10:00:00+09:00" },
"end": { "dateTime": "2026-03-10T11:00:00+09:00" }
}
]
}
}에러 응답 (클라이언트 → 서버)
json
{
"jsonrpc": "2.0",
"id": "call_abc123",
"error": {
"code": -32010,
"message": "PermissionRequired",
"data": {
"requiredPermission": "calendar.read",
"canPromptAgain": true
}
}
}에러 코드
| 코드 | 이름 | 설명 |
|---|---|---|
-32010 | PermissionRequired | 캘린더 권한 미부여 |
-32001 | PermissionDenied | 사용자가 권한 거부 |
-32002 | UserCanceled | 사용자가 작업 취소 |
Deferred Tool 전체 흐름
사용자 메시지 수신
사용자가 Sendbird로 "이번 주 일정 알려줘" 전송 → Webhook으로 서버 수신
Agent 처리 및 Tool 호출 결정
Agent가 메시지를 분석하고 get_calendar_events 도구 호출을 결정
CallDeferred 발생
도구가 CallDeferred 예외를 발생 → Agent가 DeferredToolRequests 반환
Pending Call 저장 및 JSON-RPC 전송
서버가 Redis에 Pending call 저장 (TTL: 1시간) → Sendbird로 JSON-RPC 요청 전송 (custom_type: "agent-rpc")
클라이언트 처리
클라이언트가 Sendbird 메시지 수신 → OS 캘린더 권한 요청 → 캘린더 이벤트 조회
Tool 결과 제출
클라이언트가 POST /api/v1/agent/tool-message로 JSON-RPC 응답 전송
Agent 재개 및 최종 응답
서버가 Redis에서 Pending call 조회 → DeferredToolResults로 Agent 재개 → 최종 응답을 Sendbird로 전송 → Pending call 삭제
PendingCallService
Redis에 Deferred Tool 호출을 추적합니다:
| 항목 | 설명 |
|---|---|
| 저장소 | Redis |
| 키 형식 | Tool call ID 기반 |
| TTL | 1시간 |
| 저장 데이터 | Tool call ID, 메서드, 파라미터, 사용자 정보, 채널 정보 |
클라이언트 구현 가이드
1. Sendbird 메시지 감지
typescript
// custom_type이 "agent-rpc"인 메시지 감지
if (message.customType === 'agent-rpc') {
const rpcRequest = JSON.parse(message.data)
handleRPCRequest(rpcRequest)
}2. OS 권한 요청 및 실행
typescript
const permission = await requestCalendarPermission()
if (!permission.granted) {
// 에러 응답 전송
return submitToolError(rpcRequest.id, { code: -32010, ... })
}
const events = await queryCalendar(params)3. 결과 제출
typescript
await fetch('/assistant/api/v1/agent/tool-message', {
method: 'POST',
headers: {
'Authorization': `Bearer ${jwtToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: rpcRequest.id,
result: { events }
})
})Deferred Tool Handler 패턴
각 Deferred Tool은 Handler 클래스를 가지며, DeferredToolHandler 프로토콜을 구현합니다:
python
class InteractionToolHandler:
def get_tool_name(self) -> str: # "confirm_interaction"
def get_method_name(self) -> str: # "interaction.confirm"
def create_params(self, call) -> ...: # ToolCallPart → JSON-RPC params
def validate_result(self, data) -> ...: # Raw dict → 타입 모델
def transform_result(self, v) -> ...: # 결과 변환 (필요 시)| Handler | Tool 이름 | JSON-RPC 메서드 |
|---|---|---|
EventToolHandler | get_calendar_events | calendar.getEvents |
InteractionToolHandler | confirm_interaction | interaction.confirm |
PermissionToolHandler | get_permission_status | permissions.getStatus |
변경 이력
| 날짜 | 변경 내용 |
|---|---|
| 2026-03-12 | event_tools에 availability_deadline 파라미터 지원 추가 (create_event, update_event) |
| 2026-03-11 | 비회원 Agent / SMS two-way 기능을 미구현 상태로 정정 |
| 2026-03-10 | 도구 목록 전면 업데이트 (회원/비회원/알림 안전 분류), FunctionToolset 패턴, Deferred Tool Handler 추가 |