137 lines
7.1 KiB
Python
137 lines
7.1 KiB
Python
"""Ebene 3.5 Diagramme: LLM-Diagramme (Automat/Ablauf/Baum), Spec→Mermaid, Assembly."""
|
|
|
|
import db
|
|
import diagramme
|
|
import llm
|
|
from conftest import run_anlegen, topic_anlegen
|
|
|
|
|
|
def _baustein_mit_kette(topic, n=4):
|
|
"""Baustein mit n Atomen und einer braucht-Kette a0←a1←…←a(n-1) (n-1 Kanten)."""
|
|
soll = db.insert("soll", topic=topic, punkt="P", status="bestaetigt", belege=db.j([]))
|
|
ziel = db.insert("lernziele", topic=topic, text="Kann P", soll_id=soll, status="aktiv")
|
|
b = db.insert("bausteine", topic=topic, ziel_id=ziel, titel="B", ord=0, status="neu")
|
|
atome = [db.insert("atome", topic=topic, titel=f"Atom {i}", typ="begriff",
|
|
definition="d", status="neu", baustein_id=b, ord=i, braucht=db.j([]))
|
|
for i in range(n)]
|
|
for i in range(1, n): # Atom i braucht Atom i-1
|
|
db.execute("INSERT INTO kanten(topic, von_atom, zu_atom, art) VALUES(?,?,?,'braucht')",
|
|
(topic, atome[i], atome[i - 1]))
|
|
return b, atome
|
|
|
|
|
|
def test_spec_zu_mermaid_flowchart():
|
|
spec = {"typ": "flow",
|
|
"knoten": [{"id": "1", "label": "A"}, {"id": "2", "label": 'B "x" [y]'}],
|
|
"kanten": [{"von": "1", "zu": "2", "label": ""}]}
|
|
out = diagramme._spec_zu_mermaid(spec)
|
|
assert out.startswith("flowchart TD")
|
|
assert 'n1["A"]' in out and "n1 --> n2" in out
|
|
assert '"' not in out.split("\n", 1)[1].replace('["', "").replace('"]', "") # Label entschärft
|
|
|
|
|
|
async def test_fake_judge_nein_keine_diagramme():
|
|
topic = topic_anlegen("diag-leer")
|
|
b, _ = _baustein_mit_kette(topic, n=2)
|
|
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
|
|
await diagramme.bauen(ctx) # Fake-Judge sagt noetig=False → nur 'kein'-Merkzeile
|
|
assert db.query("SELECT id FROM diagramme WHERE topic=? AND status='verifiziert'",
|
|
(topic,)) == []
|
|
assert db.one("SELECT status FROM diagramme WHERE baustein_id=?", (b,))["status"] == "kein"
|
|
|
|
|
|
async def test_llm_diagramm_plan_spec_verify(monkeypatch):
|
|
"""LLM-Zweig: Judge sagt nötig → Spec → Mermaid → Grounding-Panel verifiziert."""
|
|
import fake_agents
|
|
topic = topic_anlegen("diag-llm")
|
|
# Baustein OHNE genug Kanten (kein DAG) → LLM-Zweig entscheidet
|
|
soll = db.insert("soll", topic=topic, punkt="P", status="bestaetigt", belege=db.j([]))
|
|
ziel = db.insert("lernziele", topic=topic, text="Kann P", soll_id=soll, status="aktiv")
|
|
b = db.insert("bausteine", topic=topic, ziel_id=ziel, titel="Automat", ord=0, status="neu")
|
|
for i in range(3):
|
|
db.insert("atome", topic=topic, titel=f"Zustand {i}", typ="begriff", definition="d",
|
|
status="neu", baustein_id=b, ord=i, braucht=db.j([]))
|
|
monkeypatch.setitem(fake_agents._HANDLER, "Diagramm-Plan",
|
|
lambda p: {"noetig": True, "typ": "state"})
|
|
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
|
|
await diagramme.bauen(ctx)
|
|
d = db.one("SELECT * FROM diagramme WHERE baustein_id=?", (b,))
|
|
assert d["status"] == "verifiziert" and d["quelle"] == "llm" and d["typ"] == "state"
|
|
assert d["mermaid"].startswith("stateDiagram-v2")
|
|
|
|
|
|
async def test_llm_diagramm_grounding_verwirft(monkeypatch):
|
|
"""Grounding-Panel einstimmig ok=false → Diagramm verworfen (fail-closed)."""
|
|
import fake_agents
|
|
topic = topic_anlegen("diag-verwirf")
|
|
soll = db.insert("soll", topic=topic, punkt="P", status="bestaetigt", belege=db.j([]))
|
|
ziel = db.insert("lernziele", topic=topic, text="Kann P", soll_id=soll, status="aktiv")
|
|
b = db.insert("bausteine", topic=topic, ziel_id=ziel, titel="X", ord=0, status="neu")
|
|
for i in range(3):
|
|
db.insert("atome", topic=topic, titel=f"K{i}", typ="begriff", definition="d",
|
|
status="neu", baustein_id=b, ord=i, braucht=db.j([]))
|
|
monkeypatch.setitem(fake_agents._HANDLER, "Diagramm-Plan",
|
|
lambda p: {"noetig": True, "typ": "flow"})
|
|
monkeypatch.setitem(fake_agents._HANDLER, "Diagramm-Verify",
|
|
lambda p: [{"ok": False, "mangel": "erfundener Knoten"}])
|
|
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
|
|
await diagramme.bauen(ctx)
|
|
assert db.one("SELECT status FROM diagramme WHERE baustein_id=?", (b,))["status"] == "verworfen"
|
|
|
|
|
|
async def test_marker_wird_zu_fence():
|
|
"""Assembly: platzierter <!-- diagramm: id --> → ```mermaid-Fence."""
|
|
import guide
|
|
topic = topic_anlegen("diag-place")
|
|
b, _ = _baustein_mit_kette(topic, n=2)
|
|
mermaid = "flowchart TD\n n1[\"A\"]\n n2[\"B\"]\n n1 --> n2"
|
|
d = db.insert("diagramme", topic=topic, baustein_id=b, typ="flow", quelle="llm",
|
|
spec=db.j({}), mermaid=mermaid, status="verifiziert")
|
|
db.insert("sections", baustein_id=b, stage="done", text_kompakt="- k",
|
|
text_lang=f"Text.\n\n<!-- diagramm: {d} -->\n\nMehr Text.")
|
|
lang = guide.kapitel_struktur(topic)[0]["sections"][0]["lang"]
|
|
assert "```mermaid" in lang and "flowchart TD" in lang
|
|
assert "<!-- diagramm:" not in lang # Marker ersetzt
|
|
|
|
|
|
async def test_toter_diagramm_marker_wird_entfernt():
|
|
import guide
|
|
topic = topic_anlegen("diag-tot")
|
|
b, _ = _baustein_mit_kette(topic, n=2) # kein Diagramm gebaut
|
|
db.insert("sections", baustein_id=b, stage="done", text_kompakt="- k",
|
|
text_lang="Text.\n\n<!-- diagramm: 999 -->\n\nMehr.")
|
|
lang = guide.kapitel_struktur(topic)[0]["sections"][0]["lang"]
|
|
assert "<!-- diagramm:" not in lang and "```mermaid" not in lang
|
|
|
|
|
|
async def test_guide_messen_meldet_toten_marker():
|
|
"""Ein Marker ohne verifiziertes Diagramm → diagramm_marker_tot in guide.messen."""
|
|
import guide
|
|
topic = topic_anlegen("diag-qa")
|
|
b, _ = _baustein_mit_kette(topic, n=4)
|
|
db.insert("sections", baustein_id=b, stage="done", text_kompakt="- k",
|
|
text_lang="Text.\n\n<!-- diagramm: 777 -->\n\n" + "Wort " * 60)
|
|
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
|
|
ctx.ebene = "guide"
|
|
assert "diagramm_marker_tot" in {x["art"] for x in guide.messen(ctx)}
|
|
|
|
|
|
async def test_marker_platzieren_deterministisch():
|
|
"""Der Writer setzt keine Beispiel-Marker mehr — _marker_platzieren fügt sie
|
|
hinter den Atom-Absatz ein (atom-genau)."""
|
|
import guide
|
|
topic = topic_anlegen("platz")
|
|
b = db.insert("bausteine", topic=topic, ziel_id=1, titel="B", ord=0, status="neu")
|
|
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
|
|
status="neu", baustein_id=b, ord=0, braucht=db.j([]))
|
|
art = db.insert("artefakte", atom_id=a, typ="beispiel", status="verifiziert",
|
|
inhalt=db.j({"form": "tabelle", "tabelle": "| A | B |\n|---|---|\n| 1 | 2 |"}))
|
|
lang = f"#### X\n<!-- atom: {a} | X -->\nErklärung von X.\n\nWeiterer Absatz zum Thema."
|
|
out = guide._marker_platzieren(db.one("SELECT * FROM bausteine WHERE id=?", (b,)), lang)
|
|
assert f"<!-- beispiel: {art} -->" in out
|
|
# der Marker steht NACH dem Atom-Absatz
|
|
assert out.index("<!-- beispiel:") < out.index("Weiterer Absatz")
|
|
# idempotent: zweiter Lauf dupliziert nicht
|
|
assert guide._marker_platzieren(db.one("SELECT * FROM bausteine WHERE id=?", (b,)),
|
|
out).count("<!-- beispiel:") == 1
|