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:
@@ -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(
|
||||||
}]
|
|
||||||
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",
|
"OnePager-Recherche-Fix",
|
||||||
topic=topic, source=source, recherche=recherche,
|
topic=topic, source=source, recherche=recherche,
|
||||||
probleme="\n".join(f"- {p}" for p in probleme),
|
probleme="\n".join(f"- {p}" for p in probleme),
|
||||||
out_path=recherche_path, extra=_extra(instructions),
|
out_path=recherche_path, extra=_extra(instructions),
|
||||||
),
|
),
|
||||||
"role": "quick", "capabilities": "files" if project else "full",
|
fix_payload=recherche_payload, fix_timeout=_timeout("onepager_recherche"),
|
||||||
"payload": recherche_payload,
|
fix_role="quick", fix_caps="files" if project else "full",
|
||||||
}]
|
)
|
||||||
res = await _race(topic, "Recherche-Fix", slots, 1, _timeout("onepager_recherche"), provider, cancelled=is_cancelled)
|
if status == CANCELLED:
|
||||||
if is_cancelled():
|
|
||||||
return None
|
return None
|
||||||
if res is None:
|
if status == FAILED:
|
||||||
_log(topic, "Recherche-Fix ungültig — ursprüngliche Recherche bleibt")
|
await _fail(guide_id, "Recherche-Prüfung fehlgeschlagen")
|
||||||
else:
|
return None
|
||||||
recherche = res[0]
|
if fixed is not None:
|
||||||
|
recherche = fixed
|
||||||
|
|
||||||
# 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(
|
||||||
}]
|
|
||||||
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",
|
"OnePager-Fix",
|
||||||
topic=topic, recherche=recherche, karten=karten_block(),
|
topic=topic, recherche=recherche, karten=karten_block(),
|
||||||
probleme="\n".join(f"- {p}" for p in probleme),
|
probleme="\n".join(f"- {p}" for p in probleme),
|
||||||
out_path=karten_path, extra=_extra(instructions),
|
out_path=karten_path, extra=_extra(instructions),
|
||||||
),
|
),
|
||||||
"role": "fast", "capabilities": "files",
|
fix_payload=lambda result: karten_schema(_json_datei(karten_path)),
|
||||||
"payload": (lambda result: karten_schema(_json_datei(karten_path))),
|
fix_timeout=_timeout("onepager_bauen"),
|
||||||
}]
|
on_fix_invalid=lambda: atomic_write_json(
|
||||||
res = await _race(topic, "OnePager-Fix", slots, 1, _timeout("onepager_bauen"), provider, cancelled=is_cancelled)
|
karten_path, {"karten": {k["key"]: {"titel": k["titel"], "md": k["md"]} for k in karten}},
|
||||||
if is_cancelled():
|
),
|
||||||
|
)
|
||||||
|
if status == CANCELLED:
|
||||||
return None
|
return None
|
||||||
if res is None:
|
if status == FAILED:
|
||||||
_log(topic, "OnePager-Fix ungültig — ursprüngliche Karten bleiben")
|
await _fail(guide_id, "OnePager-Prüfung fehlgeschlagen")
|
||||||
atomic_write_json(karten_path, {"karten": {k["key"]: {"titel": k["titel"], "md": k["md"]} for k in karten}})
|
return None
|
||||||
else:
|
if fixed is not None:
|
||||||
karten = res[0]
|
karten = fixed
|
||||||
|
|
||||||
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),
|
||||||
),
|
),
|
||||||
"role": "fast", "capabilities": "files",
|
check_path=files["auswahl_check"], check_timeout=_timeout("guide_check", len(auswahl)),
|
||||||
"payload": (lambda result: _probleme_schema(_json_datei(files["auswahl_check"]))),
|
fix_key=f"{guide_id}-auswahl-fix",
|
||||||
}]
|
build_fix_prompt=lambda probleme: _prompt(
|
||||||
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",
|
"Guide-Auswahl-Fix",
|
||||||
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(),
|
||||||
probleme="\n".join(f"- {p}" for p in probleme),
|
probleme="\n".join(f"- {p}" for p in probleme),
|
||||||
out_path=files["auswahl"], extra=_extra(instructions),
|
out_path=files["auswahl"], extra=_extra(instructions),
|
||||||
),
|
),
|
||||||
"role": "guide", "capabilities": "files",
|
fix_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)),
|
fix_timeout=_timeout("guide_auswahl", n), fix_role="guide",
|
||||||
}]
|
on_fix_invalid=lambda: atomic_write_text(files["auswahl"], auswahl_json()),
|
||||||
res = await _race(topic, "Auswahl-Fix", 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:
|
||||||
_log(topic, "Auswahl-Fix ungültig — ursprüngliche Auswahl bleibt")
|
await _fail(guide_id, "Auswahl-Prüfung fehlgeschlagen")
|
||||||
atomic_write_text(files["auswahl"], auswahl_json())
|
return None
|
||||||
else:
|
if fixed is not None:
|
||||||
auswahl = res[0]
|
auswahl = fixed
|
||||||
|
|
||||||
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),
|
||||||
),
|
),
|
||||||
"role": "fast", "capabilities": "files",
|
check_path=files["gliederung_check"], check_timeout=_timeout("guide_check", soll),
|
||||||
"payload": (lambda result: _probleme_schema(_json_datei(files["gliederung_check"]))),
|
fix_key=f"{guide_id}-gliederung-fix",
|
||||||
}]
|
build_fix_prompt=lambda probleme: _prompt(
|
||||||
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",
|
"Guide-Gliederung-Fix",
|
||||||
topic=topic, format_name=format_name,
|
topic=topic, format_name=format_name,
|
||||||
auswahl=auswahl_titel(), gliederung=gliederung_text(),
|
auswahl=auswahl_titel(), gliederung=gliederung_text(),
|
||||||
probleme="\n".join(f"- {p}" for p in probleme),
|
probleme="\n".join(f"- {p}" for p in probleme),
|
||||||
out_path=files["gliederung"], extra=_extra(instructions),
|
out_path=files["gliederung"], extra=_extra(instructions),
|
||||||
),
|
),
|
||||||
"role": "guide", "capabilities": "files",
|
fix_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)),
|
fix_timeout=_timeout("plan", soll), fix_role="guide",
|
||||||
}]
|
on_fix_invalid=lambda: atomic_write_text(files["gliederung"], gliederung_json()),
|
||||||
res = await _race(topic, "Gliederungs-Fix", 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:
|
||||||
_log(topic, "Gliederungs-Fix ungültig — ursprüngliche Gliederung bleibt")
|
await _fail(guide_id, "Gliederungs-Prüfung fehlgeschlagen")
|
||||||
atomic_write_text(files["gliederung"], gliederung_json())
|
return None
|
||||||
else:
|
if fixed is not None:
|
||||||
plan = res[0]
|
plan = fixed
|
||||||
|
|
||||||
# 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user