This commit is contained in:
team3
2026-06-21 20:00:13 +02:00
parent 885c4811d0
commit 8ba4b94498
18 changed files with 537 additions and 1188 deletions

View File

@@ -11,6 +11,7 @@ die Gesamtliste.
""" """
import asyncio import asyncio
import json
import logging import logging
import math import math
import shutil import shutil
@@ -22,7 +23,7 @@ from config import KONSENS_GRACE, KONSENS_MAX_RUNDEN, DEFAULT_PROVIDER
from fsutil import atomic_write_text, atomic_write_json from fsutil import atomic_write_text, atomic_write_json
from jsonio import read_json_file as _json_datei from jsonio import read_json_file as _json_datei
from lernen import FRAGETYPEN 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 paths import arbeit_dir, bausteine_path, frage_muster_path, grafik_path, project_dir, subbausteine_path, quelle_path, quelle_crawl_dir, safe_ordner
from crawl import crawl from crawl import crawl
from pipeline import ( from pipeline import (
CANCELLED, FAILED, GenContext, _extra, _log, _prompt, _race, _relevanz_schema, _rest_schema, CANCELLED, FAILED, GenContext, _extra, _log, _prompt, _race, _relevanz_schema, _rest_schema,
@@ -53,6 +54,7 @@ BAUSTEINE_STEPS = (
"Stufen finden", "Stufen wählen", "Stufen klären", "Stufen finden", "Stufen wählen", "Stufen klären",
"Relevanz finden", "Relevanz wählen", "Relevanz klären", "Relevanz finden", "Relevanz wählen", "Relevanz klären",
"Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen", "Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen",
"Grafik finden", "Grafik prüfen",
) )
@@ -109,6 +111,19 @@ def lade_frage_muster(topic: str, baustein: str) -> list[dict]:
] ]
def lade_grafiken(topic: str) -> dict:
"""Alle Baustein-Grafiken eines Themas: {Baustein-Titel: {knoten, kanten, richtung}}.
Leere/ungültige Datei → {} (Frontend zeigt Fallback)."""
g = _json_datei(grafik_path(topic))
return g if isinstance(g, dict) else {}
def lade_grafik(topic: str, baustein: str) -> dict | None:
"""Grafik eines Bausteins (None = keine → Fallback)."""
g = lade_grafiken(topic).get(baustein)
return g if isinstance(g, dict) and g.get("knoten") else None
def lade_uebersicht(topic: str) -> list[dict]: def lade_uebersicht(topic: str) -> list[dict]:
"""Strukturierte Baustein-Liste für die Übersicht: Titel + Beschreibung + Subbausteine/Stufen. """Strukturierte Baustein-Liste für die Übersicht: Titel + Beschreibung + Subbausteine/Stufen.
@@ -149,6 +164,7 @@ def _bausteine_steps(topic: str) -> tuple:
"Stufen finden", "Stufen wählen", "Stufen klären", "Stufen finden", "Stufen wählen", "Stufen klären",
"Relevanz finden", "Relevanz wählen", "Relevanz klären", "Relevanz finden", "Relevanz wählen", "Relevanz klären",
"Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen", "Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen",
"Grafik finden", "Grafik prüfen",
) )
mitte = base + (("Ergänzung",) if q["type"] == "projekt" else ()) + rest mitte = base + (("Ergänzung",) if q["type"] == "projekt" else ()) + rest
return (("Quelle laden",) if q["type"] == "link" else ()) + mitte return (("Quelle laden",) if q["type"] == "link" else ()) + mitte
@@ -166,6 +182,7 @@ PHASEN = (
("Stufen", ("Stufen finden", "Stufen wählen", "Stufen klären")), ("Stufen", ("Stufen finden", "Stufen wählen", "Stufen klären")),
("Relevanz", ("Relevanz finden", "Relevanz wählen", "Relevanz klären")), ("Relevanz", ("Relevanz finden", "Relevanz wählen", "Relevanz klären")),
("Fragen", ("Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen")), ("Fragen", ("Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen")),
("Grafiken", ("Grafik finden", "Grafik prüfen")),
) )
@@ -206,18 +223,19 @@ def _bausteine_files(topic: str) -> dict:
"sub_roh": arbeit / "subbausteine-roh.json", "sub_roh": arbeit / "subbausteine-roh.json",
"sidecar": subbausteine_path(topic), "sidecar": subbausteine_path(topic),
"frage_muster": frage_muster_path(topic), "frage_muster": frage_muster_path(topic),
"grafik": grafik_path(topic),
} }
def _alle_slot_dateien(files: dict) -> list[Path]: def _alle_slot_dateien(files: dict) -> list[Path]:
arbeit = files["arbeit"] arbeit = files["arbeit"]
# Subbaustein-/Stufen-Slots sind pro Chunk dynamisch — per Glob einsammeln. # 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-*"))) if arbeit.is_dir() else [] dyn = (list(arbeit.glob("subbaustein-*")) + list(arbeit.glob("stufe-*")) + list(arbeit.glob("relevanz-*")) + list(arbeit.glob("frage-muster-*")) + list(arbeit.glob("grafik-*"))) if arbeit.is_dir() else []
return [ return [
*files["recherche"], files["recherche_mapping"], *files["recherche"], files["recherche_mapping"],
*(p for slots in files["auswahl"].values() for p in slots), *(p for slots in files["auswahl"].values() for p in slots),
*files["mapping"].values(), files["ergaenzung"], *files["mapping"].values(), files["ergaenzung"],
files["sub_roh"], files["sidecar"], files["frage_muster"], *dyn, files["sub_roh"], files["sidecar"], files["frage_muster"], files["grafik"], *dyn,
] ]
@@ -256,6 +274,9 @@ def _resume_step(topic: str) -> int:
# Relevanz fertig; nur noch Frage-Muster offen? # Relevanz fertig; nur noch Frage-Muster offen?
if not _frage_muster_komplett(topic): if not _frage_muster_komplett(topic):
return _step_idx(topic, "Fragen finden") return _step_idx(topic, "Fragen finden")
# Frage-Muster fertig; nur noch Grafiken offen?
if not _grafik_komplett(topic):
return _step_idx(topic, "Grafik finden")
return len(_bausteine_steps(topic)) return len(_bausteine_steps(topic))
if _sub_roh_schema(_json_datei(files["sub_roh"])) is None: if _sub_roh_schema(_json_datei(files["sub_roh"])) is None:
return _step_idx(topic, "Subbausteine finden") return _step_idx(topic, "Subbausteine finden")
@@ -294,6 +315,7 @@ def reset_bausteine(topic: str) -> None:
files["final"].unlink(missing_ok=True) files["final"].unlink(missing_ok=True)
files["sidecar"].unlink(missing_ok=True) # liegt im Themen-Root, nicht in arbeit/ files["sidecar"].unlink(missing_ok=True) # liegt im Themen-Root, nicht in arbeit/
files["frage_muster"].unlink(missing_ok=True) # ebenfalls im Themen-Root files["frage_muster"].unlink(missing_ok=True) # ebenfalls im Themen-Root
files["grafik"].unlink(missing_ok=True) # ebenfalls im Themen-Root
quelle_path(topic).unlink(missing_ok=True) quelle_path(topic).unlink(missing_ok=True)
shutil.rmtree(quelle_crawl_dir(topic), ignore_errors=True) # gecrawlte Link-Quelle shutil.rmtree(quelle_crawl_dir(topic), ignore_errors=True) # gecrawlte Link-Quelle
shutil.rmtree(files["arbeit"], ignore_errors=True) shutil.rmtree(files["arbeit"], ignore_errors=True)
@@ -312,6 +334,9 @@ def _reset_ab_phase(topic: str, phase: int) -> None:
for p in arbeit.glob(pat): for p in arbeit.glob(pat):
p.unlink(missing_ok=True) p.unlink(missing_ok=True)
if phase <= 6: # Grafiken
files["grafik"].unlink(missing_ok=True)
glob_del("grafik-*")
if phase <= 5: # Fragen if phase <= 5: # Fragen
files["frage_muster"].unlink(missing_ok=True) files["frage_muster"].unlink(missing_ok=True)
glob_del("frage-muster-*") glob_del("frage-muster-*")
@@ -471,6 +496,45 @@ def _frage_muster_komplett(topic: str) -> bool:
return isinstance(_json_datei(frage_muster_path(topic)), dict) return isinstance(_json_datei(frage_muster_path(topic)), dict)
def _grafik_schema(data) -> dict | None:
"""{"knoten": [{id, text}], "kanten": [{von, nach, text?}], "richtung"?} → bereinigtes
Grafik-Dict · sonst None. Knoten nicht leer, IDs eindeutig, jede Kante referenziert
existierende Knoten (sonst verworfen). Leere Knotenliste → None."""
if not isinstance(data, dict) or not isinstance(data.get("knoten"), list):
return None
knoten, ids = [], set()
for k in data["knoten"]:
if not isinstance(k, dict):
return None
kid = str(k.get("id", "")).strip()
text = str(k.get("text", "")).strip()
if not kid or not text or kid in ids:
continue
ids.add(kid)
knoten.append({"id": kid, "text": text})
if not knoten:
return None
kanten = []
for e in (data.get("kanten") or []):
if not isinstance(e, dict):
continue
von, nach = str(e.get("von", "")).strip(), str(e.get("nach", "")).strip()
if von not in ids or nach not in ids:
continue # hängende Kante (halluzinierte ID) verwerfen
kante = {"von": von, "nach": nach}
if (t := str(e.get("text", "")).strip()):
kante["text"] = t
kanten.append(kante)
richtung = data.get("richtung") if data.get("richtung") in ("TB", "LR") else "TB"
return {"richtung": richtung, "knoten": knoten, "kanten": kanten}
def _grafik_komplett(topic: str) -> bool:
"""Grafik-Sidecar existiert (Build gelaufen)? Einzelne leere Bausteine fallen im
Frontend auf den Fallback-Hinweis zurück — daher genügt die Datei."""
return isinstance(_json_datei(grafik_path(topic)), dict)
def _read(p: Path) -> str: def _read(p: Path) -> str:
return p.read_text(encoding="utf-8") if p.exists() else "" return p.read_text(encoding="utf-8") if p.exists() else ""
@@ -989,6 +1053,109 @@ async def _frage_muster_block(ctx: GenContext, set_p, files: dict, sidecar: dict
return ergebnis return ergebnis
async def _grafik_block(ctx: GenContext, set_p, files: dict, sidecar: dict, instructions: str) -> dict | None:
"""Block F: zwei Phasen — Finden (1 Generator je Baustein, parallel) erzeugt eine
Graph-Grafik (Knoten/Kanten) als JSON, Prüfen (Kritiker je Baustein) fixt Struktur +
Inhalt; Bausteine ohne Grafik eine Runde nachholen.
{Baustein-Titel: {knoten, kanten, richtung}} oder None bei Abbruch."""
topic, is_cancelled = ctx.topic, ctx.is_cancelled
arbeit = files["arbeit"]
beschr = {b["titel"]: b["beschreibung"] for b in lade_uebersicht(topic)}
# Je Baustein die relevanten Subbausteine (relevanz != 'rand') als Graph-Grundlage.
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))
if not bausteine:
return {}
def roh_path(c):
return arbeit / f"grafik-c{c}.json"
def final_path(c):
return arbeit / f"grafik-final-c{c}.json"
# Phase „Grafik finden": pro Baustein 1 Generator, alle parallel.
async def _finde(c, titel, rel):
fp = roh_path(c)
if _grafik_schema(_json_datei(fp)):
return # Resume
sub_block = "\n".join(f"- {s}" for s in rel)
status, _ = await run_single_slot(
ctx, f"Grafik {c}",
key=f"bausteine-{topic}-grafik-c{c}",
prompt=_prompt("Baustein-Grafik", topic=topic, baustein=titel,
beschreibung=beschr.get(titel, ""), subbausteine=sub_block,
out_path=fp, extra=_extra(instructions)),
role="guide", capabilities="files",
payload=lambda result, p=fp: _grafik_schema(_json_datei(p)),
timeout=_timeout("grafik", len(rel)),
)
if status == FAILED:
_log(topic, f"Grafik Baustein {c} fehlgeschlagen — keine Grafik (Fallback)")
set_p(f"Grafik finden ({len(bausteine)} Bausteine)…", step=_step_idx(topic, "Grafik finden"))
await asyncio.gather(*[_finde(c, t, r) for c, (t, r) in enumerate(bausteine, 1)], return_exceptions=True)
if is_cancelled():
return None
# Phase „Grafik prüfen": Kritiker je Baustein fixt Struktur/Inhalt; sonst Roh übernehmen.
async def _pruefe(c, titel, rel):
roh = _grafik_schema(_json_datei(roh_path(c)))
fp = final_path(c)
if _grafik_schema(_json_datei(fp)) or not roh:
return
sub_block = "\n".join(f"- {s}" for s in rel)
status, _ = await run_single_slot(
ctx, f"Grafik-Prüfung {c}",
key=f"bausteine-{topic}-grafik-final-c{c}",
prompt=_prompt("Baustein-Grafik-Check", topic=topic, baustein=titel,
subbausteine=sub_block,
grafik=json.dumps(roh, ensure_ascii=False, indent=1),
out_path=fp, extra=_extra(instructions)),
role="judge", capabilities="files",
payload=lambda result, p=fp: _grafik_schema(_json_datei(p)),
timeout=_timeout("grafik_check", len(rel)),
)
if status == FAILED:
_log(topic, f"Grafik-Prüfung Baustein {c} fehlgeschlagen — Roh-Grafik übernommen")
set_p(f"Grafik prüfen ({len(bausteine)} Bausteine)…", step=_step_idx(topic, "Grafik prüfen"))
await asyncio.gather(*[_pruefe(c, t, r) for c, (t, r) in enumerate(bausteine, 1)], return_exceptions=True)
if is_cancelled():
return None
def _finalisiere(c):
return _grafik_schema(_json_datei(final_path(c))) or _grafik_schema(_json_datei(roh_path(c)))
ergebnis = {}
for c, (titel, rel) in enumerate(bausteine, 1):
if (g := _finalisiere(c)):
ergebnis[titel] = g
# Bausteine ohne Grafik eine Runde nachholen.
leer = [(c, t, r) for c, (t, r) in enumerate(bausteine, 1) if t not in ergebnis]
if leer:
_log(topic, f"Grafik: {len(leer)} Baustein(e) ohne Grafik — Nachrunde")
for c, t, r in leer:
roh_path(c).unlink(missing_ok=True)
final_path(c).unlink(missing_ok=True)
await asyncio.gather(*[_finde(c, t, r) for c, t, r in leer], return_exceptions=True)
if is_cancelled():
return None
await asyncio.gather(*[_pruefe(c, t, r) for c, t, r in leer], return_exceptions=True)
if is_cancelled():
return None
for c, t, r in leer:
if (g := _finalisiere(c)):
ergebnis[t] = g
rest = [t for c, t, r in leer if t not in ergebnis]
if rest:
_log(topic, f"Grafik: {len(rest)} Baustein(e) bleiben leer (Fallback): {rest[:5]}")
return ergebnis
async def generate_bausteine(topic: str, instructions: str = "", provider: str = DEFAULT_PROVIDER, ab_phase: int | None = None) -> None: async def generate_bausteine(topic: str, instructions: str = "", provider: str = DEFAULT_PROVIDER, ab_phase: int | None = None) -> None:
if topic in _bausteine_progress: if topic in _bausteine_progress:
return return
@@ -1285,6 +1452,19 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
if muster is None: if muster is None:
return # Abbruch return # Abbruch
atomic_write_json(files["frage_muster"], muster, indent=1) atomic_write_json(files["frage_muster"], muster, indent=1)
# Block F: pro Baustein eine Graph-Grafik (Knoten/Kanten) der relevanten
# Subbausteine → eigenes Sidecar. Von allen Guide-Formaten geteilt; das
# Frontend rendert sie mit Auto-Layout (dagre) + KaTeX.
sidecar = _json_datei(files["sidecar"])
if _sidecar_schema(sidecar) is not None and _relevanz_komplett(sidecar) and _frage_muster_komplett(topic) and not _grafik_komplett(topic):
grafik = await _grafik_block(ctx, set_p, files, sidecar, instructions)
if is_cancelled():
abgebrochen()
return
if grafik is None:
return # Abbruch
atomic_write_json(files["grafik"], grafik, indent=1)
except Exception as e: except Exception as e:
log.exception("[%s] Bausteine-Generierung fehlgeschlagen", topic) log.exception("[%s] Bausteine-Generierung fehlgeschlagen", topic)
_bausteine_errors[topic] = str(e)[:2000] _bausteine_errors[topic] = str(e)[:2000]

