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}, ]