Bug: 9 von 17 Profilen (phone-call, brainstorm, one-on-one, postmortem,
schichtuebergabe, pflegedoku, visite, biografie) zeigen in ihren Prompts
JSON-Schema-Beispiele wie {"owner", "task"}. Python's str.format() liest
diese geschweiften Klammern als Platzhalter und wirft KeyError →
HTTP 500 vom Mac-Worker → Job FAILED bei summarize/protocol.
Fix:
- Neuer render(template, **vars) Helper in ollama_client: ersetzt nur
explizit benannte Platzhalter, lässt alles andere literal stehen.
- summarize/make_protocol nutzen diesen Helper statt .format()
- profiles.validate_all() warnt beim Worker-Start, wenn ein erwarteter
Platzhalter fehlt — Defekte werden früh sichtbar
- Mac-Worker-Middleware fängt unhandled exceptions und liefert JSON-Body
mit Type+Message+Traceback-Tail (statt generic "Internal Server Error")
- LXC MacClient liest diesen Error-Body und packt ihn in die HTTPStatusError-
Message → landet im Job-error-Feld + Diagnose-Panel mit echter Wurzel-Info
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
113 lines
3.5 KiB
Python
113 lines
3.5 KiB
Python
"""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
|