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:
@@ -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: 3–8 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}
|
||||
|
||||
Reference in New Issue
Block a user