This commit is contained in:
team3
2026-06-22 12:55:52 +02:00
parent 04900eef55
commit 0adc223c0a
7 changed files with 75 additions and 26 deletions

View File

@@ -24,7 +24,7 @@ LESBARKEIT_HART_ANTEIL = 0.30 # … ODER wenn dieser Anteil der Sätze hart ist
# Deckel für gleichzeitige CLI-Agenten-Prozesse (über alle Generierungen hinweg).
# Eigene Spur für interaktive Aufrufe (Chat, Elemente), damit sie nicht hinter
# laufenden Writern in der Warteschlange hängen.
MAX_CONCURRENT_AGENTS = 20
MAX_CONCURRENT_AGENTS = 30
MAX_CONCURRENT_INTERACTIVE = 4
# Grace-Fenster der Konsens-Races (Bausteine, Guide, OnePager): Nach dem ersten

View File

@@ -2,8 +2,8 @@
Auswahl: 5 Agenten (min. 3, Grace) → Code-Voting (Mehrheit = Konsens) →
Mapping-Agent sortiert Strittiges → Klärungs-Loop (max. KONSENS_MAX_RUNDEN).
Gliederung: 5 Vorschläge (min. 3, Grace) → ein Judge wählt und kombiniert.
Schreiben: Writer pro Chunk. Lese-Prüfung: Check→Fix-Loop (max. Runden-Cap),
Gliederung: 3 Vorschläge (Grace) → ein Judge wählt und kombiniert.
Schreiben: Writer je Baustein. Lese-Prüfung: Check→Fix (eine Runde),
Folgerunden prüfen nur ersetzte Sections; danach bleiben Beanstandungen stehen.
Schritt-Dateien bleiben liegen → Abbruch erhält Fortschritt, ▶ setzt am offenen Schritt fort.
"""
@@ -46,6 +46,10 @@ GUIDE_STEPS = ("Auswahl", "Gliederung", "Inhalte", "Inhalts-Check", "Schreiben",
WRITER_SECTIONS = 30
WRITER_MAX = 20
# Lese-Prüfung: nur EINE Runde (Check + Fix). Folgerunden brachten kaum Mehrwert
# (1 Agent je Baustein prüft ohnehin fein), kosten aber extra Agenten.
LESE_RUNDEN = 1
def _load_subbausteine(topic: str) -> dict[str, list[dict]]:
"""Sidecar laden: {Baustein-Titel: [{titel, stufe}, …]}. Fehlt sie → {} (Fallback)."""
@@ -85,7 +89,7 @@ def _guide_files(content_path: Path) -> dict:
for n in runden
},
"auswahl_mapping": {n: d / f"{stem}.auswahl-mapping-r{n}.json" for n in runden},
"gliederung_slots": [d / f"{stem}.gliederung-{i}.json" for i in (1, 2, 3, 4, 5)],
"gliederung_slots": [d / f"{stem}.gliederung-{i}.json" for i in (1, 2, 3)],
"gliederung": d / f"{stem}.gliederung.json", # Judge-Ausgabe
# chunk-/lese-check-/fix-Dateien sind dynamisch:
# {stem}.chunk-i.md, {stem}.lese-check-r{n}-{i}.json, {stem}.fix-r{n}-{i}.md
@@ -97,6 +101,28 @@ def guide_slot_dateien(content_path: Path) -> list[Path]:
return [p for p in content_path.parent.glob(f"{content_path.stem}.*") if p != content_path]
# Slot-Datei-Globs je Schritt (Index = GUIDE_STEPS). Stem-verankert, kollisionsfrei.
_STEP_GLOBS = (
("auswahl-*", "auswahl-mapping-*"), # 0 Auswahl (deterministisch → meist leer)
("gliederung*",), # 1 Gliederung
("inhalt-chunk-*",), # 2 Inhalte
("inhalt-check-*", "inhalt-fix-*"), # 3 Inhalts-Check
("chunk-*",), # 4 Schreiben
("lese-check-*", "fix-r*"), # 5 Lese-Prüfung
)
def _reset_guide_ab_step(content_path: Path, step: int) -> None:
"""Re-Run ab Schritt: Content + alle Slot-Dateien der Schritte ≥ step löschen.
Frühere Schritte bleiben → der Resume baut ab `step` neu (alles darunter wiederverwendet)."""
content_path.unlink(missing_ok=True) # nicht mehr „done" → kein Frischstart-Wipe
d, stem = content_path.parent, content_path.stem
for globs in _STEP_GLOBS[step:]:
for pat in globs:
for p in d.glob(f"{stem}.{pat}"):
p.unlink(missing_ok=True)
def _resolve_auswahl(data, entries: dict[int, str], k_min: int, k_max: int) -> list[int] | None:
"""{"bausteine": [Titel]} → Nummern; None bei Schema-Verstoß/Drift/falschem Umfang."""
if not isinstance(data, dict) or not isinstance(data.get("bausteine"), list):
@@ -455,12 +481,12 @@ async def _generate_sections(
soll = len(sel_entries)
sel_liste = "\n".join(f"- {t}" for t in sel_entries.values())
# Schritt 1: Gliederung — bis zu 5 Vorschläge (Grace), ein Judge wählt. Bricht NIE ab:
# Schritt 1: Gliederung — bis zu 3 Vorschläge (Grace), ein Judge wählt. Bricht NIE ab:
# 0 gültige → Code-Fallback, 1 → direkt, ≥2 → Judge (mit Vorschlag als Rückfall).
# Gültiges gliederung.json (auch aus Altläufen) überspringt den Schritt.
plan = _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll)
if plan is None:
await _set_step(guide_id, 1, "Gliederungs-Vorschläge (5 Agenten)…")
await _set_step(guide_id, 1, "Gliederungs-Vorschläge (3 Agenten)…")
files["gliederung"].unlink(missing_ok=True)
vorschlaege: list[list[dict]] = []
offen = []
@@ -706,11 +732,11 @@ async def _generate_sections(
)
scope = chunk_nums
for runde in range(1, KONSENS_MAX_RUNDEN + 1):
for runde in range(1, LESE_RUNDEN + 1):
check_paths = [content_path.parent / f"{content_path.stem}.lese-check-r{runde}-{i}.json" for i in range(1, writer_count + 1)]
offen_checks = [i for i, p in enumerate(check_paths) if scope[i] and _lese_probleme_schema(_json_datei(p)) is None]
if offen_checks:
await _set_step(guide_id, 5, f"Prüfe Lesbarkeit (Runde {runde}/{KONSENS_MAX_RUNDEN})")
await _set_step(guide_id, 5, "Prüfe Lesbarkeit…")
slots = [{
"key": f"{guide_id}-lese-check-r{runde}-{i + 1}",
"prompt": _prompt(
@@ -787,8 +813,8 @@ async def _generate_sections(
_log(topic, f"Lese-Prüfung Runde {runde}: {len(ersetzt)} Section(s) überarbeitet")
if not ersetzt:
break
if runde == KONSENS_MAX_RUNDEN:
_log(topic, f"Lese-Prüfung: Cap erreicht — letzte Überarbeitung bleibt ungeprüft")
if runde == LESE_RUNDEN:
_log(topic, f"Lese-Prüfung: 1 Runde — Überarbeitung bleibt ungeprüft")
break
scope = [[num for num in nums if num in ersetzt] for nums in chunk_nums]
@@ -835,7 +861,7 @@ async def reconcile_guides() -> None:
await update_guide(g["id"], status="error", error_msg="Inhalt fehlt — neu generieren", updated_at=now)
async def generate_guide(guide_id: str, topic: str, format_name: str, instructions: str = "", provider: str = DEFAULT_PROVIDER) -> None:
async def generate_guide(guide_id: str, topic: str, format_name: str, instructions: str = "", provider: str = DEFAULT_PROVIDER, ab_step: int | None = None) -> None:
async with _semaphore:
now = datetime.now(timezone.utc).isoformat()
await update_guide(guide_id, status="generating", progress="Starte…", updated_at=now)
@@ -851,9 +877,12 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
if project:
await asyncio.to_thread(_pdfs_konvertieren, project)
# „Neu erstellen": fertiger Guide → kompletter Frischstart.
# Re-Run ab Schritt: Content + Slots ab `ab_step` löschen, Rest bleibt → Resume baut ab dort.
# Sonst „Neu erstellen": fertiger Guide → kompletter Frischstart.
# Sonst sind Schritt-Dateien Reste eines Abbruchs/Fehlers → Resume.
if content_path.exists():
if ab_step is not None:
_reset_guide_ab_step(content_path, ab_step)
elif content_path.exists():
for p_alt in guide_slot_dateien(content_path):
p_alt.unlink(missing_ok=True)

View File

@@ -17,6 +17,7 @@ class GuideCreateRequest(BaseModel):
format: FormatType
instructions: str = Field(default="", max_length=2000)
provider: ProviderType = "claude"
ab_step: int | None = Field(default=None, ge=0, le=5) # Re-Run ab Guide-Schritt (0 Auswahl … 5 Lese-Prüfung); None = voll/Resume
class TopicCreateRequest(BaseModel):

View File

@@ -458,7 +458,7 @@ async def create(req: GuideCreateRequest):
"updated_at": now,
}
await create_guide(guide)
asyncio.create_task(generate_guide(guide["id"], guide["topic"], guide["format"], guide["instructions"], req.provider))
asyncio.create_task(generate_guide(guide["id"], guide["topic"], guide["format"], guide["instructions"], req.provider, ab_step=req.ab_step))
return guide