View File

@@ -60,8 +60,8 @@ TIMEOUTS = {
"frage_muster_check": (300, 10), # Kritiker bereinigt die Muster-Tabelle je Baustein "frage_muster_check": (300, 10), # Kritiker bereinigt die Muster-Tabelle je Baustein
"writer": (600, 120), # pro Section im Chunk "writer": (600, 120), # pro Section im Chunk
"lese_check": (300, 10), # pro Section im Paket "lese_check": (300, 10), # pro Section im Paket
"grafik": (600, 60), # Mermaid-Grafik je Section im Chunk "grafik": (600, 60), # Graph-Grafik je Baustein (Knoten/Kanten), n = Subbausteine
"grafik_check": (300, 15), # Grafik-Prüfung/Fix je Section im Paket "grafik_check": (300, 15), # Grafik-Prüfung/Fix je Baustein, n = Subbausteine
"onepager_recherche": (900, 0), "onepager_recherche": (900, 0),
"onepager_mapping": (600, 0), # Konsolidierung der Recherchen "onepager_mapping": (600, 0), # Konsolidierung der Recherchen
"onepager_bauen": (300, 0), "onepager_bauen": (300, 0),

View File

@@ -33,13 +33,13 @@ from pipeline import (
is_guide_cancelled, run_single_slot, is_guide_cancelled, run_single_slot,
) )
from textkit import ( from textkit import (
_eindeutige_titel, _lade_bausteine, _parse_fragment, _parse_grafik, _split_chunks, _eindeutige_titel, _lade_bausteine, _parse_fragment, _split_chunks,
_titel, _titel_aufloesen, _titel_index, _zuteilung_text, _titel, _titel_aufloesen, _titel_index, _zuteilung_text,
) )
log = logging.getLogger("creator.guide") log = logging.getLogger("creator.guide")
GUIDE_STEPS = ("Auswahl", "Gliederung", "Inhalte", "Inhalts-Check", "Schreiben", "Lese-Prüfung", "Grafik", "Grafik-Check") GUIDE_STEPS = ("Auswahl", "Gliederung", "Inhalte", "Inhalts-Check", "Schreiben", "Lese-Prüfung")
# Writer skalieren mit der Section-Zahl: 1 Writer je ~30 Sections (gedeckelt). # Writer skalieren mit der Section-Zahl: 1 Writer je ~30 Sections (gedeckelt).
# Kleine Pakete vermeiden Lazy-Output bei langen Listen und begrenzen den Schaden # Kleine Pakete vermeiden Lazy-Output bei langen Listen und begrenzen den Schaden
@@ -764,100 +764,12 @@ async def _generate_sections(
break break
scope = [[num for num in nums if num in ersetzt] for nums in chunk_nums] scope = [[num for num in nums if num in ersetzt] for nums in chunk_nums]
# Schritt 6: Grafik — pro Chunk ein Agent erzeugt je Section ein Mermaid-Diagramm (Resume).
grafik_paths = [content_path.parent / f"{content_path.stem}.grafik-{i}.md" for i in range(1, writer_count + 1)]
offen_g = [i for i, p in enumerate(grafik_paths) if chunk_nums[i] and not p.exists()]
if offen_g:
await _set_step(guide_id, 6, f"Erstelle Grafiken ({writer_count})…" if writer_count > 1 else "Erstelle Grafiken…")
results = await asyncio.gather(*[
run_agent(
f"{guide_id}-g{i + 1}",
_prompt("Guide-Grafik", topic=topic, format_name=format_name,
sections=sections_text(chunk_nums[i]), out_path=grafik_paths[i], extra=_extra(instructions)),
_timeout("grafik", chunk_sizes[i]), provider=provider, role="guide", capabilities="files",
)
for i in offen_g
], return_exceptions=True)
if is_cancelled():
return None
for i, r in zip(offen_g, results):
if isinstance(r, BaseException) or (not isinstance(r, BaseException) and r[0] != 0):
_log(topic, f"Grafik {i + 1} fehlgeschlagen — Section ohne Grafik")
for p in grafik_paths:
if not p.exists():
continue
for titel, code in _parse_grafik(p.read_text(encoding="utf-8")).items():
num = _titel_aufloesen(idx, titel)
if num in by_num and code.strip():
by_num[num]["grafik"] = code.strip()
# Schritt 7: Grafik-Check — Kritiker je Chunk (Syntax + Inhalt), Fix für beanstandete (eine Runde).
def grafik_text(nums):
return "\n\n".join(
f"SECTION: {_titel(entries[num])}\n```mermaid\n{by_num[num].get('grafik', '')}\n```"
for num in nums if by_num.get(num, {}).get("grafik")
)
def grafik_auftraege(nums, probleme):
return "\n\n".join(
f"SECTION: {_titel(entries[num])}\nPROBLEM: {probleme[num]}\nAKTUELLE GRAFIK:\n```mermaid\n{by_num[num].get('grafik', '')}\n```"
for num in nums
)
g_scope = [[num for num in nums if by_num.get(num, {}).get("grafik")] for nums in chunk_nums]
gcheck_paths = [content_path.parent / f"{content_path.stem}.grafik-check-{i}.json" for i in range(1, writer_count + 1)]
g_offen = [i for i, p in enumerate(gcheck_paths) if g_scope[i] and _lese_probleme_schema(_json_datei(p)) is None]
if g_offen:
await _set_step(guide_id, 7, "Prüfe Grafiken…")
slots = [{
"key": f"{guide_id}-gcheck-{i + 1}",
"prompt": _prompt("Guide-Grafik-Check", topic=topic, format_name=format_name,
sections=grafik_text(g_scope[i]), out_path=gcheck_paths[i]),
"role": "judge", "capabilities": "files",
"payload": (lambda result, p=gcheck_paths[i]: _lese_probleme_schema(_json_datei(p))),
} for i in g_offen]
if await _race(topic, "Grafik-Check", slots, len(slots), _timeout("grafik_check", max(chunk_sizes)), provider, cancelled=is_cancelled) is None:
_log(topic, "Grafik-Check fehlgeschlagen — Grafiken bleiben ungeprüft")
if is_cancelled():
return None
g_probleme: dict[int, str] = {}
for i, p in enumerate(gcheck_paths):
geltung = set(g_scope[i])
for item in (_lese_probleme_schema(_json_datei(p)) or []):
num = _titel_aufloesen(idx, item["section"])
if num in geltung and num not in g_probleme:
g_probleme[num] = item["problem"]
if g_probleme:
_log(topic, f"Grafik-Check: {len(g_probleme)} Grafik(en) beanstandet")
await _set_step(guide_id, 7, f"Überarbeite {len(g_probleme)} Grafik(en)…")
gfix_chunks = [[num for num in nums if num in g_probleme] for nums in chunk_nums]
gfix_paths = [content_path.parent / f"{content_path.stem}.grafik-fix-{i + 1}.md" for i in range(writer_count)]
gfix_offen = [i for i, nums in enumerate(gfix_chunks) if nums and not gfix_paths[i].exists()]
results = await asyncio.gather(*[
run_agent(
f"{guide_id}-gfix-w{i + 1}",
_prompt("Guide-Grafik-Fix", topic=topic, format_name=format_name,
auftraege=grafik_auftraege(gfix_chunks[i], g_probleme), out_path=gfix_paths[i], extra=_extra(instructions)),
_timeout("grafik_check", len(gfix_chunks[i])), provider=provider, role="guide", capabilities="files",
)
for i in gfix_offen
], return_exceptions=True)
if is_cancelled():
return None
for p in gfix_paths:
if not p.exists():
continue
for titel, code in _parse_grafik(p.read_text(encoding="utf-8")).items():
num = _titel_aufloesen(idx, titel)
if num in g_probleme and code.strip():
by_num[num]["grafik"] = code.strip()
await _set_progress(guide_id, "Setze zusammen…") await _set_progress(guide_id, "Setze zusammen…")
chapters: list[dict] = [] chapters: list[dict] = []
for ch in plan: for ch in plan:
sections = [ sections = [
{"num": num, "title": _titel(entries[num]), "md": by_num[num]["md"], {"num": num, "title": _titel(entries[num]), "md": by_num[num]["md"],
"kompakt": by_num[num].get("kompakt", ""), "grafik": by_num[num].get("grafik", "")} "kompakt": by_num[num].get("kompakt", "")}
for num in ch["nums"] if num in by_num for num in ch["nums"] if num in by_num
] ]
if sections: if sections:

View File

@@ -31,6 +31,11 @@ def frage_muster_path(topic: str) -> Path:
return topic_dir(topic) / "frage_muster.json" return topic_dir(topic) / "frage_muster.json"
def grafik_path(topic: str) -> Path:
"""Sidecar: pro Baustein eine Graph-Grafik (Knoten/Kanten), von allen Guides geteilt."""
return topic_dir(topic) / "grafik.json"
def quelle_path(topic: str) -> Path: def quelle_path(topic: str) -> Path:
"""Persistierte Quellen-Wahl pro Thema: {type, ort, spec}.""" """Persistierte Quellen-Wahl pro Thema: {type, ort, spec}."""
return topic_dir(topic) / "quelle.json" return topic_dir(topic) / "quelle.json"

View File

@@ -16,7 +16,7 @@ from database import (
list_baustein_progress, get_baustein_progress, set_offene_frage, list_baustein_progress, get_baustein_progress, set_offene_frage,
set_baustein_score, set_baustein_absolviert, set_baustein_verstanden, set_baustein_gemeistert, delete_baustein_daten, set_baustein_score, set_baustein_absolviert, set_baustein_verstanden, set_baustein_gemeistert, delete_baustein_daten,
) )
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht, subbausteine_titel, lade_frage_muster from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht, subbausteine_titel, lade_frage_muster, lade_grafiken
from elements import generate_element, chat_with_guide, chat_with_element, check_element, style_element, refine_suggestion from elements import generate_element, chat_with_guide, chat_with_element, check_element, style_element, refine_suggestion
from lernen import NOETIG, MASTERY, MEISTERN, NIVEAUS, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_bewertung_schnell, pruefung_frage, pruefung_frage_variante, quiz_generieren, lueckwahl_generieren, lueckentext_generieren, lueckentext_pruefen, score_berechnen, floor_aus_meilensteinen, _zufall_malus from lernen import NOETIG, MASTERY, MEISTERN, NIVEAUS, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_bewertung_schnell, pruefung_frage, pruefung_frage_variante, quiz_generieren, lueckwahl_generieren, lueckentext_generieren, lueckentext_pruefen, score_berechnen, floor_aus_meilensteinen, _zufall_malus
from guide import generate_guide, guide_slot_dateien from guide import generate_guide, guide_slot_dateien
@@ -212,6 +212,13 @@ async def get_frage_muster(topic: str, baustein: str):
return {"muster": lade_frage_muster(topic, baustein)} return {"muster": lade_frage_muster(topic, baustein)}
@router.get("/bausteine/grafiken")
async def get_grafiken(topic: str):
"""Alle Baustein-Grafiken eines Themas: {Baustein-Titel: {knoten, kanten, richtung}}.
Einmal je Thema geladen; das Frontend schlägt pro Baustein nach dem Titel nach."""
return {"grafiken": lade_grafiken(topic)}
# --- Baustein-Lernen: Chat, Prüfung --- # --- Baustein-Lernen: Chat, Prüfung ---
@router.get("/bausteine/lernstand", response_model=BausteinLernstandResponse) @router.get("/bausteine/lernstand", response_model=BausteinLernstandResponse)

