Add CalDAV calendar integration, LLM-suggested tags, and settings refinements
- CalDAV integration: per-user calendar config, create/list/search events via caldav library, LLM tools for calendar operations from chat - LLM-suggested tags: new tag_suggestions service prompts LLM with existing tags and note content to suggest 3-5 relevant tags; exposed via API endpoints (suggest-tags, append-tag); integrated into editor views (suggest button + clickable pills) and chat tool calls (pills in ToolCallCard with one-click apply) - Settings/model UI refinements, generation task improvements Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,11 +3,19 @@
|
||||
import logging
|
||||
from datetime import date, datetime
|
||||
|
||||
from fabledassistant.services.caldav import (
|
||||
create_event,
|
||||
is_caldav_configured,
|
||||
list_events,
|
||||
search_events,
|
||||
)
|
||||
from fabledassistant.services.notes import create_note, list_notes
|
||||
from fabledassistant.services.tag_suggestions import suggest_tags
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TOOL_DEFINITIONS = [
|
||||
# Core tools — always available
|
||||
_CORE_TOOLS = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
@@ -78,6 +86,94 @@ TOOL_DEFINITIONS = [
|
||||
},
|
||||
]
|
||||
|
||||
# CalDAV tools — only included when user has CalDAV configured
|
||||
_CALDAV_TOOLS = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "create_event",
|
||||
"description": "Create a calendar event for the user. Use this when the user asks to schedule, add, or create a meeting, appointment, or event.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "The event title",
|
||||
},
|
||||
"start": {
|
||||
"type": "string",
|
||||
"description": "Start date/time in ISO 8601 format (e.g. 2025-01-15T14:00:00)",
|
||||
},
|
||||
"end": {
|
||||
"type": "string",
|
||||
"description": "Optional end date/time in ISO 8601 format",
|
||||
},
|
||||
"duration": {
|
||||
"type": "integer",
|
||||
"description": "Optional duration in minutes (default 60, ignored if end is set)",
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Optional event description",
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "Optional event location",
|
||||
},
|
||||
},
|
||||
"required": ["title", "start"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_events",
|
||||
"description": "List calendar events in a date range. Use this when the user asks what events or meetings they have.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date_from": {
|
||||
"type": "string",
|
||||
"description": "Start of range in ISO 8601 format (e.g. 2025-01-15T00:00:00)",
|
||||
},
|
||||
"date_to": {
|
||||
"type": "string",
|
||||
"description": "End of range in ISO 8601 format (e.g. 2025-01-22T23:59:59)",
|
||||
},
|
||||
},
|
||||
"required": ["date_from", "date_to"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_events",
|
||||
"description": "Search calendar events by keyword. Use this when the user asks to find a specific event or meeting.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search keyword to match against event titles and locations",
|
||||
},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def get_tools_for_user(user_id: int) -> list[dict]:
|
||||
"""Build the tool list for a user based on their configured integrations."""
|
||||
tools = list(_CORE_TOOLS)
|
||||
if await is_caldav_configured(user_id):
|
||||
tools.extend(_CALDAV_TOOLS)
|
||||
logger.debug("User %d: %d tools available", user_id, len(tools))
|
||||
return tools
|
||||
|
||||
|
||||
def _parse_due_date(value: str | None) -> date | None:
|
||||
"""Parse a due date string, returning None on failure."""
|
||||
@@ -94,14 +190,17 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict:
|
||||
"""Execute a tool call and return the result."""
|
||||
try:
|
||||
if tool_name == "create_task":
|
||||
task_title = arguments.get("title", "Untitled Task")
|
||||
task_body = arguments.get("body", "")
|
||||
note = await create_note(
|
||||
user_id=user_id,
|
||||
title=arguments.get("title", "Untitled Task"),
|
||||
body=arguments.get("body", ""),
|
||||
title=task_title,
|
||||
body=task_body,
|
||||
status="todo",
|
||||
priority=arguments.get("priority", "none"),
|
||||
due_date=_parse_due_date(arguments.get("due_date")),
|
||||
)
|
||||
suggested = await suggest_tags(user_id, task_title, task_body)
|
||||
return {
|
||||
"success": True,
|
||||
"type": "task",
|
||||
@@ -112,14 +211,18 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict:
|
||||
"priority": note.priority,
|
||||
"due_date": str(note.due_date) if note.due_date else None,
|
||||
},
|
||||
"suggested_tags": suggested,
|
||||
}
|
||||
|
||||
elif tool_name == "create_note":
|
||||
note_title = arguments.get("title", "Untitled Note")
|
||||
note_body = arguments.get("body", "")
|
||||
note = await create_note(
|
||||
user_id=user_id,
|
||||
title=arguments.get("title", "Untitled Note"),
|
||||
body=arguments.get("body", ""),
|
||||
title=note_title,
|
||||
body=note_body,
|
||||
)
|
||||
suggested = await suggest_tags(user_id, note_title, note_body)
|
||||
return {
|
||||
"success": True,
|
||||
"type": "note",
|
||||
@@ -127,6 +230,7 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict:
|
||||
"id": note.id,
|
||||
"title": note.title,
|
||||
},
|
||||
"suggested_tags": suggested,
|
||||
}
|
||||
|
||||
elif tool_name == "search_notes":
|
||||
@@ -151,6 +255,52 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict:
|
||||
},
|
||||
}
|
||||
|
||||
elif tool_name == "create_event":
|
||||
result = await create_event(
|
||||
user_id=user_id,
|
||||
title=arguments.get("title", "Untitled Event"),
|
||||
start=arguments["start"],
|
||||
end=arguments.get("end"),
|
||||
duration=arguments.get("duration"),
|
||||
description=arguments.get("description"),
|
||||
location=arguments.get("location"),
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"type": "event",
|
||||
"data": result,
|
||||
}
|
||||
|
||||
elif tool_name == "list_events":
|
||||
events = await list_events(
|
||||
user_id=user_id,
|
||||
date_from=arguments["date_from"],
|
||||
date_to=arguments["date_to"],
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"type": "events",
|
||||
"data": {
|
||||
"count": len(events),
|
||||
"events": events,
|
||||
},
|
||||
}
|
||||
|
||||
elif tool_name == "search_events":
|
||||
events = await search_events(
|
||||
user_id=user_id,
|
||||
query=arguments.get("query", ""),
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"type": "events",
|
||||
"data": {
|
||||
"query": arguments.get("query", ""),
|
||||
"count": len(events),
|
||||
"events": events,
|
||||
},
|
||||
}
|
||||
|
||||
else:
|
||||
return {"success": False, "error": f"Unknown tool: {tool_name}"}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user