refactor(tools): consolidate LLM tools from 42 to 38

Merge create_task into create_note (set status='todo' for tasks, omit
for notes), merge delete_task into delete_note, consolidate entity
tools (create/update_person → save_person, create/update_place →
save_place), rename get_note → read_note with clearer descriptions,
move calculate out of rag.py into utility.py, and extract shared
duplicate detection into check_duplicate() helper.

Updates all downstream references in generation_task.py, quick_capture.py,
ToolCallCard.vue, and WorkspaceView.vue.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-12 13:38:45 -04:00
parent e95ad90055
commit 77339d5c58
12 changed files with 278 additions and 378 deletions
+2 -139
View File
@@ -1,121 +1,15 @@
"""Task tools: create, list, delete, log work."""
"""Task tools: list, log work."""
from __future__ import annotations
from fabledassistant.services.notes import create_note, delete_note, get_note_by_title, list_notes, update_note
from fabledassistant.services.tag_suggestions import suggest_tags
from fabledassistant.services.notes import get_note_by_title, list_notes
from fabledassistant.services.tools._helpers import (
_PUNCT_RE,
fuzzy_title_match,
parse_due_date,
resolve_project,
schedule_embedding,
)
from fabledassistant.services.tools._registry import tool
@tool(
name="create_task",
description="Create a new task for the user. Use this when the user asks you to add a task, todo, or reminder.",
parameters={
"title": {"type": "string", "description": "The task title"},
"body": {"type": "string", "description": "Optional task description or details"},
"status": {"type": "string", "enum": ["todo", "in_progress", "done", "cancelled"], "description": "Initial task status (default: todo)"},
"due_date": {"type": "string", "description": "Optional due date in YYYY-MM-DD format"},
"priority": {"type": "string", "enum": ["none", "low", "medium", "high"], "description": "Task priority level"},
"tags": {"type": "array", "items": {"type": "string"}, "description": "Tags for the task (without # prefix)"},
"project": {"type": "string", "description": "Optional project name to assign this task to. Only set this if the user explicitly named a project. Do NOT infer a project from the task content or context."},
"parent_task": {"type": "string", "description": "Optional title of a parent task to make this a sub-task of"},
"milestone": {"type": "string", "description": "Optional milestone title within the project to assign this task to"},
"confirmed": {"type": "boolean", "description": "Set to true only when the user has explicitly confirmed creation after being warned about a similar existing task."},
"recurrence_rule": {"type": "object", "description": 'Optional recurrence. Interval form: {"type":"interval","every":3,"unit":"month"} (units: day/week/month/year). Calendar form: {"type":"calendar","unit":"month","day_of_month":1}. Annual: add "month":6.'},
},
required=["title"],
)
async def create_task_tool(*, user_id, arguments, **_ctx):
task_title = arguments.get("title", "Untitled Task")
task_body = arguments.get("body", "")
if not isinstance(task_title, str):
return {"success": False, "error": "title must be a string. Call create_task once per task."}
if not isinstance(task_body, str):
task_body = ""
project_name = arguments.get("project")
milestone_name = arguments.get("milestone")
parent_task_name = arguments.get("parent_task")
pre_fields: dict = {}
if project_name:
proj = await resolve_project(user_id, project_name)
if proj is None:
return {"success": False, "error": f"Project '{project_name}' not found. Use list_projects to find the correct project name and retry with the exact name."}
pre_fields["project_id"] = proj.id
if milestone_name:
from fabledassistant.services.milestones import get_milestone_by_title as _gmbt
ms = await _gmbt(user_id, proj.id, milestone_name)
if ms is None:
return {"success": False, "error": f"Milestone '{milestone_name}' not found in project '{proj.title}'. Use list_milestones to see available milestones."}
pre_fields["milestone_id"] = ms.id
existing_tasks, _ = await list_notes(user_id=user_id, q=task_title, is_task=True, limit=1)
exact = next((t for t in existing_tasks if t.title.lower() == task_title.lower()), None)
if exact is not None:
return {"success": False, "error": f"A task titled '{task_title}' already exists (id: {exact.id}). Use update_note to modify it instead of creating a duplicate."}
clean_q = _PUNCT_RE.sub(" ", task_title).strip()
candidates, _ = await list_notes(user_id=user_id, q=clean_q, is_task=True, limit=20)
near, ratio = fuzzy_title_match(task_title, candidates)
if near is not None:
return {"success": False, "requires_confirmation": True, "similar_note": {"id": near.id, "title": near.title}, "error": f"A task with a very similar title '{near.title}' already exists (similarity: {ratio:.0%}). Ask the user to confirm before creating a separate entry."}
if not arguments.get("confirmed") and len(task_body.strip()) >= 80:
from fabledassistant.services.embeddings import semantic_search_notes as _ssn
sem_query = f"{task_title}\n{task_body}".strip()
sem_hits = await _ssn(user_id, sem_query, limit=3, threshold=0.90, is_task=True)
if sem_hits:
best_score, best_note = sem_hits[0]
return {"success": False, "requires_confirmation": True, "similar_note": {"id": best_note.id, "title": best_note.title}, "error": f"A task with very similar content exists: '{best_note.title}' (semantic similarity: {best_score:.0%}). Ask the user to confirm before creating a separate entry."}
task_tags = arguments.get("tags", [])
if not isinstance(task_tags, list):
task_tags = []
task_status = arguments.get("status", "todo")
note = await create_note(
user_id=user_id,
title=task_title,
body=task_body,
tags=task_tags,
status=task_status,
priority=arguments.get("priority", "none"),
due_date=parse_due_date(arguments.get("due_date")),
project_id=pre_fields.get("project_id"),
milestone_id=pre_fields.get("milestone_id"),
)
suggested = await suggest_tags(user_id, task_title, task_body)
schedule_embedding(note.id, user_id, task_title, task_body)
update_fields: dict = {}
if parent_task_name:
parent_notes, _ = await list_notes(user_id=user_id, q=parent_task_name, is_task=True, limit=1)
if parent_notes:
update_fields["parent_id"] = parent_notes[0].id
if update_fields:
note = await update_note(user_id, note.id, **update_fields)
return {
"success": True,
"type": "task",
"data": {
"id": note.id,
"title": note.title,
"status": note.status,
"priority": note.priority,
"due_date": str(note.due_date) if note.due_date else None,
"project_id": note.project_id,
"milestone_id": note.milestone_id,
"parent_id": note.parent_id,
},
"suggested_tags": suggested,
}
@tool(
name="list_tasks",
description="List tasks with optional filters. For overdue tasks, set due_before to today's date.",
@@ -184,37 +78,6 @@ async def list_tasks_tool(*, user_id, arguments, **_ctx):
}
@tool(
name="delete_task",
description="Delete a task permanently. Use ONLY when the user explicitly asks to delete or remove a task. Always confirm with the user first — this cannot be undone.",
parameters={
"query": {"type": "string", "description": "Title or keyword to find the task to delete"},
"confirmed": {"type": "boolean", "description": "Must be true — only set after the user has explicitly confirmed they want this task deleted."},
},
required=["query"],
)
async def delete_task_tool(*, user_id, arguments, **_ctx):
query = arguments.get("query", "")
note = await get_note_by_title(user_id, query)
if note is None:
notes, _ = await list_notes(user_id=user_id, q=query, is_task=True, limit=3)
if not notes:
return {"success": False, "error": f"No task found matching '{query}'."}
note = notes[0]
if note.status is None:
return {"success": False, "error": f"'{note.title}' is a note. Use delete_note instead."}
if not arguments.get("confirmed"):
return {
"success": False,
"requires_confirmation": True,
"error": f"Deleting '{note.title}' is permanent. Ask the user to confirm, then retry with confirmed=true.",
}
deleted = await delete_note(user_id, note.id)
if not deleted:
return {"success": False, "error": "Failed to delete task."}
return {"success": True, "type": "task_deleted", "data": {"id": note.id, "title": note.title}}
@tool(
name="log_work",
description="Add a work log entry to a task to record progress, work done, or time spent. Use this when the user says they worked on, completed, or spent time on a task.",