View File

@@ -187,24 +187,6 @@ def _parse_fragment(text: str) -> list[dict]:
return sections return sections
_GRAFIK_MERMAID_RE = re.compile(r"```(?:mermaid)?\s*\n(.*?)```", re.DOTALL)
def _parse_grafik(text: str) -> dict[str, str]:
"""Grafik-Datei → {section-titel: mermaid-code}. Je `<!-- section: titel -->` den ersten
```mermaid…```-Block (sonst den Rohtext des Abschnitts)."""
out: dict[str, str] = {}
parts = re.split(r"<!--\s*section\s*:\s*(.*?)\s*-->", text, flags=re.IGNORECASE)
for i in range(1, len(parts), 2):
titel = parts[i].strip()
body = parts[i + 1] if i + 1 < len(parts) else ""
m = _GRAFIK_MERMAID_RE.search(body)
code = (m.group(1) if m else body).strip()
if titel and code:
out[titel] = code
return out
def _parse_subbausteine(text: str) -> dict[str, list[str]]: def _parse_subbausteine(text: str) -> dict[str, list[str]]:
"""Parst eine Subbaustein-Datei → {Baustein-Titel: [Subbaustein, …]} in Reihenfolge. """Parst eine Subbaustein-Datei → {Baustein-Titel: [Subbaustein, …]} in Reihenfolge.

