Remove CalDAV todo tools; overhaul quick-capture

- Remove all 6 CalDAV todo tools (create/list/update/complete/delete/search_todos)
  from tools.py definitions, imports, execute_tool branches, intent routing rules,
  generation_task labels/actions, and llm.py system prompt hints. CalDAV event
  tools remain. Todo functions still exist in caldav.py but are no longer exposed.

- Quick-capture now uses a dedicated classify_capture_intent() with a focused
  _CAPTURE_SYSTEM_PROMPT that always routes to a tool (never null). Tool set
  expanded: create_note/task/event + update_note + research_topic.

- research_topic in quick-capture calls run_research_pipeline() directly (no SSE
  buffer). run_research_pipeline() now accepts buf=None; all buf.append_event
  calls are guarded so status events are skipped when no buffer is provided.

- Fallback note now always sets body=text (was empty for texts ≤80 chars).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 19:13:07 -05:00
parent 4c77eab84b
commit 6f5170854b
7 changed files with 122 additions and 318 deletions
+32 -15
View File
@@ -11,17 +11,16 @@ from quart import Blueprint, jsonify, request
from fabledassistant.auth import get_current_user_id, login_required
from fabledassistant.config import Config
from fabledassistant.services.intent import classify_intent
from fabledassistant.services.intent import classify_capture_intent
from fabledassistant.services.tools import execute_tool, get_tools_for_user
logger = logging.getLogger(__name__)
quick_capture_bp = Blueprint("quick_capture", __name__, url_prefix="/api/quick-capture")
# Only creation tools are offered to the intent classifier here — no delete,
# update, search, or research. This keeps the fallback safe (worst case we
# create a plain note) and avoids accidental destructive actions.
_CAPTURE_TOOL_NAMES = {"create_note", "create_task", "create_event", "create_todo"}
# Tools offered to the quick-capture classifier. Excludes destructive ops
# (delete_*) and read-only queries — worst-case fallback is a plain note.
_CAPTURE_TOOL_NAMES = {"create_note", "create_task", "create_event", "update_note", "research_topic"}
@quick_capture_bp.route("", methods=["POST"])
@@ -42,21 +41,43 @@ async def quick_capture_route():
t for t in all_tools if t.get("function", {}).get("name") in _CAPTURE_TOOL_NAMES
]
intent = await classify_intent(text, capture_tools, intent_model)
intent = await classify_capture_intent(text, capture_tools, intent_model)
if intent.should_execute:
# research_topic bypasses execute_tool — run the pipeline directly
if intent.tool_name == "research_topic" and Config.searxng_enabled():
from fabledassistant.services.research import run_research_pipeline
topic = intent.arguments.get("topic", text)
model = Config.OLLAMA_MODEL
try:
note = await run_research_pipeline(topic, uid, model, intent_model)
logger.info(
"Quick-capture uid=%d: research note id=%d '%s'",
uid, note.id, note.title,
)
return jsonify({
"success": True,
"type": "note",
"message": f"Research note created: {note.title}",
"data": {"id": note.id, "title": note.title},
})
except Exception as exc:
logger.exception("Quick-capture research failed for topic: %s", topic)
return jsonify({"error": f"Research failed: {exc}"}), 500
result = await execute_tool(uid, intent.tool_name, intent.arguments)
if result.get("success"):
item_type = result.get("type", "note")
title = (result.get("data") or {}).get("title", "")
logger.info(
"Quick-capture uid=%d: created %s '%s' via intent '%s'",
"Quick-capture uid=%d: %s '%s' via intent '%s'",
uid, item_type, title, intent.tool_name,
)
return jsonify({
"success": True,
"type": item_type,
"message": f"{item_type.capitalize()} created: {title}",
"message": f"{item_type.capitalize()}: {title}",
"data": result.get("data"),
})
logger.warning(
@@ -66,13 +87,9 @@ async def quick_capture_route():
# Fall through to plain-note fallback
# Fallback: create a plain note with the raw text as the body.
# Use the first 80 chars as the title so short snippets look clean.
if len(text) <= 80:
fallback_title = text
fallback_body = ""
else:
fallback_title = text[:77] + "..."
fallback_body = text
# Always preserve the full text as the body; derive a short title.
fallback_title = text if len(text) <= 80 else text[:77] + "..."
fallback_body = text
result = await execute_tool(
uid, "create_note", {"title": fallback_title, "body": fallback_body}