This commit is contained in:
team3
2026-06-12 17:18:42 +02:00
parent cfc666055c
commit 78d5833fe4
38 changed files with 1854 additions and 740 deletions

View File

@@ -1,20 +1,30 @@
"""OnePager-Pipeline: Recherche → Recherche-Prüfung → Bauen → Prüfung (7 Karten im 3×3-Raster)."""
"""OnePager-Pipeline als Konsens-Kette (7 Karten im 3×3-Raster).
Recherche: 3 Agenten (min. 2, Grace) → Mapping konsolidiert zu EINER Faktenbasis.
Bauen: 3 Agenten bauen je einen Karten-Satz → ein Judge wählt und kombiniert.
Prüfung: Verify→Fix-Loop (max. KONSENS_MAX_RUNDEN); Runde 1 ist fatal, danach
bleibt bei Fehlern die letzte gültige Version. Schritt-Dateien bleiben liegen →
Abbruch erhält Fortschritt, ▶ setzt am offenen Schritt fort.
"""
from pathlib import Path
from fsutil import atomic_write_json
from config import KONSENS_GRACE, KONSENS_MAX_RUNDEN
from jsonio import read_json_file as _json_datei
from pipeline import (
CANCELLED, FAILED, GenContext, _check_then_fix, _extra, _fail,
_prompt, _set_step, _timeout, is_guide_cancelled, run_single_slot,
CANCELLED, FAILED, GenContext, _extra, _fail, _log, _probleme_schema,
_prompt, _race, _set_step, _timeout, is_guide_cancelled, run_single_slot,
)
ONEPAGER_STEPS = ("Recherche", "Bauen", "Prüfung")
async def _generate_onepager(
guide_id: str, topic: str, instructions: str, provider: str,
project: Path | None, content_path: Path,
) -> list[dict] | None:
ctx = GenContext(topic=topic, provider=provider, is_cancelled=lambda: is_guide_cancelled(guide_id), guide_id=guide_id)
is_cancelled = lambda: is_guide_cancelled(guide_id)
ctx = GenContext(topic=topic, provider=provider, is_cancelled=is_cancelled, guide_id=guide_id)
# 3×3-Raster: 7 Karten mit festen Schlüsseln (Reihenfolge = Lesereihenfolge mobil)
KARTEN_KEYS = ("info", "eigenschaften", "beispiel", "zusammenhaenge", "voraussetzungen", "modern", "veraltet")
@@ -38,116 +48,193 @@ async def _generate_onepager(
return out
d, stem = content_path.parent, content_path.stem
recherche_path = d / f"{stem}.recherche.md"
recherche_check_path = d / f"{stem}.recherche-check.json"
karten_path = d / f"{stem}.karten.json"
check_path = d / f"{stem}.onepager-check.json"
recherche_slots = [d / f"{stem}.recherche-{i}.md" for i in (1, 2, 3)]
recherche_path = d / f"{stem}.recherche.md" # konsolidierte Faktenbasis
karten_slots = [d / f"{stem}.karten-{i}.json" for i in (1, 2, 3)]
karten_path = d / f"{stem}.karten.json" # Judge-Ausgabe
verify_paths = {n: d / f"{stem}.verify-r{n}.json" for n in range(1, KONSENS_MAX_RUNDEN + 1)}
fix_paths = {n: d / f"{stem}.karten-fix-r{n}.json" for n in range(1, KONSENS_MAX_RUNDEN + 1)}
# Projekte bekommen eigene Recherche-Dimensionen — Produkt-Fragen
# (Version, Lizenz, Alternativen) laufen dort ins Leere.
if project:
source = _prompt("OnePager-Quelle-Projekt", project=project)
recherche_template = "OnePager-Recherche-Projekt"
recherche_check_template = "OnePager-Recherche-Check-Projekt"
else:
source = _prompt("OnePager-Quelle-Thema", topic=topic)
recherche_template = "OnePager-Recherche"
recherche_check_template = "OnePager-Recherche-Check"
def recherche_payload(result=None):
if not recherche_path.exists():
def text_payload(path: Path):
if not path.exists():
return None
text = recherche_path.read_text(encoding="utf-8").strip()
text = path.read_text(encoding="utf-8").strip()
return text or None
# Schritt 1: Recherche — vorhandene Datei wird übernommen (Resume)
recherche = recherche_payload()
# Schritt 0: Recherche — 3 Agenten (min. 2, Grace), Mapping konsolidiert.
# Eine gültige recherche.md (auch aus Altläufen) überspringt den Schritt.
recherche = text_payload(recherche_path)
if recherche is None:
await _set_step(guide_id, 0, "Recherchiere…")
await _set_step(guide_id, 0, "Recherchiere (3 Agenten)")
recherchen = []
offen = []
for i, path in enumerate(recherche_slots, 1):
text = text_payload(path)
if text is not None:
recherchen.append(text)
else:
offen.append((i, path))
if len(recherchen) < 2:
slots = [
{
"key": f"{guide_id}-recherche-{i}",
"prompt": _prompt(recherche_template, topic=topic, source=source, out_path=path, extra=_extra(instructions)),
"role": "quick", "capabilities": "files" if project else "full",
"payload": (lambda result, p=path: text_payload(p)),
}
for i, path in offen
]
neue = await _race(
topic, "OnePager-Recherche", slots, 2 - len(recherchen),
_timeout("onepager_recherche"), provider, cancelled=is_cancelled, grace=KONSENS_GRACE,
)
if is_cancelled():
return None
if neue is None:
await _fail(guide_id, "OnePager-Recherche fehlgeschlagen (Minimum nicht erreicht)")
return None
recherchen += neue
await _set_step(guide_id, 0, "Konsolidiere Recherche…")
recherchen_block = "\n\n".join(f"### Recherche {i}\n\n{text}" for i, text in enumerate(recherchen, 1))
status, recherche = await run_single_slot(
ctx, "OnePager-Recherche",
key=f"{guide_id}-recherche",
prompt=_prompt(recherche_template, topic=topic, source=source, out_path=recherche_path, extra=_extra(instructions)),
role="quick", capabilities="files" if project else "full",
payload=recherche_payload, timeout=_timeout("onepager_recherche"),
ctx, "Recherche-Mapping",
key=f"{guide_id}-recherche-mapping",
prompt=_prompt(
"OnePager-Recherche-Mapping",
topic=topic, n=len(recherchen), recherchen=recherchen_block, out_path=recherche_path,
),
role="judge", capabilities="files",
payload=lambda result: text_payload(recherche_path),
timeout=_timeout("onepager_mapping"),
)
if status == CANCELLED:
return None
if status == FAILED:
await _fail(guide_id, "OnePager-Recherche fehlgeschlagen")
await _fail(guide_id, "Recherche-Konsolidierung fehlgeschlagen")
return None
# Schritt 2: Recherche-Prüfung — notiert Probleme; Anpassung macht ein Recherche-Agent
status, fixed = await _check_then_fix(
ctx, name="Recherche", step=1,
check_key=f"{guide_id}-recherche-check",
check_prompt=_prompt(recherche_check_template, topic=topic, recherche=recherche, out_path=recherche_check_path),
check_path=recherche_check_path, check_timeout=_timeout("onepager_verify"),
fix_key=f"{guide_id}-recherche-fix",
build_fix_prompt=lambda probleme: _prompt(
"OnePager-Recherche-Fix",
topic=topic, source=source, recherche=recherche,
probleme="\n".join(f"- {p}" for p in probleme),
out_path=recherche_path, extra=_extra(instructions),
),
fix_payload=recherche_payload, fix_timeout=_timeout("onepager_recherche"),
fix_role="quick", fix_caps="files" if project else "full",
)
if status == CANCELLED:
return None
if status == FAILED:
await _fail(guide_id, "Recherche-Prüfung fehlgeschlagen")
return None
if fixed is not None:
recherche = fixed
# Schritt 3: Bauen — Karten nur aus der Faktenbasis (Resume: gültige Datei wird übernommen)
# Schritt 1: Bauen — 3 Entwürfe (min. 2, Grace), ein Judge kombiniert.
# Gültiges karten.json (auch aus Altläufen) überspringt den Schritt.
karten = karten_schema(_json_datei(karten_path))
if karten is None:
await _set_step(guide_id, 2, "Baue OnePager…")
karten_path.unlink(missing_ok=True)
await _set_step(guide_id, 1, "Baue OnePager (3 Entwürfe)")
entwuerfe = []
offen = []
for i, path in enumerate(karten_slots, 1):
res = karten_schema(_json_datei(path))
if res is not None:
entwuerfe.append(res)
else:
offen.append((i, path))
if len(entwuerfe) < 2:
slots = [
{
"key": f"{guide_id}-bauen-{i}",
"prompt": _prompt("OnePager-Bauen", topic=topic, recherche=recherche, out_path=path, extra=_extra(instructions)),
"role": "fast", "capabilities": "files",
"payload": (lambda result, p=path: karten_schema(_json_datei(p))),
}
for i, path in offen
]
neue = await _race(
topic, "OnePager-Bauen", slots, 2 - len(entwuerfe),
_timeout("onepager_bauen"), provider, cancelled=is_cancelled, grace=KONSENS_GRACE,
)
if is_cancelled():
return None
if neue is None:
await _fail(guide_id, "OnePager-Bau fehlgeschlagen (Minimum nicht erreicht)")
return None
entwuerfe += neue
await _set_step(guide_id, 1, "Wähle besten Entwurf…")
saetze_block = "\n\n".join(
f"## Entwurf {i}\n\n" + "\n\n".join(f"### {k['titel']} [{k['key']}]\n{k['md']}" for k in satz)
for i, satz in enumerate(entwuerfe, 1)
)
status, karten = await run_single_slot(
ctx, "OnePager-Bauen",
key=f"{guide_id}-bauen",
prompt=_prompt("OnePager-Bauen", topic=topic, recherche=recherche, out_path=karten_path, extra=_extra(instructions)),
role="fast", capabilities="files",
ctx, "Bauen-Judge",
key=f"{guide_id}-bauen-judge",
prompt=_prompt(
"OnePager-Bauen-Judge",
topic=topic, n=len(entwuerfe), recherche=recherche, kartensaetze=saetze_block,
out_path=karten_path, extra=_extra(instructions),
),
role="judge", capabilities="files",
payload=lambda result: karten_schema(_json_datei(karten_path)),
timeout=_timeout("onepager_bauen"),
timeout=_timeout("onepager_judge"),
)
if status == CANCELLED:
return None
if status == FAILED:
await _fail(guide_id, "OnePager-Bau fehlgeschlagen")
await _fail(guide_id, "OnePager-Bau fehlgeschlagen (Judge ohne gültiges Ergebnis)")
return None
def karten_block() -> str:
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
status, fixed = await _check_then_fix(
ctx, name="OnePager", step=3,
check_key=f"{guide_id}-verify",
check_prompt=_prompt("OnePager-Verifikation", topic=topic, recherche=recherche, karten=karten_block(), out_path=check_path),
check_path=check_path, check_timeout=_timeout("onepager_verify"),
fix_key=f"{guide_id}-karten-fix",
build_fix_prompt=lambda probleme: _prompt(
"OnePager-Fix",
topic=topic, recherche=recherche, karten=karten_block(),
probleme="\n".join(f"- {p}" for p in probleme),
out_path=karten_path, extra=_extra(instructions),
),
fix_payload=lambda result: karten_schema(_json_datei(karten_path)),
fix_timeout=_timeout("onepager_bauen"),
on_fix_invalid=lambda: atomic_write_json(
karten_path, {"karten": {k["key"]: {"titel": k["titel"], "md": k["md"]} for k in karten}},
),
)
if status == CANCELLED:
return None
if status == FAILED:
await _fail(guide_id, "OnePager-Prüfung fehlgeschlagen")
return None
if fixed is not None:
# Schritt 2: Prüf-Loop — Verify notiert Probleme, Fix behebt; max. Runden-Cap.
# Runde 1 ist fatal (wie früher der Einzel-Check), danach bleibt bei Fehlern
# die letzte gültige Version stehen.
for runde in range(1, KONSENS_MAX_RUNDEN + 1):
probleme = _probleme_schema(_json_datei(verify_paths[runde]))
if probleme is None:
await _set_step(guide_id, 2, f"Prüfe OnePager (Runde {runde}/{KONSENS_MAX_RUNDEN})…")
verify_paths[runde].unlink(missing_ok=True)
status, probleme = await run_single_slot(
ctx, f"OnePager-Prüfung r{runde}",
key=f"{guide_id}-verify-r{runde}",
prompt=_prompt("OnePager-Verifikation", topic=topic, recherche=recherche, karten=karten_block(), out_path=verify_paths[runde]),
role="judge", capabilities="files",
payload=lambda result, p=verify_paths[runde]: _probleme_schema(_json_datei(p)),
timeout=_timeout("onepager_verify"),
)
if status == CANCELLED:
return None
if status == FAILED:
if runde == 1:
await _fail(guide_id, "OnePager-Prüfung fehlgeschlagen")
return None
_log(topic, f"OnePager-Prüfung Runde {runde} fehlgeschlagen — letzte gültige Version bleibt")
break
if not probleme:
break
if runde == KONSENS_MAX_RUNDEN:
_log(topic, f"OnePager-Prüfung: {len(probleme)} Problem(e) bleiben nach Runde {runde} stehen")
break
_log(topic, f"OnePager-Prüfung Runde {runde}: {len(probleme)} Problem(e) notiert")
fixed = karten_schema(_json_datei(fix_paths[runde])) # Resume
if fixed is None:
await _set_step(guide_id, 2, f"Überarbeite OnePager (Runde {runde})…")
status, fixed = await run_single_slot(
ctx, f"OnePager-Fix r{runde}",
key=f"{guide_id}-karten-fix-r{runde}",
prompt=_prompt(
"OnePager-Fix",
topic=topic, recherche=recherche, karten=karten_block(),
probleme="\n".join(f"- {p}" for p in probleme),
out_path=fix_paths[runde], extra=_extra(instructions),
),
role="fast", capabilities="files",
payload=lambda result, p=fix_paths[runde]: karten_schema(_json_datei(p)),
timeout=_timeout("onepager_bauen"),
)
if status == CANCELLED:
return None
if status == FAILED:
_log(topic, f"OnePager-Fix Runde {runde} ungültig — letzte gültige Version bleibt")
break
karten = fixed
sections = [