Expose manage_notes via native function calling (#759)

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.
This commit is contained in:
tanmayraut45
2026-06-02 08:03:32 +05:30
committed by GitHub
parent a2f6183c4a
commit eff762cdd9
3 changed files with 52 additions and 3 deletions

View File

@@ -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"])

View File

@@ -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"]

View File

@@ -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": {