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:
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user