diff --git a/src/fabledassistant/app.py b/src/fabledassistant/app.py index 5824dba..575284f 100644 --- a/src/fabledassistant/app.py +++ b/src/fabledassistant/app.py @@ -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) diff --git a/src/fabledassistant/routes/quick_capture.py b/src/fabledassistant/routes/quick_capture.py new file mode 100644 index 0000000..e0c60ab --- /dev/null +++ b/src/fabledassistant/routes/quick_capture.py @@ -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