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
80 lines
2.6 KiB
Python
80 lines
2.6 KiB
Python
"""The blueprints that ship with the product.
|
|
|
|
Nothing in the catalog is active. It is a shelf an admin takes from: adding a
|
|
blueprint copies it into an ordinary template row, which the customer then owns
|
|
and edits. The catalog never touches that row again.
|
|
"""
|
|
|
|
from typing import Annotated
|
|
|
|
from fastapi import Depends
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.api.templates.blueprints import parse_or_422, taken_config_ids
|
|
from app.api.templates.routing import editor_router
|
|
from app.api.templates.schemas import CatalogDetail, CatalogSummary, TemplateDetail
|
|
from app.api.templates.view import detail
|
|
from app.db import get_db
|
|
from app.errors import ApiError
|
|
from app.template_catalog import catalog_for_locale, get_catalog_entry
|
|
from app.template_import import upsert_template
|
|
|
|
router = editor_router()
|
|
|
|
|
|
@router.get("/catalog")
|
|
async def list_catalog(
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
) -> list[CatalogSummary]:
|
|
"""The blueprints shipped with Pablan, each marked with whether this
|
|
instance has already added it."""
|
|
added = await taken_config_ids(db)
|
|
return [
|
|
CatalogSummary(
|
|
id=entry.id,
|
|
name=entry.name,
|
|
description=entry.description,
|
|
sections=entry.sections,
|
|
added=entry.id in added,
|
|
)
|
|
for entry in catalog_for_locale()
|
|
]
|
|
|
|
|
|
@router.get("/catalog/{catalog_id}")
|
|
async def get_catalog_blueprint(catalog_id: str) -> CatalogDetail:
|
|
"""Read a blueprint before adding it — the whole point of "view" is that
|
|
an admin can see its structure before committing to it."""
|
|
entry = get_catalog_entry(catalog_id)
|
|
if entry is None:
|
|
raise ApiError(404, "Blueprint not found.", "not_found")
|
|
return CatalogDetail(
|
|
id=entry.id,
|
|
name=entry.name,
|
|
description=entry.description,
|
|
sections=entry.sections,
|
|
added=False,
|
|
yaml=entry.source,
|
|
)
|
|
|
|
|
|
@router.post("/catalog/{catalog_id}")
|
|
async def add_from_catalog(
|
|
catalog_id: str,
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
) -> TemplateDetail:
|
|
"""Copy a blueprint into this instance. The result is an ordinary
|
|
template row: editable, and never touched by the catalog again."""
|
|
entry = get_catalog_entry(catalog_id)
|
|
if entry is None:
|
|
raise ApiError(404, "Blueprint not found.", "not_found")
|
|
if entry.id in await taken_config_ids(db):
|
|
raise ApiError(
|
|
409,
|
|
"This template has already been added — edit or duplicate it instead.",
|
|
"already_added",
|
|
)
|
|
row, _created = await upsert_template(db, parse_or_422(entry.source))
|
|
await db.commit()
|
|
return detail(row)
|