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:
@@ -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
|
||||
Reference in New Issue
Block a user