File diff suppressed because it is too large Load Diff

View File

@@ -9,12 +9,13 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@dagrejs/dagre": "^3.0.0",
"@vue-flow/core": "^1.48.2",
"dompurify": "^3.4.7", "dompurify": "^3.4.7",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"katex": "^0.17.0", "katex": "^0.17.0",
"marked": "^18.0.4", "marked": "^18.0.4",
"marked-highlight": "^2.2.4", "marked-highlight": "^2.2.4",
"mermaid": "^11.15.0",
"vue": "^3.5.32" "vue": "^3.5.32"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -94,6 +94,11 @@ export async function fetchFrageMuster(topic, baustein) {
return jsonOrThrow(res) return jsonOrThrow(res)
} }
export async function fetchGrafiken(topic) {
const res = await fetch(`${BASE}/bausteine/grafiken?topic=${encodeURIComponent(topic)}`)
return jsonOrThrow(res)
}
export async function fetchTopicFortschritt(topic) { export async function fetchTopicFortschritt(topic) {
const res = await fetch(`${BASE}/topics/fortschritt?topic=${encodeURIComponent(topic)}`) const res = await fetch(`${BASE}/topics/fortschritt?topic=${encodeURIComponent(topic)}`)
return res.json() return res.json()

View File

@@ -1,10 +1,11 @@
<script setup> <script setup>
import BausteinPanel from './BausteinPanel.vue' import BausteinPanel from './BausteinPanel.vue'
import MermaidView from './MermaidView.vue' import GraphView from './GraphView.vue'
import { renderMarkdown } from '../markdown.js' import { renderMarkdown } from '../markdown.js'
const props = defineProps({ const props = defineProps({
baustein: { type: Object, required: true }, // { title, md, num } baustein: { type: Object, required: true }, // { title, md, num }
graph: { type: Object, default: null }, // {knoten,kanten,richtung} aus dem Grafik-Sidecar
topic: { type: String, required: true }, topic: { type: String, required: true },
provider: { type: String, default: 'claude' }, provider: { type: String, default: 'claude' },
status: { type: Object, default: null }, status: { type: Object, default: null },
@@ -89,7 +90,7 @@ const standFarbe = computed(() => {
</div> </div>
<div class="fokus-body"> <div class="fokus-body">
<div ref="guideEl" class="fokus-col left"> <div ref="guideEl" class="fokus-col left">
<MermaidView v-if="ansicht === 'grafik'" :code="baustein.grafik || ''" /> <GraphView v-if="ansicht === 'grafik'" :graph="graph" height="70vh" />
<div v-else class="markdown" v-html="renderMarkdown(ansicht === 'kompakt' ? (baustein.kompakt || baustein.md) : baustein.md)"></div> <div v-else class="markdown" v-html="renderMarkdown(ansicht === 'kompakt' ? (baustein.kompakt || baustein.md) : baustein.md)"></div>
</div> </div>
<div ref="rightEl" class="fokus-col right"> <div ref="rightEl" class="fokus-col right">

View File

@@ -0,0 +1,133 @@
<script setup>
import { ref, computed, watch, nextTick } from 'vue'
import { VueFlow, useVueFlow, Handle, Position, MarkerType } from '@vue-flow/core'
import '@vue-flow/core/dist/style.css'
import dagre from '@dagrejs/dagre'
import { renderMarkdownInline } from '../markdown.js'
// Grafik = {knoten:[{id,text}], kanten:[{von,nach,text?}], richtung:'TB'|'LR'}.
// KI liefert nur die Semantik; Layout macht dagre, Mathe rendert KaTeX im Knoten.
const props = defineProps({
graph: { type: Object, default: null },
height: { type: String, default: '380px' },
})
let _seq = 0
const flowId = `gv-${++_seq}-${Math.floor(performance.now())}`
const { findNode, fitView, onNodesInitialized } = useVueFlow(flowId)
const nodes = ref([])
const edges = ref([])
const bereit = ref(false) // erst nach dem Layout sichtbar (sonst kurz bei 0,0 gestapelt)
const fehler = ref(false)
const richtung = computed(() => (props.graph?.richtung === 'LR' ? 'LR' : 'TB'))
const istLR = computed(() => richtung.value === 'LR')
const targetPos = computed(() => (istLR.value ? Position.Left : Position.Top))
const sourcePos = computed(() => (istLR.value ? Position.Right : Position.Bottom))
function build() {
bereit.value = false
const g = props.graph
const kn = g && Array.isArray(g.knoten) ? g.knoten : []
if (!kn.length) {
fehler.value = true
nodes.value = []
edges.value = []
return
}
fehler.value = false
const ids = new Set(kn.map((k) => String(k.id)))
nodes.value = kn.map((k) => ({
id: String(k.id),
type: 'math',
position: { x: 0, y: 0 },
data: { text: String(k.text || '') },
}))
edges.value = (g.kanten || [])
.filter((e) => ids.has(String(e.von)) && ids.has(String(e.nach)))
.map((e, i) => ({
id: `e${i}`,
source: String(e.von),
target: String(e.nach),
label: e.text ? String(e.text) : undefined,
type: 'smoothstep',
markerEnd: MarkerType.ArrowClosed,
}))
}
// Two-Pass: Vue Flow misst die Knoten (KaTeX!), erst dann rechnet dagre das Layout.
function layout() {
const g = new dagre.graphlib.Graph()
g.setDefaultEdgeLabel(() => ({}))
g.setGraph({ rankdir: richtung.value, nodesep: 40, ranksep: 55 })
for (const n of nodes.value) {
const dim = findNode(n.id)?.dimensions
g.setNode(n.id, { width: dim?.width || 160, height: dim?.height || 40 })
}
for (const e of edges.value) g.setEdge(e.source, e.target)
dagre.layout(g)
nodes.value = nodes.value.map((n) => {
const p = g.node(n.id)
return { ...n, position: { x: p.x - p.width / 2, y: p.y - p.height / 2 } }
})
}
onNodesInitialized(async () => {
if (!nodes.value.length) return
await document.fonts.ready // sonst misst der Browser Fallback-Font → falsche Breite
layout()
bereit.value = true
await nextTick()
fitView({ padding: 0.2 })
})
watch(() => props.graph, build, { immediate: true })
</script>
<template>
<div class="graph-view" :style="{ height }">
<p v-if="fehler" class="graph-fallback">Grafik nicht verfügbar — Bausteine ab „Grafiken" neu bauen.</p>
<VueFlow
v-else
:id="flowId"
:nodes="nodes"
:edges="edges"
:style="{ opacity: bereit ? 1 : 0 }"
:nodes-draggable="false"
:nodes-connectable="false"
:elements-selectable="false"
:zoom-on-scroll="false"
:zoom-on-double-click="false"
:min-zoom="0.2"
:max-zoom="2"
fit-view-on-init
>
<template #node-math="{ data }">
<Handle type="target" :position="targetPos" />
<div class="gv-node markdown" v-html="renderMarkdownInline(data.text)"></div>
<Handle type="source" :position="sourcePos" />
</template>
</VueFlow>
</div>
</template>
<style scoped>
.graph-view { width: 100%; position: relative; }
.graph-fallback { color: var(--text-muted); font-size: 0.85rem; padding: 1rem 0; }
.gv-node {
padding: 0.4rem 0.65rem;
border: 1px solid var(--border-strong, #999);
border-radius: 8px;
background: var(--panel, #fff);
color: var(--text, #111);
font-size: 0.8rem;
line-height: 1.3;
text-align: center;
max-width: 240px;
}
/* KaTeX im Knoten nicht umbrechen lassen */
.gv-node :deep(.katex) { white-space: nowrap; }
.graph-view :deep(.vue-flow__edge-text) { font-size: 0.72rem; fill: var(--text-muted, #666); }
.graph-view :deep(.vue-flow__edge-textbg) { fill: var(--panel, #fff); }
</style>

View File

@@ -1,41 +0,0 @@
<script setup>
import { ref, watch, onMounted } from 'vue'
import mermaid from 'mermaid'
mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'neutral' })
const props = defineProps({ code: { type: String, default: '' } })
const svg = ref('')
const fehler = ref(false)
let n = 0
// Async rendern; bricht das Mermaid-Parsing (kaputte Syntax) → Fallback statt Absturz.
async function render() {
const code = (props.code || '').trim()
svg.value = ''
fehler.value = false
if (!code) { fehler.value = true; return }
try {
const { svg: out } = await mermaid.render(`mmd-${Date.now()}-${n++}`, code)
svg.value = out
} catch {
fehler.value = true
}
}
watch(() => props.code, render)
onMounted(render)
</script>
<template>
<div class="mermaid-view">
<div v-if="svg" v-html="svg"></div>
<p v-else class="mermaid-fallback">Grafik nicht verfügbar Guide neu bauen.</p>
</div>
</template>
<style scoped>
.mermaid-view { display: flex; justify-content: center; padding: 0.5rem 0; }
.mermaid-view :deep(svg) { max-width: 100%; height: auto; }
.mermaid-fallback { color: var(--text-muted); font-size: 0.85rem; padding: 1rem 0; }
</style>

View File

@@ -1,11 +1,11 @@
<script setup> <script setup>
import { computed, ref, watch, nextTick, onMounted, onUnmounted } from 'vue' import { computed, ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { fetchGuideContent, chatGuide, fetchBausteinLernstand } from '../api.js' import { fetchGuideContent, chatGuide, fetchBausteinLernstand, fetchGrafiken } from '../api.js'
import { renderMarkdown } from '../markdown.js' import { renderMarkdown } from '../markdown.js'
import { useChat } from '../composables/useChat.js' import { useChat } from '../composables/useChat.js'
import BausteinPanel from './BausteinPanel.vue' import BausteinPanel from './BausteinPanel.vue'
import BausteinFokus from './BausteinFokus.vue' import BausteinFokus from './BausteinFokus.vue'
import MermaidView from './MermaidView.vue' import GraphView from './GraphView.vue'
const props = defineProps({ const props = defineProps({
previewGuide: { type: Object, default: null }, previewGuide: { type: Object, default: null },
@@ -28,6 +28,7 @@ const CH_COLORS = ['#3b82f6', '#8b5cf6', '#14b8a6', '#f59e0b', '#22c55e', '#6366
const content = ref(null) const content = ref(null)
const loadError = ref(null) const loadError = ref(null)
const scrollEl = ref(null) const scrollEl = ref(null)
const grafiken = ref({}) // Baustein-Titel → {knoten,kanten,richtung} (Sidecar, von allen Guides geteilt)
const lernstand = ref({}) // Prüfungs-Stand pro Baustein-Titel — VOR dem immediate-Watch (loadContent nutzt es) const lernstand = ref({}) // Prüfungs-Stand pro Baustein-Titel — VOR dem immediate-Watch (loadContent nutzt es)
watch(() => props.previewGuide?.id, loadContent, { immediate: true }) watch(() => props.previewGuide?.id, loadContent, { immediate: true })
@@ -36,6 +37,7 @@ async function loadContent() {
content.value = null content.value = null
loadError.value = null loadError.value = null
lernstand.value = {} lernstand.value = {}
grafiken.value = {}
const g = props.previewGuide const g = props.previewGuide
if (!g || g.status !== 'done') return if (!g || g.status !== 'done') return
try { try {
@@ -49,6 +51,9 @@ async function loadContent() {
try { try {
lernstand.value = (await fetchBausteinLernstand(g.topic)).bausteine || {} lernstand.value = (await fetchBausteinLernstand(g.topic)).bausteine || {}
} catch { /* offline → leer */ } } catch { /* offline → leer */ }
try {
grafiken.value = (await fetchGrafiken(g.topic)).grafiken || {}
} catch { /* keine Grafiken → Fallback */ }
} }
} }
@@ -205,7 +210,7 @@ function extractContext() {
<span v-else-if="lernstand[s.title]?.verstanden" class="baustein-done verstanden" title="Vollständig verstanden (10/10)"> Verstanden</span> <span v-else-if="lernstand[s.title]?.verstanden" class="baustein-done verstanden" title="Vollständig verstanden (10/10)"> Verstanden</span>
<span v-else-if="lernstand[s.title]?.absolviert" class="baustein-done" title="Prüfung bestanden"> Absolviert</span> <span v-else-if="lernstand[s.title]?.absolviert" class="baustein-done" title="Prüfung bestanden"> Absolviert</span>
</h3> </h3>
<MermaidView v-if="!isOnePager && ansichtModus === 'grafik'" :code="s.grafik || ''" /> <GraphView v-if="!isOnePager && ansichtModus === 'grafik'" :graph="grafiken[s.title] || null" />
<div v-else class="section-body markdown" v-html="renderMarkdown(!isOnePager && ansichtModus === 'kompakt' ? (s.kompakt || s.md) : s.md)"></div> <div v-else class="section-body markdown" v-html="renderMarkdown(!isOnePager && ansichtModus === 'kompakt' ? (s.kompakt || s.md) : s.md)"></div>
<BausteinPanel <BausteinPanel
v-if="!isOnePager" v-if="!isOnePager"
@@ -233,6 +238,7 @@ function extractContext() {
<BausteinFokus <BausteinFokus
v-if="fokusBaustein" v-if="fokusBaustein"
:baustein="fokusBaustein" :baustein="fokusBaustein"
:graph="grafiken[fokusBaustein.title] || null"
:topic="previewGuide.topic" :topic="previewGuide.topic"
:provider="provider" :provider="provider"
:fortschritt="fortschritt" :fortschritt="fortschritt"

View File

@@ -0,0 +1,30 @@
Du prüfst und korrigierst eine **Grafik** (gerichteter Graph als JSON) zum Thema "{topic}", Baustein "{baustein}". Die Grafik soll die Subbausteine als Muster/Ablauf/Struktur zeigen.
RELEVANTE SUBBAUSTEINE (sollen abgedeckt sein):
{subbausteine}
AKTUELLE GRAFIK (JSON):
{grafik}
PRÜFE auf:
- **Struktur:** jede Kante verweist mit `von`/`nach` auf eine existierende Knoten-`id`; keine hängenden Kanten; `id`s eindeutig; Graph zusammenhängend (keine losen Knoten).
- **Inhalt:** die relevanten Subbausteine kommen vor; Richtung/Kanten geben die Beziehungen korrekt wieder; nichts Erfundenes.
- **Mathe:** Symbole stehen als echte KaTeX-Notation in Dollarzeichen (`$\Sigma$`, `$w \in L$`), nicht als Wort ausgeschrieben. Korrigiere Verstöße.
- **Kanten-Label:** nur ein kurzes Wort (ja/nein/kodiert), **keine Formeln/Dollarzeichen** in Kanten — Formeln gehören in die Knoten.
- **Knappheit:** Knotentexte kurz.
Korrigiere alle Mängel. Ist die Grafik bereits gut, gib sie unverändert zurück.
{extra}
Schreibe die geprüfte Grafik als JSON in die Datei {out_path} (nutze dein Schreib-Werkzeug), GENAU in diesem Format:
{{
"richtung": "TB",
"knoten": [
{{"id": "a", "text": "Begriff $(L, \\Sigma)$"}}
],
"kanten": [
{{"von": "a", "nach": "b", "text": "ja"}}
]
}}
Gib sonst keinen Text aus.

View File

@@ -0,0 +1,30 @@
Du erstellst für einen Lern-Guide zum Thema "{topic}" eine **Grafik** zum Baustein "{baustein}". Ziel: Der Lernende erkennt auf einen Blick das Muster, den Ablauf oder die Struktur der Subbausteine — kein Fließtext, sondern ein Graph aus Knoten und Kanten.
BAUSTEIN-BESCHREIBUNG:
{beschreibung}
RELEVANTE SUBBAUSTEINE (die Grafik soll diese zeigen):
{subbausteine}
AUFGABE:
- Baue EINEN gerichteten Graphen: **Knoten** = die Subbausteine (plus ggf. Verbindungs-Knoten wie Start/Ende/Verzweigung), **Kanten** = ihre Beziehungen (Reihenfolge, Abhängigkeit, Ober-/Unterbegriff, Ja/Nein).
- Decke die relevanten Subbausteine ab. Erfinde keine Inhalte außerhalb des Bausteins.
- Wähle die Richtung passend: `TB` (oben→unten, für Abläufe/Hierarchien) oder `LR` (links→rechts, für Ketten).
- **Knotentext kurz** (Stichwort/kurzer Satz). Mathematik schreibst du als **echte Notation** mit KaTeX in Dollarzeichen: `$\Sigma$`, `$(L, \Sigma)$`, `$w \in L$`, `$2^n$`. Niemals Symbole als Wort ausschreiben (nicht „Sigma", nicht „Vereinigung").
- Kanten-Label (`text`) ist optional und **nur ein kurzes Wort** (z.B. „kodiert", „ja", „nein") — **keine Formeln/Dollarzeichen** in Kanten. Formeln gehören in die Knoten.
- Jede `id` ist ein kurzes Kürzel (az, 09), eindeutig. Jede Kante verweist mit `von`/`nach` auf existierende Knoten-`id`s.
{extra}
Schreibe das Ergebnis als JSON in die Datei {out_path} (nutze dein Schreib-Werkzeug), GENAU in diesem Format:
{{
"richtung": "TB",
"knoten": [
{{"id": "a", "text": "Erster Begriff $(L, \\Sigma)$"}},
{{"id": "b", "text": "Zweiter Begriff"}}
],
"kanten": [
{{"von": "a", "nach": "b", "text": "ja"}}
]
}}
Gib sonst keinen Text aus.

View File

@@ -1,17 +0,0 @@
Du bist Qualitäts-Prüfer für Mermaid-Grafiken in einem Lern-Guide zum Thema "{topic}" (Format {format_name}). Pro Section gibt es ein Diagramm. Prüfe jedes streng.
GRAFIKEN (Section-Titel + Mermaid-Code):
{sections}
PRÜFE GEGEN DIESE KRITERIEN:
- **Syntax:** gültiger Mermaid-Header (`flowchart`/`graph`/`sequenceDiagram`/…), jede Kante/jeder
Knoten korrekt, keine offenen Klammern/Anführungszeichen, keine `$…$`-Mathe. Muss renderbar sein.
- **Inhalt:** zeigt die Subbausteine der Section + sinnvolle Zusammenhänge (Ablauf/Hierarchie/
Beziehung). Kein Widerspruch zum Inhalt, nichts Erfundenes.
- **Klar:** kurze Knotentexte, kein Fließtext-Klotz.
Beanstande NUR echte Mängel. Ist die Grafik renderbar, korrekt und klar, ist sie in Ordnung.
Gib NUR JSON aus (kein weiterer Text):
- Alles in Ordnung: {{"ok": true}}
- Sonst: {{"probleme": [{{"section": "Exakter Titel", "problem": "kurzer Mangel"}}]}}

View File

@@ -1,22 +0,0 @@
Du überarbeitest beanstandete Mermaid-Grafiken in einem Lern-Guide zum Thema "{topic}" (Format {format_name}). Behebe die genannten Mängel — sonst lasse die Grafik, wie sie ist.
AUFTRÄGE (Section-Titel + Problem + aktuelle Grafik):
{auftraege}
REGELN:
- Renderbares Mermaid: gültiger Header, korrekte Kanten/Knoten, keine `$…$`-Mathe (Symbole als Wort),
keine offenen Klammern/Anführungszeichen.
- Inhalt: Subbausteine + sinnvolle Zusammenhänge (Ablauf/Hierarchie/Beziehung), kurze Knotentexte.
- Genau EIN Diagramm je Section.
Schreibe die überarbeiteten Grafiken in die Datei {out_path} (nutze dein Schreib-Werkzeug),
GENAU so — nur die beanstandeten Sections:
<!-- section: Exakter Section-Titel -->
```mermaid
flowchart TD
A[Begriff] --> B[Begriff]
```
Gib sonst keinen Text aus.
{extra}

View File

@@ -1,26 +0,0 @@
Du erstellst für einen Lern-Guide zum Thema "{topic}" (Format {format_name}) pro Baustein-Section EINE **Mermaid-Grafik**. Die Grafik soll die Subbausteine + ihre Zusammenhänge zeigen, damit der Lerner Muster, Abläufe oder Strukturen auf einen Blick erkennt.
SECTIONS (jeweils Titel + ausführlicher Inhalt):
{sections}
AUFGABE:
- Für JEDE Section genau EIN Mermaid-Diagramm.
- Wähle den passenden Typ selbst: `flowchart`, `graph`, `sequenceDiagram`, `mindmap`,
`stateDiagram-v2`, … — was die Sache am besten zeigt (Ablauf, Hierarchie, Beziehung).
- Zeige die **Subbausteine** als Knoten und ihre Verbindungen (Reihenfolge, Abhängigkeit,
Ober-/Unterbegriff). Ziel: Flow/Muster erkennen, nicht Fließtext wiederholen.
- **Knotentexte kurz** (Stichwort). Keine `$…$`-Mathe in Mermaid — schreibe Symbole als Wort
(z. B. „A und B" statt `A ∧ B`). Anführungszeichen in Texten vermeiden.
- Syntaktisch sauberes, renderbares Mermaid (gültiger Diagramm-Header, jede Kante korrekt).
Schreibe das Ergebnis in die Datei {out_path} (nutze dein Schreib-Werkzeug), GENAU so —
pro Section ein Marker + ein Mermaid-Codeblock:
<!-- section: Exakter Section-Titel -->
```mermaid
flowchart TD
A[Erster Begriff] --> B[Zweiter Begriff]
```
Gib sonst keinen Text aus.
{extra}