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:
ProfessorNova
2026-09-04 09:21:37 +02:00
co-authored by Claude Opus 5
parent 68d3a43191
commit 784b76baf7
346 changed files with 43430 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
"""Template import: YAML → validated config → templates table.
Every row in `templates` is the customer's own, whichever way it got there
— pasted YAML, a fork, or added from the shipped catalog
(`template_catalog.py`). None of them is read-only.
"""
import logging
import yaml
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.authoring.schema import AuthoringTemplate
from app.models import Template
logger = logging.getLogger("pablan.templates")
class TemplateImportError(Exception):
"""Sanitized import failure — safe to surface to an admin."""
def parse_template(source: str) -> AuthoringTemplate:
try:
raw = yaml.safe_load(source)
except yaml.YAMLError as exc:
raise TemplateImportError(f"Invalid YAML: {type(exc).__name__}") from None
if not isinstance(raw, dict):
raise TemplateImportError("Template must be a YAML mapping.")
try:
return AuthoringTemplate.model_validate(raw)
except ValueError as exc:
first_error = str(exc).splitlines()[1] if "\n" in str(exc) else str(exc)
raise TemplateImportError(
f"Template failed validation: {first_error.strip()}"
) from None
async def upsert_template(
db: AsyncSession, template: AuthoringTemplate
) -> tuple[Template, bool]:
"""Insert or update by the template's config id. Returns (row, created)."""
existing = (
await db.execute(
select(Template).where(Template.config["id"].astext == template.id)
)
).scalar_one_or_none()
config = template.model_dump()
if existing is not None:
existing.name = template.name
existing.version = template.version
existing.config = config
return existing, False
row = Template(
name=template.name,
version=template.version,
config=config,
)
db.add(row)
await db.flush()
return row, True