Quick-capture notes: enrich content via main model

Add _process_note() — a second LLM pass using the main model that
transforms raw capture text into a well-formed note with a genuine
summary title and formatted body. Replaces the previous behaviour of
using the captured text verbatim as both title and body.

The processing prompt instructs the model to:
- Generate a 3-8 word summary title (never a verbatim copy)
- Format the body appropriately: bullet lists for items, clean prose
  for stream-of-thought, organised paragraphs for raw notes/fragments
- Preserve all original information without inventing new facts

The enrichment pass runs for both the intent-classified create_note
path and the fallback path. On LLM/parse failure it degrades safely
to the old verbatim behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 17:57:29 -05:00
parent 2c2874a1cc
commit 1106399883
+57 -4
View File
@@ -5,13 +5,16 @@ 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__)
@@ -22,6 +25,48 @@ quick_capture_bp = Blueprint("quick_capture", __name__, url_prefix="/api/quick-c
# (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
@@ -66,6 +111,14 @@ async def quick_capture_route():
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":
model = Config.OLLAMA_MODEL
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")
@@ -86,10 +139,10 @@ async def quick_capture_route():
)
# Fall through to plain-note fallback
# Fallback: create a plain note with the raw text as the body.
# Always preserve the full text as the body; derive a short title.
fallback_title = text if len(text) <= 80 else text[:77] + "..."
fallback_body = text
# Fallback: classify_capture_intent returned no-tool (e.g. LLM parse failure).
# Still process the text through the note enrichment pass.
model = Config.OLLAMA_MODEL
fallback_title, fallback_body = await _process_note(text, model)
result = await execute_tool(
uid, "create_note", {"title": fallback_title, "body": fallback_body}