Backend: GenContext, run_single_slot, generisches Check→Fix-Muster (4 Stellen)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
team3
2026-06-12 07:58:52 +02:00
parent c0b7d236bb
commit 0b4a086e89

View File

@@ -6,8 +6,10 @@ import shutil
import subprocess
import re
import uuid
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Callable
from agents import run_agent, kill_process
from config import (
@@ -185,6 +187,82 @@ async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout:
await asyncio.gather(*tasks.keys(), return_exceptions=True)
@dataclass
class GenContext:
"""Durchgereichte Pipeline-Parameter — erspart lange Argument-Signaturen."""
topic: str
provider: str
is_cancelled: Callable[[], bool]
guide_id: str | None = None
# Ergebnis-Status von run_single_slot/_check_then_fix
OK, CANCELLED, FAILED = "ok", "cancelled", "failed"
async def run_single_slot(
ctx: GenContext, label: str, *,
key: str, prompt: str, role: str, capabilities: str, payload, timeout: int,
) -> tuple[str, object]:
"""Ein Agent, ein gültiges Ergebnis (Race mit Quorum 1).
→ (OK, wert) | (CANCELLED, None) | (FAILED, None)
"""
slots = [{"key": key, "prompt": prompt, "role": role, "capabilities": capabilities, "payload": payload}]
res = await _race(ctx.topic, label, slots, 1, timeout, ctx.provider, cancelled=ctx.is_cancelled)
if ctx.is_cancelled():
return CANCELLED, None
if res is None:
return FAILED, None
return OK, res[0]
async def _check_then_fix(
ctx: GenContext, *, name: str, step: int,
check_key: str, check_prompt: str, check_path: Path, check_timeout: int,
fix_key: str, build_fix_prompt, fix_payload, fix_timeout: int,
fix_role: str = "fast", fix_caps: str = "files",
on_fix_invalid=None,
) -> tuple[str, object]:
"""Check→Fix-Muster: Prüf-Agent notiert Probleme (JSON), Fix-Agent behebt sie.
Resume: existierende Check-Datei überspringt den ganzen Schritt.
Check ist fatal (FAILED), Fix nicht — Original bleibt; on_fix_invalid kann
das kanonische Original zurückschreiben, falls der Fix-Agent die
Artefakt-Datei zerschrieben hat.
Lese-Check (Multi-Slot, Section-genau) und Bausteine-Auswahl-Check
(Patch-Semantik) passen bewusst NICHT in dieses Muster.
→ (OK, neues_artefakt | None=unverändert) | (CANCELLED, None) | (FAILED, None)
"""
if check_path.exists():
return OK, None
await _set_step(ctx.guide_id, step, f"Prüfe {name}")
status, probleme = await run_single_slot(
ctx, f"{name}-Prüfung", key=check_key, prompt=check_prompt,
role="fast", capabilities="files",
payload=lambda result: _probleme_schema(_json_datei(check_path)),
timeout=check_timeout,
)
if status != OK:
return status, None
if not probleme:
return OK, None
_log(ctx.topic, f"{name}-Prüfung: {len(probleme)} Problem(e) notiert")
await _set_step(ctx.guide_id, step, f"Passe {name} an…")
status, fixed = await run_single_slot(
ctx, f"{name}-Fix", key=fix_key, prompt=build_fix_prompt(probleme),
role=fix_role, capabilities=fix_caps, payload=fix_payload, timeout=fix_timeout,
)
if status == CANCELLED:
return CANCELLED, None
if status == FAILED:
_log(ctx.topic, f"{name}-Fix ungültig — Original bleibt")
if on_fix_invalid:
on_fix_invalid()
return OK, None
return OK, fixed
# --- Bausteine-Pipeline: 4x Recherche (3) → 2x Auswahl (1) → Prüfung — reines Inventar, unsortiert ---
_bausteine_progress: dict[str, str] = {}
@@ -426,6 +504,8 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
def abgebrochen() -> None:
_bausteine_errors[topic] = "Abgebrochen — Fortschritt bleibt erhalten"
ctx = GenContext(topic=topic, provider=provider, is_cancelled=is_cancelled)
try:
async with _semaphore:
files["arbeit"].mkdir(parents=True, exist_ok=True)
@@ -508,20 +588,21 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
f"### Recherche {i}\n" + "\n".join(f"- {_titel(t)}" for t in _parse_auswahl(text).values())
for i, text in enumerate(recherchen, 1)
)
slots = [{
"key": f"bausteine-{topic}-auswahlcheck-1",
"prompt": _prompt("Bausteine-Auswahl-Check", topic=topic, results=titel_listen, auswahl=flat, out_path=check_path),
"role": "fast", "capabilities": "files",
"payload": (lambda result: _auswahl_check_schema(_json_datei(check_path))),
}]
checks = await _race(topic, "Auswahl-Check", slots, 1, _timeout("auswahl_check", len(entries)), provider, cancelled=is_cancelled)
if is_cancelled():
status, check = await run_single_slot(
ctx, "Auswahl-Check",
key=f"bausteine-{topic}-auswahlcheck-1",
prompt=_prompt("Bausteine-Auswahl-Check", topic=topic, results=titel_listen, auswahl=flat, out_path=check_path),
role="fast", capabilities="files",
payload=lambda result: _auswahl_check_schema(_json_datei(check_path)),
timeout=_timeout("auswahl_check", len(entries)),
)
if status == CANCELLED:
abgebrochen()
return
if checks is None:
if status == FAILED:
_log(topic, "Auswahl-Check fehlgeschlagen — fahre ohne Korrekturen fort")
else:
patch = checks[0]
patch = check
if patch is not None and (patch["streichen"] or patch["nachtraege"]):
idx = _titel_index(entries)
weg = {num for t in patch["streichen"] if (num := _titel_aufloesen(idx, t)) is not None}
@@ -541,24 +622,24 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
ergaenzungen = _ergaenzung_schema(_json_datei(erg_path))
if ergaenzungen is None:
erg_path.unlink(missing_ok=True)
slots = [{
"key": f"bausteine-{topic}-ergaenzung-1",
"prompt": _prompt(
status, ergaenzungen = await run_single_slot(
ctx, "Ergänzung",
key=f"bausteine-{topic}-ergaenzung-1",
prompt=_prompt(
"Bausteine-Ergaenzung",
topic=topic, bausteine="\n".join(f"- {t}" for t in entries.values()),
out_path=erg_path, extra=_extra(instructions),
),
"role": "quick", "capabilities": "full",
"payload": (lambda result: _ergaenzung_schema(_json_datei(erg_path))),
}]
res = await _race(topic, "Ergänzung", slots, 1, _timeout("ergaenzung"), provider, cancelled=is_cancelled)
if is_cancelled():
role="quick", capabilities="full",
payload=lambda result: _ergaenzung_schema(_json_datei(erg_path)),
timeout=_timeout("ergaenzung"),
)
if status == CANCELLED:
abgebrochen()
return
if res is None:
if status == FAILED:
_bausteine_errors[topic] = "Ergänzung fehlgeschlagen (kein gültiges Ergebnis)"
return
ergaenzungen = res[0]
idx = _titel_index(entries)
neu = [(t, b) for t, b in ergaenzungen if _titel_aufloesen(idx, t) is None]
if neu:
@@ -763,8 +844,7 @@ async def _generate_onepager(
guide_id: str, topic: str, instructions: str, provider: str,
project: Path | None, content_path: Path,
) -> list[dict] | None:
def is_cancelled() -> bool:
return guide_id in _cancelled
ctx = GenContext(topic=topic, provider=provider, is_cancelled=lambda: guide_id in _cancelled, guide_id=guide_id)
# 3×3-Raster: 7 Karten mit festen Schlüsseln (Reihenfolge = Lesereihenfolge mobil)
KARTEN_KEYS = ("info", "eigenschaften", "beispiel", "zusammenhaenge", "voraussetzungen", "modern", "veraltet")
@@ -814,118 +894,91 @@ async def _generate_onepager(
recherche = recherche_payload()
if recherche is None:
await _set_step(guide_id, 0, "Recherchiere…")
slots = [{
"key": f"{guide_id}-recherche",
"prompt": _prompt(recherche_template, topic=topic, source=source, out_path=recherche_path, extra=_extra(instructions)),
"role": "quick", "capabilities": "files" if project else "full",
"payload": recherche_payload,
}]
res = await _race(topic, "OnePager-Recherche", slots, 1, _timeout("onepager_recherche"), provider, cancelled=is_cancelled)
if is_cancelled():
status, recherche = await run_single_slot(
ctx, "OnePager-Recherche",
key=f"{guide_id}-recherche",
prompt=_prompt(recherche_template, topic=topic, source=source, out_path=recherche_path, extra=_extra(instructions)),
role="quick", capabilities="files" if project else "full",
payload=recherche_payload, timeout=_timeout("onepager_recherche"),
)
if status == CANCELLED:
return None
if res is None:
if status == FAILED:
await _fail(guide_id, "OnePager-Recherche fehlgeschlagen")
return None
recherche = res[0]
# Schritt 2: Recherche-Prüfung — notiert Probleme; Anpassung macht ein Recherche-Agent
if not recherche_check_path.exists():
await _set_step(guide_id, 1, "Prüfe Recherche")
slots = [{
"key": f"{guide_id}-recherche-check",
"prompt": _prompt(recherche_check_template, topic=topic, recherche=recherche, out_path=recherche_check_path),
"role": "fast", "capabilities": "files",
"payload": (lambda result: _probleme_schema(_json_datei(recherche_check_path))),
}]
res = await _race(topic, "Recherche-Prüfung", slots, 1, _timeout("onepager_verify"), provider, cancelled=is_cancelled)
if is_cancelled():
return None
if res is None:
await _fail(guide_id, "Recherche-Prüfung fehlgeschlagen")
return None
probleme = res[0]
if probleme:
_log(topic, f"Recherche-Prüfung: {len(probleme)} Problem(e) notiert")
await _set_step(guide_id, 1, "Passe Recherche an…")
slots = [{
"key": f"{guide_id}-recherche-fix",
"prompt": _prompt(
"OnePager-Recherche-Fix",
topic=topic, source=source, recherche=recherche,
probleme="\n".join(f"- {p}" for p in probleme),
out_path=recherche_path, extra=_extra(instructions),
),
"role": "quick", "capabilities": "files" if project else "full",
"payload": recherche_payload,
}]
res = await _race(topic, "Recherche-Fix", slots, 1, _timeout("onepager_recherche"), provider, cancelled=is_cancelled)
if is_cancelled():
return None
if res is None:
_log(topic, "Recherche-Fix ungültig — ursprüngliche Recherche bleibt")
else:
recherche = res[0]
status, fixed = await _check_then_fix(
ctx, name="Recherche", step=1,
check_key=f"{guide_id}-recherche-check",
check_prompt=_prompt(recherche_check_template, topic=topic, recherche=recherche, out_path=recherche_check_path),
check_path=recherche_check_path, check_timeout=_timeout("onepager_verify"),
fix_key=f"{guide_id}-recherche-fix",
build_fix_prompt=lambda probleme: _prompt(
"OnePager-Recherche-Fix",
topic=topic, source=source, recherche=recherche,
probleme="\n".join(f"- {p}" for p in probleme),
out_path=recherche_path, extra=_extra(instructions),
),
fix_payload=recherche_payload, fix_timeout=_timeout("onepager_recherche"),
fix_role="quick", fix_caps="files" if project else "full",
)
if status == CANCELLED:
return None
if status == FAILED:
await _fail(guide_id, "Recherche-Prüfung fehlgeschlagen")
return None
if fixed is not None:
recherche = fixed
# Schritt 3: Bauen — Karten nur aus der Faktenbasis (Resume: gültige Datei wird übernommen)
karten = karten_schema(_json_datei(karten_path))
if karten is None:
await _set_step(guide_id, 2, "Baue OnePager…")
karten_path.unlink(missing_ok=True)
slots = [{
"key": f"{guide_id}-bauen",
"prompt": _prompt("OnePager-Bauen", topic=topic, recherche=recherche, out_path=karten_path, extra=_extra(instructions)),
"role": "fast", "capabilities": "files",
"payload": (lambda result: karten_schema(_json_datei(karten_path))),
}]
res = await _race(topic, "OnePager-Bauen", slots, 1, _timeout("onepager_bauen"), provider, cancelled=is_cancelled)
if is_cancelled():
status, karten = await run_single_slot(
ctx, "OnePager-Bauen",
key=f"{guide_id}-bauen",
prompt=_prompt("OnePager-Bauen", topic=topic, recherche=recherche, out_path=karten_path, extra=_extra(instructions)),
role="fast", capabilities="files",
payload=lambda result: karten_schema(_json_datei(karten_path)),
timeout=_timeout("onepager_bauen"),
)
if status == CANCELLED:
return None
if res is None:
if status == FAILED:
await _fail(guide_id, "OnePager-Bau fehlgeschlagen")
return None
karten = res[0]
def karten_block() -> str:
return "\n\n".join(f"### {k['titel']} [{k['key']}]\n{k['md']}" for k in karten)
# Schritt 4: Prüfung — notiert Probleme; Anpassung macht ein Bauen-Agent
if not check_path.exists():
await _set_step(guide_id, 3, "Prüfe OnePager")
slots = [{
"key": f"{guide_id}-verify",
"prompt": _prompt("OnePager-Verifikation", topic=topic, recherche=recherche, karten=karten_block(), out_path=check_path),
"role": "fast", "capabilities": "files",
"payload": (lambda result: _probleme_schema(_json_datei(check_path))),
}]
res = await _race(topic, "OnePager-Prüfung", slots, 1, _timeout("onepager_verify"), provider, cancelled=is_cancelled)
if is_cancelled():
return None
if res is None:
await _fail(guide_id, "OnePager-Prüfung fehlgeschlagen")
return None
probleme = res[0]
if probleme:
_log(topic, f"OnePager-Prüfung: {len(probleme)} Problem(e) notiert")
await _set_step(guide_id, 3, "Passe OnePager an…")
slots = [{
"key": f"{guide_id}-karten-fix",
"prompt": _prompt(
"OnePager-Fix",
topic=topic, recherche=recherche, karten=karten_block(),
probleme="\n".join(f"- {p}" for p in probleme),
out_path=karten_path, extra=_extra(instructions),
),
"role": "fast", "capabilities": "files",
"payload": (lambda result: karten_schema(_json_datei(karten_path))),
}]
res = await _race(topic, "OnePager-Fix", slots, 1, _timeout("onepager_bauen"), provider, cancelled=is_cancelled)
if is_cancelled():
return None
if res is None:
_log(topic, "OnePager-Fix ungültig — ursprüngliche Karten bleiben")
atomic_write_json(karten_path, {"karten": {k["key"]: {"titel": k["titel"], "md": k["md"]} for k in karten}})
else:
karten = res[0]
status, fixed = await _check_then_fix(
ctx, name="OnePager", step=3,
check_key=f"{guide_id}-verify",
check_prompt=_prompt("OnePager-Verifikation", topic=topic, recherche=recherche, karten=karten_block(), out_path=check_path),
check_path=check_path, check_timeout=_timeout("onepager_verify"),
fix_key=f"{guide_id}-karten-fix",
build_fix_prompt=lambda probleme: _prompt(
"OnePager-Fix",
topic=topic, recherche=recherche, karten=karten_block(),
probleme="\n".join(f"- {p}" for p in probleme),
out_path=karten_path, extra=_extra(instructions),
),
fix_payload=lambda result: karten_schema(_json_datei(karten_path)),
fix_timeout=_timeout("onepager_bauen"),
on_fix_invalid=lambda: atomic_write_json(
karten_path, {"karten": {k["key"]: {"titel": k["titel"], "md": k["md"]} for k in karten}},
),
)
if status == CANCELLED:
return None
if status == FAILED:
await _fail(guide_id, "OnePager-Prüfung fehlgeschlagen")
return None
if fixed is not None:
karten = fixed
sections = [
{"num": i, "title": k["titel"], "md": k["md"], "key": k["key"]}
@@ -942,6 +995,7 @@ async def _generate_sections(
def is_cancelled() -> bool:
return guide_id in _cancelled
ctx = GenContext(topic=topic, provider=provider, is_cancelled=is_cancelled, guide_id=guide_id)
spec = (TEMPLATES_DIR / "Format" / "Section.md").read_text(encoding="utf-8")
files = _guide_files(content_path)
bausteine_liste = "\n".join(f"- {t}" for t in entries.values())
@@ -959,23 +1013,23 @@ async def _generate_sections(
if auswahl is None:
await _set_step(guide_id, 0, "Wähle Bausteine…")
files["auswahl"].unlink(missing_ok=True)
slots = [{
"key": f"{guide_id}-auswahl",
"prompt": _prompt(
status, auswahl = await run_single_slot(
ctx, "Guide-Auswahl",
key=f"{guide_id}-auswahl",
prompt=_prompt(
"Guide-Auswahl",
topic=topic, format_name=format_name, bausteine=bausteine_liste,
auswahl_auftrag=auswahl_auftrag, out_path=files["auswahl"], extra=_extra(instructions),
),
"role": "guide", "capabilities": "files",
"payload": (lambda result: _resolve_auswahl(_json_datei(files["auswahl"]), entries, k_min, k_max)),
}]
res = await _race(topic, "Guide-Auswahl", slots, 1, _timeout("guide_auswahl", n), provider, cancelled=is_cancelled)
if is_cancelled():
role="guide", capabilities="files",
payload=lambda result: _resolve_auswahl(_json_datei(files["auswahl"]), entries, k_min, k_max),
timeout=_timeout("guide_auswahl", n),
)
if status == CANCELLED:
return None
if res is None:
if status == FAILED:
await _fail(guide_id, "Auswahl fehlgeschlagen")
return None
auswahl = res[0]
def auswahl_titel() -> str:
return "\n".join(f"- {_titel(entries[num])}" for num in auswahl)
@@ -984,49 +1038,35 @@ async def _generate_sections(
return json.dumps({"bausteine": [_titel(entries[num]) for num in auswahl]}, ensure_ascii=False)
# Schritt 2: Auswahl-Prüfung — notiert Probleme; Anpassung macht ein Auswahl-Agent
if not files["auswahl_check"].exists():
await _set_step(guide_id, 1, "Prüfe Auswahl")
slots = [{
"key": f"{guide_id}-auswahl-check",
"prompt": _prompt(
"Guide-Auswahl-Check",
topic=topic, format_name=format_name, auswahl_auftrag=auswahl_auftrag,
bausteine=bausteine_liste, auswahl=auswahl_titel(),
out_path=files["auswahl_check"], extra=_extra(instructions),
),
"role": "fast", "capabilities": "files",
"payload": (lambda result: _probleme_schema(_json_datei(files["auswahl_check"]))),
}]
res = await _race(topic, "Auswahl-Prüfung", slots, 1, _timeout("guide_check", len(auswahl)), provider, cancelled=is_cancelled)
if is_cancelled():
return None
if res is None:
await _fail(guide_id, "Auswahl-Prüfung fehlgeschlagen")
return None
probleme = res[0]
if probleme:
_log(topic, f"Auswahl-Prüfung: {len(probleme)} Problem(e) notiert")
await _set_step(guide_id, 1, "Passe Auswahl an…")
slots = [{
"key": f"{guide_id}-auswahl-fix",
"prompt": _prompt(
"Guide-Auswahl-Fix",
topic=topic, format_name=format_name, auswahl_auftrag=auswahl_auftrag,
bausteine=bausteine_liste, auswahl=auswahl_titel(),
probleme="\n".join(f"- {p}" for p in probleme),
out_path=files["auswahl"], extra=_extra(instructions),
),
"role": "guide", "capabilities": "files",
"payload": (lambda result: _resolve_auswahl(_json_datei(files["auswahl"]), entries, k_min, k_max)),
}]
res = await _race(topic, "Auswahl-Fix", slots, 1, _timeout("guide_auswahl", n), provider, cancelled=is_cancelled)
if is_cancelled():
return None
if res is None:
_log(topic, "Auswahl-Fix ungültig — ursprüngliche Auswahl bleibt")
atomic_write_text(files["auswahl"], auswahl_json())
else:
auswahl = res[0]
status, fixed = await _check_then_fix(
ctx, name="Auswahl", step=1,
check_key=f"{guide_id}-auswahl-check",
check_prompt=_prompt(
"Guide-Auswahl-Check",
topic=topic, format_name=format_name, auswahl_auftrag=auswahl_auftrag,
bausteine=bausteine_liste, auswahl=auswahl_titel(),
out_path=files["auswahl_check"], extra=_extra(instructions),
),
check_path=files["auswahl_check"], check_timeout=_timeout("guide_check", len(auswahl)),
fix_key=f"{guide_id}-auswahl-fix",
build_fix_prompt=lambda probleme: _prompt(
"Guide-Auswahl-Fix",
topic=topic, format_name=format_name, auswahl_auftrag=auswahl_auftrag,
bausteine=bausteine_liste, auswahl=auswahl_titel(),
probleme="\n".join(f"- {p}" for p in probleme),
out_path=files["auswahl"], extra=_extra(instructions),
),
fix_payload=lambda result: _resolve_auswahl(_json_datei(files["auswahl"]), entries, k_min, k_max),
fix_timeout=_timeout("guide_auswahl", n), fix_role="guide",
on_fix_invalid=lambda: atomic_write_text(files["auswahl"], auswahl_json()),
)
if status == CANCELLED:
return None
if status == FAILED:
await _fail(guide_id, "Auswahl-Prüfung fehlgeschlagen")
return None
if fixed is not None:
auswahl = fixed
sel_entries = {num: entries[num] for num in auswahl}
soll = len(sel_entries)
@@ -1037,23 +1077,23 @@ async def _generate_sections(
if plan is None:
await _set_step(guide_id, 2, "Plane Gliederung…")
files["gliederung"].unlink(missing_ok=True)
slots = [{
"key": f"{guide_id}-gliederung",
"prompt": _prompt(
status, plan = await run_single_slot(
ctx, "Gliederung",
key=f"{guide_id}-gliederung",
prompt=_prompt(
"Guide-Gliederung",
topic=topic, format_name=format_name, bausteine=sel_liste,
out_path=files["gliederung"], extra=_extra(instructions),
),
"role": "guide", "capabilities": "files",
"payload": (lambda result: _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll)),
}]
res = await _race(topic, "Gliederung", slots, 1, _timeout("plan", soll), provider, cancelled=is_cancelled)
if is_cancelled():
role="guide", capabilities="files",
payload=lambda result: _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll),
timeout=_timeout("plan", soll),
)
if status == CANCELLED:
return None
if res is None:
if status == FAILED:
await _fail(guide_id, "Gliederung fehlgeschlagen")
return None
plan = res[0]
def gliederung_text() -> str:
return "\n".join(_zuteilung_text([ch], {num: _titel(entries[num]) for num in ch["nums"]}) for ch in plan)
@@ -1065,49 +1105,35 @@ async def _generate_sections(
)
# Schritt 4: Gliederungs-Prüfung
if not files["gliederung_check"].exists():
await _set_step(guide_id, 3, "Prüfe Gliederung")
slots = [{
"key": f"{guide_id}-gliederung-check",
"prompt": _prompt(
"Guide-Gliederung-Check",
topic=topic, format_name=format_name, zweck=zweck,
auswahl=auswahl_titel(), gliederung=gliederung_text(),
out_path=files["gliederung_check"], extra=_extra(instructions),
),
"role": "fast", "capabilities": "files",
"payload": (lambda result: _probleme_schema(_json_datei(files["gliederung_check"]))),
}]
res = await _race(topic, "Gliederungs-Prüfung", slots, 1, _timeout("guide_check", soll), provider, cancelled=is_cancelled)
if is_cancelled():
return None
if res is None:
await _fail(guide_id, "Gliederungs-Prüfung fehlgeschlagen")
return None
probleme = res[0]
if probleme:
_log(topic, f"Gliederungs-Prüfung: {len(probleme)} Problem(e) notiert")
await _set_step(guide_id, 3, "Passe Gliederung an…")
slots = [{
"key": f"{guide_id}-gliederung-fix",
"prompt": _prompt(
"Guide-Gliederung-Fix",
topic=topic, format_name=format_name,
auswahl=auswahl_titel(), gliederung=gliederung_text(),
probleme="\n".join(f"- {p}" for p in probleme),
out_path=files["gliederung"], extra=_extra(instructions),
),
"role": "guide", "capabilities": "files",
"payload": (lambda result: _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll)),
}]
res = await _race(topic, "Gliederungs-Fix", slots, 1, _timeout("plan", soll), provider, cancelled=is_cancelled)
if is_cancelled():
return None
if res is None:
_log(topic, "Gliederungs-Fix ungültig — ursprüngliche Gliederung bleibt")
atomic_write_text(files["gliederung"], gliederung_json())
else:
plan = res[0]
status, fixed = await _check_then_fix(
ctx, name="Gliederung", step=3,
check_key=f"{guide_id}-gliederung-check",
check_prompt=_prompt(
"Guide-Gliederung-Check",
topic=topic, format_name=format_name, zweck=zweck,
auswahl=auswahl_titel(), gliederung=gliederung_text(),
out_path=files["gliederung_check"], extra=_extra(instructions),
),
check_path=files["gliederung_check"], check_timeout=_timeout("guide_check", soll),
fix_key=f"{guide_id}-gliederung-fix",
build_fix_prompt=lambda probleme: _prompt(
"Guide-Gliederung-Fix",
topic=topic, format_name=format_name,
auswahl=auswahl_titel(), gliederung=gliederung_text(),
probleme="\n".join(f"- {p}" for p in probleme),
out_path=files["gliederung"], extra=_extra(instructions),
),
fix_payload=lambda result: _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll),
fix_timeout=_timeout("plan", soll), fix_role="guide",
on_fix_invalid=lambda: atomic_write_text(files["gliederung"], gliederung_json()),
)
if status == CANCELLED:
return None
if status == FAILED:
await _fail(guide_id, "Gliederungs-Prüfung fehlgeschlagen")
return None
if fixed is not None:
plan = fixed
# Schritt 5: Schreiben — vorhandene Chunk-Dateien werden übernommen (Resume)
total_sections = sum(len(c["nums"]) for c in plan)