"""Entity tools: people, places, and lists.""" from __future__ import annotations from fabledassistant.services.notes import create_note, list_notes, update_note from fabledassistant.services.tools._helpers import schedule_embedding from fabledassistant.services.tools._registry import tool @tool( name="create_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." ), parameters={ "name": {"type": "string", "description": "Full name of the person"}, "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"}, "birthday": {"type": "string", "description": "Birthday in YYYY-MM-DD format"}, "address": {"type": "string", "description": "Home or mailing address"}, "notes": {"type": "string", "description": "Any additional free-form notes about this person"}, }, required=["name"], ) async def create_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"): 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}") note = await create_note( user_id=user_id, title=name, body="\n".join(body_lines), tags=["person"], note_type="person", entity_meta=meta, ) schedule_embedding(note.id, user_id, name, note.body) return {"success": True, "type": "person", "data": {"id": note.id, "name": name}} @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.", parameters={ "name": {"type": "string", "description": "Name of the place (e.g. 'Dr. Smith Dental', 'Costco')"}, "address": {"type": "string", "description": "Street address"}, "phone": {"type": "string", "description": "Phone number"}, "hours": {"type": "string", "description": "Opening hours (e.g. 'Mon-Fri 9am-5pm')"}, "url": {"type": "string", "description": "Website URL"}, "notes": {"type": "string", "description": "Any additional free-form notes about this place"}, }, required=["name"], ) async def create_place_tool(*, user_id, arguments, **_ctx): name = str(arguments.get("name", "")).strip() if not name: return {"success": False, "error": "name is required"} meta = {} for field in ("address", "phone", "hours", "url"): 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}") note = await create_note( user_id=user_id, title=name, body="\n".join(body_lines), 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.", parameters={ "name": {"type": "string", "description": "List name (e.g. 'Grocery List', 'Hardware Store')"}, "items": {"type": "array", "items": {"type": "string"}, "description": "Initial list items (unchecked)"}, "store": {"type": "string", "description": "Optional associated store or place name"}, "tags": {"type": "array", "items": {"type": "string"}, "description": "Tags for the list"}, }, required=["name"], ) async def create_list_tool(*, user_id, arguments, **_ctx): name = str(arguments.get("name", "")).strip() if not name: return {"success": False, "error": "name is required"} items = arguments.get("items") or [] if not isinstance(items, list): items = [] body = "\n".join(f"- [ ] {item}" for item in items if str(item).strip()) meta = {} store = arguments.get("store") if store: meta["store"] = str(store) tags = arguments.get("tags") or [] if not isinstance(tags, list): tags = [] note = await create_note( user_id=user_id, title=name, body=body, tags=tags, note_type="list", entity_meta=meta, ) schedule_embedding(note.id, user_id, name, body) return {"success": True, "type": "list", "data": {"id": note.id, "name": name, "item_count": len(items)}} @tool( name="add_to_list", description="Add one or more items to an existing list. Items are appended as unchecked entries.", parameters={ "list_name": {"type": "string", "description": "Name of the list to add items to"}, "items": {"type": "array", "items": {"type": "string"}, "description": "Items to add"}, }, required=["list_name", "items"], ) async def add_to_list_tool(*, user_id, arguments, **_ctx): list_name = str(arguments.get("list_name", "")).strip() items = arguments.get("items") or [] if not list_name: return {"success": False, "error": "list_name is required"} if not isinstance(items, list) or not items: return {"success": False, "error": "items must be a non-empty array"} existing, _ = await list_notes(user_id=user_id, q=list_name, is_task=False, limit=10) target = next((n for n in existing if n.note_type == "list" and n.title.lower() == list_name.lower()), None) if target is None: target = next((n for n in existing if n.note_type == "list"), None) if target is None: return {"success": False, "error": f"No list named '{list_name}' found. Use create_list to create it first."} new_lines = "\n".join(f"- [ ] {item}" for item in items if str(item).strip()) existing_body = (target.body or "").rstrip() new_body = f"{existing_body}\n{new_lines}".lstrip() await update_note(user_id=user_id, note_id=target.id, body=new_body) schedule_embedding(target.id, user_id, target.title, new_body) return {"success": True, "type": "list_updated", "data": {"id": target.id, "name": target.title, "added": len(items)}} @tool( name="clear_checked_items", description="Remove all checked/completed items from a list, keeping only unchecked items.", parameters={ "list_name": {"type": "string", "description": "Name of the list to clear"}, }, required=["list_name"], ) async def clear_checked_items_tool(*, user_id, arguments, **_ctx): import re as _re list_name = str(arguments.get("list_name", "")).strip() if not list_name: return {"success": False, "error": "list_name is required"} existing, _ = await list_notes(user_id=user_id, q=list_name, is_task=False, limit=10) target = next((n for n in existing if n.note_type == "list" and n.title.lower() == list_name.lower()), None) if target is None: target = next((n for n in existing if n.note_type == "list"), None) if target is None: return {"success": False, "error": f"No list named '{list_name}' found."} body = target.body or "" cleaned = _re.sub(r"^- \[[xX]\] .+\n?", "", body, flags=_re.MULTILINE).strip() await update_note(user_id=user_id, note_id=target.id, body=cleaned) schedule_embedding(target.id, user_id, target.title, cleaned) return {"success": True, "type": "list_cleared", "data": {"id": target.id, "name": target.title}}