"""Lädt Verarbeitungs-Profile aus mac-worker/profiles/*.yaml. Ein Profil definiert: - Prompts für Summary und Protocol - DOCX-Layout (Meta-Zeilen + Sektionen mit Typ) Sind keine Profile vorhanden, wirft get() FileNotFoundError beim ersten Zugriff. """ from __future__ import annotations import logging from dataclasses import dataclass, field from pathlib import Path import yaml log = logging.getLogger("mac-worker.profiles") PROFILES_DIR = Path(__file__).resolve().parent.parent.parent / "profiles" DEFAULT_PROFILE_NAME = "meeting" @dataclass class Profile: name: str display_name: str language: str = "de" summary_prompt: str = "" protocol_prompt: str = "" docx: dict = field(default_factory=dict) _cache: dict[str, Profile] = {} def _load() -> dict[str, Profile]: if _cache: return _cache if not PROFILES_DIR.exists(): log.warning("Profile-Verzeichnis %s existiert nicht", PROFILES_DIR) return _cache for f in sorted(PROFILES_DIR.glob("*.yaml")): try: data = yaml.safe_load(f.read_text(encoding="utf-8")) or {} p = Profile( name=data["name"], display_name=data.get("display_name", data["name"]), language=data.get("language", "de"), summary_prompt=data.get("summary_prompt", ""), protocol_prompt=data.get("protocol_prompt", ""), docx=data.get("docx") or {}, ) _cache[p.name] = p log.info("Profil geladen: %s (%s)", p.name, p.display_name) except Exception as e: # noqa: BLE001 log.error("Profil %s konnte nicht geladen werden: %s", f.name, e) return _cache def list_all() -> list[dict]: """Zur Auslieferung über /api/profiles.""" return [ {"name": p.name, "display_name": p.display_name, "language": p.language} for p in _load().values() ] def get(name: str | None) -> Profile: profiles = _load() if not profiles: raise FileNotFoundError( f"Keine Profile gefunden in {PROFILES_DIR} — bitte mind. eine YAML-Datei anlegen." ) if name and name in profiles: return profiles[name] if DEFAULT_PROFILE_NAME in profiles: return profiles[DEFAULT_PROFILE_NAME] return next(iter(profiles.values())) def reload() -> None: """Cache leeren (z. B. für Tests oder Hot-Reload via SIGHUP).""" _cache.clear() # Erwartete Platzhalter pro Prompt — Vorhandensein wird beim Start gewarnt # (fehlt nicht-fatal, weil ein Profil bewusst weniger nutzen darf). _REQUIRED_VARS = { "summary_prompt": ["title", "transcript"], "protocol_prompt": ["title", "transcript", "summary_json"], } def validate_all() -> list[str]: """Beim Worker-Start aufrufen: prüft jedes Profil auf grundlegende Lesbarkeit und Platzhalter-Vorkommen. Gibt eine Liste von Warnungen zurück (defekte Profile bleiben dennoch gelistet — Aufruf scheitert dann zur Laufzeit). """ warnings: list[str] = [] for p in _load().values(): for field_name, required in _REQUIRED_VARS.items(): template = getattr(p, field_name, "") if not template: continue for var in required: placeholder = "{" + var + "}" if placeholder not in template: msg = f"Profil '{p.name}': {field_name} enthält keinen {placeholder}" warnings.append(msg) log.warning(msg) return warnings