From ab1a28f0e7dd01bc519fb33ba183c9ea94c68e60 Mon Sep 17 00:00:00 2001 From: team3 Date: Fri, 10 Jul 2026 17:36:28 +0200 Subject: [PATCH] update --- backend/config.py | 1 + backend/fake_agents.py | 8 ++++++- backend/struktur.py | 34 +++++++++++++++++++++++++++ frontend/src/components/Guide.vue | 2 +- templates/Atom-Extraktion-Aufgaben.md | 7 ++++-- templates/Atom-Extraktion.md | 9 +++++-- templates/Level-Kalibrierung.md | 21 +++++++++++++++++ tests/test_struktur.py | 22 +++++++++++++++++ 8 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 templates/Level-Kalibrierung.md diff --git a/backend/config.py b/backend/config.py index 9968bd9..6fd7f12 100644 --- a/backend/config.py +++ b/backend/config.py @@ -98,6 +98,7 @@ TIMEOUTS = { "verify": (200, 10), "fix": (300, 15), "ziele": (300, 6), + "level": (200, 4), "kapitel": (200, 2), "zyklus": (150, 0), "writer": (450, 60), diff --git a/backend/fake_agents.py b/backend/fake_agents.py index d7db1a7..c5f84ca 100644 --- a/backend/fake_agents.py +++ b/backend/fake_agents.py @@ -175,6 +175,12 @@ def _kapitel_intro(prompt: str): " in aufeinander aufbauenden Abschnitten."} +def _level_kalibrierung(prompt: str): + ids = _ids(_text_nach(prompt, "ATOME:")) + # deterministisch: erstes Atom wird Grundgerüst, Rest Standardstoff + return [{"atom": i, "level": "E" if n == 0 else "M"} for n, i in enumerate(ids)] + + def _kapitel_schnitt(prompt: str): ids = [int(i) for i in re.findall(r"^(\d+): ", _text_nach(prompt, "BAUSTEINE:"), re.MULTILINE)] @@ -238,7 +244,7 @@ _HANDLER = { "Atom-Luecke": _luecke, "Soll-Abgedeckt": lambda p: {"abgedeckt": True}, "Artefakt-Generate": _artefakt_generate, "Artefakt-Verify": _artefakt_verify, "Artefakt-Fix": _artefakt_fix, "Lernziele": _lernziele, "Kapitel-Intro": _kapitel_intro, - "Kapitel-Schnitt": _kapitel_schnitt, + "Kapitel-Schnitt": _kapitel_schnitt, "Level-Kalibrierung": _level_kalibrierung, "Kanten-Zyklus": _zyklus, "Guide-Writer": _writer, "Guide-Pruefer": _pruefer, "Guide-Fix": _fix, "QA-Guide-Falsch": _qa_falsch, "QA-Guide-Falsch-Check": _qa_falsch_check, diff --git a/backend/struktur.py b/backend/struktur.py index 52fbf81..61d9340 100644 --- a/backend/struktur.py +++ b/backend/struktur.py @@ -297,6 +297,38 @@ def _bausteine_ordnen(topic: str, kanten: list[tuple[int, int]], rang: dict) -> db.update("bausteine", "id", b_id, ord=pos) +async def _level_kalibrieren(ctx: llm.Kontext) -> None: + """Stufen als Komplexitätsschichten (4C/ID): Die Extraktion sieht nur ihren + Chunk und stuft absolut (aak: 13 % E → Einstiegs-Durchgang war ein + Lücken-Gerippe). Hier stuft ein Judge je Baustein RELATIV nach — er sieht + alle Atome nebeneinander. E-Atome müssen allein einen kohärenten + Kurzdurchgang tragen; rein vertiefende Bausteine bleiben ohne E.""" + bausteine = db.query("SELECT * FROM bausteine WHERE topic=?", (ctx.topic,)) + atome = _atome(ctx.topic) + + async def einer(b: dict) -> None: + eigene = [a for a in atome if a["baustein_id"] == b["id"]] + if not eigene: + return + # bewusst OHNE bisheriges Level: der Extraktions-Prior ist verzerrt + # (Chunk-blind, M-Anchoring) und würde die Neubewertung ankern + liste = "\n".join(f"{a['id']}: {a['titel']} — {a['definition']}" for a in eigene) + res = await llm.call(ctx, stage="level", template="Level-Kalibrierung", + werte={"titel": b["titel"], "atome": liste}, + role="judge", n=len(eigene), item=f"lv{b['id']}", + erwartet=list) + gueltig = {a["id"]: a["level"] for a in eigene} + for e in res or []: + if not isinstance(e, dict): + continue + atom_id = e.get("atom") + level = str(e.get("level", "")).strip().upper() + if atom_id in gueltig and level in ("E", "M", "S") and level != gueltig[atom_id]: + db.update("atome", "id", atom_id, level=level) + + await llm.alle(einer(b) for b in bausteine) + + async def _kapitel_bilden(ctx: llm.Kontext) -> None: """Kapitel = kontiguierliche Segmente der geordneten Baustein-Folge. Das Soll ist seit der Entkopplung reine Checkliste — Kapitel sind Struktur. Der Judge @@ -360,6 +392,7 @@ async def bauen(ctx: llm.Kontext) -> None: await _ziele_bilden(ctx) await _zyklen_brechen(ctx) _bausteine_schneiden(ctx) + await _level_kalibrieren(ctx) await _kapitel_bilden(ctx) @@ -420,5 +453,6 @@ async def reparieren(ctx: llm.Kontext, befunde: list[dict]) -> bool: await _ziele_bilden(ctx) # fängt Partition-Lücken await _zyklen_brechen(ctx) # fängt Zyklen _bausteine_schneiden(ctx) # Neu-Schnitt fängt Band + Vorwärts-Kanten + await _level_kalibrieren(ctx) await _kapitel_bilden(ctx) # fängt Kapitel-Befunde return True diff --git a/frontend/src/components/Guide.vue b/frontend/src/components/Guide.vue index 97b9225..6b19431 100644 --- a/frontend/src/components/Guide.vue +++ b/frontend/src/components/Guide.vue @@ -8,7 +8,7 @@ const daten = ref(null) const level = ref('E') // Default: Einstieg — Umfang wächst per Toggle (E→M→S) const ansicht = ref('erklaerend') // erklaerend = Fließtext | kompakt = Stichpunkte const LEVEL_ICON = { E: '●○○', M: '●●○', S: '●●●' } -const LEVEL_NAME = { E: 'Umfang: nur Einstieg', M: 'Umfang: bis Mittel', S: 'Umfang: alles' } +const LEVEL_NAME = { E: 'Umfang: Grundgerüst', M: 'Umfang: + Standardstoff', S: 'Umfang: alles' } const fokus = ref(false) // Fokus: Vollbild — App-Chrome + Navigation weg, Karten aufgelöst watch(fokus, (f) => document.body.classList.toggle('vollbild', f)) onUnmounted(() => document.body.classList.remove('vollbild')) diff --git a/templates/Atom-Extraktion-Aufgaben.md b/templates/Atom-Extraktion-Aufgaben.md index b2735e6..269ae23 100644 --- a/templates/Atom-Extraktion-Aufgaben.md +++ b/templates/Atom-Extraktion-Aufgaben.md @@ -11,7 +11,9 @@ Regeln: ergibt das Atom „NP-Vollständigkeit des Hamiltonkreis-Problems" — nie „X mit deg ≥ 42". - Aufgabeninstanzen, Aufgabennummern, Punktzahlen sind KEINE Atome und gehören in kein Feld außer dem Zitat. -- „typ": begriff | aussage | verfahren. „level": E | M | S. +- „typ": begriff | aussage | verfahren. +- „level": E = Grundgerüst (braucht man beim ERSTEN Durchgang) | M = Standardstoff + | S = Feinheiten/Spezialfälle. Relativ zum restlichen Stoff einstufen. - „zitat": die Aufgabenstelle WÖRTLICH kopiert (5–40 Wörter) — sie ist der Beleg, dass dieses Konzept geübt wird. - Prüft eine Aufgabe mehrere Konzepte, wird jedes ein eigenes Atom. @@ -22,4 +24,5 @@ QUELLTEXT: {text} Antworte NUR mit JSON (keine Code-Fences, keine Prosa, KEINE Datei): -[{{"titel": "…", "typ": "aussage", "definition": "…", "zitat": "…", "level": "M", "braucht": []}}] +[{{"titel": "…", "typ": "aussage", "definition": "…", "zitat": "…", "level": "E", "braucht": []}}, + {{"titel": "…", "typ": "verfahren", "definition": "…", "zitat": "…", "level": "M", "braucht": []}}] diff --git a/templates/Atom-Extraktion.md b/templates/Atom-Extraktion.md index 302cb4d..4bb4d6a 100644 --- a/templates/Atom-Extraktion.md +++ b/templates/Atom-Extraktion.md @@ -15,7 +15,11 @@ KEINE Atome. Regeln: - „definition": 1–2 Sätze, eigenständig verständlich (dient später dem Dubletten-Vergleich). - „zitat": die Kernpassage WÖRTLICH aus dem Quelltext kopiert (exakt, Zeichen für Zeichen, 5–40 Wörter). Ohne wörtliches Zitat kein Atom. -- „level": E (Einstieg) | M (Mittel) | S (Schwer). +- „level": E = Grundgerüst (Kernbegriffe/Kernaussagen, die man beim ERSTEN Durchgang + durchs Thema braucht) | M = Standardstoff (Sätze, Verfahren, Standardbeweise) + | S = Feinheiten (Spezialfälle, Beweisdetails, Optimierungen). Relativ zum + restlichen Stoff des Auszugs einstufen — Stufen sind Komplexitätsschichten, + keine Bauchgefühl-Schwierigkeit. - „braucht": Titel anderer Atome DIESES Auszugs, die man vorher verstanden haben muss (nur echte Voraussetzungen, keine bloße Verwandtschaft). Leer, wenn keine. - Vollständigkeit zählt: JEDES lernbare Konzept des Auszugs wird ein Atom. @@ -24,4 +28,5 @@ QUELLTEXT: {text} Antworte NUR mit JSON (keine Code-Fences, keine Prosa, KEINE Datei): -[{{"titel": "…", "typ": "begriff", "definition": "…", "zitat": "…", "level": "M", "braucht": []}}] +[{{"titel": "…", "typ": "begriff", "definition": "…", "zitat": "…", "level": "E", "braucht": []}}, + {{"titel": "…", "typ": "aussage", "definition": "…", "zitat": "…", "level": "M", "braucht": ["…"]}}] diff --git a/templates/Level-Kalibrierung.md b/templates/Level-Kalibrierung.md new file mode 100644 index 0000000..3461fb8 --- /dev/null +++ b/templates/Level-Kalibrierung.md @@ -0,0 +1,21 @@ + +Unten die Atome EINES Guide-Bausteins „{titel}" (Format „id: titel — definition"). + +Kalibriere die Lernstufen RELATIV zueinander. Die Stufen sind Komplexitätsschichten +(nicht „leicht/schwer"): +- E = Grundgerüst: das Minimum, das man beim ERSTEN Durchgang braucht, um den + Baustein zu verstehen — Kernbegriffe, die Kernaussage. Die E-Atome allein müssen + einen kohärenten Kurzdurchgang ergeben. +- M = Standardstoff: Sätze, Verfahren, Standardbeweise — der zweite Durchgang. +- S = Feinheiten: Spezialfälle, Beweisdetails, Optimierungen, Randnotizen. + +Regeln: +- Behandelt der Baustein grundlegenden Stoff, gehört sein tragender Kern zu E. +- Rein vertiefende Bausteine (reine Beweis-/Spezialbausteine) dürfen ohne E bleiben. +- Jede id genau einmal, keine erfinden. + +ATOME: +{atome} + +Antworte NUR mit JSON (keine Code-Fences): +[{{"atom": 12, "level": "E"}}, {{"atom": 13, "level": "M"}}] diff --git a/tests/test_struktur.py b/tests/test_struktur.py index 33f3183..880a1cc 100644 --- a/tests/test_struktur.py +++ b/tests/test_struktur.py @@ -125,3 +125,25 @@ async def test_kapitel_retry_und_fallback(monkeypatch): 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)} + + +async def test_level_kalibrierung(monkeypatch): + import fake_agents + topic = topic_anlegen("level") + run = run_anlegen(topic) + ziel = db.insert("lernziele", topic=topic, text="Kann X", status="aktiv") + b_id = db.insert("bausteine", topic=topic, ziel_id=ziel, titel="B", ord=0, status="neu") + a1 = db.insert("atome", topic=topic, titel="Kern", typ="begriff", definition="d", + level="M", status="neu", baustein_id=b_id, braucht=db.j([])) + a2 = db.insert("atome", topic=topic, titel="Detail", typ="aussage", definition="d", + level="M", status="neu", baustein_id=b_id, braucht=db.j([])) + + def judge(prompt): + return [{"atom": a1, "level": "E"}, {"atom": a2, "level": "S"}, + {"atom": 99999, "level": "E"}, {"atom": a2, "level": "X"}] # Müll ignorieren + monkeypatch.setitem(fake_agents._HANDLER, "Level-Kalibrierung", judge) + ctx = llm.Kontext(run, topic, "minimax") + ctx.ebene = "struktur" + await struktur._level_kalibrieren(ctx) + assert db.one("SELECT level FROM atome WHERE id=?", (a1,))["level"] == "E" + assert db.one("SELECT level FROM atome WHERE id=?", (a2,))["level"] == "S"