Add update_task fields, list_tasks, and update_todo tools
- update_note: extend with status/priority/due_date fields so task attributes can be changed via chat (mark done, set priority, move due date). body is now optional — task field updates work without touching content. - list_tasks: new core tool with status/priority/due_before/due_after/limit filters backed by list_notes(is_task=True). Enables queries like "overdue tasks", "high priority tasks", "what's in progress". - update_todo: new CalDAV tool to modify VTODO summary, due date, description, and priority — follows update_event pattern (modify component, rebuild ical, save). Completes the CalDAV todo CRUD suite. - tools.py: add update_todo import + execute case (type: todo_updated) - llm.py: add list_tasks and update_todo to available actions + guidance - intent.py: routing rules for mark-done/priority/due-date → update_note, overdue/in-progress/high-priority queries → list_tasks, CalDAV todo updates → update_todo - ToolCallCard.vue: tasks list block (linked titles + due + priority badges), todo_updated label, tool-task-priority CSS classes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -575,6 +575,81 @@ async def complete_todo(
|
||||
return await asyncio.to_thread(_complete)
|
||||
|
||||
|
||||
async def update_todo(
|
||||
user_id: int,
|
||||
query: str,
|
||||
summary: str | None = None,
|
||||
due: str | None = None,
|
||||
description: str | None = None,
|
||||
priority: int | None = None,
|
||||
timezone: str | None = None,
|
||||
calendar_name: str | None = None,
|
||||
) -> dict:
|
||||
"""Update a CalDAV todo matching the query."""
|
||||
config = await get_caldav_config(user_id)
|
||||
_check_config(config)
|
||||
tz = timezone or config.get("caldav_timezone") or None
|
||||
|
||||
def _do_update():
|
||||
client = _make_client(config)
|
||||
cal_name = calendar_name or config.get("caldav_calendar_name", "")
|
||||
calendar = _get_calendar(client, cal_name)
|
||||
todos = calendar.todos(include_completed=True)
|
||||
|
||||
q = query.lower()
|
||||
matches = []
|
||||
for t in todos:
|
||||
cal_obj = icalendar.Calendar.from_ical(t.data)
|
||||
for component in cal_obj.walk():
|
||||
if component.name == "VTODO":
|
||||
s = str(component.get("SUMMARY", ""))
|
||||
if q in s.lower():
|
||||
matches.append((t, component))
|
||||
|
||||
if not matches:
|
||||
raise ValueError(f"No todo found matching '{query}'.")
|
||||
if len(matches) > 3:
|
||||
titles = [str(m[1].get("SUMMARY", "")) for m in matches]
|
||||
raise ValueError(
|
||||
f"Too many matches ({len(matches)}) for '{query}'. "
|
||||
f"Be more specific. Found: {', '.join(titles[:10])}"
|
||||
)
|
||||
|
||||
todo_obj, component = matches[0]
|
||||
|
||||
if summary:
|
||||
component["SUMMARY"] = summary
|
||||
if description is not None:
|
||||
if "DESCRIPTION" in component:
|
||||
del component["DESCRIPTION"]
|
||||
component.add("description", description)
|
||||
if priority is not None:
|
||||
if "PRIORITY" in component:
|
||||
del component["PRIORITY"]
|
||||
component.add("priority", priority)
|
||||
if due:
|
||||
if "DUE" in component:
|
||||
del component["DUE"]
|
||||
try:
|
||||
dt = datetime.fromisoformat(due)
|
||||
dt = _apply_timezone(dt, tz)
|
||||
component.add("due", dt)
|
||||
except ValueError:
|
||||
component.add("due", date_type.fromisoformat(due))
|
||||
|
||||
# Rebuild ical data and save
|
||||
cal_data = icalendar.Calendar()
|
||||
cal_data.add("prodid", "-//FabledAssistant//EN")
|
||||
cal_data.add("version", "2.0")
|
||||
cal_data.add_component(component)
|
||||
todo_obj.data = cal_data.to_ical().decode("utf-8")
|
||||
todo_obj.save()
|
||||
|
||||
return _parse_vtodo(component)
|
||||
|
||||
return await asyncio.to_thread(_do_update)
|
||||
|
||||
|
||||
async def delete_todo(
|
||||
user_id: int,
|
||||
query: str,
|
||||
|
||||
Reference in New Issue
Block a user