fix(profiles): KeyError bei JSON-Beispielen in Prompt-Templates

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>
This commit is contained in:
root
2026-05-15 06:55:55 +00:00
co-authored by Claude Opus 4.7
parent bc497a1665
commit 031d52c77a
4 changed files with 91 additions and 7 deletions
+17 -2
View File
@@ -39,6 +39,19 @@ def _truncate(text: str, max_chars: int = 60000) -> str:
return head + "\n\n[... gekürzt ...]\n\n" + tail
def render(template: str, **vars: str) -> str:
"""Ersetzt ausschließlich die übergebenen `{name}`-Platzhalter.
Bewusst KEIN str.format() — sonst würden literale `{...}` im Template
(z. B. JSON-Schema-Beispiele wie `{"owner", "task"}` in den Profil-Prompts)
als Platzhalter interpretiert und einen KeyError werfen.
"""
out = template
for k, v in vars.items():
out = out.replace("{" + k + "}", str(v))
return out
async def _chat(prompt: str) -> str:
"""Ruft Ollama im Streaming-Modus auf.
@@ -89,7 +102,8 @@ async def _chat(prompt: str) -> str:
async def summarize(transcript: str, title: str = "", profile_name: str | None = None) -> dict[str, Any]:
profile = profiles.get(profile_name)
prompt = profile.summary_prompt.format(
prompt = render(
profile.summary_prompt,
title=title or "(ohne Titel)",
transcript=_truncate(transcript),
)
@@ -106,7 +120,8 @@ async def make_protocol(
transcript: str, summary: dict, title: str = "", profile_name: str | None = None
) -> dict:
profile = profiles.get(profile_name)
prompt = profile.protocol_prompt.format(
prompt = render(
profile.protocol_prompt,
title=title or "(ohne Titel)",
summary_json=json.dumps(summary, ensure_ascii=False),
transcript=_truncate(transcript),
+28
View File
@@ -82,3 +82,31 @@ def get(name: str | None) -> Profile:
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