Kansho/backend/journal_generation_policy.py

865 lines
31 KiB
Python
Raw Normal View History

2026-08-28 08:40:28 +02:00
"""Named, versioned journal generation guidelines. Not model temperature.
Four independent dimensions are selected by ID. Instruction text lives in
generation_guidelines, seeded from JSON. This module selects, validates and
composes. It does not own prompt wording and has no numeric 0–100 path.
"""
from __future__ import annotations
import json
import re
import uuid
from dataclasses import dataclass
from pathlib import Path
from db import get_db, row_to_dict
from journal_policy import PolicyError
from placeholders import CONTEXT_PATTERN
PURPOSE_JOURNAL = "journal_generate"
USER_SLOTS = ("transformation", "detail", "voice", "narrative")
ALLOWED_SLOTS = USER_SLOTS
LEGACY_SOURCE_MODE_SLOT = "source_mode"
SELECTION_KEYS = (
"transformation_id",
"detail_id",
"voice_id",
"narrative_id",
)
SLOT_TO_ID_KEY = {
"transformation": "transformation_id",
"detail": "detail_id",
"voice": "voice_id",
"narrative": "narrative_id",
}
ID_KEY_TO_SLOT = {value: key for key, value in SLOT_TO_ID_KEY.items()}
SLOT_INSTRUCTION_KEYS = {
"transformation": "transformation_instructions",
"detail": "detail_instructions",
"voice": "voice_instructions",
"narrative": "narrative_instructions",
}
REQUIRED_JOURNAL_PLACEHOLDERS = (
"transformation_instructions",
"detail_instructions",
"voice_instructions",
"narrative_instructions",
"writing_profile",
"style_examples",
"reconstruction",
"existing_text",
)
RETIRED_JOURNAL_PLACEHOLDERS = (
"source_mode_instructions",
"editorial_mode",
"editorial_instructions",
)
STATUS_DRAFT = "draft"
STATUS_ACTIVE = "active"
STATUS_ARCHIVED = "archived"
STATUSES = (STATUS_DRAFT, STATUS_ACTIVE, STATUS_ARCHIVED)
MAX_INSTRUCTION_CHARS = 1000
MAX_LABEL_CHARS = 80
MAX_SUMMARY_CHARS = 160
KEY_RE = re.compile(r"^[a-z][a-z0-9_]{0,40}$")
SEED_PATH = Path(__file__).resolve().parent / "config" / "generation_instructions.seed.json"
PUBLIC_FIELDS = (
"id",
"purpose",
"slot",
"guideline_key",
"label",
"summary",
"sort_order",
"status",
"revision",
"cloned_from",
"is_default",
"is_system_seed",
"seed_revision",
"used_at",
"created",
"updated",
)
class GenerationPolicyError(PolicyError):
def __init__(
self,
message: str,
*,
code: str = "invalid_generation_selection",
status_code: int = 400,
):
super().__init__(code, message, status_code)
class CatalogError(GenerationPolicyError):
def __init__(self, message: str, *, code: str = "generation_policy_invalid", status_code: int = 409):
super().__init__(message, code=code, status_code=status_code)
@dataclass(frozen=True)
class CompiledPolicy:
ids: dict[str, str]
keys: dict[str, str]
labels: dict[str, str]
summaries: dict[str, str]
revisions: dict[str, int]
instructions: dict[str, str]
seed_revision: str
def load_seed_document() -> dict:
return json.loads(SEED_PATH.read_text(encoding="utf-8"))
def _as_bool(raw) -> bool:
return bool(int(raw)) if not isinstance(raw, bool) else raw
def _limits() -> tuple[int, int, int]:
seed = load_seed_document()
return (
int(seed.get("max_instruction_chars") or MAX_INSTRUCTION_CHARS),
int(seed.get("max_label_chars") or MAX_LABEL_CHARS),
int(seed.get("max_summary_chars") or MAX_SUMMARY_CHARS),
)
def _clean_text(raw, field: str, *, max_chars: int, allow_empty: bool = False) -> str:
text = (raw or "").strip()
if not text and not allow_empty:
raise CatalogError(f"{field} darf nicht leer sein.", code="invalid_generation_guideline", status_code=400)
if len(text) > max_chars:
raise CatalogError(f"{field} darf höchstens {max_chars} Zeichen haben.", code="invalid_generation_guideline", status_code=400)
return text
def _clean_key(raw) -> str:
key = (raw or "").strip()
if not KEY_RE.match(key):
raise CatalogError(
"guideline_key muss aus Kleinbuchstaben, Ziffern und Unterstrich bestehen.",
code="invalid_generation_guideline",
status_code=400,
)
return key
def _row(item: dict | None, *, include_instruction: bool = False) -> dict | None:
if not item:
return None
payload = {field: item.get(field) for field in PUBLIC_FIELDS}
payload["is_default"] = _as_bool(item.get("is_default", 0))
payload["is_system_seed"] = _as_bool(item.get("is_system_seed", 0))
payload["revision"] = int(item.get("revision") or 1)
payload["sort_order"] = int(item.get("sort_order") or 0)
payload["guideline_key"] = item.get("guideline_key") or ""
if include_instruction:
payload["instruction"] = item.get("instruction") or ""
payload["seed_id"] = item.get("seed_id") or ""
return payload
def _fetch(guideline_id: str) -> dict | None:
with get_db() as conn:
return row_to_dict(
conn.execute("SELECT * FROM generation_guidelines WHERE id = ?", (guideline_id,)).fetchone()
)
def get_guideline(guideline_id: str, *, include_instruction: bool = True) -> dict:
row = _fetch(guideline_id)
if not row:
raise CatalogError("Ausprägung nicht gefunden.", code="guideline_missing", status_code=404)
return _row(row, include_instruction=include_instruction)
def list_guidelines(
purpose: str = PURPOSE_JOURNAL,
*,
slot: str | None = None,
statuses: tuple[str, ...] | None = None,
include_instruction: bool = False,
) -> list[dict]:
query = "SELECT * FROM generation_guidelines WHERE purpose = ?"
params: list = [purpose]
if slot:
query += " AND slot = ?"
params.append(slot)
if statuses:
query += " AND status IN (" + ",".join("?" for _ in statuses) + ")"
params.extend(statuses)
query += " ORDER BY slot, sort_order, revision, label"
with get_db() as conn:
rows = [row_to_dict(row) for row in conn.execute(query, params).fetchall()]
return [_row(row, include_instruction=include_instruction) for row in rows]
def overview_payload(purpose: str = PURPOSE_JOURNAL) -> dict:
seed = load_seed_document()
items = list_guidelines(purpose)
slots = {slot: [item for item in items if item["slot"] == slot] for slot in ALLOWED_SLOTS}
return {
"purpose": purpose,
"seed_revision": seed.get("seed_revision") or "",
"max_instruction_chars": int(seed.get("max_instruction_chars") or MAX_INSTRUCTION_CHARS),
"max_label_chars": int(seed.get("max_label_chars") or MAX_LABEL_CHARS),
"max_summary_chars": int(seed.get("max_summary_chars") or MAX_SUMMARY_CHARS),
"slots": slots,
}
def active_user_options(purpose: str = PURPOSE_JOURNAL) -> dict[str, list[dict]]:
items = list_guidelines(purpose, statuses=(STATUS_ACTIVE,))
return {
slot: [
{
"id": item["id"],
"key": item["guideline_key"],
"label": item["label"],
"summary": item["summary"],
"revision": item["revision"],
"is_default": item["is_default"],
}
for item in items
if item["slot"] == slot
]
for slot in USER_SLOTS
}
def default_selection_ids(purpose: str = PURPOSE_JOURNAL) -> dict[str, str]:
items = list_guidelines(purpose, statuses=(STATUS_ACTIVE,))
chosen = {}
for slot in USER_SLOTS:
slot_items = [item for item in items if item["slot"] == slot]
if not slot_items:
raise CatalogError(f"{slot} hat keine aktive Ausprägung.")
preferred = next((item for item in slot_items if item["is_default"]), slot_items[0])
chosen[SLOT_TO_ID_KEY[slot]] = preferred["id"]
return chosen
def _require_active_for_run(row: dict, slot: str) -> dict:
if not row:
raise GenerationPolicyError(f"{slot} ist unbekannt.")
if row.get("slot") != slot:
raise GenerationPolicyError(f"{slot} verweist auf die falsche Dimension.")
if row.get("status") != STATUS_ACTIVE:
raise GenerationPolicyError(
f"{slot} ist nicht für neue Läufe verfügbar.",
code="invalid_generation_selection",
)
if not (row.get("instruction") or "").strip():
raise CatalogError(f"{slot} hat keine Anweisung.")
return row
def validate_selection(raw, *, purpose: str = PURPOSE_JOURNAL) -> dict[str, dict]:
if not isinstance(raw, dict):
raise GenerationPolicyError("generation_selection muss die vier Ausprägungs-IDs enthalten.")
unknown = sorted(set(raw) - set(SELECTION_KEYS))
if unknown:
raise GenerationPolicyError("Unbekannte Auswahl: " + ", ".join(unknown))
missing = [key for key in SELECTION_KEYS if not (raw.get(key) or "").strip()]
if missing:
raise GenerationPolicyError("generation_selection ist unvollständig: " + ", ".join(missing))
chosen = {}
for key in SELECTION_KEYS:
slot = ID_KEY_TO_SLOT[key]
row = _fetch(str(raw[key]).strip())
chosen[slot] = _require_active_for_run(row, slot)
if (row.get("purpose") or purpose) != purpose:
raise GenerationPolicyError(f"{slot} gehört nicht zu diesem Zweck.")
return chosen
def compile_selection(
selection: dict[str, str],
*,
purpose: str = PURPOSE_JOURNAL,
) -> CompiledPolicy:
chosen = validate_selection(selection, purpose=purpose)
instructions = {
SLOT_INSTRUCTION_KEYS[slot]: chosen[slot]["instruction"] for slot in USER_SLOTS
}
revision = ""
for row in chosen.values():
revision = row.get("seed_revision") or revision
return CompiledPolicy(
ids={slot: chosen[slot]["id"] for slot in USER_SLOTS},
keys={slot: chosen[slot]["guideline_key"] for slot in USER_SLOTS},
labels={slot: chosen[slot]["label"] for slot in USER_SLOTS},
summaries={slot: chosen[slot].get("summary") or "" for slot in USER_SLOTS},
revisions={slot: int(chosen[slot].get("revision") or 1) for slot in USER_SLOTS},
instructions=instructions,
seed_revision=revision,
)
def policy_trace(
compiled: CompiledPolicy,
*,
source: str,
remembered: bool,
) -> dict:
return {
"source": source,
"remembered": remembered,
"ids": dict(compiled.ids),
"keys": dict(compiled.keys),
"labels": dict(compiled.labels),
"revisions": dict(compiled.revisions),
"seed_revision": compiled.seed_revision,
}
def draft_snapshot(compiled: CompiledPolicy, *, prompt: dict | None = None, model: str = "") -> dict:
"""Persistable run snapshot. No prompt bodies or instruction texts."""
return {
"transformation": {
"id": compiled.ids["transformation"],
"key": compiled.keys["transformation"],
"label": compiled.labels["transformation"],
"revision": compiled.revisions["transformation"],
},
"detail": {
"id": compiled.ids["detail"],
"key": compiled.keys["detail"],
"label": compiled.labels["detail"],
"revision": compiled.revisions["detail"],
},
"voice": {
"id": compiled.ids["voice"],
"key": compiled.keys["voice"],
"label": compiled.labels["voice"],
"revision": compiled.revisions["voice"],
},
"narrative": {
"id": compiled.ids["narrative"],
"key": compiled.keys["narrative"],
"label": compiled.labels["narrative"],
"revision": compiled.revisions["narrative"],
},
"prompt_slug": (prompt or {}).get("slug") or "",
"prompt_revision": (prompt or {}).get("seed_revision") or "",
"model": model or "",
}
def snapshot_summary(snapshot: dict | None) -> str:
data = snapshot or {}
labels = [
(data.get("transformation") or {}).get("label") or "",
(data.get("detail") or {}).get("label") or "",
(data.get("voice") or {}).get("label") or "",
(data.get("narrative") or {}).get("label") or "",
]
labels = [item for item in labels if item]
if not labels:
return ""
line = " · ".join(item[:1].upper() + item[1:] if item else item for item in labels)
return f"Erzeugt mit:\n{line}"
def assert_journal_prompt_contract(template: str) -> None:
keys = CONTEXT_PATTERN.findall(template or "")
retired = [key for key in RETIRED_JOURNAL_PLACEHOLDERS if key in keys]
if retired:
shown = ", ".join(f"{{{{{key}}}}}" for key in retired)
raise CatalogError(
"Dieser Journal-Prompt verwendet den veralteten Platzhalter "
+ shown
+ ". Die Unterscheidung zwischen Fließtext- und Stichpunktmodus ist entfallen. "
"Bitte den Prompt unter Admin → Prompts aktualisieren.",
code="prompt_contract_incompatible",
)
missing = [key for key in REQUIRED_JOURNAL_PLACEHOLDERS if key not in keys]
if missing:
shown = ", ".join(f"{{{{{key}}}}}" for key in missing)
raise CatalogError(
"Dieser Journal-Prompt erfüllt den aktuellen Vertrag nicht. Es fehlen: "
+ shown
+ ". Bitte den Prompt unter Admin → Prompts aktualisieren.",
code="prompt_contract_incompatible",
)
def assert_template_resolved(rendered: str) -> None:
leftover = CONTEXT_PATTERN.findall(rendered or "")
if leftover:
raise CatalogError(
"Promptplatzhalter konnten nicht aufgelöst werden: "
+ ", ".join(f"{{{{{key}}}}}" for key in leftover),
code="unresolved_placeholder",
)
def load_selection(profile_id: str) -> dict[str, str] | None:
with get_db() as conn:
row = row_to_dict(
conn.execute(
"""
SELECT transformation_id, detail_id, voice_id, narrative_id, updated
FROM journal_generation_selection
WHERE profile_id = ?
""",
(profile_id,),
).fetchone()
)
if not row:
return None
values = {key: row[key] for key in SELECTION_KEYS}
values["updated"] = row.get("updated") or ""
return values
def save_selection(profile_id: str, selection: dict[str, str]) -> dict[str, str]:
chosen = validate_selection(selection)
checked = {SLOT_TO_ID_KEY[slot]: chosen[slot]["id"] for slot in USER_SLOTS}
with get_db() as conn:
conn.execute(
"""
INSERT INTO journal_generation_selection (
profile_id, transformation_id, detail_id, voice_id, narrative_id, updated
)
VALUES (?, ?, ?, ?, ?, datetime('now'))
ON CONFLICT(profile_id) DO UPDATE SET
transformation_id = excluded.transformation_id,
detail_id = excluded.detail_id,
voice_id = excluded.voice_id,
narrative_id = excluded.narrative_id,
updated = datetime('now')
""",
(
profile_id,
checked["transformation_id"],
checked["detail_id"],
checked["voice_id"],
checked["narrative_id"],
),
)
stored = load_selection(profile_id) or {**checked, "updated": ""}
return stored
def get_or_create_selection(profile_id: str) -> dict[str, str]:
existing = load_selection(profile_id)
if existing is not None:
return existing
return save_selection(profile_id, default_selection_ids())
def settings_payload(profile_id: str) -> dict:
stored = get_or_create_selection(profile_id)
selection = {key: stored[key] for key in SELECTION_KEYS}
return {
"selection": selection,
"updated": stored.get("updated") or "",
"options": active_user_options(),
"defaults": default_selection_ids(),
}
def resolve_run_selection(
profile_id: str,
snapshot: dict | None,
remember: bool,
) -> tuple[dict[str, str], dict]:
if snapshot is None:
stored = get_or_create_selection(profile_id)
values = {key: stored[key] for key in SELECTION_KEYS}
validate_selection(values)
return values, {"source": "profile", "remembered": False}
values = {key: str(snapshot.get(key) or "").strip() for key in SELECTION_KEYS}
validate_selection(values)
remembered = False
if remember:
save_selection(profile_id, values)
remembered = True
return values, {"source": "request", "remembered": remembered}
def mark_guidelines_used(ids: list[str]) -> None:
clean = [item for item in ids if item]
if not clean:
return
with get_db() as conn:
conn.executemany(
"""
UPDATE generation_guidelines
SET used_at = COALESCE(used_at, datetime('now'))
WHERE id = ?
""",
[(item,) for item in clean],
)
def _write_fields(raw: dict) -> dict:
max_instruction, max_label, max_summary = _limits()
return {
"guideline_key": _clean_key(raw.get("guideline_key") or raw.get("key")),
"label": _clean_text(raw.get("label"), "label", max_chars=max_label),
"summary": _clean_text(raw.get("summary"), "summary", max_chars=max_summary, allow_empty=True),
"instruction": _clean_text(raw.get("instruction"), "instruction", max_chars=max_instruction),
"sort_order": int(raw.get("sort_order") or 0),
}
def create_guideline(purpose: str, slot: str, body: dict) -> dict:
if slot not in ALLOWED_SLOTS:
raise CatalogError("Unbekannter Slot.", code="invalid_generation_guideline", status_code=400)
fields = _write_fields(body)
guideline_id = str(uuid.uuid4())
with get_db() as conn:
count = conn.execute(
"SELECT COALESCE(MAX(sort_order), -1) AS n FROM generation_guidelines WHERE purpose = ? AND slot = ?",
(purpose, slot),
).fetchone()["n"]
conn.execute(
"""
INSERT INTO generation_guidelines (
id, purpose, slot, guideline_key, label, summary, instruction, sort_order,
status, revision, cloned_from, is_default, is_system_seed, seed_id, seed_revision,
created, updated
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, NULL, 0, 0, '', '', datetime('now'), datetime('now'))
""",
(
guideline_id,
purpose,
slot,
fields["guideline_key"],
fields["label"],
fields["summary"],
fields["instruction"],
fields["sort_order"] if body.get("sort_order") is not None else int(count) + 1,
STATUS_DRAFT,
),
)
return get_guideline(guideline_id)
def clone_guideline(guideline_id: str) -> dict:
current = _fetch(guideline_id)
if not current:
raise CatalogError("Ausprägung nicht gefunden.", code="guideline_missing", status_code=404)
if current.get("slot") == LEGACY_SOURCE_MODE_SLOT:
raise CatalogError(
"Der Quellenmodus ist kein aktiver Slot mehr.",
code="invalid_generation_guideline",
status_code=400,
)
new_id = str(uuid.uuid4())
with get_db() as conn:
conn.execute(
"""
INSERT INTO generation_guidelines (
id, purpose, slot, guideline_key, label, summary, instruction, sort_order,
status, revision, cloned_from, is_default, is_system_seed, seed_id, seed_revision,
created, updated
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, '', ?, datetime('now'), datetime('now'))
""",
(
new_id,
current["purpose"],
current["slot"],
current["guideline_key"],
current["label"],
current["summary"],
current["instruction"],
int(current.get("sort_order") or 0),
STATUS_DRAFT,
int(current.get("revision") or 1) + 1,
current["id"],
current.get("seed_revision") or "",
),
)
return get_guideline(new_id)
def update_guideline(guideline_id: str, body: dict) -> dict:
current = _fetch(guideline_id)
if not current:
raise CatalogError("Ausprägung nicht gefunden.", code="guideline_missing", status_code=404)
if current.get("status") != STATUS_DRAFT:
raise CatalogError(
"Aktive oder archivierte Ausprägungen sind unveränderlich. Bitte klonen.",
code="guideline_immutable",
status_code=409,
)
fields = _write_fields({**current, **body, "guideline_key": body.get("guideline_key") or current["guideline_key"]})
with get_db() as conn:
conn.execute(
"""
UPDATE generation_guidelines
SET guideline_key = ?, label = ?, summary = ?, instruction = ?, sort_order = ?,
updated = datetime('now')
WHERE id = ?
""",
(
fields["guideline_key"],
fields["label"],
fields["summary"],
fields["instruction"],
fields["sort_order"],
guideline_id,
),
)
return get_guideline(guideline_id)
def publish_guideline(guideline_id: str) -> dict:
current = _fetch(guideline_id)
if not current:
raise CatalogError("Ausprägung nicht gefunden.", code="guideline_missing", status_code=404)
if current.get("status") == STATUS_ARCHIVED:
raise CatalogError("Archivierte Ausprägungen können nicht veröffentlicht werden.", code="guideline_archived", status_code=409)
max_instruction, max_label, max_summary = _limits()
_clean_text(current.get("label"), "label", max_chars=max_label)
_clean_text(current.get("summary"), "summary", max_chars=max_summary, allow_empty=True)
_clean_text(current.get("instruction"), "instruction", max_chars=max_instruction)
with get_db() as conn:
conn.execute(
"UPDATE generation_guidelines SET status = ?, updated = datetime('now') WHERE id = ?",
(STATUS_ACTIVE, guideline_id),
)
return get_guideline(guideline_id, include_instruction=False)
def archive_guideline(guideline_id: str) -> dict:
current = _fetch(guideline_id)
if not current:
raise CatalogError("Ausprägung nicht gefunden.", code="guideline_missing", status_code=404)
if current.get("status") == STATUS_DRAFT:
raise CatalogError("Drafts werden gelöscht, nicht archiviert.", code="invalid_generation_guideline", status_code=400)
with get_db() as conn:
remaining = conn.execute(
"""
SELECT COUNT(*) AS n FROM generation_guidelines
WHERE purpose = ? AND slot = ? AND status = ? AND id != ?
""",
(current["purpose"], current["slot"], STATUS_ACTIVE, guideline_id),
).fetchone()["n"]
if int(remaining or 0) < 1:
raise CatalogError(
"Die letzte aktive Ausprägung dieser Dimension kann nicht archiviert werden.",
code="guideline_last_active",
status_code=409,
)
with get_db() as conn:
conn.execute(
"""
UPDATE generation_guidelines
SET status = ?, is_default = 0, updated = datetime('now')
WHERE id = ?
""",
(STATUS_ARCHIVED, guideline_id),
)
return get_guideline(guideline_id, include_instruction=False)
def set_default_guideline(guideline_id: str) -> dict:
current = _fetch(guideline_id)
if not current:
raise CatalogError("Ausprägung nicht gefunden.", code="guideline_missing", status_code=404)
if current.get("status") != STATUS_ACTIVE:
raise CatalogError("Nur aktive Ausprägungen können Standard sein.", code="invalid_generation_guideline", status_code=400)
if current["slot"] not in USER_SLOTS:
raise CatalogError("Nur die vier Gestaltungsdimensionen haben einen Nutzerstandard.", code="invalid_generation_guideline", status_code=400)
with get_db() as conn:
conn.execute(
"""
UPDATE generation_guidelines
SET is_default = CASE WHEN id = ? THEN 1 ELSE 0 END, updated = datetime('now')
WHERE purpose = ? AND slot = ?
""",
(guideline_id, current["purpose"], current["slot"]),
)
return get_guideline(guideline_id, include_instruction=False)
def delete_guideline(guideline_id: str) -> None:
current = _fetch(guideline_id)
if not current:
raise CatalogError("Ausprägung nicht gefunden.", code="guideline_missing", status_code=404)
if current.get("status") != STATUS_DRAFT:
raise CatalogError("Nur unverwendete Drafts können gelöscht werden.", code="guideline_immutable", status_code=409)
if current.get("used_at"):
raise CatalogError("Verwendete Drafts können nicht gelöscht werden.", code="guideline_in_use", status_code=409)
with get_db() as conn:
conn.execute("DELETE FROM generation_guidelines WHERE id = ?", (guideline_id,))
def reset_seed_drafts(purpose: str = PURPOSE_JOURNAL) -> dict:
"""Create new drafts from the current seed. Never overwrite published variants."""
seed = load_seed_document()
created = []
slots = seed.get("slots") or {}
for slot in ALLOWED_SLOTS:
for index, item in enumerate((slots.get(slot) or {}).get("variants") or []):
created.append(
create_guideline(
purpose,
slot,
{
"guideline_key": item.get("guideline_key") or item.get("variant_key"),
"label": item.get("label") or "",
"summary": item.get("summary") or "",
"instruction": item.get("instruction") or "",
"sort_order": index,
},
)
)
overview = overview_payload(purpose)
overview["reset_drafts"] = created
return overview
def preview_selection(selection: dict[str, str], *, purpose: str = PURPOSE_JOURNAL) -> dict:
from engine import load_active_prompt, preview_prompt
compiled = compile_selection(selection, purpose=purpose)
prompt = load_active_prompt("mvp.journal_generate")
assert_journal_prompt_contract(prompt.get("template") or "")
rendered = preview_prompt(
prompt,
{
**compiled.instructions,
"writing_profile": "(nicht enthalten)",
"style_examples": "(nicht enthalten)",
"reconstruction": "(nicht enthalten)",
"existing_text": "",
},
)
assert_template_resolved(rendered.get("rendered") or "")
return {
**compiled.instructions,
"rendered": rendered.get("rendered") or "",
"prompt_slug": prompt.get("slug") or "",
"prompt_revision": prompt.get("seed_revision") or "",
"selection": {
"ids": dict(compiled.ids),
"keys": dict(compiled.keys),
"labels": dict(compiled.labels),
"revisions": dict(compiled.revisions),
},
"seed_revision": compiled.seed_revision,
}
def _seed_rows(seed: dict, purpose: str) -> list[tuple]:
revision = seed.get("seed_revision") or ""
rows = []
slots = seed.get("slots") or {}
for slot in ALLOWED_SLOTS:
variants = (slots.get(slot) or {}).get("variants") or []
for index, item in enumerate(variants):
rows.append(
(
item.get("id") or str(uuid.uuid4()),
purpose,
slot,
item.get("guideline_key") or item.get("variant_key"),
item.get("label") or "",
item.get("summary") or "",
item.get("instruction") or "",
index,
STATUS_ACTIVE,
1,
1 if item.get("is_default") else 0,
1,
item.get("id") or "",
revision,
)
)
return rows
def seed_generation_instructions(conn) -> None:
"""Insert missing seed guidelines. Never overwrite published or admin-created rows."""
seed = load_seed_document()
purpose = seed.get("purpose") or PURPOSE_JOURNAL
conn.execute(
"""
UPDATE generation_guidelines
SET status = ?, is_default = 0, updated = datetime('now')
WHERE purpose = ? AND slot = ? AND status != ?
""",
(STATUS_ARCHIVED, purpose, LEGACY_SOURCE_MODE_SLOT, STATUS_ARCHIVED),
)
existing = {
row["id"]
for row in conn.execute("SELECT id FROM generation_guidelines WHERE purpose = ?", (purpose,)).fetchall()
}
existing_seed_ids = {
row["seed_id"]
for row in conn.execute(
"SELECT seed_id FROM generation_guidelines WHERE purpose = ? AND seed_id != ''",
(purpose,),
).fetchall()
if row["seed_id"]
}
for row in _seed_rows(seed, purpose):
row_id = row[0]
seed_id = row[12]
if row_id in existing or seed_id in existing_seed_ids:
continue
conn.execute(
"""
INSERT INTO generation_guidelines (
id, purpose, slot, guideline_key, label, summary, instruction, sort_order,
status, revision, cloned_from, is_default, is_system_seed, seed_id, seed_revision,
created, updated
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, datetime('now'), datetime('now'))
""",
row,
)
def backfill_missing_settings(conn) -> None:
seed_generation_instructions(conn)
defaults = {}
for slot, key in SLOT_TO_ID_KEY.items():
row = conn.execute(
"""
SELECT id FROM generation_guidelines
WHERE purpose = ? AND slot = ? AND status = ? AND is_default = 1
ORDER BY sort_order
LIMIT 1
""",
(PURPOSE_JOURNAL, slot, STATUS_ACTIVE),
).fetchone()
if not row:
row = conn.execute(
"""
SELECT id FROM generation_guidelines
WHERE purpose = ? AND slot = ? AND status = ?
ORDER BY sort_order
LIMIT 1
""",
(PURPOSE_JOURNAL, slot, STATUS_ACTIVE),
).fetchone()
if row:
defaults[key] = row["id"]
if len(defaults) != 4:
return
conn.execute(
"""
INSERT OR IGNORE INTO journal_generation_selection (
profile_id, transformation_id, detail_id, voice_id, narrative_id, updated
)
SELECT id, ?, ?, ?, ?, datetime('now') FROM profiles
""",
(
defaults["transformation_id"],
defaults["detail_id"],
defaults["voice_id"],
defaults["narrative_id"],
),
)