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
This commit is contained in:
co-authored by
Claude Opus 5
parent
68d3a43191
commit
784b76baf7
@@ -0,0 +1,74 @@
|
||||
"""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 []
|
||||
Reference in New Issue
Block a user