"""Quick-capture endpoint for mobile/external clients. POST /api/quick-capture — sends text through the main LLM tool-calling pipeline and returns a single synchronous JSON response. No SSE, no conversation ID. """ import logging from datetime import date from quart import Blueprint, jsonify, request from fabledassistant.auth import get_current_user_id, login_required from fabledassistant.config import Config from fabledassistant.services.generation_task import _should_think from fabledassistant.services.llm import stream_chat_with_tools 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") # Tools offered to the quick-capture endpoint. Excludes destructive ops, # read-only queries, and conversational-only tools. _CAPTURE_TOOL_NAMES = {"create_note", "create_event", "update_note", "research_topic"} _SYSTEM_PROMPT = """\ Today is {today}. You are a quick-capture assistant. The user has sent a short \ snippet from their mobile device. Create the appropriate item — note, task, or \ calendar event — using the available tools. Always call a tool; never reply \ conversationally.""" @quick_capture_bp.route("", methods=["POST"]) @login_required async def quick_capture_route(): """Classify text via native tool-calling and create the appropriate item.""" 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 from fabledassistant.services.settings import get_setting model = await get_setting(uid, "default_model", Config.OLLAMA_MODEL) 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 ] messages = [ {"role": "system", "content": _SYSTEM_PROMPT.format(today=date.today().isoformat())}, {"role": "user", "content": text}, ] think = _should_think(text, think_requested=True) tool_calls: list[dict] = [] try: async for chunk in stream_chat_with_tools(messages, model, tools=capture_tools, think=think, num_ctx=4096): if chunk.type == "tool_calls" and chunk.tool_calls: tool_calls = chunk.tool_calls except Exception: logger.warning("Quick-capture LLM call failed for uid=%d", uid, exc_info=True) if tool_calls: tc = tool_calls[0] tool_name = tc.get("function", {}).get("name", "") arguments = tc.get("function", {}).get("arguments", {}) if tool_name == "research_topic" and Config.searxng_enabled(): from fabledassistant.services.research import run_research_pipeline topic = arguments.get("topic", text) try: note = await run_research_pipeline(topic, uid, 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: %s", topic) return jsonify({"error": f"Research failed: {exc}"}), 500 result = await execute_tool(uid, tool_name, arguments) if result.get("success"): item_type = result.get("type", "note") title = (result.get("data") or {}).get("title", "") logger.info("Quick-capture uid=%d: %s '%s'", uid, item_type, title) return jsonify({ "success": True, "type": item_type, "message": f"{item_type.capitalize()}: {title}", "data": result.get("data"), }) logger.warning("Quick-capture uid=%d: tool '%s' failed: %s", uid, tool_name, result.get("error")) # Fallback: create a plain note with the raw text result = await execute_tool(uid, "create_note", {"title": text[:80], "body": text}) if result.get("success"): title = (result.get("data") or {}).get("title", "") logger.info("Quick-capture uid=%d: fallback note '%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