update
This commit is contained in:
@@ -14,6 +14,14 @@ def _atom(topic, titel, ziel_id=None, soll_id=1):
|
||||
soll_id=soll_id, ziel_id=ziel_id)
|
||||
|
||||
|
||||
def _thema(topic, soll_ids, titel="T", ord=0):
|
||||
"""Themen-Seed: eine themen-Zeile, Soll-Punkte darauf zeigen lassen."""
|
||||
t = db.insert("themen", topic=topic, titel=titel, ord=ord, art="judge")
|
||||
for s in soll_ids:
|
||||
db.update("soll", "id", s, thema_id=t)
|
||||
return t
|
||||
|
||||
|
||||
def _baustein_groessen(topic):
|
||||
from collections import Counter
|
||||
c = Counter(a["baustein_id"] for a in struktur._atome(topic))
|
||||
@@ -88,13 +96,14 @@ async def test_band_split():
|
||||
assert not [x for x in befunde if x["art"] in ("band", "partition")]
|
||||
|
||||
|
||||
async def test_merge_ohne_soll_schranke_und_kapitel():
|
||||
# Entkopplung: kleines Ziel merged über Soll-Punkt-Grenzen (Partner per
|
||||
# Quell-Nähe); Kapitel entstehen danach als kontiguierliche Segmente
|
||||
async def test_merge_im_thema_ueber_soll_grenzen():
|
||||
# kleines Ziel merged über Soll-Punkt-Grenzen, solange BEIDE Punkte im
|
||||
# selben Thema liegen; Kapitel = (Level, Thema) deterministisch
|
||||
topic = topic_anlegen("kapitel")
|
||||
run = run_anlegen(topic)
|
||||
s1 = db.insert("soll", topic=topic, punkt="P1", status="bestaetigt", belege=db.j([]))
|
||||
s2 = db.insert("soll", topic=topic, punkt="P2", status="bestaetigt", belege=db.j([]))
|
||||
t = _thema(topic, [s1, s2], "Gemeinsames Thema")
|
||||
z1 = db.insert("lernziele", topic=topic, text="Kann P1", soll_id=s1, status="aktiv")
|
||||
z2 = db.insert("lernziele", topic=topic, text="Kann P2", soll_id=s2, status="aktiv")
|
||||
_atom(topic, "A", z1, s1) # 1-Atom-Ziel, anderer Soll-Punkt als z2
|
||||
@@ -105,9 +114,12 @@ async def test_merge_ohne_soll_schranke_und_kapitel():
|
||||
await struktur._bausteine_schneiden(ctx)
|
||||
atome = struktur._atome(topic)
|
||||
assert len({x["baustein_id"] for x in atome}) == 1 # gemerged trotz fremdem Soll
|
||||
await struktur._kapitel_bilden(ctx)
|
||||
struktur._kapitel_bilden(topic)
|
||||
bausteine = db.query("SELECT * FROM bausteine WHERE topic=?", (topic,))
|
||||
assert bausteine and all(x["kapitel_id"] for x in bausteine)
|
||||
assert all(x["thema_id"] == t for x in bausteine)
|
||||
kaps = db.query("SELECT * FROM kapitel WHERE topic=?", (topic,))
|
||||
assert [k["titel"] for k in kaps] == ["Gemeinsames Thema"]
|
||||
assert struktur.messen(ctx) == []
|
||||
|
||||
|
||||
@@ -183,32 +195,30 @@ def test_baustein_rang_median_gegen_merge_gift():
|
||||
assert b_rang[2] < b_rang[1] # trotz 464-Import bleibt Baustein 1 hinten
|
||||
|
||||
|
||||
async def test_kapitel_retry_und_fallback(monkeypatch):
|
||||
topic = topic_anlegen("kapfall")
|
||||
run = run_anlegen(topic)
|
||||
def test_kapitel_deterministisch(monkeypatch):
|
||||
"""Kapitel = (Level, Thema)-Segment ohne LLM: Titel = Thema-Titel, art='det',
|
||||
Reihenfolge folgt der globalen Baustein-ord."""
|
||||
topic = topic_anlegen("kapdet")
|
||||
t1 = db.insert("themen", topic=topic, titel="Thema Eins", ord=0, art="judge")
|
||||
t2 = db.insert("themen", topic=topic, titel="Thema Zwei", ord=1, art="judge")
|
||||
ziel = db.insert("lernziele", topic=topic, text="Kann X", status="aktiv")
|
||||
b = [db.insert("bausteine", topic=topic, ziel_id=ziel, titel=f"B{i}", ord=i,
|
||||
status="neu") for i in range(4)]
|
||||
ctx = llm.Kontext(run, topic, "minimax")
|
||||
ctx.ebene = "struktur"
|
||||
|
||||
aufrufe = {"n": 0}
|
||||
def judge(prompt):
|
||||
aufrufe["n"] += 1
|
||||
if aufrufe["n"] == 1:
|
||||
return [{"titel": "kaputt", "bis": 999999}] # ungültige Grenze
|
||||
return [{"titel": "Gutes Kapitel", "bis": b[-1]}]
|
||||
monkeypatch.setitem(fake_agents._HANDLER, "Kapitel-Schnitt", judge)
|
||||
await struktur._kapitel_bilden(ctx)
|
||||
kaps = db.query("SELECT * FROM kapitel WHERE topic=?", (topic,))
|
||||
assert aufrufe["n"] == 2 and len(kaps) == 1 and kaps[0]["art"] == "judge"
|
||||
|
||||
monkeypatch.setitem(fake_agents._HANDLER, "Kapitel-Schnitt",
|
||||
lambda p: [{"titel": "x", "bis": 999999}])
|
||||
await struktur._kapitel_bilden(ctx) # beide Versuche ungültig → Fallback
|
||||
kaps = db.query("SELECT * FROM kapitel WHERE topic=?", (topic,))
|
||||
assert kaps and all(k["art"] == "fallback" for k in kaps)
|
||||
assert "kapitel_fallback" in {bf["art"] for bf in struktur.messen(ctx)}
|
||||
ordn = 0
|
||||
for level in ("E", "M"):
|
||||
for thema in (t1, t2):
|
||||
db.insert("bausteine", topic=topic, ziel_id=ziel, titel=f"B{ordn}",
|
||||
ord=ordn, status="neu", level=level, thema_id=thema)
|
||||
ordn += 1
|
||||
def explodiert(prompt): # beweist: kein LLM-Call nötig
|
||||
raise AssertionError("Kapitel-Bildung darf keinen Judge rufen")
|
||||
monkeypatch.setitem(fake_agents._HANDLER, "Themen-Schnitt", explodiert)
|
||||
struktur._kapitel_bilden(topic)
|
||||
kaps = db.query("SELECT * FROM kapitel WHERE topic=? ORDER BY ord", (topic,))
|
||||
assert [k["titel"] for k in kaps] == ["Thema Eins", "Thema Zwei"] * 2
|
||||
assert [k["level"] for k in kaps] == ["E", "E", "M", "M"]
|
||||
assert all(k["art"] == "det" for k in kaps)
|
||||
zu_kapitel = {b["ord"]: b["kapitel_id"] for b in
|
||||
db.query("SELECT * FROM bausteine WHERE topic=?", (topic,))}
|
||||
assert len(set(zu_kapitel.values())) == 4 # je (Level, Thema) ein Kapitel
|
||||
|
||||
|
||||
async def test_level_kalibrierung(monkeypatch):
|
||||
@@ -231,12 +241,13 @@ async def test_level_kalibrierung(monkeypatch):
|
||||
assert db.one("SELECT level FROM atome WHERE id=?", (a2,))["level"] == "S"
|
||||
|
||||
|
||||
async def test_level_split_ordnung_kapitel():
|
||||
# Ein Ziel mit 4×E + 4×M → zwei Bausteine mit level, E-ord < M-ord,
|
||||
# Kapitel je Durchgang, kein level_mix/kapitel_level
|
||||
topic = topic_anlegen("split")
|
||||
async def test_dominantes_level():
|
||||
# Ein Ziel mit 4×E + 4×M → EIN Baustein (kein Level-Split mehr);
|
||||
# Level = Mehrheit, Gleichstand → niedrigeres (E). 3×E + 5×M → M.
|
||||
topic = topic_anlegen("dominant")
|
||||
run = run_anlegen(topic)
|
||||
soll = db.insert("soll", topic=topic, punkt="P", status="bestaetigt", belege=db.j([]))
|
||||
_thema(topic, [soll])
|
||||
ziel = db.insert("lernziele", topic=topic, text="Kann P", soll_id=soll, status="aktiv")
|
||||
for i in range(4):
|
||||
db.insert("atome", topic=topic, titel=f"E{i}", typ="begriff", definition="d",
|
||||
@@ -247,29 +258,106 @@ async def test_level_split_ordnung_kapitel():
|
||||
ctx.ebene = "struktur"
|
||||
await struktur._bausteine_schneiden(ctx)
|
||||
bausteine = db.query("SELECT * FROM bausteine WHERE topic=? ORDER BY ord", (topic,))
|
||||
assert [b["level"] for b in bausteine] == ["E", "M"]
|
||||
await struktur._kapitel_bilden(ctx)
|
||||
kaps = db.query("SELECT * FROM kapitel WHERE topic=? ORDER BY ord", (topic,))
|
||||
assert [k["level"] for k in kaps] == ["E", "M"]
|
||||
assert [b["level"] for b in bausteine] == ["E"] # Tie 4/4 → niedrigeres
|
||||
struktur._kapitel_bilden(topic)
|
||||
arten = {b["art"] for b in struktur.messen(ctx)}
|
||||
assert not arten & {"level_mix", "kapitel_level", "band", "partition"}
|
||||
assert not arten & {"kapitel_level", "band", "partition", "baustein_thema_mix"}
|
||||
# Mehrheit gewinnt: reine Funktion
|
||||
assert struktur._dominantes_level([{"level": "E"}] * 3 + [{"level": "M"}] * 5) == "M"
|
||||
assert struktur._dominantes_level([{"level": "S"}]) == "S"
|
||||
|
||||
|
||||
async def test_kein_merge_ueber_level():
|
||||
# kleine E-Gruppe darf NICHT mit kleiner M-Gruppe mergen
|
||||
topic = topic_anlegen("levelmerge")
|
||||
async def test_kein_merge_ueber_thema():
|
||||
# kleine Ziele in VERSCHIEDENEN Themen mergen NICHT — zwei Bausteine,
|
||||
# und kein band-Befund (kein Partner im eigenen Thema)
|
||||
topic = topic_anlegen("themamerge")
|
||||
run = run_anlegen(topic)
|
||||
soll = db.insert("soll", topic=topic, punkt="P", status="bestaetigt", belege=db.j([]))
|
||||
z1 = db.insert("lernziele", topic=topic, text="Kann P1", soll_id=soll, status="aktiv")
|
||||
z2 = db.insert("lernziele", topic=topic, text="Kann P2", soll_id=soll, status="aktiv")
|
||||
s1 = db.insert("soll", topic=topic, punkt="P1", status="bestaetigt", belege=db.j([]))
|
||||
s2 = db.insert("soll", topic=topic, punkt="P2", status="bestaetigt", belege=db.j([]))
|
||||
_thema(topic, [s1], "T1", 0)
|
||||
_thema(topic, [s2], "T2", 1)
|
||||
z1 = db.insert("lernziele", topic=topic, text="Kann P1", soll_id=s1, status="aktiv")
|
||||
z2 = db.insert("lernziele", topic=topic, text="Kann P2", soll_id=s2, status="aktiv")
|
||||
for i in range(2):
|
||||
db.insert("atome", topic=topic, titel=f"E{i}", typ="begriff", definition="d",
|
||||
level="E", status="neu", soll_id=soll, ziel_id=z1)
|
||||
db.insert("atome", topic=topic, titel=f"M{i}", typ="begriff", definition="d",
|
||||
level="M", status="neu", soll_id=soll, ziel_id=z2)
|
||||
_atom(topic, f"A{i}", z1, s1)
|
||||
_atom(topic, f"B{i}", z2, s2)
|
||||
ctx = llm.Kontext(run, topic, "minimax")
|
||||
ctx.ebene = "struktur"
|
||||
await struktur._bausteine_schneiden(ctx)
|
||||
for b in db.query("SELECT * FROM bausteine WHERE topic=?", (topic,)):
|
||||
levels = {a["level"] for a in struktur._atome(topic)
|
||||
if a["baustein_id"] == b["id"]}
|
||||
assert levels == {b["level"]}
|
||||
bausteine = db.query("SELECT * FROM bausteine WHERE topic=?", (topic,))
|
||||
assert len(bausteine) == 2 and {b["ziel_id"] for b in bausteine} == {z1, z2}
|
||||
struktur._kapitel_bilden(topic)
|
||||
assert "band" not in {b["art"] for b in struktur.messen(ctx)}
|
||||
|
||||
|
||||
async def test_themen_bilden_gate_retry_fallback(monkeypatch):
|
||||
topic = topic_anlegen("themengate")
|
||||
run = run_anlegen(topic)
|
||||
punkte = [db.insert("soll", topic=topic, punkt=f"P{i}", status="bestaetigt",
|
||||
belege=db.j([])) for i in range(4)]
|
||||
aufrufe = {"n": 0}
|
||||
|
||||
def judge(prompt):
|
||||
aufrufe["n"] += 1
|
||||
if aufrufe["n"] == 1: # unvollständig → Gate schlägt zu, Retry
|
||||
return [{"titel": "T", "punkte": [1, 2]}]
|
||||
return [{"titel": "Grundlagen", "punkte": [1, 3]},
|
||||
{"titel": "Vertiefung", "punkte": [2, 4]}]
|
||||
monkeypatch.setitem(fake_agents._HANDLER, "Themen-Schnitt", judge)
|
||||
ctx = llm.Kontext(run, topic, "minimax")
|
||||
ctx.ebene = "struktur"
|
||||
await struktur._themen_bilden(ctx)
|
||||
assert aufrufe["n"] == 2 # Retry hat gegriffen
|
||||
themen = db.query("SELECT * FROM themen WHERE topic=? ORDER BY ord", (topic,))
|
||||
assert [t["titel"] for t in themen] == ["Grundlagen", "Vertiefung"]
|
||||
zu = {p: db.one("SELECT thema_id FROM soll WHERE id=?", (p,))["thema_id"]
|
||||
for p in punkte}
|
||||
assert zu[punkte[0]] == zu[punkte[2]] == themen[0]["id"]
|
||||
assert zu[punkte[1]] == zu[punkte[3]] == themen[1]["id"]
|
||||
assert "thema_partition" not in {b["art"] for b in struktur.messen(ctx)}
|
||||
|
||||
# beide Versuche Müll → √n-Fallback + Befund
|
||||
db.execute("DELETE FROM themen WHERE topic=?", (topic,))
|
||||
db.execute("UPDATE soll SET thema_id=NULL WHERE topic=?", (topic,))
|
||||
monkeypatch.setitem(fake_agents._HANDLER, "Themen-Schnitt", lambda p: [{"x": 1}])
|
||||
await struktur._themen_bilden(ctx)
|
||||
themen = db.query("SELECT * FROM themen WHERE topic=?", (topic,))
|
||||
assert themen and all(t["art"] == "fallback" for t in themen)
|
||||
assert "themen_fallback" in {b["art"] for b in struktur.messen(ctx)}
|
||||
offen = db.query("SELECT * FROM soll WHERE topic=? AND thema_id IS NULL", (topic,))
|
||||
assert offen == [] # Fallback partitioniert vollständig
|
||||
|
||||
|
||||
async def test_themen_skip_wenn_vollstaendig(monkeypatch):
|
||||
"""Gültige Partition wird NIE neu gewürfelt (Churn-Schutz je Repair-Runde)."""
|
||||
topic = topic_anlegen("themenskip")
|
||||
run = run_anlegen(topic)
|
||||
s = db.insert("soll", topic=topic, punkt="P", status="bestaetigt", belege=db.j([]))
|
||||
_thema(topic, [s])
|
||||
|
||||
def explodiert(prompt):
|
||||
raise AssertionError("vollständige Partition darf keinen Call auslösen")
|
||||
monkeypatch.setitem(fake_agents._HANDLER, "Themen-Schnitt", explodiert)
|
||||
ctx = llm.Kontext(run, topic, "minimax")
|
||||
ctx.ebene = "struktur"
|
||||
await struktur._themen_bilden(ctx) # kein Call → kein Raise
|
||||
|
||||
|
||||
async def test_thema_befunde():
|
||||
topic = topic_anlegen("themabefund")
|
||||
run = run_anlegen(topic)
|
||||
s1 = db.insert("soll", topic=topic, punkt="P1", status="bestaetigt", belege=db.j([]))
|
||||
s2 = db.insert("soll", topic=topic, punkt="P2", status="bestaetigt", belege=db.j([]))
|
||||
t = _thema(topic, [s1], "T1")
|
||||
ctx = llm.Kontext(run, topic, "minimax")
|
||||
ctx.ebene = "struktur"
|
||||
arten = {b["art"] for b in struktur.messen(ctx)}
|
||||
assert "thema_partition" in arten # s2 hat kein Thema
|
||||
# Baustein trägt T1, enthält aber ein Atom mit Soll-Punkt ohne dieses Thema
|
||||
ziel = db.insert("lernziele", topic=topic, text="Z", soll_id=s1, status="aktiv")
|
||||
b = db.insert("bausteine", topic=topic, ziel_id=ziel, titel="B", ord=0,
|
||||
status="neu", level="E", thema_id=t)
|
||||
db.insert("atome", topic=topic, titel="fremd", typ="begriff", definition="d",
|
||||
status="neu", soll_id=s2, ziel_id=ziel, baustein_id=b)
|
||||
arten = {x["art"] for x in struktur.messen(ctx)}
|
||||
assert "baustein_thema_mix" in arten
|
||||
|
||||
Reference in New Issue
Block a user