"""Pydantic schema for authoring templates (schema version 1.0). A template 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. It is declarative configuration, not code (see docs/authoring-templates.md), stored in `templates.config` (JSONB) and validated on load. """ from typing import Literal from pydantic import BaseModel, field_validator class TemplateModelHints(BaseModel): temperature: float = 0.4 # UI warning when the configured endpoint is weaker than the template # expects; read by api/templates.py. min_class_hint: str | None = None class SectionHint(BaseModel): """Steers what the refinement model should draw out of one section. `heading` is matched to a skeleton heading by its exact text, so the hint only reaches the model while the author is writing under that heading. """ heading: str hint: str class TemplateMetadata(BaseModel): visibility: Literal["public", "department", "restricted"] = "department" class AuthoringTemplate(BaseModel): id: str name: str version: str # Names the template's shape, so a differently-shaped config is rejected # rather than silently loaded as an authoring template. kind: Literal["authoring"] = "authoring" # The language this template's CONTENT is written in — persona, skeleton # and hints, not the UI. The picker lists matching templates first. locale: Literal["de", "en"] | None = None description: str = "" model: TemplateModelHints = TemplateModelHints() persona: str # The Markdown the editor opens with: headings the author fills in. This # IS the starting content, not a description of it. skeleton: str sections: list[SectionHint] = [] title_template: str metadata: TemplateMetadata = TemplateMetadata() @field_validator("version", mode="before") @classmethod def _version_to_string(cls, value: object) -> str: # YAML reads an unquoted 1.0 as a float. return str(value) def hint_for(self, heading: str) -> str | None: for section in self.sections: if section.heading == heading: return section.hint return None