This commit is contained in:
team3
2026-06-21 22:53:54 +02:00
parent b4c686f74f
commit 2e33fa5c47
23 changed files with 442 additions and 560 deletions

View File

@@ -1,4 +1,4 @@
"""Guide-Generierung als Konsens-Pipeline (OnePager hat einen eigenen Weg).
"""Guide-Generierung als Konsens-Pipeline (Roadmap hat einen eigenen Weg).
Auswahl: 5 Agenten (min. 3, Grace) → Code-Voting (Mehrheit = Konsens) →
Mapping-Agent sortiert Strittiges → Klärungs-Loop (max. KONSENS_MAX_RUNDEN).
@@ -24,7 +24,7 @@ import lesbarkeit
from database import list_guides, update_guide
from fsutil import atomic_write_json
from jsonio import read_json_file as _json_datei
from onepager import _generate_onepager
from roadmap import _generate_roadmap
from paths import bausteine_path, guide_content_path, project_dir, subbausteine_path
from pipeline import (
CANCELLED, FAILED, GenContext, _claude_error, _extra,
@@ -172,6 +172,20 @@ def _resolve_gliederung(data, entries: dict[int, str], soll_min: int, soll_max:
return chapters
def _fallback_gliederung(entries: dict[int, str]) -> list[dict]:
"""Deterministische Gliederung, wenn die Agenten keine liefern: ein Kapitel mit
allen gewählten Bausteinen in Reihenfolge. Garantiert vollständige Abdeckung."""
return [{"title": "Inhalte", "nums": list(entries)}]
def _mit_resten(plan: list[dict], entries: dict[int, str]) -> list[dict]:
"""Stellt sicher, dass JEDER gewählte Baustein im Plan steht — fehlende landen in
einem Kapitel „Weitere" (gegen Agenten/Judge, die Bausteine weglassen)."""
drin = {num for ch in plan for num in ch.get("nums", [])}
fehlen = [num for num in entries if num not in drin]
return [*plan, {"title": "Weitere", "nums": fehlen}] if fehlen else plan
def _voting(stimmen: list[list[int]]) -> tuple[list[int], dict[int, int]]:
"""Mehrheit (> Hälfte der Stimmen) → Konsens; ≥1 Stimme → Rest mit Votenzahl."""
zaehler: dict[int, int] = {}
@@ -433,7 +447,8 @@ async def _generate_sections(
soll = len(sel_entries)
sel_liste = "\n".join(f"- {t}" for t in sel_entries.values())
# Schritt 1: Gliederung — 5 Vorschläge (min. 3, Grace), ein Judge wählt.
# Schritt 1: Gliederung — bis zu 5 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:
@@ -447,7 +462,7 @@ async def _generate_sections(
vorschlaege.append(res)
else:
offen.append((i, path))
if len(vorschlaege) < 3:
if len(vorschlaege) < 3 and offen:
slots = [
{
"key": f"{guide_id}-gliederung-{i}",
@@ -461,41 +476,48 @@ async def _generate_sections(
}
for i, path in offen
]
# Quorum 1: nimm, was kommt — kein Mindestzwang, kein Abbruch.
neue = await _race(
topic, "Gliederung", slots, 3 - len(vorschlaege), _timeout("plan", soll),
topic, "Gliederung", slots, 1, _timeout("plan", soll),
provider, cancelled=is_cancelled, grace=KONSENS_GRACE,
)
if is_cancelled():
return None
if neue is None:
await _fail(guide_id, "Gliederung fehlgeschlagen (Minimum nicht erreicht)")
return None
vorschlaege += neue
vorschlaege += neue or []
await _set_step(guide_id, 1, "Wähle beste Gliederung…")
bloecke = "\n\n".join(
f"### Vorschlag {i}\n"
+ "\n".join(_zuteilung_text([ch], {num: _titel(entries[num]) for num in ch["nums"]}) for ch in v)
for i, v in enumerate(vorschlaege, 1)
)
status, plan = await run_single_slot(
ctx, "Gliederungs-Judge",
key=f"{guide_id}-gliederung-judge",
prompt=_prompt(
"Guide-Gliederung-Judge",
topic=topic, format_name=format_name, zweck=zweck, n=len(vorschlaege),
bausteine=sel_liste, gliederungen=bloecke,
out_path=files["gliederung"], extra=_extra(instructions),
),
role="judge", capabilities="files",
payload=lambda result: _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll),
timeout=_timeout("plan_judge", soll),
)
if status == CANCELLED:
return None
if status == FAILED:
await _fail(guide_id, "Gliederung fehlgeschlagen (Judge ohne gültiges Ergebnis)")
return None
if not vorschlaege:
_log(topic, "Gliederung: kein gültiger Vorschlag — deterministischer Fallback")
plan = _fallback_gliederung(sel_entries)
elif len(vorschlaege) == 1:
plan = vorschlaege[0] # ein Vorschlag → kein Judge nötig
else:
await _set_step(guide_id, 1, "Wähle beste Gliederung…")
bloecke = "\n\n".join(
f"### Vorschlag {i}\n"
+ "\n".join(_zuteilung_text([ch], {num: _titel(entries[num]) for num in ch["nums"]}) for ch in v)
for i, v in enumerate(vorschlaege, 1)
)
status, plan = await run_single_slot(
ctx, "Gliederungs-Judge",
key=f"{guide_id}-gliederung-judge",
prompt=_prompt(
"Guide-Gliederung-Judge",
topic=topic, format_name=format_name, zweck=zweck, n=len(vorschlaege),
bausteine=sel_liste, gliederungen=bloecke,
out_path=files["gliederung"], extra=_extra(instructions),
),
role="judge", capabilities="files",
payload=lambda result: _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll),
timeout=_timeout("plan_judge", soll),
)
if status == CANCELLED:
return None
if status == FAILED or plan is None:
_log(topic, "Gliederung-Judge ohne Ergebnis — bester Vorschlag bleibt")
plan = vorschlaege[0]
# Garantie: jeder gewählte Baustein steht im Plan (gegen weglassende Agenten/Judges).
plan = _mit_resten(plan, sel_entries)
# Chunks festlegen — Inhalte, Inhalts-Prüfung, Writer und Lese-Check nutzen
# dieselbe Aufteilung: 1 Agent je ~30 Bausteine (gedeckelt).
@@ -817,8 +839,11 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
for p_alt in guide_slot_dateien(content_path):
p_alt.unlink(missing_ok=True)
if format_name == "OnePager":
chapters = await _generate_onepager(guide_id, topic, instructions, provider, project, content_path)
if format_name == "Roadmap":
graph = await _generate_roadmap(guide_id, topic, instructions, provider, content_path)
if graph is None or is_guide_cancelled(guide_id):
return
content = {"topic": topic, "format": format_name, "graph": graph}
else:
alle = _lade_bausteine(bausteine_path(topic).read_text(encoding="utf-8"))
if not alle:
@@ -830,10 +855,11 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
guide_id, topic, format_name, entries,
facts, instructions, provider, content_path,
)
if chapters is None or is_guide_cancelled(guide_id):
return
if chapters is None or is_guide_cancelled(guide_id):
return
content = {"topic": topic, "format": format_name, "chapters": chapters}
atomic_write_json(content_path, {"topic": topic, "format": format_name, "chapters": chapters}, indent=1)
atomic_write_json(content_path, content, indent=1)
now = datetime.now(timezone.utc).isoformat()
await update_guide(guide_id, status="done", progress=None, step=None, updated_at=now)