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:
2026-02-15 22:40:20 -05:00
parent 8996b45e50
commit d7bc3f3222
22 changed files with 1158 additions and 837 deletions
+155 -5
View File
@@ -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}"}