Files
FabledScribe/src/fabledassistant/services/assist.py
T
bvandeusen 48f070f773 Project-aware assist, link suggestions, project-scoped RAG, semantic search tool, SSE race fix
- Writing assistant: inject project notes as context (definition-tagged first), wikilink suggestions
- Link suggestions: server-side endpoint finds unlinked term occurrences, NoteEditorView sidebar panel
- Project-scoped RAG: ChatView ProjectSelector filters semantic+keyword search to selected project
- Semantic search tool: LLM search_notes upgraded to hybrid semantic (0.40 threshold) + keyword merge
- SSE race condition fix: drain remaining events after stream loop exits in chat.py and notes.py
- RAG_AUTO_SNIPPET raised 800→4000; sidebar include uses full note body; MAX_BODY_CHARS 8000→24000
- Enter-to-submit on writing assistant instruction textareas (note and task editors)
- DiffView: equal-line collapsing with 3-line context around changes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 14:02:54 -05:00

93 lines
4.3 KiB
Python

MAX_BODY_CHARS = 24000
MAX_CONTEXT_NOTE_CHARS = 1500
def _build_context_block(context_notes: list[dict]) -> str:
"""Format project context notes into a prompt block."""
lines = [
"--- Project Context ---",
"The following notes are from the same project. Notes tagged 'definition' are "
"canonical term definitions — treat them as authoritative. Use them to maintain "
"consistency in terminology, tone, style, and [[wikilinks]] to other documents.",
"",
]
for n in context_notes:
tags_str = f" [tags: {', '.join(n['tags'])}]" if n.get("tags") else ""
body_preview = (n["body"] or "")[:MAX_CONTEXT_NOTE_CHARS]
if len(n["body"] or "") > MAX_CONTEXT_NOTE_CHARS:
body_preview += "…"
lines.append(f"**{n['title']}**{tags_str}")
if body_preview:
lines.append(body_preview)
lines.append("")
lines.append("--- End Project Context ---")
return "\n".join(lines)
def build_assist_messages(
body: str,
target_section: str,
instruction: str,
whole_doc: bool = False,
context_notes: list[dict] | None = None,
) -> list[dict]:
"""Build Ollama messages for writing assist.
When whole_doc=True, the model revises the entire document.
When whole_doc=False (section mode), the full body is provided as read-only
context and the model outputs only the replacement for the target section.
context_notes: optional list of {title, tags, body} dicts from the same project,
injected so the model can maintain consistency with related documents.
"""
truncated_body = body[:MAX_BODY_CHARS]
if len(body) > MAX_BODY_CHARS:
truncated_body += "\n… (truncated)"
context_block = (_build_context_block(context_notes) + "\n\n") if context_notes else ""
if whole_doc:
system_content = (
"You are a writing assistant. Revise the document per the instruction. "
"Output ONLY the complete revised document. Preserve all structure and "
"content not addressed by the instruction. "
"Preserve all markdown formatting exactly — including bullet lists (- item), "
"numbered lists (1. item), nested/indented sub-items ( - sub), bold (**text**), "
"italic (_text_), and code blocks. Never flatten nested lists into plain text. "
"When referencing concepts defined in the Project Context, use [[wikilinks]] "
"to link to the relevant note by its exact title."
)
user_content = (
f"{context_block}"
f"--- Document ---\n{truncated_body}\n--- End Document ---\n\n"
f"Instruction: {instruction}"
)
else:
system_content = (
"You are an AI writing assistant integrated into a note-taking app. "
"The user is editing a document. The full document is shown below for context.\n\n"
f"--- Full Document ---\n{truncated_body}\n--- End Document ---\n\n"
"The user will give you a specific section of the document and an instruction. "
"Output ONLY the replacement text for that section. "
"If the target section starts with a markdown heading (e.g. ## Heading), "
"your output MUST also start with a heading at the same level. "
"You may revise the heading text but do not remove it. "
"Do not include other sections, explanatory text, or markdown code fences around the output. "
"Match the document's existing tone and style. "
"IMPORTANT: Preserve all markdown formatting exactly — including bullet lists (- item), "
"numbered lists (1. item), nested/indented sub-items ( - sub), bold (**text**), "
"italic (_text_), and code blocks. Never flatten nested lists into plain text. "
"When referencing concepts defined in the Project Context, use [[wikilinks]] "
"to link to the relevant note by its exact title."
)
user_content = (
f"{context_block}"
f"--- Target Section ---\n{target_section}\n--- End Target Section ---\n\n"
f"Instruction: {instruction}"
)
return [
{"role": "system", "content": system_content},
{"role": "user", "content": user_content},
]