From eff762cdd9fd37aa0bd9288796edb619ceb544ed Mon Sep 17 00:00:00 2001 From: tanmayraut45 Date: Tue, 2 Jun 2026 08:03:32 +0530 Subject: [PATCH] Expose manage_notes via native function calling (#759) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agent's RAG tool selector retrieves manage_notes as relevant for note / todo / reminder requests, but two gaps stopped it from actually firing on local llama.cpp / vLLM endpoints: 1. FUNCTION_TOOL_SCHEMAS had no entry for manage_notes. Even when the tool was marked relevant, no JSON schema was sent on the function tools list, so native-function-calling models had nothing to call. In practice the model would describe creating the note in prose while the actual note stayed blank — the symptom reported in #713 ("checklist hallucinated as blank"). 2. _API_HOSTS only listed hosted providers (OpenAI, Anthropic, etc.). For local endpoints like http://localhost:8080 or http://host.docker.internal:8000, _is_api_model fell back to keyword-sniffing the model name, so any model whose slug didn't happen to match the keyword list silently lost native tool schemas entirely. Fixes: - src/tool_schemas.py: add a manage_notes function schema covering list/add/update/delete/toggle_item with the full Keep-style field set. note_type is exposed as an enum ("note" | "checklist") so the model picks the mode explicitly instead of inferring it from content shape. Items are named checklist_items in the schema — consistent with the description's wording and avoiding the Python-built-in name clash that #713 calls out. - src/tool_implementations.py: do_manage_notes accepts both checklist_items (new, schema-exposed) and items (legacy / internal). Direct API callers and existing code paths keep working unchanged; native function calls following the new schema route through the same path. - src/agent_loop.py: add localhost, 127.0.0.1, and host.docker.internal to _API_HOSTS so the function-tool path is not gated behind model-name guessing for local servers. Closes #174. Closes #713. --- src/agent_loop.py | 5 +++++ src/tool_implementations.py | 15 ++++++++++++--- src/tool_schemas.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/agent_loop.py b/src/agent_loop.py index ae62891..b72a855 100644 --- a/src/agent_loop.py +++ b/src/agent_loop.py @@ -457,6 +457,11 @@ _API_HOSTS = frozenset([ "api.together.xyz", "api.fireworks.ai", "api.perplexity.ai", "api.x.ai", "ollama.com", + # Local OpenAI-compatible endpoints (llama.cpp, vLLM, LM Studio, etc.). + # Without these, `_is_api_model` falls back to keyword sniffing on the + # model name, so well-behaved local servers don't get native tool + # schemas and the agent silently degrades to fenced-block parsing. + "localhost", "127.0.0.1", "host.docker.internal", ]) _MCP_KEYWORDS = frozenset(["browse", "browser", "website", "calendar", "event", "email", "gmail", "screenshot", "navigate", "click", "miniflux", "rss", "feed"]) diff --git a/src/tool_implementations.py b/src/tool_implementations.py index e5b0524..09b0306 100644 --- a/src/tool_implementations.py +++ b/src/tool_implementations.py @@ -1853,7 +1853,13 @@ async def do_manage_notes(content: str, owner: Optional[str] = None) -> Dict: title = text_raw.strip() elif not content_raw and text_raw: content_raw = text_raw - items_raw = args.get("items") + # Accept both `items` (legacy/internal field) and `checklist_items` + # (the schema-exposed name used by native function calls). Models + # following the schema emit `checklist_items`; older code paths + # and direct API callers still use `items`. + items_raw = args.get("checklist_items") + if items_raw is None: + items_raw = args.get("items") items_json = json.dumps(items_raw) if items_raw is not None else None note_type = args.get("note_type", "checklist" if items_raw else "note") # Accept natural-language due_date ("tomorrow at 1pm") in @@ -1918,8 +1924,11 @@ async def do_manage_notes(content: str, owner: Optional[str] = None) -> Dict: for field in ("title", "content", "note_type", "color", "label", "due_date"): if field in args and args[field] is not None: setattr(note, field, args[field]) - if "items" in args and args["items"] is not None: - note.items = json.dumps(args["items"]) + new_items = args.get("checklist_items") + if new_items is None: + new_items = args.get("items") + if new_items is not None: + note.items = json.dumps(new_items) flag_modified(note, "items") if "pinned" in args: note.pinned = args["pinned"] diff --git a/src/tool_schemas.py b/src/tool_schemas.py index f0a69e0..8e10122 100644 --- a/src/tool_schemas.py +++ b/src/tool_schemas.py @@ -448,6 +448,41 @@ FUNCTION_TOOL_SCHEMAS = [ } } }, + { + "type": "function", + "function": { + "name": "manage_notes", + "description": "Manage notes and checklists (Google Keep-style): list, add, update, delete, toggle_item. IMPORTANT: For to-do lists / checklists, set note_type='checklist' and pass the items as the `checklist_items` array — do NOT serialize them into `content` as plain text. For freeform notes, use note_type='note' and put the body in `content`. `due_date` accepts natural language like 'tomorrow at 9am' (parsed in the user's timezone) and fires a notification — do not also create a calendar event for the same reminder.", + "parameters": { + "type": "object", + "properties": { + "action": {"type": "string", + "enum": ["list", "add", "update", "delete", "toggle_item"], + "description": "The action to perform"}, + "id": {"type": "string", "description": "Note id (for update/delete/toggle_item); 8-char prefix is fine"}, + "title": {"type": "string", "description": "Note title (for add/update)"}, + "content": {"type": "string", "description": "Freeform body text. Use this for note_type='note'. Do NOT use this for checklists — pass `checklist_items` instead."}, + "note_type": {"type": "string", "enum": ["note", "checklist"], + "description": "'note' = freeform text in `content`. 'checklist' = structured to-do items in `checklist_items`. Defaults to 'checklist' if checklist_items is supplied, else 'note'."}, + "checklist_items": {"type": "array", + "items": {"type": "object", + "properties": { + "text": {"type": "string", "description": "The to-do item text"}, + "done": {"type": "boolean", "description": "Whether the item is checked off"} + }, + "required": ["text"]}, + "description": "Checklist items for note_type='checklist'. Each item is {text, done}. REQUIRED for checklists — leaving this empty produces a blank note."}, + "color": {"type": "string", "description": "Optional color label (e.g. 'yellow', 'blue', 'green')"}, + "label": {"type": "string", "description": "Optional category label (also used as a list filter)"}, + "pinned": {"type": "boolean", "description": "Pin the note to the top"}, + "archived": {"type": "boolean", "description": "For update: archive/unarchive. For list: show archived notes when true."}, + "due_date": {"type": "string", "description": "Reminder time. Accepts natural language ('tomorrow at 9am', '11pm today') or ISO 8601. Fires a notification at that time."}, + "index": {"type": "integer", "description": "Checklist item index (for toggle_item, 0-based)"} + }, + "required": ["action"] + } + } + }, { "type": "function", "function": {