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 subprocess
import re import re
import uuid import uuid
from dataclasses import dataclass
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Callable
from agents import run_agent, kill_process from agents import run_agent, kill_process
from config import ( 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) 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-Pipeline: 4x Recherche (3) → 2x Auswahl (1) → Prüfung — reines Inventar, unsortiert ---
_bausteine_progress: dict[str, str] = {} _bausteine_progress: dict[str, str] = {}
@@ -426,6 +504,8 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
def abgebrochen() -> None: def abgebrochen() -> None:
_bausteine_errors[topic] = "Abgebrochen — Fortschritt bleibt erhalten" _bausteine_errors[topic] = "Abgebrochen — Fortschritt bleibt erhalten"
ctx = GenContext(topic=topic, provider=provider, is_cancelled=is_cancelled)
try: try:
async with _semaphore: async with _semaphore:
files["arbeit"].mkdir(parents=True, exist_ok=True) 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()) f"### Recherche {i}\n" + "\n".join(f"- {_titel(t)}" for t in _parse_auswahl(text).values())
for i, text in enumerate(recherchen, 1) for i, text in enumerate(recherchen, 1)
) )
slots = [{ status, check = await run_single_slot(
"key": f"bausteine-{topic}-auswahlcheck-1", ctx, "Auswahl-Check",
"prompt": _prompt("Bausteine-Auswahl-Check", topic=topic, results=titel_listen, auswahl=flat, out_path=check_path), key=f"bausteine-{topic}-auswahlcheck-1",
"role": "fast", "capabilities": "files", prompt=_prompt("Bausteine-Auswahl-Check", topic=topic, results=titel_listen, auswahl=flat, out_path=check_path),
"payload": (lambda result: _auswahl_check_schema(_json_datei(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) timeout=_timeout("auswahl_check", len(entries)),
if is_cancelled(): )
if status == CANCELLED:
abgebrochen() abgebrochen()
return return
if checks is None: if status == FAILED:
_log(topic, "Auswahl-Check fehlgeschlagen — fahre ohne Korrekturen fort") _log(topic, "Auswahl-Check fehlgeschlagen — fahre ohne Korrekturen fort")
else: else:
patch = checks[0] patch = check
if patch is not None and (patch["streichen"] or patch["nachtraege"]): if patch is not None and (patch["streichen"] or patch["nachtraege"]):
idx = _titel_index(entries) idx = _titel_index(entries)
weg = {num for t in patch["streichen"] if (num := _titel_aufloesen(idx, t)) is not None} 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)) ergaenzungen = _ergaenzung_schema(_json_datei(erg_path))
if ergaenzungen is None: if ergaenzungen is None:
erg_path.unlink(missing_ok=True) erg_path.unlink(missing_ok=True)
slots = [{ status, ergaenzungen = await run_single_slot(
"key": f"bausteine-{topic}-ergaenzung-1", ctx, "Ergänzung",
"prompt": _prompt( key=f"bausteine-{topic}-ergaenzung-1",
prompt=_prompt(
"Bausteine-Ergaenzung", "Bausteine-Ergaenzung",
topic=topic, bausteine="\n".join(f"- {t}" for t in entries.values()), topic=topic, bausteine="\n".join(f"- {t}" for t in entries.values()),
out_path=erg_path, extra=_extra(instructions), out_path=erg_path, extra=_extra(instructions),
), ),
"role": "quick", "capabilities": "full", role="quick", capabilities="full",
"payload": (lambda result: _ergaenzung_schema(_json_datei(erg_path))), payload=lambda result: _ergaenzung_schema(_json_datei(erg_path)),
}] timeout=_timeout("ergaenzung"),
res = await _race(topic, "Ergänzung", slots, 1, _timeout("ergaenzung"), provider, cancelled=is_cancelled) )
if is_cancelled(): if status == CANCELLED:
abgebrochen() abgebrochen()
return return
if res is None: if status == FAILED:
_bausteine_errors[topic] = "Ergänzung fehlgeschlagen (kein gültiges Ergebnis)" _bausteine_errors[topic] = "Ergänzung fehlgeschlagen (kein gültiges Ergebnis)"
return return
ergaenzungen = res[0]
idx = _titel_index(entries) idx = _titel_index(entries)
neu = [(t, b) for t, b in ergaenzungen if _titel_aufloesen(idx, t) is None] neu = [(t, b) for t, b in ergaenzungen if _titel_aufloesen(idx, t) is None]
if neu: if neu:
@@ -763,8 +844,7 @@ async def _generate_onepager(
guide_id: str, topic: str, instructions: str, provider: str, guide_id: str, topic: str, instructions: str, provider: str,
project: Path | None, content_path: Path, project: Path | None, content_path: Path,
) -> list[dict] | None: ) -> list[dict] | None:
def is_cancelled() -> bool: ctx = GenContext(topic=topic, provider=provider, is_cancelled=lambda: guide_id in _cancelled, guide_id=guide_id)
return guide_id in _cancelled
# 3×3-Raster: 7 Karten mit festen Schlüsseln (Reihenfolge = Lesereihenfolge mobil) # 3×3-Raster: 7 Karten mit festen Schlüsseln (Reihenfolge = Lesereihenfolge mobil)
KARTEN_KEYS = ("info", "eigenschaften", "beispiel", "zusammenhaenge", "voraussetzungen", "modern", "veraltet") KARTEN_KEYS = ("info", "eigenschaften", "beispiel", "zusammenhaenge", "voraussetzungen", "modern", "veraltet")
@@ -814,118 +894,91 @@ async def _generate_onepager(
recherche = recherche_payload() recherche = recherche_payload()
if recherche is None: if recherche is None:
await _set_step(guide_id, 0, "Recherchiere…") await _set_step(guide_id, 0, "Recherchiere…")
slots = [{ status, recherche = await run_single_slot(
"key": f"{guide_id}-recherche", ctx, "OnePager-Recherche",
"prompt": _prompt(recherche_template, topic=topic, source=source, out_path=recherche_path, extra=_extra(instructions)), key=f"{guide_id}-recherche",
"role": "quick", "capabilities": "files" if project else "full", prompt=_prompt(recherche_template, topic=topic, source=source, out_path=recherche_path, extra=_extra(instructions)),
"payload": recherche_payload, role="quick", capabilities="files" if project else "full",
}] payload=recherche_payload, timeout=_timeout("onepager_recherche"),
res = await _race(topic, "OnePager-Recherche", slots, 1, _timeout("onepager_recherche"), provider, cancelled=is_cancelled) )
if is_cancelled(): if status == CANCELLED:
return None return None
if res is None: if status == FAILED:
await _fail(guide_id, "OnePager-Recherche fehlgeschlagen") await _fail(guide_id, "OnePager-Recherche fehlgeschlagen")
return None return None
recherche = res[0]
# Schritt 2: Recherche-Prüfung — notiert Probleme; Anpassung macht ein Recherche-Agent # Schritt 2: Recherche-Prüfung — notiert Probleme; Anpassung macht ein Recherche-Agent
if not recherche_check_path.exists(): status, fixed = await _check_then_fix(
await _set_step(guide_id, 1, "Prüfe Recherche") ctx, name="Recherche", step=1,
slots = [{ check_key=f"{guide_id}-recherche-check",
"key": f"{guide_id}-recherche-check", check_prompt=_prompt(recherche_check_template, topic=topic, recherche=recherche, out_path=recherche_check_path),
"prompt": _prompt(recherche_check_template, topic=topic, recherche=recherche, out_path=recherche_check_path), check_path=recherche_check_path, check_timeout=_timeout("onepager_verify"),
"role": "fast", "capabilities": "files", fix_key=f"{guide_id}-recherche-fix",
"payload": (lambda result: _probleme_schema(_json_datei(recherche_check_path))), build_fix_prompt=lambda probleme: _prompt(
}] "OnePager-Recherche-Fix",
res = await _race(topic, "Recherche-Prüfung", slots, 1, _timeout("onepager_verify"), provider, cancelled=is_cancelled) topic=topic, source=source, recherche=recherche,
if is_cancelled(): probleme="\n".join(f"- {p}" for p in probleme),
return None out_path=recherche_path, extra=_extra(instructions),
if res is None: ),
await _fail(guide_id, "Recherche-Prüfung fehlgeschlagen") fix_payload=recherche_payload, fix_timeout=_timeout("onepager_recherche"),
return None fix_role="quick", fix_caps="files" if project else "full",
probleme = res[0] )
if probleme: if status == CANCELLED:
_log(topic, f"Recherche-Prüfung: {len(probleme)} Problem(e) notiert") return None
await _set_step(guide_id, 1, "Passe Recherche an…") if status == FAILED:
slots = [{ await _fail(guide_id, "Recherche-Prüfung fehlgeschlagen")
"key": f"{guide_id}-recherche-fix", return None
"prompt": _prompt( if fixed is not None:
"OnePager-Recherche-Fix", recherche = fixed
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]
# Schritt 3: Bauen — Karten nur aus der Faktenbasis (Resume: gültige Datei wird übernommen) # Schritt 3: Bauen — Karten nur aus der Faktenbasis (Resume: gültige Datei wird übernommen)
karten = karten_schema(_json_datei(karten_path)) karten = karten_schema(_json_datei(karten_path))
if karten is None: if karten is None:
await _set_step(guide_id, 2, "Baue OnePager…") await _set_step(guide_id, 2, "Baue OnePager…")
karten_path.unlink(missing_ok=True) karten_path.unlink(missing_ok=True)
slots = [{ status, karten = await run_single_slot(
"key": f"{guide_id}-bauen", ctx, "OnePager-Bauen",
"prompt": _prompt("OnePager-Bauen", topic=topic, recherche=recherche, out_path=karten_path, extra=_extra(instructions)), key=f"{guide_id}-bauen",
"role": "fast", "capabilities": "files", prompt=_prompt("OnePager-Bauen", topic=topic, recherche=recherche, out_path=karten_path, extra=_extra(instructions)),
"payload": (lambda result: karten_schema(_json_datei(karten_path))), 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) timeout=_timeout("onepager_bauen"),
if is_cancelled(): )
if status == CANCELLED:
return None return None
if res is None: if status == FAILED:
await _fail(guide_id, "OnePager-Bau fehlgeschlagen") await _fail(guide_id, "OnePager-Bau fehlgeschlagen")
return None return None
karten = res[0]
def karten_block() -> str: def karten_block() -> str:
return "\n\n".join(f"### {k['titel']} [{k['key']}]\n{k['md']}" for k in karten) 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 # Schritt 4: Prüfung — notiert Probleme; Anpassung macht ein Bauen-Agent
if not check_path.exists(): status, fixed = await _check_then_fix(
await _set_step(guide_id, 3, "Prüfe OnePager") ctx, name="OnePager", step=3,
slots = [{ check_key=f"{guide_id}-verify",
"key": f"{guide_id}-verify", check_prompt=_prompt("OnePager-Verifikation", topic=topic, recherche=recherche, karten=karten_block(), out_path=check_path),
"prompt": _prompt("OnePager-Verifikation", topic=topic, recherche=recherche, karten=karten_block(), out_path=check_path), check_path=check_path, check_timeout=_timeout("onepager_verify"),
"role": "fast", "capabilities": "files", fix_key=f"{guide_id}-karten-fix",
"payload": (lambda result: _probleme_schema(_json_datei(check_path))), build_fix_prompt=lambda probleme: _prompt(
}] "OnePager-Fix",
res = await _race(topic, "OnePager-Prüfung", slots, 1, _timeout("onepager_verify"), provider, cancelled=is_cancelled) topic=topic, recherche=recherche, karten=karten_block(),
if is_cancelled(): probleme="\n".join(f"- {p}" for p in probleme),
return None out_path=karten_path, extra=_extra(instructions),
if res is None: ),
await _fail(guide_id, "OnePager-Prüfung fehlgeschlagen") fix_payload=lambda result: karten_schema(_json_datei(karten_path)),
return None fix_timeout=_timeout("onepager_bauen"),
probleme = res[0] on_fix_invalid=lambda: atomic_write_json(
if probleme: karten_path, {"karten": {k["key"]: {"titel": k["titel"], "md": k["md"]} for k in karten}},
_log(topic, f"OnePager-Prüfung: {len(probleme)} Problem(e) notiert") ),
await _set_step(guide_id, 3, "Passe OnePager an…") )
slots = [{ if status == CANCELLED:
"key": f"{guide_id}-karten-fix", return None
"prompt": _prompt( if status == FAILED:
"OnePager-Fix", await _fail(guide_id, "OnePager-Prüfung fehlgeschlagen")
topic=topic, recherche=recherche, karten=karten_block(), return None
probleme="\n".join(f"- {p}" for p in probleme), if fixed is not None:
out_path=karten_path, extra=_extra(instructions), karten = fixed
),
"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]
sections = [ sections = [
{"num": i, "title": k["titel"], "md": k["md"], "key": k["key"]} {"num": i, "title": k["titel"], "md": k["md"], "key": k["key"]}
@@ -942,6 +995,7 @@ async def _generate_sections(
def is_cancelled() -> bool: def is_cancelled() -> bool:
return guide_id in _cancelled 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") spec = (TEMPLATES_DIR / "Format" / "Section.md").read_text(encoding="utf-8")
files = _guide_files(content_path) files = _guide_files(content_path)
bausteine_liste = "\n".join(f"- {t}" for t in entries.values()) bausteine_liste = "\n".join(f"- {t}" for t in entries.values())
@@ -959,23 +1013,23 @@ async def _generate_sections(
if auswahl is None: if auswahl is None:
await _set_step(guide_id, 0, "Wähle Bausteine…") await _set_step(guide_id, 0, "Wähle Bausteine…")
files["auswahl"].unlink(missing_ok=True) files["auswahl"].unlink(missing_ok=True)
slots = [{ status, auswahl = await run_single_slot(
"key": f"{guide_id}-auswahl", ctx, "Guide-Auswahl",
"prompt": _prompt( key=f"{guide_id}-auswahl",
prompt=_prompt(
"Guide-Auswahl", "Guide-Auswahl",
topic=topic, format_name=format_name, bausteine=bausteine_liste, topic=topic, format_name=format_name, bausteine=bausteine_liste,
auswahl_auftrag=auswahl_auftrag, out_path=files["auswahl"], extra=_extra(instructions), auswahl_auftrag=auswahl_auftrag, out_path=files["auswahl"], extra=_extra(instructions),
), ),
"role": "guide", "capabilities": "files", role="guide", capabilities="files",
"payload": (lambda result: _resolve_auswahl(_json_datei(files["auswahl"]), entries, k_min, k_max)), payload=lambda result: _resolve_auswahl(_json_datei(files["auswahl"]), entries, k_min, k_max),
}] timeout=_timeout("guide_auswahl", n),
res = await _race(topic, "Guide-Auswahl", slots, 1, _timeout("guide_auswahl", n), provider, cancelled=is_cancelled) )
if is_cancelled(): if status == CANCELLED:
return None return None
if res is None: if status == FAILED:
await _fail(guide_id, "Auswahl fehlgeschlagen") await _fail(guide_id, "Auswahl fehlgeschlagen")
return None return None
auswahl = res[0]
def auswahl_titel() -> str: def auswahl_titel() -> str:
return "\n".join(f"- {_titel(entries[num])}" for num in auswahl) 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) 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 # Schritt 2: Auswahl-Prüfung — notiert Probleme; Anpassung macht ein Auswahl-Agent
if not files["auswahl_check"].exists(): status, fixed = await _check_then_fix(
await _set_step(guide_id, 1, "Prüfe Auswahl") ctx, name="Auswahl", step=1,
slots = [{ check_key=f"{guide_id}-auswahl-check",
"key": f"{guide_id}-auswahl-check", check_prompt=_prompt(
"prompt": _prompt( "Guide-Auswahl-Check",
"Guide-Auswahl-Check", topic=topic, format_name=format_name, auswahl_auftrag=auswahl_auftrag,
topic=topic, format_name=format_name, auswahl_auftrag=auswahl_auftrag, bausteine=bausteine_liste, auswahl=auswahl_titel(),
bausteine=bausteine_liste, auswahl=auswahl_titel(), out_path=files["auswahl_check"], extra=_extra(instructions),
out_path=files["auswahl_check"], extra=_extra(instructions), ),
), check_path=files["auswahl_check"], check_timeout=_timeout("guide_check", len(auswahl)),
"role": "fast", "capabilities": "files", fix_key=f"{guide_id}-auswahl-fix",
"payload": (lambda result: _probleme_schema(_json_datei(files["auswahl_check"]))), build_fix_prompt=lambda probleme: _prompt(
}] "Guide-Auswahl-Fix",
res = await _race(topic, "Auswahl-Prüfung", slots, 1, _timeout("guide_check", len(auswahl)), provider, cancelled=is_cancelled) topic=topic, format_name=format_name, auswahl_auftrag=auswahl_auftrag,
if is_cancelled(): bausteine=bausteine_liste, auswahl=auswahl_titel(),
return None probleme="\n".join(f"- {p}" for p in probleme),
if res is None: out_path=files["auswahl"], extra=_extra(instructions),
await _fail(guide_id, "Auswahl-Prüfung fehlgeschlagen") ),
return None fix_payload=lambda result: _resolve_auswahl(_json_datei(files["auswahl"]), entries, k_min, k_max),
probleme = res[0] fix_timeout=_timeout("guide_auswahl", n), fix_role="guide",
if probleme: on_fix_invalid=lambda: atomic_write_text(files["auswahl"], auswahl_json()),
_log(topic, f"Auswahl-Prüfung: {len(probleme)} Problem(e) notiert") )
await _set_step(guide_id, 1, "Passe Auswahl an…") if status == CANCELLED:
slots = [{ return None
"key": f"{guide_id}-auswahl-fix", if status == FAILED:
"prompt": _prompt( await _fail(guide_id, "Auswahl-Prüfung fehlgeschlagen")
"Guide-Auswahl-Fix", return None
topic=topic, format_name=format_name, auswahl_auftrag=auswahl_auftrag, if fixed is not None:
bausteine=bausteine_liste, auswahl=auswahl_titel(), auswahl = fixed
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]
sel_entries = {num: entries[num] for num in auswahl} sel_entries = {num: entries[num] for num in auswahl}
soll = len(sel_entries) soll = len(sel_entries)
@@ -1037,23 +1077,23 @@ async def _generate_sections(
if plan is None: if plan is None:
await _set_step(guide_id, 2, "Plane Gliederung…") await _set_step(guide_id, 2, "Plane Gliederung…")
files["gliederung"].unlink(missing_ok=True) files["gliederung"].unlink(missing_ok=True)
slots = [{ status, plan = await run_single_slot(
"key": f"{guide_id}-gliederung", ctx, "Gliederung",
"prompt": _prompt( key=f"{guide_id}-gliederung",
prompt=_prompt(
"Guide-Gliederung", "Guide-Gliederung",
topic=topic, format_name=format_name, bausteine=sel_liste, topic=topic, format_name=format_name, bausteine=sel_liste,
out_path=files["gliederung"], extra=_extra(instructions), out_path=files["gliederung"], extra=_extra(instructions),
), ),
"role": "guide", "capabilities": "files", role="guide", capabilities="files",
"payload": (lambda result: _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll)), payload=lambda result: _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll),
}] timeout=_timeout("plan", soll),
res = await _race(topic, "Gliederung", slots, 1, _timeout("plan", soll), provider, cancelled=is_cancelled) )
if is_cancelled(): if status == CANCELLED:
return None return None
if res is None: if status == FAILED:
await _fail(guide_id, "Gliederung fehlgeschlagen") await _fail(guide_id, "Gliederung fehlgeschlagen")
return None return None
plan = res[0]
def gliederung_text() -> str: def gliederung_text() -> str:
return "\n".join(_zuteilung_text([ch], {num: _titel(entries[num]) for num in ch["nums"]}) for ch in plan) 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 # Schritt 4: Gliederungs-Prüfung
if not files["gliederung_check"].exists(): status, fixed = await _check_then_fix(
await _set_step(guide_id, 3, "Prüfe Gliederung") ctx, name="Gliederung", step=3,
slots = [{ check_key=f"{guide_id}-gliederung-check",
"key": f"{guide_id}-gliederung-check", check_prompt=_prompt(
"prompt": _prompt( "Guide-Gliederung-Check",
"Guide-Gliederung-Check", topic=topic, format_name=format_name, zweck=zweck,
topic=topic, format_name=format_name, zweck=zweck, auswahl=auswahl_titel(), gliederung=gliederung_text(),
auswahl=auswahl_titel(), gliederung=gliederung_text(), out_path=files["gliederung_check"], extra=_extra(instructions),
out_path=files["gliederung_check"], extra=_extra(instructions), ),
), check_path=files["gliederung_check"], check_timeout=_timeout("guide_check", soll),
"role": "fast", "capabilities": "files", fix_key=f"{guide_id}-gliederung-fix",
"payload": (lambda result: _probleme_schema(_json_datei(files["gliederung_check"]))), build_fix_prompt=lambda probleme: _prompt(
}] "Guide-Gliederung-Fix",
res = await _race(topic, "Gliederungs-Prüfung", slots, 1, _timeout("guide_check", soll), provider, cancelled=is_cancelled) topic=topic, format_name=format_name,
if is_cancelled(): auswahl=auswahl_titel(), gliederung=gliederung_text(),
return None probleme="\n".join(f"- {p}" for p in probleme),
if res is None: out_path=files["gliederung"], extra=_extra(instructions),
await _fail(guide_id, "Gliederungs-Prüfung fehlgeschlagen") ),
return None fix_payload=lambda result: _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll),
probleme = res[0] fix_timeout=_timeout("plan", soll), fix_role="guide",
if probleme: on_fix_invalid=lambda: atomic_write_text(files["gliederung"], gliederung_json()),
_log(topic, f"Gliederungs-Prüfung: {len(probleme)} Problem(e) notiert") )
await _set_step(guide_id, 3, "Passe Gliederung an…") if status == CANCELLED:
slots = [{ return None
"key": f"{guide_id}-gliederung-fix", if status == FAILED:
"prompt": _prompt( await _fail(guide_id, "Gliederungs-Prüfung fehlgeschlagen")
"Guide-Gliederung-Fix", return None
topic=topic, format_name=format_name, if fixed is not None:
auswahl=auswahl_titel(), gliederung=gliederung_text(), plan = fixed
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]
# Schritt 5: Schreiben — vorhandene Chunk-Dateien werden übernommen (Resume) # Schritt 5: Schreiben — vorhandene Chunk-Dateien werden übernommen (Resume)
total_sections = sum(len(c["nums"]) for c in plan) total_sections = sum(len(c["nums"]) for c in plan)