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
63 lines
1.9 KiB
Python
63 lines
1.9 KiB
Python
"""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
|