update
This commit is contained in:
@@ -11,6 +11,7 @@ die Gesamtliste.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import shutil
|
||||
@@ -19,11 +20,10 @@ import time
|
||||
from pathlib import Path
|
||||
|
||||
import database as db
|
||||
from agents import kill_process, cancel_scope, clear_scope
|
||||
from agents import kill_process, cancel_scope, clear_scope, run_agent
|
||||
from config import KONSENS_GRACE, RECHERCHE_GRACE, KONSENS_MAX_RUNDEN, DEFAULT_PROVIDER, CRAWL_KEEP_PATTERNS, CRAWL_NOISE_PATTERNS, CRAWL_MIN_CHARS
|
||||
from fsutil import atomic_write_text, atomic_write_json
|
||||
from jsonio import read_json_file as _json_datei
|
||||
from lernen import FRAGETYPEN
|
||||
from paths import arbeit_dir, bausteine_path, frage_muster_path, project_dir, subbausteine_path, quelle_path, quelle_crawl_dir, safe_ordner
|
||||
from crawl import crawl
|
||||
from pipeline import (
|
||||
@@ -51,6 +51,8 @@ SUBBAUSTEIN_KAPPE = 900 # Subbaustein-Finde-Loop je Chunk (15 min)
|
||||
KONSOLIDIERUNG_CHUNK = 600 # bis hierher EIN globaler Judge (dedupt alles); darüber chunked + Merge-Pass
|
||||
# Frage-Muster-Chunks per LPT nach Sub-Last balancieren (Makespan), statt nach Baustein-Anzahl.
|
||||
FRAGE_CHUNK_SUBS = 50 # Ziel-Summe relevanter Subs je Chunk
|
||||
FAKTEN_CHUNK_SUBS = 25 # Fakten-Extraktion: kleinere Chunks (Fakten sind umfangreicher als Muster)
|
||||
FAKTEN_CHECK_PANEL = 3 # Judges je Chunk im Fakten-Check (Mehrheit beanstandet)
|
||||
|
||||
log = logging.getLogger("creator.bausteine")
|
||||
|
||||
@@ -62,10 +64,14 @@ _bausteine_step: dict[str, int] = {}
|
||||
BAUSTEINE_STEPS = (
|
||||
"Recherche", "Konsolidierung", "Klärung",
|
||||
"Subbausteine finden", "Subbausteine wählen", "Subbausteine klären",
|
||||
"Fakten finden", "Fakten prüfen", "Fakten fix",
|
||||
"Stufen finden", "Stufen wählen", "Stufen klären",
|
||||
"Relevanz finden", "Relevanz wählen", "Relevanz klären",
|
||||
"Gliederung",
|
||||
"Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen",
|
||||
"Karteikarten", "Beispiele", "Diagramme",
|
||||
)
|
||||
ARTEFAKT_TYPEN = ("karteikarte", "beispiel", "diagramm")
|
||||
|
||||
|
||||
def lade_quelle(topic: str) -> dict:
|
||||
@@ -93,7 +99,8 @@ def _crawl_fertig(topic: str) -> bool:
|
||||
return (quelle_crawl_dir(topic) / ".done").exists() # Marker erst bei sauberem Abschluss
|
||||
|
||||
|
||||
_STUFEN = ("einfach", "mittel", "schwer")
|
||||
# Lernpfad-Stufen (anfaenger/fortgeschritten/experte); alte Schwierigkeits-Werte abwärtskompatibel.
|
||||
_STUFEN = ("anfaenger", "fortgeschritten", "experte", "einfach", "mittel", "schwer")
|
||||
|
||||
|
||||
async def subbausteine_titel(topic: str, baustein: str) -> list[str]:
|
||||
@@ -115,19 +122,36 @@ async def lade_frage_muster(topic: str, baustein: str) -> list[dict]:
|
||||
"""Vordefinierte Frage-Muster eines Bausteins — DB-first, Fallback Sidecar (leer = Live)."""
|
||||
rows = await db.list_frage_muster(topic, _norm_titel(baustein))
|
||||
if rows:
|
||||
return [{"subbaustein": r["sub_titel"], "typ": r["typ"], "frage": r["frage"]} for r in rows if r["frage"]]
|
||||
return [{"subbaustein": r["sub_titel"], "frage": r["frage"]} for r in rows if r["frage"]]
|
||||
fm = _json_datei(frage_muster_path(topic))
|
||||
if not isinstance(fm, dict):
|
||||
return []
|
||||
return [
|
||||
{"subbaustein": str(e.get("subbaustein", "")).strip(),
|
||||
"typ": str(e.get("typ", "")).strip(),
|
||||
"frage": frage}
|
||||
{"subbaustein": str(e.get("subbaustein", "")).strip(), "frage": frage}
|
||||
for e in (fm.get(baustein) or [])
|
||||
if isinstance(e, dict) and (frage := str(e.get("frage", "")).strip())
|
||||
]
|
||||
|
||||
|
||||
async def subbausteine_frei(topic: str, baustein: str, max_ebene: int) -> list[str]:
|
||||
"""Subbaustein-Titel bis zur freigeschalteten Ebene (≤ max_ebene). Fallback ohne
|
||||
Ebenen-Wissen (Altbestand/Sidecar): alle Subbaustein-Titel."""
|
||||
rows = await db.subs_mit_ebene(topic, baustein)
|
||||
if not rows:
|
||||
return await subbausteine_titel(topic, baustein)
|
||||
return [s["titel"] for s in rows if s["ebene"] <= max_ebene and s["titel"]]
|
||||
|
||||
|
||||
async def lade_frage_muster_frei(topic: str, baustein: str, max_ebene: int) -> list[dict]:
|
||||
"""Frage-Muster, gefiltert auf Subbausteine bis zur freigeschalteten Ebene. Ohne
|
||||
Ebenen-Wissen (Altbestand/Sidecar) ungefiltert."""
|
||||
rows = await db.subs_mit_ebene(topic, baustein)
|
||||
if not rows:
|
||||
return await lade_frage_muster(topic, baustein)
|
||||
frei = {s["norm"] for s in rows if s["ebene"] <= max_ebene}
|
||||
return [m for m in await lade_frage_muster(topic, baustein) if _norm_titel(m["subbaustein"]) in frei]
|
||||
|
||||
|
||||
async def lade_uebersicht(topic: str) -> list[dict]:
|
||||
"""Strukturierte Baustein-Liste für die Übersicht — DB-first (Konsens + Subs/Stufen/Relevanz),
|
||||
Fallback bausteine.md + Sidecar (Alt-Themen)."""
|
||||
@@ -140,7 +164,7 @@ async def lade_uebersicht(topic: str) -> list[dict]:
|
||||
"num": num, "titel": b["titel"], "beschreibung": b["beschreibung"],
|
||||
"subbausteine": [
|
||||
{"titel": s["sub_titel"],
|
||||
"stufe": s["stufe"] if s["stufe"] in _STUFEN else "mittel",
|
||||
"stufe": s["stufe"] if s["stufe"] in _STUFEN else "fortgeschritten",
|
||||
"relevanz": s["relevanz"] if s["relevanz"] in ("relevant", "rand") else None}
|
||||
for s in subs if s["sub_titel"]
|
||||
],
|
||||
@@ -157,7 +181,7 @@ async def lade_uebersicht(topic: str) -> list[dict]:
|
||||
subbausteine = [
|
||||
{
|
||||
"titel": t,
|
||||
"stufe": s.get("stufe") if s.get("stufe") in _STUFEN else "mittel",
|
||||
"stufe": s.get("stufe") if s.get("stufe") in _STUFEN else "fortgeschritten",
|
||||
"relevanz": s.get("relevanz") if s.get("relevanz") in ("relevant", "rand") else None,
|
||||
}
|
||||
for s in (sidecar.get(titel) or [])
|
||||
@@ -177,9 +201,12 @@ def _bausteine_steps(topic: str) -> tuple:
|
||||
base = ("Recherche", "Konsolidierung", "Klärung")
|
||||
rest = (
|
||||
"Subbausteine finden", "Subbausteine wählen", "Subbausteine klären",
|
||||
"Fakten finden", "Fakten prüfen", "Fakten fix",
|
||||
"Stufen finden", "Stufen wählen", "Stufen klären",
|
||||
"Relevanz finden", "Relevanz wählen", "Relevanz klären",
|
||||
"Gliederung",
|
||||
"Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen",
|
||||
"Karteikarten", "Beispiele", "Diagramme",
|
||||
)
|
||||
mitte = base + (("Ergänzung",) if q["type"] == "projekt" else ()) + rest
|
||||
return (("Quelle aufbereiten",) if q["type"] == "link" else ()) + mitte
|
||||
@@ -203,9 +230,12 @@ PHASEN = (
|
||||
("Quelle", ("Quelle aufbereiten",)),
|
||||
("Inventar", ("Recherche", "Konsolidierung", "Klärung", "Ergänzung")),
|
||||
("Subbausteine", ("Subbausteine finden", "Subbausteine wählen", "Subbausteine klären")),
|
||||
("Fakten", ("Fakten finden", "Fakten prüfen", "Fakten fix")),
|
||||
("Stufen", ("Stufen finden", "Stufen wählen", "Stufen klären")),
|
||||
("Relevanz", ("Relevanz finden", "Relevanz wählen", "Relevanz klären")),
|
||||
("Gliederung", ("Gliederung",)),
|
||||
("Fragen", ("Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen")),
|
||||
("Artefakte", ("Karteikarten", "Beispiele", "Diagramme")),
|
||||
)
|
||||
|
||||
|
||||
@@ -244,22 +274,28 @@ def _bausteine_files(topic: str) -> dict:
|
||||
"mapping": {n: arbeit / f"auswahl-mapping-r{n}.json" for n in runden},
|
||||
"ergaenzung": arbeit / "ergaenzung.json",
|
||||
"sub_roh": arbeit / "subbausteine-roh.json",
|
||||
"fakten": arbeit / "subbausteine-fakten.json",
|
||||
"sidecar": subbausteine_path(topic),
|
||||
"frage_muster": frage_muster_path(topic),
|
||||
"gliederung": arbeit / "gliederung.json",
|
||||
"gliederung_slots": [arbeit / f"gliederung-{i}.json" for i in (1, 2, 3)],
|
||||
"artefakte": arbeit / "artefakte.json",
|
||||
}
|
||||
|
||||
|
||||
def _alle_slot_dateien(files: dict) -> list[Path]:
|
||||
arbeit = files["arbeit"]
|
||||
# Subbaustein-/Stufen-Slots sind pro Chunk dynamisch — per Glob einsammeln.
|
||||
dyn = (list(arbeit.glob("subbaustein-*")) + list(arbeit.glob("stufe-*")) + list(arbeit.glob("relevanz-*"))
|
||||
+ list(arbeit.glob("frage-muster-*")) + list(arbeit.glob("recherche-*")) + list(arbeit.glob("konsolidierung-*"))
|
||||
dyn = (list(arbeit.glob("subbaustein-*")) + list(arbeit.glob("fakten-*")) + list(arbeit.glob("stufe-*")) + list(arbeit.glob("relevanz-*"))
|
||||
+ list(arbeit.glob("frage-muster-*")) + list(arbeit.glob("gliederung-*")) + list(arbeit.glob("artefakt-*"))
|
||||
+ list(arbeit.glob("recherche-*")) + list(arbeit.glob("konsolidierung-*"))
|
||||
+ list(arbeit.glob("klaerung*"))) if arbeit.is_dir() else []
|
||||
return [
|
||||
*files["recherche"], files["recherche_mapping"],
|
||||
*(p for slots in files["auswahl"].values() for p in slots),
|
||||
*files["mapping"].values(), files["ergaenzung"],
|
||||
files["sub_roh"], files["sidecar"], files["frage_muster"], *dyn,
|
||||
files["sub_roh"], files["sidecar"], files["frage_muster"],
|
||||
files["fakten"], files["gliederung"], files["artefakte"], *dyn,
|
||||
]
|
||||
|
||||
|
||||
@@ -288,12 +324,21 @@ def _resume_step(topic: str) -> int:
|
||||
# Stufen fertig; nur noch Relevanz offen?
|
||||
if not _relevanz_komplett(sidecar):
|
||||
return _step_idx(topic, "Relevanz finden")
|
||||
# Relevanz fertig; nur noch Frage-Muster offen?
|
||||
# Relevanz fertig; Gliederung (Bausteine-Artefakt für den Guide) offen?
|
||||
if not _gliederung_komplett(files):
|
||||
return _step_idx(topic, "Gliederung")
|
||||
# Gliederung fertig; Frage-Muster offen?
|
||||
if not _frage_muster_komplett(topic):
|
||||
return _step_idx(topic, "Fragen finden")
|
||||
# Fragen fertig; Lern-Artefakte (Karteikarten/Beispiele/Diagramme) offen?
|
||||
if not _artefakte_komplett(files):
|
||||
return _step_idx(topic, "Karteikarten")
|
||||
return len(_bausteine_steps(topic))
|
||||
if _sub_roh_schema(_json_datei(files["sub_roh"])) is None:
|
||||
return _step_idx(topic, "Subbausteine finden")
|
||||
# Subbausteine fertig; Fakten noch offen? (Fakten kommen vor den Stufen.)
|
||||
if not _fakten_komplett(files):
|
||||
return _step_idx(topic, "Fakten finden")
|
||||
return _step_idx(topic, "Stufen finden")
|
||||
|
||||
|
||||
@@ -356,12 +401,19 @@ def _reset_ab_phase(topic: str, label: str) -> None:
|
||||
for p in arbeit.glob(pat):
|
||||
p.unlink(missing_ok=True)
|
||||
|
||||
if idx <= 5: # Fragen
|
||||
# Phasen-Index: Quelle=0 · Inventar=1 · Subbausteine=2 · Fakten=3 · Stufen=4 · Relevanz=5 · Gliederung=6 · Fragen=7 · Artefakte=8
|
||||
if idx <= 8: # Artefakte (Karteikarten/Beispiele/Diagramme)
|
||||
files["artefakte"].unlink(missing_ok=True)
|
||||
glob_del("artefakt-*")
|
||||
if idx <= 7: # Fragen
|
||||
files["frage_muster"].unlink(missing_ok=True)
|
||||
glob_del("frage-muster-*")
|
||||
if idx <= 4: # Relevanz
|
||||
if idx <= 6: # Gliederung
|
||||
files["gliederung"].unlink(missing_ok=True)
|
||||
glob_del("gliederung-*")
|
||||
if idx <= 5: # Relevanz
|
||||
glob_del("relevanz-*")
|
||||
if idx <= 3: # Stufen + Relevanz teilen die Sidecar → ab Stufen ganz neu
|
||||
if idx <= 4: # Stufen + Relevanz teilen die Sidecar → ab Stufen ganz neu
|
||||
files["sidecar"].unlink(missing_ok=True)
|
||||
glob_del("stufe-*")
|
||||
else: # ab Relevanz: Stufen behalten, nur Relevanz-Felder strippen
|
||||
@@ -372,6 +424,9 @@ def _reset_ab_phase(topic: str, label: str) -> None:
|
||||
if isinstance(s, dict):
|
||||
s.pop("relevanz", None)
|
||||
atomic_write_json(files["sidecar"], sc, indent=1)
|
||||
if idx <= 3: # Fakten (vor den Stufen) — Fakten-Map + Arbeitsdateien weg
|
||||
files["fakten"].unlink(missing_ok=True)
|
||||
glob_del("fakten-*")
|
||||
if idx <= 2: # Subbausteine
|
||||
files["sub_roh"].unlink(missing_ok=True)
|
||||
glob_del("subbaustein-*")
|
||||
@@ -474,7 +529,7 @@ def _sidecar_schema(data):
|
||||
if not isinstance(v, list) or not v:
|
||||
return None
|
||||
for s in v:
|
||||
if not isinstance(s, dict) or not str(s.get("titel", "")).strip() or s.get("stufe") not in ("einfach", "mittel", "schwer"):
|
||||
if not isinstance(s, dict) or not str(s.get("titel", "")).strip() or s.get("stufe") not in _STUFEN:
|
||||
return None
|
||||
return data
|
||||
|
||||
@@ -493,10 +548,10 @@ def _relevanz_komplett(data) -> bool:
|
||||
|
||||
|
||||
def _frage_muster_chunk_schema(data) -> list[dict] | None:
|
||||
"""{"muster": [{baustein, subbaustein, typ, frage}, …]} → Liste valider Einträge · sonst None.
|
||||
"""{"muster": [{baustein, subbaustein, frage}, …]} → Liste valider Einträge · sonst None.
|
||||
|
||||
Mit `baustein` (Zuordnung im 10er-Chunk). Ungültige Einzel-Einträge werden
|
||||
übersprungen (nicht die ganze Liste verworfen)."""
|
||||
Ein Muster je Subbaustein (kein Typ-Kreuzprodukt — die Schwierigkeit kommt erst bei der
|
||||
Prüfung aus dem Lerner-Niveau). Ungültige Einzel-Einträge werden übersprungen."""
|
||||
if not isinstance(data, dict) or not isinstance(data.get("muster"), list):
|
||||
return None
|
||||
out = []
|
||||
@@ -505,11 +560,10 @@ def _frage_muster_chunk_schema(data) -> list[dict] | None:
|
||||
continue
|
||||
bau = str(e.get("baustein", "")).strip()
|
||||
sub = str(e.get("subbaustein", "")).strip()
|
||||
typ = str(e.get("typ", "")).strip().casefold()
|
||||
frage = str(e.get("frage", "")).strip()
|
||||
if not bau or not sub or typ not in FRAGETYPEN or not frage:
|
||||
if not bau or not sub or not frage:
|
||||
continue
|
||||
out.append({"baustein": bau, "subbaustein": sub, "typ": typ, "frage": frage})
|
||||
out.append({"baustein": bau, "subbaustein": sub, "frage": frage})
|
||||
return out or None
|
||||
|
||||
|
||||
@@ -801,10 +855,10 @@ async def _stufen_block(ctx: GenContext, set_p, files: dict, roh: dict, instruct
|
||||
timeout=_timeout("stufe_check", len(strittig)),
|
||||
)
|
||||
if status == FAILED:
|
||||
_log(topic, f"Stufen-Klärung Paket {c} fehlgeschlagen — Default 'mittel'")
|
||||
_log(topic, f"Stufen-Klärung Paket {c} fehlgeschlagen — Default 'fortgeschritten'")
|
||||
entsch = entsch if isinstance(entsch, dict) else {}
|
||||
# Strittige ohne Entscheid → 'mittel'; Vote-Gewinner bleiben; Judge überschreibt.
|
||||
ergebnis = {**{k: "mittel" for k in strittig}, **ergebnis, **entsch}
|
||||
# Strittige ohne Entscheid → 'fortgeschritten'; Vote-Gewinner bleiben; Judge überschreibt.
|
||||
ergebnis = {**{k: "fortgeschritten" for k in strittig}, **ergebnis, **entsch}
|
||||
return {item_idxs[k - 1] + 1: stufe for k, stufe in ergebnis.items()}
|
||||
|
||||
parts = await _gather_fortschritt([_klaere(c, idxs) for c, idxs in enumerate(chunks, 1)], len(chunks), _melde_p(set_p, topic, "Stufen klären"))
|
||||
@@ -813,12 +867,12 @@ async def _stufen_block(ctx: GenContext, set_p, files: dict, roh: dict, instruct
|
||||
stufe_by_id: dict[int, str] = {}
|
||||
for c, part in enumerate(parts, 1):
|
||||
if not isinstance(part, dict):
|
||||
# Klärung ist nicht fatal: Vote-Ergebnis + Default 'mittel' für Strittige.
|
||||
# Klärung ist nicht fatal: Vote-Ergebnis + Default 'fortgeschritten' für Strittige.
|
||||
if isinstance(part, BaseException):
|
||||
_log(topic, f"Stufen-Klärung Paket {c}: {type(part).__name__}: {part}")
|
||||
ergebnis, strittig = vote_by_c[c]
|
||||
item_idxs = chunks[c - 1]
|
||||
merged = {**{k: "mittel" for k in strittig}, **ergebnis}
|
||||
merged = {**{k: "fortgeschritten" for k in strittig}, **ergebnis}
|
||||
part = {item_idxs[k - 1] + 1: s for k, s in merged.items()}
|
||||
stufe_by_id.update(part)
|
||||
|
||||
@@ -829,11 +883,196 @@ async def _stufen_block(ctx: GenContext, set_p, files: dict, roh: dict, instruct
|
||||
lst = []
|
||||
for sub in subs:
|
||||
gid += 1
|
||||
lst.append({"titel": sub, "stufe": stufe_by_id.get(gid, "mittel")})
|
||||
lst.append({"titel": sub, "stufe": stufe_by_id.get(gid, "fortgeschritten")})
|
||||
sidecar[titel] = lst
|
||||
return sidecar
|
||||
|
||||
|
||||
_FAKTEN_FELDER = ("kernpunkte", "voraussetzungen", "huerden", "belegte_fakten", "beispiel_idee")
|
||||
|
||||
|
||||
def _fakten_schema(data) -> list[dict] | None:
|
||||
"""{"fakten": [{baustein, subbaustein, …}]} → valide Liste · sonst None.
|
||||
Trennt belegte_fakten (mit Quelle) hart von beispiel_idee (generativ)."""
|
||||
if not isinstance(data, dict) or not isinstance(data.get("fakten"), list):
|
||||
return None
|
||||
out = []
|
||||
for e in data["fakten"]:
|
||||
if not isinstance(e, dict):
|
||||
continue
|
||||
bau = str(e.get("baustein", "")).strip()
|
||||
sub = str(e.get("subbaustein", "")).strip()
|
||||
if not bau or not sub:
|
||||
continue
|
||||
bf = [{"text": t, "quelle": str(f.get("quelle", "")).strip()}
|
||||
for f in (e.get("belegte_fakten") or []) if isinstance(f, dict) and (t := str(f.get("text", "")).strip())]
|
||||
out.append({
|
||||
"baustein": bau, "subbaustein": sub,
|
||||
"kernpunkte": [k for x in (e.get("kernpunkte") or []) if (k := str(x).strip())],
|
||||
"voraussetzungen": str(e.get("voraussetzungen", "")).strip(),
|
||||
"huerden": str(e.get("huerden", "")).strip(),
|
||||
"belegte_fakten": bf,
|
||||
"beispiel_idee": str(e.get("beispiel_idee", "")).strip(),
|
||||
})
|
||||
return out or None
|
||||
|
||||
|
||||
def _fakten_check_schema(data) -> list[str] | None:
|
||||
"""Fakten-Check → beanstandete sub_norms · {ok:true}→[] · None bei ungültig."""
|
||||
if not isinstance(data, dict):
|
||||
return None
|
||||
if data.get("ok") is True:
|
||||
return []
|
||||
pr = data.get("probleme")
|
||||
if not isinstance(pr, list):
|
||||
return None
|
||||
return [sn for p in pr if isinstance(p, dict) and (sn := _norm_titel(str(p.get("subbaustein", ""))))]
|
||||
|
||||
|
||||
def _fakten_zeilen(fk: dict) -> str:
|
||||
z = []
|
||||
if fk.get("kernpunkte"):
|
||||
z.append("Kernpunkte: " + " · ".join(fk["kernpunkte"]))
|
||||
if fk.get("voraussetzungen"):
|
||||
z.append("Voraussetzung: " + fk["voraussetzungen"])
|
||||
if fk.get("huerden"):
|
||||
z.append("Hürde: " + fk["huerden"])
|
||||
for bf in fk.get("belegte_fakten", []):
|
||||
z.append(f"FAKT: {bf['text']} (Quelle: {bf.get('quelle', '?')})")
|
||||
if fk.get("beispiel_idee"):
|
||||
z.append("Beispiel: " + fk["beispiel_idee"])
|
||||
return "\n".join(z)
|
||||
|
||||
|
||||
def _fakten_komplett(files: dict) -> bool:
|
||||
"""Fakten-Map existiert (Block durch)? {Baustein: {sub_norm: {...}}}."""
|
||||
d = _json_datei(files["fakten"])
|
||||
return isinstance(d, dict) and bool(d)
|
||||
|
||||
|
||||
async def _fakten_block(ctx, set_p, files: dict, roh: dict, q: dict, ordner, instructions: str) -> dict | None:
|
||||
"""Block: je Sub Quell-Fakten extrahieren (finden) → verifizieren (prüfen) → korrigieren (fix).
|
||||
Extract-once-Grounding: das Ergebnis nährt Stufe/Relevanz/Fragen/Guide.
|
||||
→ {Baustein-Titel: {sub_norm: fakten-dict}} oder None bei Abbruch/Fehler."""
|
||||
topic, provider, is_cancelled = ctx.topic, ctx.provider, ctx.is_cancelled
|
||||
arbeit = files["arbeit"]
|
||||
caps = "files" if ordner else "full"
|
||||
typ = q.get("type", "thema")
|
||||
source = _prompt(_QUELLE_TEMPLATE[typ], project=ordner) if typ in _QUELLE_TEMPLATE else _prompt("Bausteine-Quelle-Thema", topic=topic)
|
||||
bausteine = [(titel, [str(s).strip() for s in subs if str(s).strip()]) for titel, subs in roh.items() if subs]
|
||||
if not bausteine:
|
||||
return {}
|
||||
chunks = _lpt_chunks([len(subs) for _, subs in bausteine], FAKTEN_CHUNK_SUBS)
|
||||
|
||||
def roh_path(ci): return arbeit / f"fakten-c{ci}.json"
|
||||
def chk_path(ci, j): return arbeit / f"fakten-check-c{ci}-j{j}.json"
|
||||
def fix_path(ci): return arbeit / f"fakten-fix-c{ci}.json"
|
||||
def ctitel(idxs): return [bausteine[i][0] for i in idxs]
|
||||
def block_text(idxs):
|
||||
return "\n\n".join(
|
||||
f"BAUSTEIN: {bausteine[i][0]}\nSUBBAUSTEINE:\n" + "\n".join(f"- {s}" for s in bausteine[i][1])
|
||||
for i in idxs)
|
||||
|
||||
# Roh-Fakten eines Chunks → {Baustein: {sub_norm: {sub, …felder}}}, auf Chunk-Titel gematcht.
|
||||
def roh_map(ci, path):
|
||||
idxs = chunks[ci]
|
||||
rel_by = {bausteine[i][0]: bausteine[i][1] for i in idxs}
|
||||
ct = ctitel(idxs)
|
||||
out: dict[str, dict] = {}
|
||||
for e in _fakten_schema(_json_datei(path)) or []:
|
||||
bt = _match_sub(e["baustein"], ct)
|
||||
if bt not in rel_by:
|
||||
continue
|
||||
sub = _match_sub(e["subbaustein"], rel_by[bt])
|
||||
out.setdefault(bt, {})[_norm_titel(sub)] = {"sub": sub, **{k: e[k] for k in _FAKTEN_FELDER}}
|
||||
return out
|
||||
|
||||
# Phase „Fakten finden": 1 Generator je Chunk.
|
||||
async def _finde(ci, idxs):
|
||||
fp = roh_path(ci)
|
||||
if _fakten_schema(_json_datei(fp)):
|
||||
return True
|
||||
subs_total = sum(len(bausteine[i][1]) for i in idxs)
|
||||
status, _r = await run_single_slot(
|
||||
ctx, f"Fakten {ci}", key=f"bausteine-{topic}-fakten-c{ci}",
|
||||
prompt=_prompt("Fakten-Recherche", topic=topic, source=source, bausteine=block_text(idxs), out_path=fp, extra=_extra(instructions)),
|
||||
role="guide", capabilities=caps,
|
||||
payload=lambda result, p=fp: _fakten_schema(_json_datei(p)),
|
||||
timeout=_timeout("inhalt", subs_total))
|
||||
return status != FAILED and _fakten_schema(_json_datei(fp)) is not None
|
||||
|
||||
oks = await _gather_fortschritt([_finde(ci, idxs) for ci, idxs in enumerate(chunks)], len(chunks), _melde_p(set_p, topic, "Fakten finden"))
|
||||
if is_cancelled():
|
||||
return None
|
||||
if not any(ok is True for ok in oks):
|
||||
_bausteine_errors[topic] = "Fakten-Extraktion fehlgeschlagen"
|
||||
return None
|
||||
|
||||
# Phase „Fakten prüfen": FAKTEN_CHECK_PANEL Judges je Chunk → beanstandete sub_norms (Mehrheit).
|
||||
async def _pruefe(ci, idxs):
|
||||
per = roh_map(ci, roh_path(ci))
|
||||
if not per:
|
||||
return ci, set()
|
||||
fakten_text = "\n\n".join(f"SUBBAUSTEIN: {fk['sub']}\n{_fakten_zeilen(fk)}" for fm in per.values() for fk in fm.values())
|
||||
offen = [j for j in (1, 2, 3)[:FAKTEN_CHECK_PANEL] if _fakten_check_schema(_json_datei(chk_path(ci, j))) is None]
|
||||
await asyncio.gather(*[
|
||||
run_agent(f"bausteine-{topic}-fakten-check-c{ci}-j{j}",
|
||||
_prompt("Fakten-Check", topic=topic, source=source, fakten=fakten_text, out_path=chk_path(ci, j), extra=_extra(instructions)),
|
||||
_timeout("inhalt_check", len(per)), provider=provider, role="judge", capabilities=caps)
|
||||
for j in offen], return_exceptions=True)
|
||||
outs = [s for j in (1, 2, 3)[:FAKTEN_CHECK_PANEL] if (s := _fakten_check_schema(_json_datei(chk_path(ci, j)))) is not None]
|
||||
votes: dict[str, int] = {}
|
||||
for s in outs:
|
||||
for sn in set(s):
|
||||
votes[sn] = votes.get(sn, 0) + 1
|
||||
schwelle = len(outs) / 2 if outs else 99
|
||||
return ci, {sn for sn, v in votes.items() if v > schwelle}
|
||||
|
||||
pruef = await _gather_fortschritt([_pruefe(ci, idxs) for ci, idxs in enumerate(chunks)], len(chunks), _melde_p(set_p, topic, "Fakten prüfen"))
|
||||
if is_cancelled():
|
||||
return None
|
||||
beanstandet = {ci: subs for r in pruef if isinstance(r, tuple) for ci, subs in [r]}
|
||||
|
||||
# Phase „Fakten fix": je Chunk mit beanstandeten Subs → diese neu extrahieren (frische Quelle).
|
||||
n_problem = sum(len(s) for s in beanstandet.values())
|
||||
if n_problem:
|
||||
set_p(f"Fakten korrigieren ({n_problem})…", step=_step_idx(topic, "Fakten fix"))
|
||||
async def _fix(ci):
|
||||
subs_norm = beanstandet.get(ci, set())
|
||||
if not subs_norm or _fakten_schema(_json_datei(fix_path(ci))):
|
||||
return
|
||||
idxs = chunks[ci]
|
||||
rel_by = {bausteine[i][0]: bausteine[i][1] for i in idxs}
|
||||
# nur die beanstandeten Subs als Block
|
||||
ziel = []
|
||||
for bt, subs in rel_by.items():
|
||||
betroffen = [s for s in subs if _norm_titel(s) in subs_norm]
|
||||
if betroffen:
|
||||
ziel.append(f"BAUSTEIN: {bt}\nSUBBAUSTEINE:\n" + "\n".join(f"- {s}" for s in betroffen))
|
||||
if not ziel:
|
||||
return
|
||||
await run_single_slot(
|
||||
ctx, f"Fakten-Fix {ci}", key=f"bausteine-{topic}-fakten-fix-c{ci}",
|
||||
prompt=_prompt("Fakten-Recherche", topic=topic, source=source, bausteine="\n\n".join(ziel), out_path=fix_path(ci), extra=_extra(instructions)),
|
||||
role="guide", capabilities=caps,
|
||||
payload=lambda result, p=fix_path(ci): _fakten_schema(_json_datei(p)),
|
||||
timeout=_timeout("inhalt", len(subs_norm)))
|
||||
await _gather_fortschritt([_fix(ci) for ci in beanstandet], len(beanstandet), _melde_p(set_p, topic, "Fakten fix"))
|
||||
if is_cancelled():
|
||||
return None
|
||||
|
||||
# Zusammensetzen: roh + Fix-Overrides für beanstandete (Vollständigkeits-Guard: fehlt der Fix, bleibt roh).
|
||||
ergebnis: dict[str, dict] = {}
|
||||
for ci in range(len(chunks)):
|
||||
per = roh_map(ci, roh_path(ci))
|
||||
fix = roh_map(ci, fix_path(ci)) if fix_path(ci).exists() else {}
|
||||
for bt, fm in per.items():
|
||||
for sn, fk in fm.items():
|
||||
gewinner = fix.get(bt, {}).get(sn, fk) if sn in beanstandet.get(ci, set()) else fk
|
||||
ergebnis.setdefault(bt, {})[sn] = {k: gewinner[k] for k in _FAKTEN_FELDER}
|
||||
return ergebnis
|
||||
|
||||
|
||||
async def _relevanz_block(ctx: GenContext, set_p, files: dict, sidecar: dict, instructions: str) -> dict | None:
|
||||
"""Block D: drei Phasen mit Barriere — Finden (relevant/rand), Wählen (Vote), Klären.
|
||||
Items aus der Sidecar; lokale IDs 1..n pro Paket → globale gid.
|
||||
@@ -963,18 +1202,17 @@ async def _frage_muster_block(ctx: GenContext, set_p, files: dict, sidecar: dict
|
||||
"""Block E (10er-Chunks): Finden (1 Generator je ~10 Bausteine, parallel), Wählen (Code:
|
||||
je Baustein gruppieren + Dedup), Klären (1 Kritiker je Chunk), Prüfen (Nachrunde).
|
||||
Zuordnung je Eintrag über das `baustein`-Feld (Chunk-Datei trägt mehrere Bausteine).
|
||||
→ {Baustein-Titel: [{subbaustein, typ, frage}, …]} oder None bei Abbruch."""
|
||||
→ {Baustein-Titel: [{subbaustein, frage}, …]} oder None bei Abbruch."""
|
||||
topic, is_cancelled = ctx.topic, ctx.is_cancelled
|
||||
arbeit = files["arbeit"]
|
||||
# Je Baustein nur RELEVANTE Subbausteine (relevanz != 'rand'; None zählt als relevant).
|
||||
# ALLE Subbausteine (auch rand) bekommen ein Muster — Rand ist in der FGuide-Ebene prüfbar.
|
||||
bausteine = []
|
||||
for titel, subs in sidecar.items():
|
||||
rel = [s["titel"] for s in subs if isinstance(s, dict) and s.get("relevanz") != "rand" and str(s.get("titel", "")).strip()]
|
||||
if rel:
|
||||
bausteine.append((titel, rel))
|
||||
alle = [s["titel"] for s in subs if isinstance(s, dict) and str(s.get("titel", "")).strip()]
|
||||
if alle:
|
||||
bausteine.append((titel, alle))
|
||||
if not bausteine:
|
||||
return {}
|
||||
typen_block = "\n".join(f"- {k}: {v}" for k, v in FRAGETYPEN.items())
|
||||
chunks = _lpt_chunks([len(rel) for _, rel in bausteine], FRAGE_CHUNK_SUBS) # lastbalanciert nach Sub-Zahl
|
||||
|
||||
def roh_path(ci):
|
||||
@@ -1000,7 +1238,7 @@ async def _frage_muster_block(ctx: GenContext, set_p, files: dict, sidecar: dict
|
||||
ctx, f"Frage-Muster {ci}",
|
||||
key=f"bausteine-{topic}-frage-muster-c{ci}",
|
||||
prompt=_prompt("Frage-Muster-Recherche", topic=topic, bausteine=block,
|
||||
typen=typen_block, out_path=fp, extra=_extra(instructions)),
|
||||
out_path=fp, extra=_extra(instructions)),
|
||||
role="fast", capabilities="files",
|
||||
payload=lambda result, p=fp: _frage_muster_chunk_schema(_json_datei(p)),
|
||||
timeout=_timeout("frage_muster", subs_total),
|
||||
@@ -1027,13 +1265,12 @@ async def _frage_muster_block(ctx: GenContext, set_p, files: dict, sidecar: dict
|
||||
titel = _match_sub(e["baustein"], ctitel)
|
||||
if titel not in rel_by:
|
||||
continue # nicht zuordenbar → verwerfen
|
||||
norm = _norm_frage(e["frage"])
|
||||
sub = _match_sub(e["subbaustein"], rel_by[titel])
|
||||
seen = gesehen.setdefault(titel, set())
|
||||
if norm in seen:
|
||||
continue
|
||||
seen.add(norm)
|
||||
out.setdefault(titel, []).append(
|
||||
{"subbaustein": _match_sub(e["subbaustein"], rel_by[titel]), "typ": e["typ"], "frage": e["frage"]})
|
||||
if sub in seen:
|
||||
continue # genau ein Muster je Subbaustein
|
||||
seen.add(sub)
|
||||
out.setdefault(titel, []).append({"subbaustein": sub, "frage": e["frage"]})
|
||||
return out
|
||||
|
||||
def _waehle_all(ci_list):
|
||||
@@ -1057,7 +1294,7 @@ async def _frage_muster_block(ctx: GenContext, set_p, files: dict, sidecar: dict
|
||||
eintraege = roh_by_titel.get(t) or []
|
||||
if not eintraege:
|
||||
continue
|
||||
zeilen = "\n".join(f"- [{e['typ']}] ({e['subbaustein']}) {e['frage']}" for e in eintraege)
|
||||
zeilen = "\n".join(f"- ({e['subbaustein']}) {e['frage']}" for e in eintraege)
|
||||
bloecke.append(f"BAUSTEIN: {t}\n{zeilen}")
|
||||
if not bloecke:
|
||||
return # nichts zu klären in diesem Chunk
|
||||
@@ -1093,7 +1330,7 @@ async def _frage_muster_block(ctx: GenContext, set_p, files: dict, sidecar: dict
|
||||
if titel not in rel_by:
|
||||
continue
|
||||
out.setdefault(titel, []).append(
|
||||
{"subbaustein": _match_sub(e["subbaustein"], rel_by[titel]), "typ": e["typ"], "frage": e["frage"]})
|
||||
{"subbaustein": _match_sub(e["subbaustein"], rel_by[titel]), "frage": e["frage"]})
|
||||
return out
|
||||
|
||||
final_by_titel = _final_by_titel(range(len(chunks)))
|
||||
@@ -1415,6 +1652,226 @@ async def _klaere_inventar(ctx: GenContext, set_p, files: dict) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
# --- Gliederung (Bausteine-Artefakt: Kapitel-Struktur, vom Guide nur gelesen) ---
|
||||
|
||||
def _gliederung_komplett(files: dict) -> bool:
|
||||
"""Gliederung steht (Kapitel-Liste vorhanden)?"""
|
||||
d = _json_datei(files["gliederung"])
|
||||
return isinstance(d, dict) and isinstance(d.get("kapitel"), list) and bool(d.get("kapitel"))
|
||||
|
||||
|
||||
def _gliederung_schema(data, valid: set[int]):
|
||||
"""{"kapitel":[{titel,nummern}]} → bereinigt (valide Nummern, je genau einmal) ·
|
||||
None bei <80 % Abdeckung (Agent/Judge hat zu viel weggelassen)."""
|
||||
if not isinstance(data, dict) or not isinstance(data.get("kapitel"), list):
|
||||
return None
|
||||
out, seen = [], set()
|
||||
for ch in data["kapitel"]:
|
||||
if not isinstance(ch, dict):
|
||||
continue
|
||||
titel = str(ch.get("titel", "")).strip() or "Kapitel"
|
||||
nums = []
|
||||
for n in (ch.get("nummern") or []):
|
||||
try:
|
||||
n = int(n)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
if n in valid and n not in seen:
|
||||
seen.add(n)
|
||||
nums.append(n)
|
||||
if nums:
|
||||
out.append({"titel": titel, "nummern": nums})
|
||||
if not out or len(seen) < 0.8 * len(valid):
|
||||
return None
|
||||
return {"kapitel": out}
|
||||
|
||||
|
||||
async def _gliederung_block(ctx: GenContext, set_p, files: dict, entries: dict, instructions: str) -> dict:
|
||||
"""Format-agnostische Gliederung über ALLE Bausteine — 3 Vorschläge → Judge merged.
|
||||
Bricht nie ab: 0 gültige → ein Kapitel mit allem; fehlende Bausteine landen in „Weitere".
|
||||
→ {"kapitel":[{titel,nummern}]} (auch in files["gliederung"])."""
|
||||
topic, is_cancelled = ctx.topic, ctx.is_cancelled
|
||||
valid = set(entries)
|
||||
liste = "\n".join(f"{n}. {t}" for n, t in entries.items())
|
||||
step = _step_idx(topic, "Gliederung")
|
||||
set_p("Gliederung — Vorschläge…", step=step)
|
||||
|
||||
async def _vorschlag(i, path):
|
||||
if _gliederung_schema(_json_datei(path), valid):
|
||||
return True
|
||||
await run_single_slot(
|
||||
ctx, f"Gliederung {i}", key=f"bausteine-{topic}-gliederung-{i}",
|
||||
prompt=_prompt("Guide-Gliederung", topic=topic, bausteine=liste, out_path=path, extra=_extra(instructions)),
|
||||
role="guide", capabilities="files",
|
||||
payload=lambda result, p=path: _gliederung_schema(_json_datei(p), valid),
|
||||
timeout=_timeout("plan", len(entries)))
|
||||
return _gliederung_schema(_json_datei(path), valid) is not None
|
||||
|
||||
slots = files["gliederung_slots"]
|
||||
await _gather_fortschritt([_vorschlag(i, p) for i, p in enumerate(slots, 1)], len(slots), _melde_p(set_p, topic, "Gliederung"))
|
||||
if is_cancelled():
|
||||
return {}
|
||||
vorschlaege = [v for p in slots if (v := _gliederung_schema(_json_datei(p), valid))]
|
||||
|
||||
if not vorschlaege:
|
||||
plan = {"kapitel": [{"titel": "Inhalte", "nummern": list(entries)}]}
|
||||
elif len(vorschlaege) == 1:
|
||||
plan = vorschlaege[0]
|
||||
else:
|
||||
set_p("Gliederung zusammenführen…", step=step)
|
||||
bloecke = "\n\n".join(
|
||||
f"### Vorschlag {i}\n" + "\n".join(
|
||||
f"KAPITEL: {ch['titel']}\n Nummern: {', '.join(str(n) for n in ch['nummern'])}" for ch in v["kapitel"])
|
||||
for i, v in enumerate(vorschlaege, 1))
|
||||
await run_single_slot(
|
||||
ctx, "Gliederung-Judge", key=f"bausteine-{topic}-gliederung-judge",
|
||||
prompt=_prompt("Guide-Gliederung-Judge", topic=topic, format_name="den Guide",
|
||||
zweck="alle Bausteine in einem roten Faden", n=len(vorschlaege),
|
||||
bausteine=liste, gliederungen=bloecke, out_path=files["gliederung"], extra=_extra(instructions)),
|
||||
role="judge", capabilities="files",
|
||||
payload=lambda result: _gliederung_schema(_json_datei(files["gliederung"]), valid),
|
||||
timeout=_timeout("plan_judge", len(entries)))
|
||||
plan = _gliederung_schema(_json_datei(files["gliederung"]), valid) or vorschlaege[0]
|
||||
|
||||
# Vollständigkeit: jeder Baustein kommt vor — fehlende in „Weitere" (gegen weglassende Agenten/Judge).
|
||||
drin = {n for ch in plan["kapitel"] for n in ch["nummern"]}
|
||||
fehlen = [n for n in entries if n not in drin]
|
||||
if fehlen:
|
||||
plan["kapitel"].append({"titel": "Weitere", "nummern": fehlen})
|
||||
atomic_write_json(files["gliederung"], plan, indent=1)
|
||||
return plan
|
||||
|
||||
|
||||
# --- Lern-Artefakte (Karteikarten/Beispiele/Diagramme aus den Fakten) ---
|
||||
|
||||
def _karten_schema(data):
|
||||
"""{"karten":[{baustein,subbaustein,frage,antwort}]} → Liste (auch leer) · None bei kaputt."""
|
||||
if not isinstance(data, dict) or not isinstance(data.get("karten"), list):
|
||||
return None
|
||||
out = []
|
||||
for e in data["karten"]:
|
||||
if isinstance(e, dict) and (f := str(e.get("frage", "")).strip()) and (a := str(e.get("antwort", "")).strip()):
|
||||
out.append({"baustein": str(e.get("baustein", "")).strip(), "subbaustein": str(e.get("subbaustein", "")).strip(),
|
||||
"frage": f, "antwort": a})
|
||||
return out
|
||||
|
||||
|
||||
def _beispiel_schema(data):
|
||||
"""{"beispiele":[{baustein,subbaustein,problem,schritte,ergebnis}]} → Liste (auch leer) · None bei kaputt."""
|
||||
if not isinstance(data, dict) or not isinstance(data.get("beispiele"), list):
|
||||
return None
|
||||
out = []
|
||||
for e in data["beispiele"]:
|
||||
if not isinstance(e, dict):
|
||||
continue
|
||||
problem = str(e.get("problem", "")).strip()
|
||||
schritte = [s for x in (e.get("schritte") or []) if (s := str(x).strip())]
|
||||
if problem and schritte:
|
||||
out.append({"baustein": str(e.get("baustein", "")).strip(), "subbaustein": str(e.get("subbaustein", "")).strip(),
|
||||
"problem": problem, "schritte": schritte, "ergebnis": str(e.get("ergebnis", "")).strip()})
|
||||
return out
|
||||
|
||||
|
||||
def _diagramm_schema(data):
|
||||
"""{"diagramme":[{baustein,mermaid}]} → Liste (auch leer) · None bei kaputt."""
|
||||
if not isinstance(data, dict) or not isinstance(data.get("diagramme"), list):
|
||||
return None
|
||||
out = []
|
||||
for e in data["diagramme"]:
|
||||
if isinstance(e, dict) and (m := str(e.get("mermaid", "")).strip()):
|
||||
out.append({"baustein": str(e.get("baustein", "")).strip(), "mermaid": m})
|
||||
return out
|
||||
|
||||
|
||||
_ARTEFAKT_SCHEMA = {"karteikarte": _karten_schema, "beispiel": _beispiel_schema, "diagramm": _diagramm_schema}
|
||||
_ARTEFAKT_PROMPT = {"karteikarte": "Artefakt-Karteikarte", "beispiel": "Artefakt-Beispiel", "diagramm": "Artefakt-Diagramm"}
|
||||
_ARTEFAKT_SCHRITT = {"karteikarte": "Karteikarten", "beispiel": "Beispiele", "diagramm": "Diagramme"}
|
||||
|
||||
|
||||
def _artefakte_komplett(files: dict) -> bool:
|
||||
"""Artefakt-Map steht (alle Typen erzeugt)? Werte dürfen leer sein (content-aware)."""
|
||||
d = _json_datei(files["artefakte"])
|
||||
return isinstance(d, dict) and all(t in d for t in ARTEFAKT_TYPEN)
|
||||
|
||||
|
||||
async def _artefakte_block(ctx: GenContext, set_p, files: dict, sidecar: dict, instructions: str) -> dict | None:
|
||||
"""Lern-Artefakte je Typ aus den gespeicherten Fakten erzeugen — ein Generierungs-Durchlauf
|
||||
je Typ über Chunks, keine Verifikation (Bonus-Material). → {typ: [eintraege]} (auch in files)."""
|
||||
topic, is_cancelled = ctx.topic, ctx.is_cancelled
|
||||
arbeit = files["arbeit"]
|
||||
# Bausteine mit Subs + Fakten-Zeilen als Input-Block (extract-once aus den Fakten).
|
||||
bausteine = []
|
||||
for btitel, subs in sidecar.items():
|
||||
if not isinstance(subs, list):
|
||||
continue
|
||||
zeilen = []
|
||||
for s in subs:
|
||||
if not isinstance(s, dict) or not (st := str(s.get("titel", "")).strip()):
|
||||
continue
|
||||
fk = s.get("fakten") if isinstance(s.get("fakten"), dict) else {}
|
||||
zeile = f"- {st}"
|
||||
if fk and (fk_text := _fakten_zeilen(fk)):
|
||||
zeile += "\n" + "\n".join(" " + l for l in fk_text.split("\n"))
|
||||
zeilen.append(zeile)
|
||||
if zeilen:
|
||||
bausteine.append((btitel, zeilen))
|
||||
if not bausteine:
|
||||
leer = {t: [] for t in ARTEFAKT_TYPEN}
|
||||
atomic_write_json(files["artefakte"], leer, indent=1)
|
||||
return leer
|
||||
|
||||
chunks = _lpt_chunks([len(z) for _, z in bausteine], FAKTEN_CHUNK_SUBS)
|
||||
def block_text(idxs):
|
||||
return "\n\n".join(f"BAUSTEIN: {bausteine[i][0]}\nSUBBAUSTEINE:\n" + "\n".join(bausteine[i][1]) for i in idxs)
|
||||
|
||||
ergebnis: dict[str, list] = {}
|
||||
for typ in ARTEFAKT_TYPEN:
|
||||
schema = _ARTEFAKT_SCHEMA[typ]
|
||||
def apath(ci, t=typ): return arbeit / f"artefakt-{t}-c{ci}.json"
|
||||
|
||||
async def _gen(ci, idxs, t=typ, schema=schema):
|
||||
p = apath(ci, t)
|
||||
if schema(_json_datei(p)) is not None:
|
||||
return True
|
||||
await run_single_slot(
|
||||
ctx, f"{_ARTEFAKT_SCHRITT[t]} {ci}", key=f"bausteine-{topic}-artefakt-{t}-c{ci}",
|
||||
prompt=_prompt(_ARTEFAKT_PROMPT[t], topic=topic, bausteine=block_text(idxs), out_path=p, extra=_extra(instructions)),
|
||||
role="guide", capabilities="files",
|
||||
payload=lambda result, p=p, schema=schema: schema(_json_datei(p)),
|
||||
timeout=_timeout("inhalt", sum(len(bausteine[i][1]) for i in idxs)))
|
||||
return schema(_json_datei(p)) is not None
|
||||
|
||||
await _gather_fortschritt([_gen(ci, idxs) for ci, idxs in enumerate(chunks)], len(chunks), _melde_p(set_p, topic, _ARTEFAKT_SCHRITT[typ]))
|
||||
if is_cancelled():
|
||||
return None
|
||||
eintraege: list = []
|
||||
for ci in range(len(chunks)):
|
||||
eintraege += schema(_json_datei(apath(ci))) or []
|
||||
ergebnis[typ] = eintraege
|
||||
atomic_write_json(files["artefakte"], ergebnis, indent=1)
|
||||
return ergebnis
|
||||
|
||||
|
||||
async def _mirror_artefakte_db(topic: str, sidecar: dict, artefakte: dict) -> None:
|
||||
"""Artefakte in die DB spiegeln. Karteikarte/Beispiel je Sub (sub_norm), Diagramm je Baustein (sub_norm='')."""
|
||||
await db.delete_sub_artefakte(topic)
|
||||
btitel_list = list(sidecar.keys())
|
||||
for typ in ("karteikarte", "beispiel"):
|
||||
for e in artefakte.get(typ, []):
|
||||
bt = _match_sub(e.get("baustein", ""), btitel_list)
|
||||
bnorm, sn = _norm_titel(bt), _norm_titel(e.get("subbaustein", ""))
|
||||
if not bnorm or not sn:
|
||||
continue
|
||||
daten = json.dumps({k: v for k, v in e.items() if k not in ("baustein", "subbaustein")}, ensure_ascii=False)
|
||||
await db.put_sub_artefakt(topic, bnorm, sn, typ, daten, bt, e.get("subbaustein", ""))
|
||||
for e in artefakte.get("diagramm", []):
|
||||
bt = _match_sub(e.get("baustein", ""), btitel_list)
|
||||
if not (bnorm := _norm_titel(bt)):
|
||||
continue
|
||||
daten = json.dumps({"mermaid": e.get("mermaid", "")}, ensure_ascii=False)
|
||||
await db.put_sub_artefakt(topic, bnorm, "", "diagramm", daten, bt, "")
|
||||
|
||||
|
||||
async def _mirror_sidecar_db(topic: str, sidecar: dict) -> None:
|
||||
"""Sidecar {Baustein-Titel: [{titel, stufe, relevanz}]} in die DB-Tabelle subbausteine spiegeln."""
|
||||
for btitel, subs in sidecar.items():
|
||||
@@ -1428,12 +1885,14 @@ async def _mirror_sidecar_db(topic: str, sidecar: dict) -> None:
|
||||
sn = _norm_titel(st)
|
||||
if not sn:
|
||||
continue
|
||||
fakten = json.dumps(s["fakten"], ensure_ascii=False) if isinstance(s.get("fakten"), dict) else None
|
||||
await db.put_subbaustein(topic, bnorm, sn, btitel, st,
|
||||
stufe=s.get("stufe"), relevanz=s.get("relevanz"), status="konsens")
|
||||
stufe=s.get("stufe"), relevanz=s.get("relevanz"),
|
||||
fakten=fakten, status="konsens")
|
||||
|
||||
|
||||
async def _mirror_frage_muster_db(topic: str, muster: dict) -> None:
|
||||
"""Frage-Muster {Baustein-Titel: [{subbaustein, typ, frage}]} in die DB-Tabelle frage_muster spiegeln."""
|
||||
"""Frage-Muster {Baustein-Titel: [{subbaustein, frage}]} in die DB-Tabelle frage_muster spiegeln."""
|
||||
await db.delete_frage_muster(topic)
|
||||
for btitel, eintraege in muster.items():
|
||||
bnorm = _norm_titel(btitel)
|
||||
@@ -1444,19 +1903,22 @@ async def _mirror_frage_muster_db(topic: str, muster: dict) -> None:
|
||||
continue
|
||||
sub = str(e.get("subbaustein", "")).strip()
|
||||
sn = _norm_titel(sub)
|
||||
typ = str(e.get("typ", "")).strip()
|
||||
frage = str(e.get("frage", "")).strip()
|
||||
if not (sn and typ and frage):
|
||||
if not (sn and frage):
|
||||
continue
|
||||
await db.upsert_frage_muster(topic, bnorm, sn, btitel, sub, typ, frage)
|
||||
await db.upsert_frage_muster(topic, bnorm, sn, btitel, sub, frage)
|
||||
|
||||
|
||||
async def _reset_db_ab_phase(topic: str, label: str) -> None:
|
||||
"""DB-Inhalt der Phasen ≥ `label` verwerfen (kanonische Reihenfolge Quelle…Fragen)."""
|
||||
"""DB-Inhalt der Phasen ≥ `label` verwerfen (kanonische Reihenfolge Quelle…Artefakte)."""
|
||||
idx = _phase_idx(label)
|
||||
if idx <= 5: # Fragen
|
||||
if idx <= 8: # Artefakte (Karteikarten/Beispiele/Diagramme)
|
||||
await db.delete_sub_artefakte(topic)
|
||||
if idx <= 7: # Fragen
|
||||
await db.delete_frage_muster(topic)
|
||||
if idx <= 2: # Subbausteine (Stufen/Relevanz greifen über Sidecar→Mirror)
|
||||
if idx <= 6: # Gliederung
|
||||
await db.delete_gliederung(topic)
|
||||
if idx <= 2: # Subbausteine (Fakten/Stufen/Relevanz greifen über Sidecar→Mirror)
|
||||
await db.delete_subbausteine(topic)
|
||||
if idx <= 1: # Inventar: Inventar + Recherche-Schritte — Sichtung bleibt
|
||||
await db.delete_bausteine(topic)
|
||||
@@ -1519,6 +1981,8 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
||||
await db.delete_subbausteine(topic)
|
||||
await db.delete_frage_muster(topic)
|
||||
await db.delete_coverage(topic)
|
||||
await db.delete_gliederung(topic)
|
||||
await db.delete_sub_artefakte(topic)
|
||||
|
||||
# Inventar (DB): Recherche-Loop → Konsolidierung → Klärung.
|
||||
if not await _recherche_batch(ctx, set_p, files, q, ordner, instructions):
|
||||
@@ -1590,12 +2054,30 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
||||
if roh is None:
|
||||
return # Fehler ist gesetzt
|
||||
atomic_write_json(files["sub_roh"], roh, indent=1)
|
||||
# Fakten je Sub (VOR der Stufe): Quell-Fakten extrahieren + verifizieren → fakten.json.
|
||||
# Extract-once-Grounding — Stufe/Relevanz/Fragen/Guide nähren sich daraus.
|
||||
if not _fakten_komplett(files):
|
||||
fakten_map = await _fakten_block(ctx, set_p, files, roh, q, ordner, instructions)
|
||||
if is_cancelled():
|
||||
abgebrochen()
|
||||
return
|
||||
if fakten_map is None:
|
||||
return # Fehler ist gesetzt
|
||||
atomic_write_json(files["fakten"], fakten_map, indent=1)
|
||||
sidecar = await _stufen_block(ctx, set_p, files, roh, instructions)
|
||||
if is_cancelled():
|
||||
abgebrochen()
|
||||
return
|
||||
if sidecar is None:
|
||||
return
|
||||
# Fakten in die Sidecar-Subs mergen (DB-Spiegel + Guide-Nutzung).
|
||||
fakten_map = _json_datei(files["fakten"])
|
||||
if isinstance(fakten_map, dict):
|
||||
for btitel, subs in sidecar.items():
|
||||
fm = fakten_map.get(btitel, {})
|
||||
for sub in subs:
|
||||
if (fk := fm.get(_norm_titel(sub["titel"]))):
|
||||
sub["fakten"] = fk
|
||||
atomic_write_json(files["sidecar"], sidecar, indent=1)
|
||||
|
||||
# Block D: Relevanz je Subbaustein (relevant/rand) → in die Sidecar mergen.
|
||||
@@ -1616,6 +2098,14 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
||||
sub["relevanz"] = relevanz_by_id.get(gid, "relevant")
|
||||
atomic_write_json(files["sidecar"], sidecar, indent=1)
|
||||
|
||||
# Block D.5: Gliederung (Bausteine-Artefakt) — Kapitel-Struktur über ALLE Bausteine,
|
||||
# vom Guide nur noch gelesen. Format-agnostisch; der Guide filtert je Format.
|
||||
if not _gliederung_komplett(files):
|
||||
await _gliederung_block(ctx, set_p, files, entries, instructions)
|
||||
if is_cancelled():
|
||||
abgebrochen()
|
||||
return
|
||||
|
||||
# Block E: Frage-Muster je relevantem Subbaustein × Typ → eigenes Sidecar.
|
||||
# Zur Prüfungszeit zieht jeder Agent ein Muster ohne Zurücklegen und formuliert
|
||||
# daraus eine Frage — distinkte Saat verhindert die Doppelfragen der Live-Generierung.
|
||||
@@ -1629,6 +2119,17 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
||||
return # Abbruch
|
||||
atomic_write_json(files["frage_muster"], muster, indent=1)
|
||||
|
||||
# Block F: Lern-Artefakte (Karteikarten/Beispiele/Diagramme) aus den Fakten — Bonus,
|
||||
# vom Frontend präsentiert. Bricht den Lauf nicht ab (Artefakte sind optional).
|
||||
sidecar = _json_datei(files["sidecar"])
|
||||
if _sidecar_schema(sidecar) is not None and not _artefakte_komplett(files):
|
||||
artefakte = await _artefakte_block(ctx, set_p, files, sidecar, instructions)
|
||||
if is_cancelled():
|
||||
abgebrochen()
|
||||
return
|
||||
if artefakte is None:
|
||||
return # Abbruch (Fehler/Cancel)
|
||||
|
||||
# DB-Spiegel (Brücke): finalen Sidecar- + Frage-Muster-Stand in die DB schreiben.
|
||||
sidecar = _json_datei(files["sidecar"])
|
||||
if _sidecar_schema(sidecar) is not None:
|
||||
@@ -1636,6 +2137,19 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
||||
muster = _json_datei(files["frage_muster"])
|
||||
if isinstance(muster, dict) and muster:
|
||||
await _mirror_frage_muster_db(topic, muster)
|
||||
# Gliederung (Titel-basiert, robust gegen Nummern-Drift) → DB.
|
||||
plan = _json_datei(files["gliederung"])
|
||||
if isinstance(plan, dict) and plan.get("kapitel"):
|
||||
kapitel = [
|
||||
{"titel": ch.get("titel", "Kapitel"),
|
||||
"bausteine": [_titel(entries[n]) for n in ch.get("nummern", []) if n in entries]}
|
||||
for ch in plan["kapitel"]
|
||||
]
|
||||
await db.set_gliederung(topic, json.dumps({"kapitel": kapitel}, ensure_ascii=False))
|
||||
# Artefakte → DB (Karteikarte/Beispiel je Sub, Diagramm je Baustein).
|
||||
artefakte = _json_datei(files["artefakte"])
|
||||
if isinstance(artefakte, dict) and _sidecar_schema(sidecar) is not None:
|
||||
await _mirror_artefakte_db(topic, sidecar, artefakte)
|
||||
except Exception as e:
|
||||
log.exception("[%s] Bausteine-Generierung fehlgeschlagen", topic)
|
||||
_bausteine_errors[topic] = str(e)[:2000]
|
||||
|
||||
Reference in New Issue
Block a user