""" Mustache-aehnliche Platzhalter {{schluessel}} fuer KI-Templates aus ai_prompts. Kein Vereinsbezug — reine Textersetzung; Aufrufe aus exercise_ai und Admin-Vorschau. """ from __future__ import annotations import re from dataclasses import dataclass from typing import Dict, List, Mapping _PLACEHOLDER_RE = re.compile(r"\{\{\s*([a-zA-Z0-9_]+)\s*\}\}") def _placeholder_pattern_for_key(key: str) -> re.Pattern[str]: return re.compile(r"\{\{\s*" + re.escape(str(key).strip()) + r"\s*\}\}") @dataclass(frozen=True) class MustacheRenderResult: """Ergebnis von render_mustache_template.""" text: str keys_in_template: List[str] keys_substituted: List[str] keys_missing_variables: List[str] placeholders_remaining: List[str] def extract_mustache_keys(template: str) -> List[str]: """Platzhalter-Namen in Vorkommensreihenfolge, ohne erstes Duplikat.""" seen: set[str] = set() ordered: List[str] = [] for m in _PLACEHOLDER_RE.finditer(template or ""): k = str(m.group(1) or "").strip() if not k or k in seen: continue seen.add(k) ordered.append(k) return ordered def render_mustache_template(template: str, variables: Mapping[str, str]) -> MustacheRenderResult: """ Ersetzt {{keys}} durch die passenden Strings. Variablen, die fuer einen im Template genutzten Key fehlen, werden als Leerstring ersetzt. Rueckgabe-liste keys_missing_variables: Keys, die im Template vorkommen, aber nicht als Map-Keys uebergeben wurden (oder None-Wert entsprachen Leerung). """ tpl_in = template or "" vars_norm: Dict[str, str] = {} for k, v in variables.items(): vars_norm[str(k)] = "" if v is None else str(v) keys_in = extract_mustache_keys(tpl_in) missing_known: List[str] = [] out = tpl_in substituted: List[str] = [] for key in keys_in: pat = _placeholder_pattern_for_key(key) repl = vars_norm.get(key) if key not in vars_norm: missing_known.append(key) repl = "" substituted.append(key) out = pat.sub(repl, out) still = extract_mustache_keys(out) return MustacheRenderResult( text=out, keys_in_template=keys_in, keys_substituted=substituted, keys_missing_variables=missing_known, placeholders_remaining=still, ) def exercise_placeholder_catalog() -> dict: """ Statischer Platzhalter-Katalog fuer Uebungs-KI-Templates — deckt aktuelle Seeds ab. (Erweiterung andere Kontexte: matrix/import folgen separat.) """ defs = [ { "key": "exercise_title", "placeholder": "{{exercise_title}}", "description": "Titel der Uebung (oder Platzhalter, wenn leer).", "used_by_slugs": ["exercise_summary", "exercise_skill_suggestions", "exercise_instruction_rewrite"], }, { "key": "exercise_focus_area", "placeholder": "{{exercise_focus_area}}", "description": "Fokuskontext (Text-Hinweis aus Formular, optional).", "used_by_slugs": ["exercise_summary", "exercise_skill_suggestions", "exercise_instruction_rewrite"], }, { "key": "exercise_goal", "placeholder": "{{exercise_goal}}", "description": "Ziel aus dem Formular, als Plaintext ohne HTML-Zeichen.", "used_by_slugs": ["exercise_summary", "exercise_skill_suggestions", "exercise_instruction_rewrite"], }, { "key": "exercise_execution", "placeholder": "{{exercise_execution}}", "description": "Durchfuehrung als Plaintext ohne HTML-Zeichen.", "used_by_slugs": ["exercise_summary", "exercise_skill_suggestions", "exercise_instruction_rewrite"], }, { "key": "exercise_preparation", "placeholder": "{{exercise_preparation}}", "description": "Vorbereitung/Aufbau als Plaintext ohne HTML.", "used_by_slugs": ["exercise_instruction_rewrite"], }, { "key": "exercise_trainer_notes", "placeholder": "{{exercise_trainer_notes}}", "description": "Trainer-Hinweise als Plaintext ohne HTML.", "used_by_slugs": ["exercise_instruction_rewrite"], }, { "key": "skills_catalog", "placeholder": "{{skills_catalog}}", "description": ( "Gewichtete, kontextbezogene Liste aus dem Skill-Katalog (retrieval_profiles). " "Nur fuer exercise_skill_suggestions." ), "used_by_slugs": ["exercise_skill_suggestions"], }, ] return {"context": "exercise", "placeholders": defs} __all__ = [ "MustacheRenderResult", "exercise_placeholder_catalog", "extract_mustache_keys", "render_mustache_template", ]