Add /api/quick-capture endpoint for mobile/external clients

Single POST that classifies natural-language text and creates the
appropriate item (note, task, event, or todo) in one synchronous
request — no SSE, no conversation context needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 21:56:16 -05:00
parent 31777ae66b
commit b079c583df
2 changed files with 95 additions and 0 deletions
+2
View File
@@ -13,6 +13,7 @@ from fabledassistant.routes.api import api
from fabledassistant.routes.auth import auth_bp
from fabledassistant.routes.chat import chat_bp
from fabledassistant.routes.notes import notes_bp
from fabledassistant.routes.quick_capture import quick_capture_bp
from fabledassistant.routes.settings import settings_bp
from fabledassistant.routes.tasks import tasks_bp
@@ -44,6 +45,7 @@ def create_app() -> Quart:
app.register_blueprint(auth_bp)
app.register_blueprint(chat_bp)
app.register_blueprint(notes_bp)
app.register_blueprint(quick_capture_bp)
app.register_blueprint(settings_bp)
app.register_blueprint(tasks_bp)
@@ -0,0 +1,93 @@
"""Quick-capture endpoint for mobile/external clients.
POST /api/quick-capture — classifies natural-language text and creates the
appropriate item (note, task, calendar event, todo) in a single synchronous
request. No SSE, no conversation ID, no streaming.
"""
import logging
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.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"}
@quick_capture_bp.route("", methods=["POST"])
@login_required
async def quick_capture_route():
"""Classify text and create the appropriate item, returning a single JSON response."""
uid = get_current_user_id()
data = await request.get_json(silent=True) or {}
text = data.get("text", "").strip()
if not text:
return jsonify({"error": "text is required"}), 400
intent_model = Config.OLLAMA_INTENT_MODEL or Config.OLLAMA_MODEL
# Build tool list for this user, then restrict to capture-only operations.
all_tools = await get_tools_for_user(uid)
capture_tools = [
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)
if intent.should_execute:
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'",
uid, item_type, title, intent.tool_name,
)
return jsonify({
"success": True,
"type": item_type,
"message": f"{item_type.capitalize()} created: {title}",
"data": result.get("data"),
})
logger.warning(
"Quick-capture uid=%d: tool '%s' returned failure: %s",
uid, intent.tool_name, result.get("error"),
)
# 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
result = await execute_tool(
uid, "create_note", {"title": fallback_title, "body": fallback_body}
)
if result.get("success"):
title = (result.get("data") or {}).get("title", "")
logger.info(
"Quick-capture uid=%d: fallback note created '%s'", uid, title
)
return jsonify({
"success": True,
"type": "note",
"message": f"Note created: {title}",
"data": result.get("data"),
"fallback": True,
})
return jsonify({"error": "Failed to create item"}), 500