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:
@@ -7,15 +7,41 @@ from fabledassistant.services.tools._helpers import schedule_embedding
|
||||
from fabledassistant.services.tools._registry import tool
|
||||
|
||||
|
||||
def _build_person_body(meta: dict, extra_notes: str | None = None) -> str:
|
||||
"""Build markdown body from person metadata."""
|
||||
lines = []
|
||||
for key, label in (("relationship", "Relationship"), ("phone", "Phone"), ("email", "Email"), ("birthday", "Birthday"), ("address", "Address")):
|
||||
if meta.get(key):
|
||||
lines.append(f"**{label}:** {meta[key]}")
|
||||
if extra_notes:
|
||||
lines.append(f"\n{extra_notes}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _build_place_body(meta: dict, extra_notes: str | None = None) -> str:
|
||||
"""Build markdown body from place metadata."""
|
||||
lines = []
|
||||
for key, label in (("address", "Address"), ("phone", "Phone"), ("hours", "Hours"), ("url", "Website")):
|
||||
if meta.get(key):
|
||||
lines.append(f"**{label}:** {meta[key]}")
|
||||
if extra_notes:
|
||||
lines.append(f"\n{extra_notes}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
_PERSON_FIELDS = ("relationship", "phone", "email", "birthday", "address")
|
||||
_PLACE_FIELDS = ("address", "phone", "hours", "url")
|
||||
|
||||
|
||||
@tool(
|
||||
name="create_person",
|
||||
name="save_person",
|
||||
description=(
|
||||
"Save a person to the user's knowledge base. Use this when the user introduces "
|
||||
"someone by name or asks to remember details about a person (family member, friend, "
|
||||
"contact, colleague). Stored people are used to resolve names in future prompts."
|
||||
"Save or update a person in the user's knowledge base. Creates a new entry if the "
|
||||
"person doesn't exist, or updates an existing one. Use when the user introduces "
|
||||
"someone by name or adds/corrects details about a known person."
|
||||
),
|
||||
parameters={
|
||||
"name": {"type": "string", "description": "Full name of the person"},
|
||||
"name": {"type": "string", "description": "Full name of the person (used to find existing entry or as new entry title)"},
|
||||
"relationship": {"type": "string", "description": "Relationship to the user (e.g. daughter, dentist, colleague)"},
|
||||
"phone": {"type": "string", "description": "Phone number"},
|
||||
"email": {"type": "string", "description": "Email address"},
|
||||
@@ -25,31 +51,36 @@ from fabledassistant.services.tools._registry import tool
|
||||
},
|
||||
required=["name"],
|
||||
)
|
||||
async def create_person_tool(*, user_id, arguments, **_ctx):
|
||||
async def save_person_tool(*, user_id, arguments, **_ctx):
|
||||
name = str(arguments.get("name", "")).strip()
|
||||
if not name:
|
||||
return {"success": False, "error": "name is required"}
|
||||
meta: dict = {}
|
||||
for field in ("relationship", "phone", "email", "birthday", "address"):
|
||||
|
||||
existing, _ = await list_notes(user_id=user_id, q=name, is_task=False, limit=10)
|
||||
target = next((n for n in existing if n.note_type == "person"), None)
|
||||
|
||||
if target is not None:
|
||||
meta = dict(target.entity_meta or {})
|
||||
for field in _PERSON_FIELDS:
|
||||
val = arguments.get(field)
|
||||
if val is not None:
|
||||
meta[field] = str(val)
|
||||
extra_notes = arguments.get("notes")
|
||||
body = _build_person_body(meta, extra_notes)
|
||||
updated = await update_note(user_id=user_id, note_id=target.id, body=body, entity_meta=meta)
|
||||
if updated is None:
|
||||
return {"success": False, "error": "Failed to update person."}
|
||||
schedule_embedding(target.id, user_id, target.title, body)
|
||||
return {"success": True, "type": "person_updated", "data": {"id": target.id, "name": target.title}}
|
||||
|
||||
meta = {}
|
||||
for field in _PERSON_FIELDS:
|
||||
val = arguments.get(field)
|
||||
if val:
|
||||
meta[field] = str(val)
|
||||
extra_notes = arguments.get("notes", "")
|
||||
body_lines = []
|
||||
if meta.get("relationship"):
|
||||
body_lines.append(f"**Relationship:** {meta['relationship']}")
|
||||
if meta.get("phone"):
|
||||
body_lines.append(f"**Phone:** {meta['phone']}")
|
||||
if meta.get("email"):
|
||||
body_lines.append(f"**Email:** {meta['email']}")
|
||||
if meta.get("birthday"):
|
||||
body_lines.append(f"**Birthday:** {meta['birthday']}")
|
||||
if meta.get("address"):
|
||||
body_lines.append(f"**Address:** {meta['address']}")
|
||||
if extra_notes:
|
||||
body_lines.append(f"\n{extra_notes}")
|
||||
body = _build_person_body(meta, arguments.get("notes", ""))
|
||||
note = await create_note(
|
||||
user_id=user_id, title=name, body="\n".join(body_lines),
|
||||
user_id=user_id, title=name, body=body,
|
||||
tags=["person"], note_type="person", entity_meta=meta,
|
||||
)
|
||||
schedule_embedding(note.id, user_id, name, note.body)
|
||||
@@ -57,57 +88,12 @@ async def create_person_tool(*, user_id, arguments, **_ctx):
|
||||
|
||||
|
||||
@tool(
|
||||
name="update_person",
|
||||
description="Update details about a saved person. Use this when the user corrects or adds new information about someone already in the knowledge base.",
|
||||
parameters={
|
||||
"query": {"type": "string", "description": "Name or keyword to find the person"},
|
||||
"relationship": {"type": "string", "description": "Updated relationship to the user"},
|
||||
"phone": {"type": "string", "description": "Updated phone number"},
|
||||
"email": {"type": "string", "description": "Updated email address"},
|
||||
"birthday": {"type": "string", "description": "Updated birthday in YYYY-MM-DD format"},
|
||||
"address": {"type": "string", "description": "Updated home or mailing address"},
|
||||
"notes": {"type": "string", "description": "Updated free-form notes about this person"},
|
||||
},
|
||||
required=["query"],
|
||||
)
|
||||
async def update_person_tool(*, user_id, arguments, **_ctx):
|
||||
query = str(arguments.get("query", "")).strip()
|
||||
if not query:
|
||||
return {"success": False, "error": "query is required"}
|
||||
existing, _ = await list_notes(user_id=user_id, q=query, is_task=False, limit=10)
|
||||
target = next((n for n in existing if n.note_type == "person"), None)
|
||||
if target is None:
|
||||
return {"success": False, "error": f"No person found matching '{query}'. Use create_person to add them."}
|
||||
meta = dict(target.entity_meta or {})
|
||||
for field in ("relationship", "phone", "email", "birthday", "address"):
|
||||
val = arguments.get(field)
|
||||
if val is not None:
|
||||
meta[field] = str(val)
|
||||
body_lines = []
|
||||
if meta.get("relationship"):
|
||||
body_lines.append(f"**Relationship:** {meta['relationship']}")
|
||||
if meta.get("phone"):
|
||||
body_lines.append(f"**Phone:** {meta['phone']}")
|
||||
if meta.get("email"):
|
||||
body_lines.append(f"**Email:** {meta['email']}")
|
||||
if meta.get("birthday"):
|
||||
body_lines.append(f"**Birthday:** {meta['birthday']}")
|
||||
if meta.get("address"):
|
||||
body_lines.append(f"**Address:** {meta['address']}")
|
||||
extra_notes = arguments.get("notes")
|
||||
if extra_notes is not None:
|
||||
body_lines.append(f"\n{extra_notes}")
|
||||
new_body = "\n".join(body_lines)
|
||||
updated = await update_note(user_id=user_id, note_id=target.id, body=new_body, entity_meta=meta)
|
||||
if updated is None:
|
||||
return {"success": False, "error": "Failed to update person."}
|
||||
schedule_embedding(target.id, user_id, target.title, new_body)
|
||||
return {"success": True, "type": "person_updated", "data": {"id": target.id, "name": target.title}}
|
||||
|
||||
|
||||
@tool(
|
||||
name="create_place",
|
||||
description="Save a named location to the user's knowledge base. Use this when the user wants to remember a place (business, venue, address). Stored places are used to auto-fill locations in calendar events and other prompts.",
|
||||
name="save_place",
|
||||
description=(
|
||||
"Save or update a named location in the user's knowledge base. Creates a new entry "
|
||||
"if the place doesn't exist, or updates an existing one. Use when the user wants to "
|
||||
"remember or correct details about a place (business, venue, address)."
|
||||
),
|
||||
parameters={
|
||||
"name": {"type": "string", "description": "Name of the place (e.g. 'Dr. Smith Dental', 'Costco')"},
|
||||
"address": {"type": "string", "description": "Street address"},
|
||||
@@ -118,81 +104,42 @@ async def update_person_tool(*, user_id, arguments, **_ctx):
|
||||
},
|
||||
required=["name"],
|
||||
)
|
||||
async def create_place_tool(*, user_id, arguments, **_ctx):
|
||||
async def save_place_tool(*, user_id, arguments, **_ctx):
|
||||
name = str(arguments.get("name", "")).strip()
|
||||
if not name:
|
||||
return {"success": False, "error": "name is required"}
|
||||
|
||||
existing, _ = await list_notes(user_id=user_id, q=name, is_task=False, limit=10)
|
||||
target = next((n for n in existing if n.note_type == "place"), None)
|
||||
|
||||
if target is not None:
|
||||
meta = dict(target.entity_meta or {})
|
||||
for field in _PLACE_FIELDS:
|
||||
val = arguments.get(field)
|
||||
if val is not None:
|
||||
meta[field] = str(val)
|
||||
extra_notes = arguments.get("notes")
|
||||
body = _build_place_body(meta, extra_notes)
|
||||
updated = await update_note(user_id=user_id, note_id=target.id, body=body, entity_meta=meta)
|
||||
if updated is None:
|
||||
return {"success": False, "error": "Failed to update place."}
|
||||
schedule_embedding(target.id, user_id, target.title, body)
|
||||
return {"success": True, "type": "place_updated", "data": {"id": target.id, "name": target.title}}
|
||||
|
||||
meta = {}
|
||||
for field in ("address", "phone", "hours", "url"):
|
||||
for field in _PLACE_FIELDS:
|
||||
val = arguments.get(field)
|
||||
if val:
|
||||
meta[field] = str(val)
|
||||
extra_notes = arguments.get("notes", "")
|
||||
body_lines = []
|
||||
if meta.get("address"):
|
||||
body_lines.append(f"**Address:** {meta['address']}")
|
||||
if meta.get("phone"):
|
||||
body_lines.append(f"**Phone:** {meta['phone']}")
|
||||
if meta.get("hours"):
|
||||
body_lines.append(f"**Hours:** {meta['hours']}")
|
||||
if meta.get("url"):
|
||||
body_lines.append(f"**Website:** {meta['url']}")
|
||||
if extra_notes:
|
||||
body_lines.append(f"\n{extra_notes}")
|
||||
body = _build_place_body(meta, arguments.get("notes", ""))
|
||||
note = await create_note(
|
||||
user_id=user_id, title=name, body="\n".join(body_lines),
|
||||
user_id=user_id, title=name, body=body,
|
||||
tags=["place"], note_type="place", entity_meta=meta,
|
||||
)
|
||||
schedule_embedding(note.id, user_id, name, note.body)
|
||||
return {"success": True, "type": "place", "data": {"id": note.id, "name": name}}
|
||||
|
||||
|
||||
@tool(
|
||||
name="update_place",
|
||||
description="Update details about a saved place. Use this when the user corrects or adds new information about a location already in the knowledge base.",
|
||||
parameters={
|
||||
"query": {"type": "string", "description": "Name or keyword to find the place"},
|
||||
"address": {"type": "string", "description": "Updated street address"},
|
||||
"phone": {"type": "string", "description": "Updated phone number"},
|
||||
"hours": {"type": "string", "description": "Updated opening hours"},
|
||||
"url": {"type": "string", "description": "Updated website URL"},
|
||||
"notes": {"type": "string", "description": "Updated free-form notes about this place"},
|
||||
},
|
||||
required=["query"],
|
||||
)
|
||||
async def update_place_tool(*, user_id, arguments, **_ctx):
|
||||
query = str(arguments.get("query", "")).strip()
|
||||
if not query:
|
||||
return {"success": False, "error": "query is required"}
|
||||
existing, _ = await list_notes(user_id=user_id, q=query, is_task=False, limit=10)
|
||||
target = next((n for n in existing if n.note_type == "place"), None)
|
||||
if target is None:
|
||||
return {"success": False, "error": f"No place found matching '{query}'. Use create_place to add it."}
|
||||
meta = dict(target.entity_meta or {})
|
||||
for field in ("address", "phone", "hours", "url"):
|
||||
val = arguments.get(field)
|
||||
if val is not None:
|
||||
meta[field] = str(val)
|
||||
body_lines = []
|
||||
if meta.get("address"):
|
||||
body_lines.append(f"**Address:** {meta['address']}")
|
||||
if meta.get("phone"):
|
||||
body_lines.append(f"**Phone:** {meta['phone']}")
|
||||
if meta.get("hours"):
|
||||
body_lines.append(f"**Hours:** {meta['hours']}")
|
||||
if meta.get("url"):
|
||||
body_lines.append(f"**Website:** {meta['url']}")
|
||||
extra_notes = arguments.get("notes")
|
||||
if extra_notes is not None:
|
||||
body_lines.append(f"\n{extra_notes}")
|
||||
new_body = "\n".join(body_lines)
|
||||
updated = await update_note(user_id=user_id, note_id=target.id, body=new_body, entity_meta=meta)
|
||||
if updated is None:
|
||||
return {"success": False, "error": "Failed to update place."}
|
||||
schedule_embedding(target.id, user_id, target.title, new_body)
|
||||
return {"success": True, "type": "place_updated", "data": {"id": target.id, "name": target.title}}
|
||||
|
||||
|
||||
@tool(
|
||||
name="create_list",
|
||||
description="Create a named list (shopping list, to-do list, packing list, etc.). Items are stored as a markdown task list. Use add_to_list to add items to an existing list.",
|
||||
|
||||
Reference in New Issue
Block a user