feat(calendar): U3 agent calendar tool for ReAct integration
Adds CalendarTool implementing the Tool ABC so the ReAct engine can create, query, update, and delete events autonomously. Resolves event_type_name and tag_names (look up or create), sets source="agent" to distinguish agent-created events from manual ones. - src/agentkit/tools/calendar_tool.py — CalendarTool(Tool) - tests/unit/tools/test_calendar_tool.py — 13 tests covering all actions
This commit is contained in:
parent
d36e45bbe7
commit
42fe7bcbc9
|
|
@ -0,0 +1,277 @@
|
|||
"""CalendarTool — Agent tool for calendar event CRUD via ReAct integration.
|
||||
|
||||
Wraps CalendarService so the LLM can create, query, update, and delete
|
||||
calendar events via function calling. The tool delegates all business logic
|
||||
to CalendarService — it only handles input validation, name→id resolution
|
||||
for event types and tags, and result formatting.
|
||||
|
||||
The tool trusts the caller (the agent framework) to provide the correct
|
||||
user_id; it does not perform auth (same pattern as DocumentTool).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from agentkit.calendar.service import CalendarService
|
||||
from agentkit.tools.base import Tool
|
||||
|
||||
|
||||
class CalendarTool(Tool):
|
||||
"""Agent tool for calendar event management.
|
||||
|
||||
Actions: create_event, query_events, update_event, delete_event.
|
||||
"""
|
||||
|
||||
def __init__(self, calendar_service: CalendarService):
|
||||
super().__init__(
|
||||
name="calendar",
|
||||
description=(
|
||||
"Create, query, update, and delete calendar events. "
|
||||
"Actions: create_event, query_events, update_event, delete_event."
|
||||
),
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"create_event",
|
||||
"query_events",
|
||||
"update_event",
|
||||
"delete_event",
|
||||
],
|
||||
"description": "Calendar operation to perform.",
|
||||
},
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"description": "User ID owning the calendar events.",
|
||||
},
|
||||
"event_id": {
|
||||
"type": "string",
|
||||
"description": "Event ID (for update_event and delete_event).",
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Event title (create_event, update_event).",
|
||||
},
|
||||
"start_time": {
|
||||
"type": "string",
|
||||
"description": "Event start time, ISO 8601 UTC (create_event, update_event).",
|
||||
},
|
||||
"end_time": {
|
||||
"type": "string",
|
||||
"description": "Event end time, ISO 8601 UTC (create_event, update_event).",
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Event description (create_event, update_event).",
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "Event location (create_event, update_event).",
|
||||
},
|
||||
"is_all_day": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the event is all-day (create_event, update_event).",
|
||||
},
|
||||
"event_type_name": {
|
||||
"type": "string",
|
||||
"description": "Event type name; looked up or created if missing (create_event).",
|
||||
},
|
||||
"tag_names": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Tag names; each looked up or created if missing (create_event).",
|
||||
},
|
||||
"rrule": {
|
||||
"type": "string",
|
||||
"description": "RFC 5545 RRULE recurrence string, e.g. FREQ=WEEKLY;BYDAY=MO;COUNT=10 (create_event).",
|
||||
},
|
||||
"conversation_id": {
|
||||
"type": "string",
|
||||
"description": "Conversation ID to associate with the event (create_event).",
|
||||
},
|
||||
"start_date": {
|
||||
"type": "string",
|
||||
"description": "Range start, ISO 8601 UTC (query_events).",
|
||||
},
|
||||
"end_date": {
|
||||
"type": "string",
|
||||
"description": "Range end, ISO 8601 UTC (query_events).",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of events to return (query_events).",
|
||||
},
|
||||
},
|
||||
"required": ["action", "user_id"],
|
||||
},
|
||||
)
|
||||
self._service = calendar_service
|
||||
|
||||
async def execute(self, **kwargs) -> dict[str, Any]:
|
||||
action = kwargs.get("action")
|
||||
|
||||
if action == "create_event":
|
||||
return await self._create_event(**kwargs)
|
||||
if action == "query_events":
|
||||
return await self._query_events(**kwargs)
|
||||
if action == "update_event":
|
||||
return await self._update_event(**kwargs)
|
||||
if action == "delete_event":
|
||||
return await self._delete_event(**kwargs)
|
||||
return {"success": False, "error": f"Unknown action: {action!r}"}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# create_event
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
async def _create_event(self, **kwargs) -> dict[str, Any]:
|
||||
user_id = kwargs.get("user_id")
|
||||
title = kwargs.get("title")
|
||||
start_time = kwargs.get("start_time")
|
||||
end_time = kwargs.get("end_time")
|
||||
|
||||
if not user_id:
|
||||
return {"success": False, "error": "Missing required field: user_id"}
|
||||
if not title:
|
||||
return {"success": False, "error": "Missing required field: title"}
|
||||
if not start_time:
|
||||
return {"success": False, "error": "Missing required field: start_time"}
|
||||
if not end_time:
|
||||
return {"success": False, "error": "Missing required field: end_time"}
|
||||
|
||||
description = kwargs.get("description", "")
|
||||
location = kwargs.get("location", "")
|
||||
is_all_day = kwargs.get("is_all_day", False)
|
||||
rrule = kwargs.get("rrule")
|
||||
conversation_id = kwargs.get("conversation_id")
|
||||
|
||||
# Resolve event_type_name → event_type_id (look up or create)
|
||||
event_type_id: str | None = None
|
||||
event_type_name = kwargs.get("event_type_name")
|
||||
if event_type_name:
|
||||
event_type_id = await self._resolve_event_type_id(user_id, event_type_name)
|
||||
|
||||
# Resolve tag_names → tag_ids (look up or create each)
|
||||
tag_ids: list[str] | None = None
|
||||
tag_names = kwargs.get("tag_names")
|
||||
if tag_names:
|
||||
tag_ids = await self._resolve_tag_ids(user_id, tag_names)
|
||||
|
||||
try:
|
||||
event = await self._service.create_event(
|
||||
user_id=user_id,
|
||||
title=title,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
description=description,
|
||||
location=location,
|
||||
is_all_day=is_all_day,
|
||||
event_type_id=event_type_id,
|
||||
rrule=rrule,
|
||||
source="agent",
|
||||
conversation_id=conversation_id,
|
||||
tag_ids=tag_ids,
|
||||
)
|
||||
return {"success": True, "event": event.to_dict()}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"create_event failed: {e}"}
|
||||
|
||||
async def _resolve_event_type_id(self, user_id: str, name: str) -> str | None:
|
||||
"""Look up an event type by name for the user; create if not found."""
|
||||
existing = await self._service.list_event_types(user_id)
|
||||
for et in existing:
|
||||
if et.name == name:
|
||||
return et.id
|
||||
et = await self._service.create_event_type(user_id, name)
|
||||
return et.id
|
||||
|
||||
async def _resolve_tag_ids(self, user_id: str, names: list[str]) -> list[str]:
|
||||
"""Look up tags by name for the user; create each if not found."""
|
||||
existing = await self._service.list_tags(user_id)
|
||||
existing_by_name = {t.name: t.id for t in existing}
|
||||
tag_ids: list[str] = []
|
||||
for name in names:
|
||||
if name in existing_by_name:
|
||||
tag_ids.append(existing_by_name[name])
|
||||
else:
|
||||
tag = await self._service.create_tag(user_id, name)
|
||||
tag_ids.append(tag.id)
|
||||
return tag_ids
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# query_events
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
async def _query_events(self, **kwargs) -> dict[str, Any]:
|
||||
user_id = kwargs.get("user_id")
|
||||
if not user_id:
|
||||
return {"success": False, "error": "Missing required field: user_id"}
|
||||
|
||||
start = kwargs.get("start_date")
|
||||
end = kwargs.get("end_date")
|
||||
limit = kwargs.get("limit")
|
||||
|
||||
try:
|
||||
events = await self._service.list_events(
|
||||
user_id=user_id,
|
||||
start=start,
|
||||
end=end,
|
||||
)
|
||||
if limit is not None:
|
||||
events = events[:limit]
|
||||
return {"success": True, "events": [e.to_dict() for e in events]}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"query_events failed: {e}"}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# update_event
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
async def _update_event(self, **kwargs) -> dict[str, Any]:
|
||||
event_id = kwargs.get("event_id")
|
||||
user_id = kwargs.get("user_id")
|
||||
if not event_id:
|
||||
return {"success": False, "error": "Missing required field: event_id"}
|
||||
if not user_id:
|
||||
return {"success": False, "error": "Missing required field: user_id"}
|
||||
|
||||
# Build fields dict from updatable params (only those explicitly provided)
|
||||
updatable = ["title", "description", "start_time", "end_time", "location", "is_all_day"]
|
||||
fields: dict[str, Any] = {}
|
||||
for key in updatable:
|
||||
if key in kwargs and kwargs[key] is not None:
|
||||
fields[key] = kwargs[key]
|
||||
|
||||
if not fields:
|
||||
return {"success": False, "error": "No fields to update"}
|
||||
|
||||
try:
|
||||
updated = await self._service.update_event(event_id, fields)
|
||||
if not updated:
|
||||
return {"success": False, "error": f"Event not found: {event_id}"}
|
||||
return {"success": True}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"update_event failed: {e}"}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# delete_event
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
async def _delete_event(self, **kwargs) -> dict[str, Any]:
|
||||
event_id = kwargs.get("event_id")
|
||||
user_id = kwargs.get("user_id")
|
||||
if not event_id:
|
||||
return {"success": False, "error": "Missing required field: event_id"}
|
||||
if not user_id:
|
||||
return {"success": False, "error": "Missing required field: user_id"}
|
||||
|
||||
try:
|
||||
deleted = await self._service.delete_event(event_id)
|
||||
if not deleted:
|
||||
return {"success": False, "error": f"Event not found: {event_id}"}
|
||||
return {"success": True}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"delete_event failed: {e}"}
|
||||
|
|
@ -0,0 +1,352 @@
|
|||
"""Tests for CalendarTool — Agent tool wrapper for ReAct integration (U3)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from agentkit.calendar.db import get_event_tags, init_calendar_db
|
||||
from agentkit.calendar.service import CalendarService
|
||||
from agentkit.tools.calendar_tool import CalendarTool
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def service(tmp_path: Path) -> CalendarService:
|
||||
"""Provide a CalendarService backed by a temp DB."""
|
||||
db_path = tmp_path / "test.db"
|
||||
asyncio.run(init_calendar_db(db_path))
|
||||
return CalendarService(db_path=db_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tool(service: CalendarService) -> CalendarTool:
|
||||
return CalendarTool(calendar_service=service)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# create_event
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_create_event_action_returns_success(
|
||||
tool: CalendarTool, service: CalendarService
|
||||
) -> None:
|
||||
"""create_event returns success and persists the event with source='agent'."""
|
||||
result = await tool.execute(
|
||||
action="create_event",
|
||||
user_id="user-1",
|
||||
title="Sprint Planning",
|
||||
start_time="2026-07-01T10:00:00+00:00",
|
||||
end_time="2026-07-01T11:00:00+00:00",
|
||||
description="Bi-weekly sprint planning",
|
||||
location="Room A",
|
||||
)
|
||||
assert result["success"] is True
|
||||
event_dict = result["event"]
|
||||
assert event_dict["title"] == "Sprint Planning"
|
||||
assert event_dict["source"] == "agent"
|
||||
assert event_dict["user_id"] == "user-1"
|
||||
|
||||
# Verify persisted in DB
|
||||
fetched = await service.get_event(event_dict["id"])
|
||||
assert fetched is not None
|
||||
assert fetched.title == "Sprint Planning"
|
||||
assert fetched.source == "agent"
|
||||
|
||||
|
||||
async def test_create_event_with_recurrence_sets_rrule(
|
||||
tool: CalendarTool, service: CalendarService
|
||||
) -> None:
|
||||
"""rrule param is stored correctly on the event."""
|
||||
rrule = "FREQ=WEEKLY;BYDAY=MO;COUNT=10"
|
||||
result = await tool.execute(
|
||||
action="create_event",
|
||||
user_id="user-1",
|
||||
title="Weekly Standup",
|
||||
start_time="2026-07-06T09:00:00+00:00",
|
||||
end_time="2026-07-06T09:30:00+00:00",
|
||||
rrule=rrule,
|
||||
)
|
||||
assert result["success"] is True
|
||||
assert result["event"]["rrule"] == rrule
|
||||
|
||||
fetched = await service.get_event(result["event"]["id"])
|
||||
assert fetched is not None
|
||||
assert fetched.rrule == rrule
|
||||
|
||||
|
||||
async def test_query_events_returns_list(tool: CalendarTool) -> None:
|
||||
"""create 2 events, query, verify both returned."""
|
||||
await tool.execute(
|
||||
action="create_event",
|
||||
user_id="user-1",
|
||||
title="Event A",
|
||||
start_time="2026-07-01T10:00:00+00:00",
|
||||
end_time="2026-07-01T11:00:00+00:00",
|
||||
)
|
||||
await tool.execute(
|
||||
action="create_event",
|
||||
user_id="user-1",
|
||||
title="Event B",
|
||||
start_time="2026-07-02T14:00:00+00:00",
|
||||
end_time="2026-07-02T15:00:00+00:00",
|
||||
)
|
||||
|
||||
result = await tool.execute(
|
||||
action="query_events",
|
||||
user_id="user-1",
|
||||
)
|
||||
assert result["success"] is True
|
||||
events = result["events"]
|
||||
assert len(events) == 2
|
||||
titles = {e["title"] for e in events}
|
||||
assert titles == {"Event A", "Event B"}
|
||||
|
||||
|
||||
async def test_update_event_action_modifies_fields(
|
||||
tool: CalendarTool, service: CalendarService
|
||||
) -> None:
|
||||
"""create then update title; verify the field is modified."""
|
||||
create_result = await tool.execute(
|
||||
action="create_event",
|
||||
user_id="user-1",
|
||||
title="Original Title",
|
||||
start_time="2026-07-01T10:00:00+00:00",
|
||||
end_time="2026-07-01T11:00:00+00:00",
|
||||
)
|
||||
assert create_result["success"] is True
|
||||
event_id = create_result["event"]["id"]
|
||||
|
||||
update_result = await tool.execute(
|
||||
action="update_event",
|
||||
user_id="user-1",
|
||||
event_id=event_id,
|
||||
title="Updated Title",
|
||||
)
|
||||
assert update_result["success"] is True
|
||||
|
||||
fetched = await service.get_event(event_id)
|
||||
assert fetched is not None
|
||||
assert fetched.title == "Updated Title"
|
||||
|
||||
|
||||
async def test_delete_event_action_removes_record(
|
||||
tool: CalendarTool, service: CalendarService
|
||||
) -> None:
|
||||
"""create then delete; verify the record is gone."""
|
||||
create_result = await tool.execute(
|
||||
action="create_event",
|
||||
user_id="user-1",
|
||||
title="To Be Deleted",
|
||||
start_time="2026-07-01T10:00:00+00:00",
|
||||
end_time="2026-07-01T11:00:00+00:00",
|
||||
)
|
||||
assert create_result["success"] is True
|
||||
event_id = create_result["event"]["id"]
|
||||
|
||||
delete_result = await tool.execute(
|
||||
action="delete_event",
|
||||
user_id="user-1",
|
||||
event_id=event_id,
|
||||
)
|
||||
assert delete_result["success"] is True
|
||||
|
||||
fetched = await service.get_event(event_id)
|
||||
assert fetched is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# error paths
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_invalid_action_returns_error(tool: CalendarTool) -> None:
|
||||
"""Unknown action returns success=False with error message."""
|
||||
result = await tool.execute(
|
||||
action="frobnicate",
|
||||
user_id="user-1",
|
||||
)
|
||||
assert result["success"] is False
|
||||
assert "Unknown action" in result["error"]
|
||||
|
||||
|
||||
async def test_missing_required_field_returns_error(tool: CalendarTool) -> None:
|
||||
"""create_event without title returns success=False."""
|
||||
result = await tool.execute(
|
||||
action="create_event",
|
||||
user_id="user-1",
|
||||
start_time="2026-07-01T10:00:00+00:00",
|
||||
end_time="2026-07-01T11:00:00+00:00",
|
||||
)
|
||||
assert result["success"] is False
|
||||
assert "Missing required field" in result["error"]
|
||||
assert "title" in result["error"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# conversation_id
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_created_event_has_conversation_id(
|
||||
tool: CalendarTool, service: CalendarService
|
||||
) -> None:
|
||||
"""conversation_id is set from context when provided."""
|
||||
result = await tool.execute(
|
||||
action="create_event",
|
||||
user_id="user-1",
|
||||
title="Chat-Initiated Event",
|
||||
start_time="2026-07-01T10:00:00+00:00",
|
||||
end_time="2026-07-01T11:00:00+00:00",
|
||||
conversation_id="conv-abc-123",
|
||||
)
|
||||
assert result["success"] is True
|
||||
assert result["event"]["conversation_id"] == "conv-abc-123"
|
||||
|
||||
fetched = await service.get_event(result["event"]["id"])
|
||||
assert fetched is not None
|
||||
assert fetched.conversation_id == "conv-abc-123"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# event_type_name resolution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_create_event_with_event_type_name(
|
||||
tool: CalendarTool, service: CalendarService
|
||||
) -> None:
|
||||
"""create event with event_type_name='Meeting' creates the type and links it."""
|
||||
result = await tool.execute(
|
||||
action="create_event",
|
||||
user_id="user-1",
|
||||
title="Strategy Sync",
|
||||
start_time="2026-07-01T10:00:00+00:00",
|
||||
end_time="2026-07-01T11:00:00+00:00",
|
||||
event_type_name="Meeting",
|
||||
)
|
||||
assert result["success"] is True
|
||||
event_type_id = result["event"]["event_type_id"]
|
||||
assert event_type_id is not None
|
||||
|
||||
# Verify the event type was created
|
||||
types = await service.list_event_types("user-1")
|
||||
meeting_types = [t for t in types if t.name == "Meeting"]
|
||||
assert len(meeting_types) == 1
|
||||
assert meeting_types[0].id == event_type_id
|
||||
|
||||
# Verify the event references the type
|
||||
fetched = await service.get_event(result["event"]["id"])
|
||||
assert fetched is not None
|
||||
assert fetched.event_type_id == event_type_id
|
||||
|
||||
|
||||
async def test_create_event_reuses_existing_event_type(
|
||||
tool: CalendarTool, service: CalendarService
|
||||
) -> None:
|
||||
"""If event_type_name matches an existing type, it is reused (not duplicated)."""
|
||||
# Pre-create the type
|
||||
existing = await service.create_event_type("user-1", "Meeting")
|
||||
|
||||
result = await tool.execute(
|
||||
action="create_event",
|
||||
user_id="user-1",
|
||||
title="Second Meeting",
|
||||
start_time="2026-07-02T10:00:00+00:00",
|
||||
end_time="2026-07-02T11:00:00+00:00",
|
||||
event_type_name="Meeting",
|
||||
)
|
||||
assert result["success"] is True
|
||||
assert result["event"]["event_type_id"] == existing.id
|
||||
|
||||
# No duplicate type created
|
||||
types = await service.list_event_types("user-1")
|
||||
meeting_types = [t for t in types if t.name == "Meeting"]
|
||||
assert len(meeting_types) == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# tag_names resolution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_create_event_with_tag_names(
|
||||
tool: CalendarTool, service: CalendarService, tmp_path: Path
|
||||
) -> None:
|
||||
"""create event with tag_names=['urgent', 'work'] creates tags and links them."""
|
||||
result = await tool.execute(
|
||||
action="create_event",
|
||||
user_id="user-1",
|
||||
title="Urgent Work Task",
|
||||
start_time="2026-07-01T10:00:00+00:00",
|
||||
end_time="2026-07-01T11:00:00+00:00",
|
||||
tag_names=["urgent", "work"],
|
||||
)
|
||||
assert result["success"] is True
|
||||
event_id = result["event"]["id"]
|
||||
|
||||
# Verify tags were created
|
||||
tags = await service.list_tags("user-1")
|
||||
tag_names = {t.name for t in tags}
|
||||
assert "urgent" in tag_names
|
||||
assert "work" in tag_names
|
||||
|
||||
# Verify tags are linked to the event
|
||||
linked_tags = await get_event_tags(event_id, service.db_path)
|
||||
linked_names = {t.name for t in linked_tags}
|
||||
assert linked_names == {"urgent", "work"}
|
||||
|
||||
|
||||
async def test_create_event_reuses_existing_tags(
|
||||
tool: CalendarTool, service: CalendarService
|
||||
) -> None:
|
||||
"""If a tag name matches an existing tag, it is reused (not duplicated)."""
|
||||
# Pre-create a tag
|
||||
existing = await service.create_tag("user-1", "urgent")
|
||||
|
||||
result = await tool.execute(
|
||||
action="create_event",
|
||||
user_id="user-1",
|
||||
title="Tagged Event",
|
||||
start_time="2026-07-01T10:00:00+00:00",
|
||||
end_time="2026-07-01T11:00:00+00:00",
|
||||
tag_names=["urgent", "new-tag"],
|
||||
)
|
||||
assert result["success"] is True
|
||||
|
||||
# 'urgent' should not be duplicated, 'new-tag' should be created
|
||||
tags = await service.list_tags("user-1")
|
||||
urgent_tags = [t for t in tags if t.name == "urgent"]
|
||||
assert len(urgent_tags) == 1
|
||||
assert urgent_tags[0].id == existing.id
|
||||
new_tags = [t for t in tags if t.name == "new-tag"]
|
||||
assert len(new_tags) == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# tool registration / schema
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_tool_name_and_schema(tool: CalendarTool) -> None:
|
||||
"""Tool has correct name and input_schema."""
|
||||
assert tool.name == "calendar"
|
||||
schema = tool.input_schema
|
||||
assert schema["type"] == "object"
|
||||
assert "action" in schema["properties"]
|
||||
assert "user_id" in schema["properties"]
|
||||
assert schema["properties"]["action"]["enum"] == [
|
||||
"create_event",
|
||||
"query_events",
|
||||
"update_event",
|
||||
"delete_event",
|
||||
]
|
||||
assert "action" in schema["required"]
|
||||
assert "user_id" in schema["required"]
|
||||
Loading…
Reference in New Issue