68eee57c9b
Removes the custom classify_capture_intent + _process_note two-pass approach. The LLM now picks the right tool directly via Ollama's native tool_calls API (same path as the main chat pipeline). _should_think decides whether extended reasoning is needed based on input length/ complexity. intent.py deleted — no longer needed. Android app and response format unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
114 lines
4.6 KiB
Python
114 lines
4.6 KiB
Python
"""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_task", "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
|