update
This commit is contained in:
102
backend/guide.py
102
backend/guide.py
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user