Files
pablan/backend/app/api/authoring/grounding.py
T
ProfessorNovaandClaude Opus 5 97dbff309c Pablan, as it stands
Self-hosted knowledge management for SMEs: a split-screen Markdown editor
whose sections an LLM refines while you write, and RAG question answering
over the documents that result. FastAPI + Postgres/pgvector on the back,
SvelteKit on the front, everything OpenAI-compatible and self-hostable.

Squashed into a single commit; the development history stays local.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CA43ZJda8Rbp2hKXNy8f6b
2026-09-04 08:36:17 +02:00

75 lines
2.6 KiB
Python

"""What the company already wrote about this.
Before refining a section, the server looks for related published material the
author may read and hands it to the prompt as a reference — so a suggestion
stays consistent with the rest of the knowledge base instead of inventing a
parallel version of it. The same chunks are surfaced to the editor's "?"
inspector, so the author can see where a suggestion drew from.
"""
import uuid
from sqlalchemy.ext.asyncio import AsyncSession
from app.llm.errors import LLMError
from app.models import User
from app.rag.chunking import HEADING_RE
from app.rag.similarity import (
CAPTURE_CONTEXT_MAX_DISTANCE,
SimilarChunk,
similar_chunks,
)
# A section needs at least this much of its own text (beyond the heading)
# before it is worth searching the knowledge base to ground the refinement:
# a bare heading matches nothing useful and only adds noise.
MIN_CHARS = 40
TOP_K = 3
# Cap each grounding excerpt so a few long ones cannot crowd out the section.
EXCERPT_CHARS = 600
def leading_heading(section_text: str) -> str | None:
"""The heading text a section starts with, to match a template hint."""
first = section_text.lstrip().splitlines()[0] if section_text.strip() else ""
match = HEADING_RE.match(first)
return match.group(2).strip() if match else None
def reference(title: str, heading_path: str, content: str) -> str:
"""One retrieved chunk, rendered for the prompt: where it comes from, then
a bounded excerpt."""
excerpt = content.strip()
if len(excerpt) > EXCERPT_CHARS:
excerpt = excerpt[:EXCERPT_CHARS].rstrip() + " ..."
where = f'"{title}" ({heading_path})' if heading_path else f'"{title}"'
return f"From {where}:\n{excerpt}"
async def for_section(
db: AsyncSession, section_text: str, user: User, document_id: uuid.UUID
) -> list[SimilarChunk]:
"""Related knowledge for one section, permission-filtered by construction.
Returns nothing when the section is still too thin to match on, when the
current document is the only match, or when the embedding endpoint is
unavailable — the refinement then simply proceeds without grounding.
"""
query = section_text.strip()
_, _, after_heading = query.partition("\n")
body = after_heading.strip() if leading_heading(query) is not None else query
if len(body) < MIN_CHARS:
return []
try:
return await similar_chunks(
db,
query,
user=user,
top_k=TOP_K,
max_distance=CAPTURE_CONTEXT_MAX_DISTANCE,
exclude_builtin=True,
exclude_document_id=document_id,
)
except LLMError:
return []