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:
@@ -0,0 +1,226 @@
|
||||
# Authoring templates
|
||||
|
||||
A template is **not** an interview guide. It is a **Markdown skeleton** — a
|
||||
starting document with headings the author fills in — plus a persona and
|
||||
optional per-section hints that steer the section-refinement model. Templates
|
||||
are **declarative configuration, not code**: a new document type (a machine
|
||||
write-up, a decision record, a process) is a new YAML file, not a new
|
||||
feature.
|
||||
|
||||
`templates/` ships a **catalog of blueprints** that an admin adds from — see
|
||||
"Catalog and lifecycle" below; the `templates` table holds only what a
|
||||
customer actually uses, and every row in it is editable. The full template is
|
||||
stored in `templates.config` (JSONB) and validated on load against
|
||||
`AuthoringTemplate` (`app/authoring/schema.py`, schema version 1.0). The schema
|
||||
is versioned (`version` field) and expected to evolve after real use.
|
||||
|
||||
## Format
|
||||
|
||||
```yaml
|
||||
id: prozess
|
||||
name: "Ablauf: wie wir das machen"
|
||||
version: "1.0"
|
||||
kind: authoring
|
||||
locale: de
|
||||
description: >
|
||||
Ein wiederkehrender Ablauf, Schritt für Schritt — so, dass jemand anderes
|
||||
ihn allein schafft.
|
||||
|
||||
model:
|
||||
temperature: 0.4
|
||||
min_class_hint: "12b" # UI warning if the endpoint is weaker
|
||||
|
||||
persona: |
|
||||
Du bist ein präziser Fachredakteur für Arbeitsanweisungen. Du schreibst
|
||||
sachlich, in vollständigen Sätzen, und machst aus einer Abfolge eine
|
||||
nummerierte Liste. Zahlen, Fristen, Systemnamen und Zuständigkeiten
|
||||
behältst du exakt bei und erfindest keine dazu.
|
||||
|
||||
title_template: "Neuer Ablauf"
|
||||
|
||||
skeleton: |
|
||||
## Wann das gilt
|
||||
|
||||
## Schritt für Schritt
|
||||
|
||||
## Wenn es klemmt
|
||||
|
||||
## Wer zuständig ist
|
||||
|
||||
sections:
|
||||
- heading: "Wann das gilt"
|
||||
hint: >
|
||||
Der Auslöser: in welcher Situation dieser Ablauf greift, und wo er
|
||||
nicht gilt.
|
||||
- heading: "Schritt für Schritt"
|
||||
hint: >
|
||||
Die Schritte in ihrer Reihenfolge, jeder als eine Handlung — mit den
|
||||
Systemen, Formularen und Fristen, die dazugehören.
|
||||
- heading: "Wenn es klemmt"
|
||||
hint: >
|
||||
Die Sonderfälle und die Stellen, an denen es erfahrungsgemäß hakt.
|
||||
- heading: "Wer zuständig ist"
|
||||
hint: >
|
||||
Wer den Ablauf verantwortet und wen man bei Rückfragen anspricht.
|
||||
|
||||
metadata:
|
||||
visibility: department
|
||||
```
|
||||
|
||||
**The headings are the questions a colleague actually asks.** That is the
|
||||
whole craft in a template: "Wann das gilt / Schritt für Schritt / Wenn es
|
||||
klemmt" gets filled in, "Worum es geht / Details / Was andere wissen müssen"
|
||||
does not — it is a blank page wearing a structure. An `skeleton: ""` with no
|
||||
sections is a legitimate template (`notiz`), and better than headings that ask
|
||||
for nothing in particular.
|
||||
|
||||
## Fields
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `id` | Config id, stable across edits; the catalog and the picker key on it. Not the row UUID. |
|
||||
| `name` | Human title in the picker. |
|
||||
| `version` | Revision string, raised by hand when the skeleton or hints change. Read as a string even when the YAML looks like a float (`1.0`). |
|
||||
| `kind` | Always `authoring`. Replaces the old `mode: interview` — it distinguishes an authoring template from any legacy config still shaped like an interview. |
|
||||
| `locale` | The language this template's **content** is written in (`de` \| `en` \| null), not a UI string. The picker lists templates in the reader's language first. |
|
||||
| `description` | One line shown in the picker and catalog. |
|
||||
| `model.temperature` | Sampling temperature for the refinement call (default `0.4`). |
|
||||
| `model.min_class_hint` | Advisory model class (e.g. `"12b"`); the UI warns when the configured endpoint looks weaker. |
|
||||
| `persona` | The **editor voice** — how the refinement model should write. Injected as the system persona for every section of this document. |
|
||||
| `skeleton` | The Markdown the editor **opens with**: headings the author fills in. This IS the starting content, not a description of it. An empty skeleton yields a blank document. |
|
||||
| `sections` | Per-heading hints: `[{heading, hint}]`. |
|
||||
| `title_template` | Draft title, with `{{user.name}}` and `{{date}}` substituted at creation. |
|
||||
| `metadata.visibility` | Default visibility of the resulting document (`public` \| `department` \| `restricted`), changeable in the editor before publishing. |
|
||||
|
||||
## How a template drives the editor
|
||||
|
||||
1. **Creating a draft.** `POST /api/documents {template_id}` loads the
|
||||
`AuthoringTemplate`, renders the skeleton (`render_skeleton`, verbatim with
|
||||
a single trailing newline) and the title (`render_title`, with `{{user.name}}`
|
||||
/ `{{date}}` substituted), and writes a `draft` document authored by the user
|
||||
(`app/authoring/document.py`). A draft is author-only and never indexed — see
|
||||
[`data-model.md`](data-model.md).
|
||||
2. **Writing.** The editor opens on the skeleton. The author writes Markdown
|
||||
directly; Markdown is the source of truth (rule 1), so there is no derived
|
||||
render to watch.
|
||||
3. **Section refinement.** After a typing pause the client asks the model to
|
||||
mature the section at the cursor (`POST /api/documents/{id}/refine`, SSE).
|
||||
The whole document travels as prefix/suffix context, but the model
|
||||
regenerates **only** that section (FIM-style) so a large document is never
|
||||
re-emitted whole. The prompt (`app/authoring/prompts.py`, natural language —
|
||||
rule 7) is `persona` + the matching section `hint` + a fixed rule block
|
||||
("return only the refined section, keep the heading, invent no facts").
|
||||
|
||||
**`sections[].heading` must match a skeleton heading exactly.** The server finds
|
||||
the section the cursor sits in (`app/authoring/sections.py::active_section`,
|
||||
sharing the heading/fence logic with `rag/chunking.py`), reads the heading text
|
||||
the section starts with, and looks up its hint by exact string match
|
||||
(`AuthoringTemplate.hint_for`). A hint whose `heading` does not appear in the
|
||||
skeleton simply never reaches the model. A section with no hint still gets
|
||||
refined — persona and the rule block are enough.
|
||||
|
||||
## Language policy
|
||||
|
||||
Shipped template **content** — persona, hints, descriptions, title templates,
|
||||
skeleton headings — is **German**. Templates are product content for the German
|
||||
market, exactly like the fixture corpus, NOT UI copy. Schema keys, structure
|
||||
and section ids stay **English**. File naming is `<id>.<locale>.yaml`
|
||||
(`prozess.de.yaml`); a file with no locale suffix belongs to the
|
||||
default locale, so a customer can drop their own YAML in without learning the
|
||||
convention.
|
||||
|
||||
## The catalog
|
||||
|
||||
`templates/` ships blueprints for common SME situations. A blueprint is inert:
|
||||
it has no row in `templates` and cannot be used until an admin adds it.
|
||||
|
||||
| id | Purpose | Visibility of the result | Starter |
|
||||
|---|---|---|---|
|
||||
| `notiz` | Anything at all — **no skeleton**, an empty document | department | yes |
|
||||
| `prozess` | How something is done here, step by step, with what snags | department | yes |
|
||||
| `stoerung` | What broke, what caused it, what fixed it | department | yes |
|
||||
| `person` | What someone does and what to ask them about | public | yes |
|
||||
| `anlage` | A machine: running it, maintaining it, its quirks | department | |
|
||||
| `entscheidung` | What was decided, why, and what follows | department | |
|
||||
| `projekt-debrief` | What a project taught, in three questions | department | |
|
||||
|
||||
Two of these deserve their reasoning written down:
|
||||
|
||||
- **`notiz` has no skeleton on purpose.** Three generic headings ("What this
|
||||
is about / Details / What others need to know") are what a blank page looks
|
||||
like when it is trying to be helpful, and nobody writes a document that way.
|
||||
Someone reaching for the open template already knows what they want to say.
|
||||
- **`person` is written by the person themselves** (the profile page starts
|
||||
it, `api/account.py PERSONAL_BLUEPRINT`) and is `public`: a directory that
|
||||
half the company cannot read answers nobody's "who knows about X?". It
|
||||
replaced an "onboarding" blueprint somebody else was supposed to fill in
|
||||
FOR a new colleague — nobody does that, and the colleague knows the answers.
|
||||
|
||||
A draft stays private to its author until they publish it, so anything
|
||||
sensitive can be removed first.
|
||||
|
||||
## Catalog and lifecycle
|
||||
|
||||
**A fresh instance seeds four starter blueprints** — `notiz`, `prozess`,
|
||||
`stoerung`, `person` (`STARTER_TEMPLATE_IDS` in `app/template_catalog.py`) —
|
||||
and only while the `templates` table is empty. They are the four occasions on
|
||||
which anyone actually writes something down: write it down, how we do this,
|
||||
what broke, who you are. Everything more specific — a machine, a decision, a
|
||||
project review — is a deliberate add from the catalog, because a picker of ten
|
||||
options is a picker nobody reads. `seed_starter_templates` is the ONLY automatic
|
||||
write to the table, and it runs exclusively against an empty table; a startup
|
||||
upsert would silently discard an admin's edits.
|
||||
|
||||
**Nothing in `templates` is read-only.** A template describes how a company
|
||||
documents its own knowledge, so the company owns it — including the seeded ones
|
||||
and every blueprint added later. Admins can edit the YAML, duplicate (fork gets
|
||||
`<id>-kopie`), and delete; deleting is safe because the catalog can always
|
||||
supply the blueprint again, and documents created from the template are
|
||||
independent and survive.
|
||||
|
||||
This is the opposite of the built-in **help documents** (`app/help_import.py`),
|
||||
which are re-imported on every start and refuse edits (409 `builtin_readonly`).
|
||||
The rule behind both: content that describes *how Pablan works* belongs to the
|
||||
product; content that describes *how this company works* belongs to the customer.
|
||||
|
||||
Adding a blueprint twice is refused (409 `already_added`) rather than
|
||||
overwriting — the second add would silently discard the admin's edits.
|
||||
|
||||
Versioning stays manual: the `version` string in the YAML identifies a revision,
|
||||
and the editor reminds the admin to raise it when the skeleton or hints change.
|
||||
There is no version history table — a template is content under the customer's
|
||||
control, not an audit trail.
|
||||
|
||||
## Validation approach
|
||||
|
||||
Cheap schema validation before any UI work: create a draft from the template,
|
||||
write a rough section under each heading, and check that refinement matures the
|
||||
prose without inventing facts, keeps the heading, and answers in the section's
|
||||
language. A broken blueprint is logged and skipped on load rather than taking
|
||||
the catalog down (`catalog_invalid`). The refinement prompt is covered by an
|
||||
eval in `tests/evals`; extend it whenever the prompt or the rule block changes.
|
||||
|
||||
## Open problem: where built-in content lives
|
||||
|
||||
Pablan ships two kinds of content it did not get from the customer: the help
|
||||
documents and the template catalog. The current split (help documents
|
||||
re-imported and locked, templates offered and owned) is a decision we are
|
||||
comfortable defending, but the surrounding questions are open:
|
||||
|
||||
- **How much should a fresh instance start with?** We seed four templates and a
|
||||
handful of help documents. Should the seed be larger (a demo document set that
|
||||
shows what a good captured document looks like), configurable at install time,
|
||||
or nothing at all?
|
||||
- **Where should the shipped content live?** Today: `templates/` and `help/` as
|
||||
files in the repo, imported at startup. Files-in-repo is right for two
|
||||
developers; it is not obviously right once the catalog has thirty entries in
|
||||
several languages.
|
||||
- **How does catalog content reach existing instances?** A blueprint improved in
|
||||
a later release does not reach anyone who already added it, by design. There is
|
||||
no "update available" signal — a diff view against the current blueprint is the
|
||||
obvious feature, and it is not built.
|
||||
- **Should deleting the last template be possible?** It is today. A member who
|
||||
opens "Capture knowledge" in that state sees an empty picker.
|
||||
|
||||
This is deliberately recorded, not solved. Decide it with a real customer install
|
||||
in front of us rather than from first principles.
|
||||
Reference in New Issue
Block a user