Files
FabledScribe/src/fabledassistant/routes/quick_capture.py
T
bvandeusen 3d7be5888e Remove intent model entirely; quick-capture uses primary model
The separate intent model (OLLAMA_INTENT_MODEL / qwen2.5:7b) is removed
from every part of the system. All classification now uses the primary model.

Changes:
- config.py: remove OLLAMA_INTENT_MODEL
- intent.py: remove classify_intent() and all supporting infrastructure
  (_SYSTEM_PROMPT_TEMPLATE, _RESEARCH_PREFIX, _PRIOR_WORK_REFS); file now
  only contains the quick-capture classifier
- quick_capture.py: classify_capture_intent() now called with Config.OLLAMA_MODEL
- generation_task.py: remove intent_model_setting DB lookup and get_setting import;
  history summarization and research pipeline use the primary model directly
- research.py: remove intent_model parameter from run_research_pipeline() and
  _generate_sub_queries(); both use the model param throughout
- routes/settings.py: remove intent_model from model-key validation and response
- app.py: remove intent model pre-warming at startup
- SettingsView.vue: remove Intent Model selector and related refs/state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 18:41:49 -05:00

161 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 json
import logging
import re
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_capture_intent
from fabledassistant.services.llm import generate_completion
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 classifier. Excludes destructive ops
# (delete_*) and read-only queries — worst-case fallback is a plain note.
_CAPTURE_TOOL_NAMES = {"create_note", "create_task", "create_event", "update_note", "research_topic"}
_NOTE_PROCESS_PROMPT = """\
You are a note-taking assistant. The user has sent a quick-capture snippet. \
Transform it into a well-formed note.
Respond with ONLY a JSON object — no other text, no code fences:
{{"title": "short descriptive title", "body": "note content in markdown"}}
Rules:
- title: 38 words, a genuine summary — do NOT copy the input verbatim
- body: process the input thoughtfully:
- Lists of items → formatted bullet list
- A stream-of-thought or observation → clean prose, lightly organised
- Raw notes or fragments → organised paragraphs with a brief intro line
- URLs → include the URL and a one-sentence description of what it points to
- Preserve ALL information from the original; do not invent new facts
- Use markdown formatting (##, -, **, etc.) where it aids readability
- Keep it concise — do not pad with filler"""
async def _process_note(text: str, model: str) -> tuple[str, str]:
"""Use the main model to transform raw capture text into a title + body.
Returns (title, body). Falls back to (truncated text, full text) on any failure.
"""
messages = [
{"role": "system", "content": _NOTE_PROCESS_PROMPT},
{"role": "user", "content": text},
]
try:
raw = await generate_completion(messages, model, max_tokens=1024, num_ctx=4096)
raw = raw.strip()
raw = re.sub(r"^```(?:json)?\s*", "", raw)
raw = re.sub(r"\s*```$", "", raw).strip()
parsed = json.loads(raw)
title = str(parsed.get("title", "")).strip() or text[:60]
body = str(parsed.get("body", "")).strip() or text
return title, body
except Exception:
logger.warning("Note processing LLM call failed, using raw text", exc_info=True)
fallback_title = text if len(text) <= 80 else text[:77] + "..."
return fallback_title, text
@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
model = 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_capture_intent(text, capture_tools, model)
if intent.should_execute:
# research_topic bypasses execute_tool — run the pipeline directly
if intent.tool_name == "research_topic" and Config.searxng_enabled():
from fabledassistant.services.research import run_research_pipeline
topic = intent.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 for topic: %s", topic)
return jsonify({"error": f"Research failed: {exc}"}), 500
# For notes, run a second LLM pass to generate a proper title and
# well-formed body rather than using the raw capture text verbatim.
if intent.tool_name == "create_note":
title, body = await _process_note(text, model)
intent.arguments["title"] = title
intent.arguments["body"] = body
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: %s '%s' via intent '%s'",
uid, item_type, title, intent.tool_name,
)
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' returned failure: %s",
uid, intent.tool_name, result.get("error"),
)
# Fall through to plain-note fallback
# Fallback: classify_capture_intent returned no-tool (e.g. LLM parse failure).
# Still process the text through the note enrichment pass.
fallback_title, fallback_body = await _process_note(text, model)
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