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.auth import auth_bp
|
||||||
from fabledassistant.routes.chat import chat_bp
|
from fabledassistant.routes.chat import chat_bp
|
||||||
from fabledassistant.routes.notes import notes_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.settings import settings_bp
|
||||||
from fabledassistant.routes.tasks import tasks_bp
|
from fabledassistant.routes.tasks import tasks_bp
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@ def create_app() -> Quart:
|
|||||||
app.register_blueprint(auth_bp)
|
app.register_blueprint(auth_bp)
|
||||||
app.register_blueprint(chat_bp)
|
app.register_blueprint(chat_bp)
|
||||||
app.register_blueprint(notes_bp)
|
app.register_blueprint(notes_bp)
|
||||||
|
app.register_blueprint(quick_capture_bp)
|
||||||
app.register_blueprint(settings_bp)
|
app.register_blueprint(settings_bp)
|
||||||
app.register_blueprint(tasks_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