update
This commit is contained in:
18
DESIGN.md
18
DESIGN.md
@@ -19,15 +19,19 @@ E0 Korpus Themen: N Recherche-Sweeps (Curricula/Lehrbuch/Syllabus/Übungen)
|
|||||||
E1 Inventar Abschnitte (~12k Zeichen) → 2 Reader je Abschnitt → Atome mit Verbatim-Anker
|
E1 Inventar Abschnitte (~12k Zeichen) → 2 Reader je Abschnitt → Atome mit Verbatim-Anker
|
||||||
Dedup: Anker-Overlap (auto) → Embedding-Kandidaten → 2er-Panel (einstimmig)
|
Dedup: Anker-Overlap (auto) → Embedding-Kandidaten → 2er-Panel (einstimmig)
|
||||||
Soll-Zuordnung je Atom; Soll-Punkt ohne Atom → gezielte Nachextraktion
|
Soll-Zuordnung je Atom; Soll-Punkt ohne Atom → gezielte Nachextraktion
|
||||||
E2 Artefakte je Soll-Punkt-Gruppe: Flashcards+Beispiel pro Atom → Verify-Panel gegen Anker → Fix
|
E2 Artefakte je Soll-Punkt-Gruppe: Flashcards + 1 typisiertes Beispiel pro Atom
|
||||||
|
(Form: text|mathe|code|tabelle|diagramm, beste Passung) → Form-Gate
|
||||||
|
(Syntax/Struktur parse-only, kein Ausführen) → Verify-Panel gegen Anker → Fix
|
||||||
E3 Struktur Lernziele+Zuordnung aus Atomen (je Soll-Punkt) → Ziel-Check → Kanten auflösen
|
E3 Struktur Lernziele+Zuordnung aus Atomen (je Soll-Punkt) → Ziel-Check → Kanten auflösen
|
||||||
→ Zyklen brechen (Judge) → Bausteine = Ziel-Gruppen im Band (split/merge)
|
→ Zyklen brechen (Judge) → Bausteine = Ziel-Gruppen im Band (split/merge)
|
||||||
→ topologische Ordnung (Bausteine + Atome darin)
|
→ topologische Ordnung (Bausteine + Atome darin)
|
||||||
E3.5 Diagramme je Baustein: planen (welche+Typ: DAG-deterministisch | Judge) → Spec-JSON
|
E3.5 Diagramme je Baustein: Judge plant welche+Typ (state/flow/tree — Term-DAGs waren
|
||||||
(LLM, kein rohes Mermaid) → deterministisch → Mermaid → Parse-Gate →
|
nutzlos, raus) → Spec-JSON (LLM, kein rohes Mermaid) → deterministisch →
|
||||||
Grounding-Panel. Optional; Platzierung macht der Guide-Writer per Marker.
|
Mermaid → Parse-Gate → Grounding-Panel. Optional.
|
||||||
E4 Guide je Baustein: Writer (Facts inline, Atom-Marker, `<!-- diagramm: id -->`)
|
E4 Guide je Baustein: Writer (Facts inline, Atom-Marker; Ausgabe DELIMITED, nicht JSON
|
||||||
→ det. Checks → Prüfer → Fix; Assembly tauscht Marker gegen ```mermaid-Fence
|
— LaTeX-Backslashes) → det. Checks → Prüfer → Fix (Fix-Cap gegen Churn).
|
||||||
|
Beispiel-/Diagramm-Marker setzt der Code DETERMINISTISCH (nicht der Writer);
|
||||||
|
Assembly tauscht Marker gegen ```-Fence/Tabelle.
|
||||||
```
|
```
|
||||||
|
|
||||||
Jede Ebene endet mit `qa.messen(ebene)`; `auto_loop` fährt QA→Repair bis Note 10,0,
|
Jede Ebene endet mit `qa.messen(ebene)`; `auto_loop` fährt QA→Repair bis Note 10,0,
|
||||||
@@ -44,6 +48,8 @@ Stillstand oder 10 Iterationen. Erst dann startet die nächste Ebene.
|
|||||||
- `anker(atom_id, quelle_id, start, ende, zitat)`
|
- `anker(atom_id, quelle_id, start, ende, zitat)`
|
||||||
- `kanten(topic, von_atom, zu_atom, art braucht|verwandt, status)`
|
- `kanten(topic, von_atom, zu_atom, art braucht|verwandt, status)`
|
||||||
- `artefakte(id, atom_id, typ flashcard|beispiel, inhalt JSON, status kandidat|verifiziert|verworfen)`
|
- `artefakte(id, atom_id, typ flashcard|beispiel, inhalt JSON, status kandidat|verifiziert|verworfen)`
|
||||||
|
— beispiel-`inhalt` trägt `form` + formspezifische Felder (text|code+sprache|tabelle|spec+mermaid);
|
||||||
|
code/tabelle/diagramm kommen verbatim via `<!-- beispiel: id -->` in den Guide (Assembly-Swap)
|
||||||
- `lernziele(id, topic, text, soll_id, status)`
|
- `lernziele(id, topic, text, soll_id, status)`
|
||||||
- `bausteine(id, topic, ziel_id, titel, ord, status)`
|
- `bausteine(id, topic, ziel_id, titel, ord, status)`
|
||||||
- `diagramme(id, topic, baustein_id, typ, quelle dag|llm, spec JSON, mermaid, status geplant|kandidat|verifiziert|verworfen|kein, befunde JSON)`
|
- `diagramme(id, topic, baustein_id, typ, quelle dag|llm, spec JSON, mermaid, status geplant|kandidat|verifiziert|verworfen|kein, befunde JSON)`
|
||||||
|
|||||||
@@ -3,13 +3,17 @@ Soll-Punkt-Gruppen (Kontext teilen spart Tokens — Lektion 52), verifiziert von
|
|||||||
2er-Panel GEGEN DIE ANKER-ZITATE (inline, Lektion 48). Einstimmig ok → verifiziert;
|
2er-Panel GEGEN DIE ANKER-ZITATE (inline, Lektion 48). Einstimmig ok → verifiziert;
|
||||||
sonst ein Fix + Re-Verify durch einen Judge; danach verifiziert oder verworfen."""
|
sonst ein Fix + Re-Verify durch einen Judge; danach verifiziert oder verworfen."""
|
||||||
|
|
||||||
|
import ast
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
|
||||||
import db
|
import db
|
||||||
|
import diagramme
|
||||||
import llm
|
import llm
|
||||||
from config import ARTEFAKT_CHUNK_ATOME, VERIFY_PANEL
|
from config import ARTEFAKT_CHUNK_ATOME, BEISPIEL_FORMEN, VERIFY_PANEL
|
||||||
|
|
||||||
log = logging.getLogger("creator2.artefakte")
|
log = logging.getLogger("creator2.artefakte")
|
||||||
|
|
||||||
@@ -25,8 +29,14 @@ _QUELLEN_REFERENZ = re.compile(
|
|||||||
re.IGNORECASE)
|
re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def _prosa_teile(inhalt: dict) -> str:
|
||||||
|
"""Nur menschenlesbare Prosa fürs Quellen-Referenz-Gate — NICHT Code/Mermaid/
|
||||||
|
Tabelle (dort matchen Bezeichner/Kommentare den Regex falsch, z. B. „blatt 3")."""
|
||||||
|
return " ".join(str(inhalt.get(k, "")) for k in ("frage", "antwort", "text"))
|
||||||
|
|
||||||
|
|
||||||
def _referenziert_quelle(inhalt: dict) -> bool:
|
def _referenziert_quelle(inhalt: dict) -> bool:
|
||||||
return _QUELLEN_REFERENZ.search(" ".join(str(v) for v in inhalt.values())) is not None
|
return _QUELLEN_REFERENZ.search(_prosa_teile(inhalt)) is not None
|
||||||
|
|
||||||
|
|
||||||
def _zitate(atom_id: int) -> str:
|
def _zitate(atom_id: int) -> str:
|
||||||
@@ -60,6 +70,21 @@ def _chunks(topic: str, nur_ohne: bool) -> list[list[dict]]:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _beispiel_inhalt(e: dict) -> dict:
|
||||||
|
"""Typisiertes Beispiel: die Form lebt im JSON `inhalt` (keine DB-Migration).
|
||||||
|
Nur relevante Felder gefüllt; unbekannte Form → Default text."""
|
||||||
|
form = str(e.get("form", "text")).strip().lower()
|
||||||
|
if form not in BEISPIEL_FORMEN:
|
||||||
|
form = "text"
|
||||||
|
return {"form": form,
|
||||||
|
"text": str(e.get("text", "")),
|
||||||
|
"code": str(e.get("code", "")),
|
||||||
|
"sprache": str(e.get("sprache", "")).strip().lower(),
|
||||||
|
"tabelle": str(e.get("tabelle", "")),
|
||||||
|
"spec": e.get("spec") if isinstance(e.get("spec"), dict) else {},
|
||||||
|
"mermaid": ""}
|
||||||
|
|
||||||
|
|
||||||
async def _generieren(ctx: llm.Kontext, chunk: list[dict]) -> None:
|
async def _generieren(ctx: llm.Kontext, chunk: list[dict]) -> None:
|
||||||
bloecke = "\n\n".join(_atom_block(a) for a in chunk)
|
bloecke = "\n\n".join(_atom_block(a) for a in chunk)
|
||||||
res = await llm.call(ctx, stage="artefakt", template="Artefakt-Generate",
|
res = await llm.call(ctx, stage="artefakt", template="Artefakt-Generate",
|
||||||
@@ -71,7 +96,10 @@ async def _generieren(ctx: llm.Kontext, chunk: list[dict]) -> None:
|
|||||||
typ = str(e.get("typ", "")).strip()
|
typ = str(e.get("typ", "")).strip()
|
||||||
if atom_id not in gueltig or typ not in ("flashcard", "beispiel"):
|
if atom_id not in gueltig or typ not in ("flashcard", "beispiel"):
|
||||||
continue
|
continue
|
||||||
inhalt = {k: str(e.get(k, "")) for k in ("frage", "antwort", "text")}
|
if typ == "beispiel":
|
||||||
|
inhalt = _beispiel_inhalt(e)
|
||||||
|
else:
|
||||||
|
inhalt = {k: str(e.get(k, "")) for k in ("frage", "antwort", "text")}
|
||||||
if _referenziert_quelle(inhalt):
|
if _referenziert_quelle(inhalt):
|
||||||
log.info("Artefakt zu Atom %s nicht übernommen: referenziert die Quelle", atom_id)
|
log.info("Artefakt zu Atom %s nicht übernommen: referenziert die Quelle", atom_id)
|
||||||
continue
|
continue
|
||||||
@@ -129,7 +157,16 @@ async def _fixen(ctx: llm.Kontext, k: dict, maengel: list[str], atom: dict) -> N
|
|||||||
"belege": _zitate(k["atom_id"])},
|
"belege": _zitate(k["atom_id"])},
|
||||||
erwartet=dict)
|
erwartet=dict)
|
||||||
if res:
|
if res:
|
||||||
inhalt = {kk: str(res.get(kk, "")) for kk in ("frage", "antwort", "text")}
|
if k["typ"] == "beispiel":
|
||||||
|
# Form behalten (aus Alt-inhalt), Felder aus dem Fix übernehmen, DANN
|
||||||
|
# form-spezifisch neu gaten — ein kaputter Fix darf nicht durchrutschen.
|
||||||
|
inhalt = _beispiel_inhalt({**db.uj(k["inhalt"], {}), **res})
|
||||||
|
ok, inhalt = _beispiel_pruefen(inhalt)
|
||||||
|
if not ok:
|
||||||
|
db.update("artefakte", "id", k["id"], status="verworfen")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
inhalt = {kk: str(res.get(kk, "")) for kk in ("frage", "antwort")}
|
||||||
if _referenziert_quelle(inhalt):
|
if _referenziert_quelle(inhalt):
|
||||||
db.update("artefakte", "id", k["id"], status="verworfen")
|
db.update("artefakte", "id", k["id"], status="verworfen")
|
||||||
return
|
return
|
||||||
@@ -151,10 +188,100 @@ async def _fixen(ctx: llm.Kontext, k: dict, maengel: list[str], atom: dict) -> N
|
|||||||
status="verifiziert" if eintrag.get("ok") else "verworfen")
|
status="verifiziert" if eintrag.get("ok") else "verworfen")
|
||||||
|
|
||||||
|
|
||||||
|
_VERBATIM_FORMEN = ("code", "tabelle", "diagramm") # kommen verbatim in den Guide
|
||||||
|
|
||||||
|
|
||||||
|
def _tabelle_ok(md: str) -> bool:
|
||||||
|
"""Wohlgeformte Markdown-Tabelle: Kopf, Trenner (---), ≥1 Datenzeile, konsistente
|
||||||
|
Spaltenzahl. Deterministisch, kein LLM."""
|
||||||
|
zeilen = [z for z in md.strip().splitlines() if z.strip()]
|
||||||
|
if len(zeilen) < 3 or not all("|" in z for z in zeilen):
|
||||||
|
return False
|
||||||
|
if not re.fullmatch(r"[\s|:\-]+", zeilen[1]) or "-" not in zeilen[1]:
|
||||||
|
return False
|
||||||
|
spalten = zeilen[0].count("|")
|
||||||
|
return spalten >= 2 and all(z.count("|") == spalten for z in zeilen)
|
||||||
|
|
||||||
|
|
||||||
|
_node_gewarnt = False
|
||||||
|
|
||||||
|
|
||||||
|
def _node_check(code: str) -> bool:
|
||||||
|
"""`node --check` parst JS OHNE es auszuführen. fail-open, wenn node fehlt."""
|
||||||
|
global _node_gewarnt
|
||||||
|
try:
|
||||||
|
with tempfile.NamedTemporaryFile("w", suffix=".js", delete=True) as f:
|
||||||
|
f.write(code)
|
||||||
|
f.flush()
|
||||||
|
res = subprocess.run(["node", "--check", f.name],
|
||||||
|
capture_output=True, text=True, timeout=15)
|
||||||
|
return res.returncode == 0
|
||||||
|
except Exception as e: # node fehlt → nicht verwerfen (nur Judge prüft dann)
|
||||||
|
if not _node_gewarnt:
|
||||||
|
_node_gewarnt = True
|
||||||
|
log.warning("node-Syntax-Gate nicht verfügbar (%s) — JS-Beispiele ungeprüft", e)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _code_ok(sprache: str, code: str) -> bool:
|
||||||
|
"""Syntax-Gate, PARSE-ONLY (führt NIE aus — keine Sandbox nötig). Python via
|
||||||
|
ast.parse in-process, JavaScript via `node --check`. Unbekannte Sprache → kein
|
||||||
|
Gate (nur der Judge prüft)."""
|
||||||
|
if not code.strip():
|
||||||
|
return False
|
||||||
|
if sprache == "python":
|
||||||
|
try:
|
||||||
|
ast.parse(code)
|
||||||
|
return True
|
||||||
|
except SyntaxError:
|
||||||
|
return False
|
||||||
|
if sprache == "javascript":
|
||||||
|
return _node_check(code)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _beispiel_pruefen(inh: dict) -> tuple[bool, dict]:
|
||||||
|
"""Form-spezifisches Gate (deterministisch, parse-only — nie Ausführung).
|
||||||
|
→ (ok, inhalt); der Diagramm-Zweig füllt `mermaid`. Unbekannte/woven Formen
|
||||||
|
passieren (der Judge prüft inhaltlich)."""
|
||||||
|
form = inh.get("form", "text")
|
||||||
|
if form == "tabelle":
|
||||||
|
return _tabelle_ok(inh.get("tabelle", "")), inh
|
||||||
|
if form == "code":
|
||||||
|
return _code_ok(inh.get("sprache", ""), inh.get("code", "")), inh
|
||||||
|
if form == "diagramm":
|
||||||
|
# Struktur-JSON → Mermaid (deterministisch) → Parse-Gate; wiederverwendet die
|
||||||
|
# Diagramm-Ebene. Der LLM schreibt NIE rohes Mermaid — nur die Spec.
|
||||||
|
spec = diagramme._spec_normalisieren(inh.get("spec") or {},
|
||||||
|
(inh.get("spec") or {}).get("typ", "flow"))
|
||||||
|
if not spec["knoten"]:
|
||||||
|
return False, inh
|
||||||
|
mermaid = diagramme._spec_zu_mermaid(spec)
|
||||||
|
if not mermaid or 0 in diagramme._mermaid_fehler([{"id": 0, "mermaid": mermaid}]):
|
||||||
|
return False, inh
|
||||||
|
return True, {**inh, "spec": spec, "mermaid": mermaid}
|
||||||
|
return True, inh
|
||||||
|
|
||||||
|
|
||||||
|
def _formen_gate(topic: str) -> None:
|
||||||
|
"""Kandidat-Beispiele durch ihr Form-Gate: Syntax/Struktur ungültig → verworfen
|
||||||
|
(fail-closed für den Inhalt; ein fehlendes Beispiel ist erlaubt, optional)."""
|
||||||
|
for b in db.query(
|
||||||
|
"SELECT ar.* FROM artefakte ar JOIN atome a ON a.id=ar.atom_id"
|
||||||
|
" WHERE a.topic=? AND ar.typ='beispiel' AND ar.status='kandidat'", (topic,)):
|
||||||
|
inh = db.uj(b["inhalt"], {})
|
||||||
|
ok, inh_neu = _beispiel_pruefen(inh)
|
||||||
|
if not ok:
|
||||||
|
db.update("artefakte", "id", b["id"], status="verworfen")
|
||||||
|
elif inh_neu is not inh:
|
||||||
|
db.update("artefakte", "id", b["id"], inhalt=db.j(inh_neu))
|
||||||
|
|
||||||
|
|
||||||
async def bauen(ctx: llm.Kontext) -> None:
|
async def bauen(ctx: llm.Kontext) -> None:
|
||||||
ctx.ebene = EBENE
|
ctx.ebene = EBENE
|
||||||
neu = _chunks(ctx.topic, nur_ohne=True)
|
neu = _chunks(ctx.topic, nur_ohne=True)
|
||||||
await llm.alle(_generieren(ctx, c) for c in neu)
|
await llm.alle(_generieren(ctx, c) for c in neu)
|
||||||
|
_formen_gate(ctx.topic) # form-spezifische Syntax/Struktur-Gates vor dem Panel
|
||||||
alle = _chunks(ctx.topic, nur_ohne=False)
|
alle = _chunks(ctx.topic, nur_ohne=False)
|
||||||
await llm.alle(_verifizieren(ctx, c) for c in alle)
|
await llm.alle(_verifizieren(ctx, c) for c in alle)
|
||||||
|
|
||||||
|
|||||||
@@ -84,8 +84,13 @@ MERGE_PANEL = 2 # Judges pro Merge-Kandidat — Merge nur einstimmi
|
|||||||
MERGE_KANDIDAT_COS = 0.75 # Definitions-Ähnlichkeit ab der ein Paar zum Panel geht
|
MERGE_KANDIDAT_COS = 0.75 # Definitions-Ähnlichkeit ab der ein Paar zum Panel geht
|
||||||
MERGE_KANDIDAT_JACCARD = 0.3 # Schwelle im lexikalischen Fallback (misst viel niedriger als Kosinus)
|
MERGE_KANDIDAT_JACCARD = 0.3 # Schwelle im lexikalischen Fallback (misst viel niedriger als Kosinus)
|
||||||
LUECKEN_RUNDEN_MAX = 2 # gezielte Nachextraktion pro Soll-Lücke
|
LUECKEN_RUNDEN_MAX = 2 # gezielte Nachextraktion pro Soll-Lücke
|
||||||
|
# braucht-Titel, die norm/kern nicht auflösen (Kompositum↔Expansion misst ~0.53 Cosinus,
|
||||||
|
# Embedding schlägt sie NIE vor — Lektion 28): Substring-Prefilter → 1 Judge (n=1, reversibel).
|
||||||
|
BRAUCHT_KANDIDATEN = 8 # Kandidaten-Cap je unaufgelöstem braucht-Titel (Lektion 34)
|
||||||
|
|
||||||
# ── Ebene 2: Artefakte ────────────────────────────────────────────────────────
|
# ── Ebene 2: Artefakte ────────────────────────────────────────────────────────
|
||||||
|
BEISPIEL_FORMEN = ("text", "mathe", "code", "tabelle", "diagramm") # Worked-Example-Formen
|
||||||
|
CODE_SPRACHEN = ("python", "javascript") # Syntax-Gate parse-only (Container: python3, node)
|
||||||
ARTEFAKT_CHUNK_ATOME = 8 # Atome pro Generate-Call (ein Soll-Punkt, gestückelt)
|
ARTEFAKT_CHUNK_ATOME = 8 # Atome pro Generate-Call (ein Soll-Punkt, gestückelt)
|
||||||
VERIFY_PANEL = 2 # unabhängige Prüfer, einstimmig = verifiziert
|
VERIFY_PANEL = 2 # unabhängige Prüfer, einstimmig = verifiziert
|
||||||
|
|
||||||
@@ -102,15 +107,15 @@ BAUSTEIN_MAX_ATOME = 8
|
|||||||
ZIELE_CHUNK_ATOME = 30 # Atome pro Lernziel-Call
|
ZIELE_CHUNK_ATOME = 30 # Atome pro Lernziel-Call
|
||||||
KAPITEL_BAUSTEINE = 5 # Richtwert Bausteine je Kapitel (Kapitel-Schnitt-Judge)
|
KAPITEL_BAUSTEINE = 5 # Richtwert Bausteine je Kapitel (Kapitel-Schnitt-Judge)
|
||||||
|
|
||||||
# ── Ebene 3.5: Diagramme ──────────────────────────────────────────────────────
|
# ── Ebene 3.5: Diagramme (nur LLM: Automat/Ablauf/Baum — Term-DAGs waren nutzlos) ──
|
||||||
DIAGRAMM_MIN_KANTEN = 3 # ab so vielen internen braucht-Kanten → Abhängigkeitsdiagramm
|
DIAGRAMM_TYPEN = ("state", "flow", "tree")
|
||||||
DIAGRAMM_TYPEN = ("dependency", "state", "flow", "tree")
|
|
||||||
|
|
||||||
# ── Ebene 4: Guide ────────────────────────────────────────────────────────────
|
# ── Ebene 4: Guide ────────────────────────────────────────────────────────────
|
||||||
SECTION_WOERTER_PRO_ATOM = (40, 220) # Längenband ausführlicher Teil je Atom
|
SECTION_WOERTER_PRO_ATOM = (40, 220) # Längenband ausführlicher Teil je Atom
|
||||||
|
|
||||||
# ── Auto-Loop ─────────────────────────────────────────────────────────────────
|
# ── Auto-Loop ─────────────────────────────────────────────────────────────────
|
||||||
LOOP_MAX_ITER = 10
|
LOOP_MAX_ITER = 10
|
||||||
|
FIX_MAX_VERSUCHE = 3 # Section einfrieren nach so vielen Fixes OHNE Textänderung
|
||||||
|
|
||||||
# ── Timeouts je Schritt: (Basis-Sekunden, Sekunden pro Item) ─────────────────
|
# ── Timeouts je Schritt: (Basis-Sekunden, Sekunden pro Item) ─────────────────
|
||||||
TIMEOUTS = {
|
TIMEOUTS = {
|
||||||
@@ -119,6 +124,7 @@ TIMEOUTS = {
|
|||||||
"soll_konsens": (300, 2),
|
"soll_konsens": (300, 2),
|
||||||
"extraktion": (450, 0),
|
"extraktion": (450, 0),
|
||||||
"merge": (150, 8),
|
"merge": (150, 8),
|
||||||
|
"braucht": (200, 0),
|
||||||
"soll_zuordnung": (300, 8),
|
"soll_zuordnung": (300, 8),
|
||||||
"luecke": (450, 0),
|
"luecke": (450, 0),
|
||||||
"artefakt": (450, 20),
|
"artefakt": (450, 20),
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ CREATE INDEX IF NOT EXISTS idx_diagramme_baustein ON diagramme(baustein_id);
|
|||||||
CREATE TABLE IF NOT EXISTS sections(
|
CREATE TABLE IF NOT EXISTS sections(
|
||||||
baustein_id INTEGER PRIMARY KEY, stage TEXT NOT NULL DEFAULT 'writer',
|
baustein_id INTEGER PRIMARY KEY, stage TEXT NOT NULL DEFAULT 'writer',
|
||||||
text_kompakt TEXT DEFAULT '', text_lang TEXT DEFAULT '', befunde TEXT DEFAULT '[]',
|
text_kompakt TEXT DEFAULT '', text_lang TEXT DEFAULT '', befunde TEXT DEFAULT '[]',
|
||||||
qa_hash TEXT DEFAULT '');
|
qa_hash TEXT DEFAULT '', fix_versuche INTEGER NOT NULL DEFAULT 0);
|
||||||
CREATE TABLE IF NOT EXISTS leitner(
|
CREATE TABLE IF NOT EXISTS leitner(
|
||||||
artefakt_id INTEGER PRIMARY KEY, box INTEGER NOT NULL DEFAULT 1,
|
artefakt_id INTEGER PRIMARY KEY, box INTEGER NOT NULL DEFAULT 1,
|
||||||
faellig TEXT DEFAULT (date('now')), historie TEXT DEFAULT '[]');
|
faellig TEXT DEFAULT (date('now')), historie TEXT DEFAULT '[]');
|
||||||
@@ -133,7 +133,8 @@ def _init_schema(con: sqlite3.Connection) -> None:
|
|||||||
"ALTER TABLE bausteine ADD COLUMN level TEXT NOT NULL DEFAULT 'M'",
|
"ALTER TABLE bausteine ADD COLUMN level TEXT NOT NULL DEFAULT 'M'",
|
||||||
"ALTER TABLE kapitel ADD COLUMN level TEXT NOT NULL DEFAULT 'M'",
|
"ALTER TABLE kapitel ADD COLUMN level TEXT NOT NULL DEFAULT 'M'",
|
||||||
"ALTER TABLE soll ADD COLUMN geprueft TEXT NOT NULL DEFAULT '[]'",
|
"ALTER TABLE soll ADD COLUMN geprueft TEXT NOT NULL DEFAULT '[]'",
|
||||||
"ALTER TABLE quellen ADD COLUMN atome_stand TEXT NOT NULL DEFAULT ''"):
|
"ALTER TABLE quellen ADD COLUMN atome_stand TEXT NOT NULL DEFAULT ''",
|
||||||
|
"ALTER TABLE sections ADD COLUMN fix_versuche INTEGER NOT NULL DEFAULT 0"):
|
||||||
try:
|
try:
|
||||||
con.execute(zusatz)
|
con.execute(zusatz)
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import db
|
import db
|
||||||
import llm
|
import llm
|
||||||
from config import DIAGRAMM_MIN_KANTEN, DIAGRAMM_TYPEN, DURCHGANG, VERIFY_PANEL
|
from config import DIAGRAMM_TYPEN, DURCHGANG, VERIFY_PANEL
|
||||||
|
|
||||||
log = logging.getLogger("creator2.diagramme")
|
log = logging.getLogger("creator2.diagramme")
|
||||||
|
|
||||||
@@ -61,26 +61,6 @@ def _atome_von(baustein_id: int) -> list[dict]:
|
|||||||
" ('gemerged','verworfen') ORDER BY ord", (baustein_id,))
|
" ('gemerged','verworfen') ORDER BY ord", (baustein_id,))
|
||||||
|
|
||||||
|
|
||||||
def _interne_kanten(topic: str, ids: set[int]) -> list[dict]:
|
|
||||||
"""aktive braucht-Kanten, deren BEIDE Enden im Baustein liegen."""
|
|
||||||
if len(ids) < 2:
|
|
||||||
return []
|
|
||||||
return [k for k in db.query(
|
|
||||||
"SELECT * FROM kanten WHERE topic=? AND art='braucht' AND status='aktiv'", (topic,))
|
|
||||||
if k["von_atom"] in ids and k["zu_atom"] in ids]
|
|
||||||
|
|
||||||
|
|
||||||
# ── Spec (JSON-Zwischenrepräsentation) ────────────────────────────────────────
|
|
||||||
|
|
||||||
def _dag_spec(atome: list[dict], kanten: list[dict]) -> dict:
|
|
||||||
"""Abhängigkeitsdiagramm direkt aus dem braucht-Graph. `von braucht zu` ⇒ Pfeil
|
|
||||||
zu→von (Voraussetzung zeigt auf das, was sie ermöglicht). Kein LLM."""
|
|
||||||
knoten = [{"id": str(a["id"]), "label": a["titel"]} for a in atome]
|
|
||||||
kanten_spec = [{"von": str(k["zu_atom"]), "zu": str(k["von_atom"]), "label": ""}
|
|
||||||
for k in kanten]
|
|
||||||
return {"typ": "dependency", "knoten": knoten, "kanten": kanten_spec}
|
|
||||||
|
|
||||||
|
|
||||||
# ── Kompilieren (deterministisch, kein LLM) ───────────────────────────────────
|
# ── Kompilieren (deterministisch, kein LLM) ───────────────────────────────────
|
||||||
|
|
||||||
def _sid(raw) -> str:
|
def _sid(raw) -> str:
|
||||||
@@ -129,21 +109,6 @@ def _kompilieren(topic: str) -> None:
|
|||||||
|
|
||||||
# ── Planung (welche + Typ) ────────────────────────────────────────────────────
|
# ── Planung (welche + Typ) ────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _planen(topic: str) -> None:
|
|
||||||
"""Deterministischer Zweig: Baustein mit ≥DIAGRAMM_MIN_KANTEN internen
|
|
||||||
braucht-Kanten → Abhängigkeitsdiagramm aus dem DAG. Idempotent: Bausteine mit
|
|
||||||
bestehender Diagramm-Zeile werden übersprungen. Kein LLM, null Halluzination."""
|
|
||||||
for b in _bausteine(topic):
|
|
||||||
if db.one("SELECT id FROM diagramme WHERE baustein_id=?", (b["id"],)):
|
|
||||||
continue
|
|
||||||
atome = _atome_von(b["id"])
|
|
||||||
ids = {a["id"] for a in atome}
|
|
||||||
kanten = _interne_kanten(topic, ids)
|
|
||||||
if len(kanten) >= DIAGRAMM_MIN_KANTEN:
|
|
||||||
db.insert("diagramme", topic=topic, baustein_id=b["id"], typ="dependency",
|
|
||||||
quelle="dag", spec=db.j(_dag_spec(atome, kanten)), status="geplant")
|
|
||||||
|
|
||||||
|
|
||||||
def _atom_liste(atome: list[dict]) -> str:
|
def _atom_liste(atome: list[dict]) -> str:
|
||||||
return "\n".join(f"{a['id']}: {a['titel']} — {a['definition']}" for a in atome)
|
return "\n".join(f"{a['id']}: {a['titel']} — {a['definition']}" for a in atome)
|
||||||
|
|
||||||
@@ -175,7 +140,7 @@ async def _plan_judge(ctx: llm.Kontext) -> None:
|
|||||||
if res is None:
|
if res is None:
|
||||||
return # Ausfall → nächste Runde erneut (kein 'kein'-Eintrag)
|
return # Ausfall → nächste Runde erneut (kein 'kein'-Eintrag)
|
||||||
typ = str(res.get("typ", "")).strip()
|
typ = str(res.get("typ", "")).strip()
|
||||||
if res.get("noetig") and typ in DIAGRAMM_TYPEN and typ != "dependency":
|
if res.get("noetig") and typ in DIAGRAMM_TYPEN:
|
||||||
db.insert("diagramme", topic=ctx.topic, baustein_id=b["id"], typ=typ,
|
db.insert("diagramme", topic=ctx.topic, baustein_id=b["id"], typ=typ,
|
||||||
quelle="llm", status="geplant")
|
quelle="llm", status="geplant")
|
||||||
else:
|
else:
|
||||||
@@ -254,16 +219,11 @@ async def _verifizieren(ctx: llm.Kontext) -> None:
|
|||||||
|
|
||||||
async def bauen(ctx: llm.Kontext) -> None:
|
async def bauen(ctx: llm.Kontext) -> None:
|
||||||
ctx.ebene = EBENE
|
ctx.ebene = EBENE
|
||||||
_planen(ctx.topic) # welche/Typ: deterministisch (DAG)
|
await _plan_judge(ctx) # welche/Typ: Judge (Automaten/Abläufe/Bäume) — kein DAG mehr
|
||||||
await _plan_judge(ctx) # welche/Typ: Judge (Automaten/Abläufe/Bäume)
|
|
||||||
await _spec_bauen(ctx) # LLM → Struktur-JSON (kein rohes Mermaid)
|
await _spec_bauen(ctx) # LLM → Struktur-JSON (kein rohes Mermaid)
|
||||||
_kompilieren(ctx.topic) # Spec → Mermaid (deterministisch)
|
_kompilieren(ctx.topic) # Spec → Mermaid (deterministisch)
|
||||||
_gate_kandidaten(ctx.topic) # Parse-Gate: ungültige Mermaid-Quelle → verworfen
|
_gate_kandidaten(ctx.topic) # Parse-Gate: ungültige Mermaid-Quelle → verworfen
|
||||||
# dag-Diagramme sind per Konstruktion gegroundet → nach bestandenem Gate verifiziert
|
await _verifizieren(ctx) # Grounding-Panel gegen die Atome
|
||||||
for d in db.query("SELECT id FROM diagramme WHERE topic=? AND status='kandidat'"
|
|
||||||
" AND quelle='dag'", (ctx.topic,)):
|
|
||||||
db.update("diagramme", "id", d["id"], status="verifiziert")
|
|
||||||
await _verifizieren(ctx) # LLM-Diagramme: Grounding-Panel gegen die Atome
|
|
||||||
|
|
||||||
|
|
||||||
def _gate_kandidaten(topic: str) -> None:
|
def _gate_kandidaten(topic: str) -> None:
|
||||||
|
|||||||
@@ -137,14 +137,24 @@ def _luecke(prompt: str):
|
|||||||
|
|
||||||
|
|
||||||
def _artefakt_generate(prompt: str):
|
def _artefakt_generate(prompt: str):
|
||||||
ids = [int(i) for i in re.findall(r"ATOM (\d+):", prompt)]
|
# Form je Atom-Typ [in Klammern] wählen (wie die typgesteuerte Regel im Prompt)
|
||||||
|
paare = re.findall(r"ATOM (\d+):[^\[\n]*\[(\w+)\]", prompt)
|
||||||
out = []
|
out = []
|
||||||
for i in ids:
|
for raw, typ in paare:
|
||||||
|
i = int(raw)
|
||||||
out += [{"atom": i, "typ": "flashcard", "frage": f"Was besagt Atom {i}?",
|
out += [{"atom": i, "typ": "flashcard", "frage": f"Was besagt Atom {i}?",
|
||||||
"antwort": "Siehe Beleg — deterministische Fake-Antwort."},
|
"antwort": "Siehe Beleg — deterministische Fake-Antwort."},
|
||||||
{"atom": i, "typ": "flashcard", "frage": f"Wozu dient Atom {i}?",
|
{"atom": i, "typ": "flashcard", "frage": f"Wozu dient Atom {i}?",
|
||||||
"antwort": "Zum Lernen — deterministische Fake-Antwort."},
|
"antwort": "Zum Lernen — deterministische Fake-Antwort."}]
|
||||||
{"atom": i, "typ": "beispiel", "text": f"Beispiel zu Atom {i}: Schritt 1, Schritt 2."}]
|
if typ == "verfahren":
|
||||||
|
out.append({"atom": i, "typ": "beispiel", "form": "code",
|
||||||
|
"sprache": "python", "code": f"print({i} + 1)"})
|
||||||
|
elif typ == "begriff":
|
||||||
|
out.append({"atom": i, "typ": "beispiel", "form": "tabelle",
|
||||||
|
"tabelle": f"| Fall | Ergebnis |\n|---|---|\n| Atom {i} | passt |"})
|
||||||
|
else: # aussage → Text-Beispiel
|
||||||
|
out.append({"atom": i, "typ": "beispiel", "form": "text",
|
||||||
|
"text": f"Beispiel zu Atom {i}: Schritt 1, Schritt 2."})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -196,24 +206,40 @@ def _zyklus(prompt: str):
|
|||||||
return {"von": int(m.group(1)), "zu": int(m.group(2))} if m else {}
|
return {"von": int(m.group(1)), "zu": int(m.group(2))} if m else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _braucht_aufloesung(prompt: str):
|
||||||
|
# je ANGABE den Kandidaten mit den meisten geteilten Tokens (Substring, ≥4 Zeichen)
|
||||||
|
# wählen, sonst null — spiegelt den Substring-Prefilter der Auflösung.
|
||||||
|
def toks(s):
|
||||||
|
return {t for t in re.findall(r"\w+", s.lower()) if len(t) >= 4}
|
||||||
|
out = []
|
||||||
|
bloecke = re.split(r"ANGABE (\d+):", prompt)[1:]
|
||||||
|
for i in range(0, len(bloecke), 2):
|
||||||
|
n, body = int(bloecke[i]), bloecke[i + 1]
|
||||||
|
mp = re.search(r"Voraussetzung „([^“]+)", body)
|
||||||
|
ph = mp.group(1) if mp else ""
|
||||||
|
pl, pt = ph.lower(), toks(ph)
|
||||||
|
ranked = []
|
||||||
|
for kid, ktitel in re.findall(r"- atom (\d+): (.+?) —", body):
|
||||||
|
geteilt = {t for t in toks(ktitel) if t in pl} | {t for t in pt if t in ktitel.lower()}
|
||||||
|
if geteilt:
|
||||||
|
ranked.append((-len(geteilt), len(ktitel), int(kid)))
|
||||||
|
ranked.sort()
|
||||||
|
out.append({"phrase": n, "atom": ranked[0][2] if ranked else None})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _writer(prompt: str):
|
def _writer(prompt: str):
|
||||||
ziel = re.search(r"Lernziel: (.+)", prompt)
|
ziel = re.search(r"Lernziel: (.+)", prompt)
|
||||||
marker = re.findall(r"MARKER \(exakt so übernehmen\): (<!-- atom: \d+ \| (.+?) -->)",
|
marker = re.findall(r"MARKER \(exakt so übernehmen\): (<!-- atom: \d+ \| (.+?) -->)",
|
||||||
prompt)
|
prompt)
|
||||||
defs = re.findall(r"Definition: (.+)", prompt)
|
defs = re.findall(r"Definition: (.+)", prompt)
|
||||||
fragen = re.findall(r"^- (.+\?)$", prompt, re.MULTILINE)[:2]
|
|
||||||
lo = int(re.search(r"hat (\d+)–", prompt).group(1)) if re.search(r"hat (\d+)–", prompt) else 40
|
lo = int(re.search(r"hat (\d+)–", prompt).group(1)) if re.search(r"hat (\d+)–", prompt) else 40
|
||||||
absaetze = [f"Ziel dieser Einheit: {ziel.group(1) if ziel else 'Lernen'}. Formal: $n \\ge 0$."]
|
absaetze = [f"Ziel dieser Einheit: {ziel.group(1) if ziel else 'Lernen'}. Formal: $n \\ge 0$."]
|
||||||
fuellung = "Daran anknüpfend gehen wir das Schritt für Schritt am Beispiel durch. " * 5
|
fuellung = "Daran anknüpfend gehen wir das Schritt für Schritt am Beispiel durch. " * 5
|
||||||
diag = re.findall(r"(<!-- diagramm: \d+ -->)", prompt) # angebotene Diagramme
|
for (mk, titel), d in zip(marker, defs):
|
||||||
for n, ((mk, titel), d) in enumerate(zip(marker, defs)):
|
# Beispiel-/Diagramm-Marker setzt jetzt _marker_platzieren deterministisch,
|
||||||
block = f"#### {titel} verstehen\n{mk}\n{d} {fuellung}"
|
# nicht mehr der Writer.
|
||||||
if n == 0 and diag: # erstes Diagramm beim ersten Absatz platzieren
|
absaetze.append(f"#### {titel} verstehen\n{mk}\n{d} {fuellung}")
|
||||||
block += f"\n\n{diag[0]}"
|
|
||||||
absaetze.append(block)
|
|
||||||
absaetze.append("**Kernpunkte:**\n" + "\n".join(f"- {t} sitzt" for _, t in marker))
|
|
||||||
if fragen:
|
|
||||||
absaetze.append("**Prüfe dich:**\n" + "\n".join(f"- {f}" for f in fragen))
|
|
||||||
lang = "\n\n".join(absaetze)
|
lang = "\n\n".join(absaetze)
|
||||||
while len(lang.split()) < lo:
|
while len(lang.split()) < lo:
|
||||||
lang += "\nNoch ein Übungssatz zur Vertiefung des Gelernten."
|
lang += "\nNoch ein Übungssatz zur Vertiefung des Gelernten."
|
||||||
@@ -222,8 +248,8 @@ def _writer(prompt: str):
|
|||||||
|
|
||||||
|
|
||||||
def _diagramm_plan(prompt: str):
|
def _diagramm_plan(prompt: str):
|
||||||
# deterministisch: die Fake-Welt braucht kein LLM-Diagramm (DAG-Pfad wird
|
# deterministisch: die Fake-Welt braucht kein Diagramm. Unit-Tests für den
|
||||||
# getestet). Unit-Tests für den LLM-Zweig monkeypatchen diesen Handler.
|
# LLM-Diagramm-Zweig monkeypatchen diesen Handler auf noetig=True.
|
||||||
return {"noetig": False, "typ": ""}
|
return {"noetig": False, "typ": ""}
|
||||||
|
|
||||||
|
|
||||||
@@ -269,7 +295,7 @@ _HANDLER = {
|
|||||||
"Artefakt-Generate": _artefakt_generate, "Artefakt-Verify": _artefakt_verify,
|
"Artefakt-Generate": _artefakt_generate, "Artefakt-Verify": _artefakt_verify,
|
||||||
"Artefakt-Fix": _artefakt_fix, "Lernziele": _lernziele, "Kapitel-Intro": _kapitel_intro,
|
"Artefakt-Fix": _artefakt_fix, "Lernziele": _lernziele, "Kapitel-Intro": _kapitel_intro,
|
||||||
"Kapitel-Schnitt": _kapitel_schnitt, "Level-Kalibrierung": _level_kalibrierung,
|
"Kapitel-Schnitt": _kapitel_schnitt, "Level-Kalibrierung": _level_kalibrierung,
|
||||||
"Kanten-Zyklus": _zyklus,
|
"Kanten-Zyklus": _zyklus, "Braucht-Aufloesung": _braucht_aufloesung,
|
||||||
"Guide-Writer": _writer, "Guide-Pruefer": _pruefer, "Guide-Fix": _fix,
|
"Guide-Writer": _writer, "Guide-Pruefer": _pruefer, "Guide-Fix": _fix,
|
||||||
"QA-Guide-Falsch": _qa_falsch, "QA-Guide-Falsch-Check": _qa_falsch_check,
|
"QA-Guide-Falsch": _qa_falsch, "QA-Guide-Falsch-Check": _qa_falsch_check,
|
||||||
"Diagramm-Plan": _diagramm_plan, "Diagramm-Spec": _diagramm_spec,
|
"Diagramm-Plan": _diagramm_plan, "Diagramm-Spec": _diagramm_spec,
|
||||||
|
|||||||
207
backend/guide.py
207
backend/guide.py
@@ -15,9 +15,10 @@ import subprocess
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import db
|
import db
|
||||||
|
import jsonx
|
||||||
import llm
|
import llm
|
||||||
import textkit
|
import textkit
|
||||||
from config import DURCHGANG, LEVEL_RANG, SECTION_WOERTER_PRO_ATOM
|
from config import DURCHGANG, FIX_MAX_VERSUCHE, LEVEL_RANG, SECTION_WOERTER_PRO_ATOM
|
||||||
|
|
||||||
log = logging.getLogger("creator2.guide")
|
log = logging.getLogger("creator2.guide")
|
||||||
|
|
||||||
@@ -55,11 +56,15 @@ _MARKER = re.compile(r"<!--\s*atom:\s*(\d+)\s*\|")
|
|||||||
# einzeilig, also kein DOTALL). Muster spiegelt frontend/src/markdown.js.
|
# einzeilig, also kein DOTALL). Muster spiegelt frontend/src/markdown.js.
|
||||||
_MARKER_VOLL = re.compile(r"<!--\s*atom:.*?-->")
|
_MARKER_VOLL = re.compile(r"<!--\s*atom:.*?-->")
|
||||||
_MARKER_DIAGRAMM = re.compile(r"<!--\s*diagramm:\s*(\d+)\s*-->")
|
_MARKER_DIAGRAMM = re.compile(r"<!--\s*diagramm:\s*(\d+)\s*-->")
|
||||||
|
_MARKER_BEISPIEL = re.compile(r"<!--\s*beispiel:\s*(\d+)\s*-->")
|
||||||
|
|
||||||
# Kurzbeschreibung je Diagramm-Typ (Writer-Angebot)
|
# Kurzbeschreibung je Diagramm-Typ (Writer-Angebot)
|
||||||
_DIAGRAMM_BESCHR = {"dependency": "Abhängigkeitsgraph der Begriffe dieses Bausteins",
|
_DIAGRAMM_BESCHR = {"dependency": "Abhängigkeitsgraph der Begriffe dieses Bausteins",
|
||||||
"state": "Zustandsdiagramm/Automat", "flow": "Ablaufdiagramm",
|
"state": "Zustandsdiagramm/Automat", "flow": "Ablaufdiagramm",
|
||||||
"tree": "Baumstruktur"}
|
"tree": "Baumstruktur"}
|
||||||
|
_BEISPIEL_BESCHR = {"code": "Code-Beispiel", "tabelle": "Tabelle/Vergleich",
|
||||||
|
"diagramm": "Diagramm-Beispiel"}
|
||||||
|
_VERBATIM_FORMEN = ("code", "tabelle", "diagramm")
|
||||||
|
|
||||||
|
|
||||||
def _ohne_marker(text: str) -> str:
|
def _ohne_marker(text: str) -> str:
|
||||||
@@ -67,8 +72,77 @@ def _ohne_marker(text: str) -> str:
|
|||||||
ALLE Text-Checks (Stil, Mathe, Länge, Vorwärtsverweise) laufen auf dem
|
ALLE Text-Checks (Stil, Mathe, Länge, Vorwärtsverweise) laufen auf dem
|
||||||
gestrippten Text — sonst erzeugen Atom-TITEL wie „a_n" oder „Satz 6.26"
|
gestrippten Text — sonst erzeugen Atom-TITEL wie „a_n" oder „Satz 6.26"
|
||||||
unfixierbare Aufträge (der Fix darf Marker nie ändern → Endlos-Loop).
|
unfixierbare Aufträge (der Fix darf Marker nie ändern → Endlos-Loop).
|
||||||
Diagramm-Marker gehören ebenso raus."""
|
Diagramm- und Beispiel-Marker gehören ebenso raus."""
|
||||||
return _MARKER_DIAGRAMM.sub("", _MARKER_VOLL.sub("", text))
|
return _MARKER_BEISPIEL.sub("", _MARKER_DIAGRAMM.sub("", _MARKER_VOLL.sub("", text)))
|
||||||
|
|
||||||
|
|
||||||
|
def _beispiel_verbatim(atom_id: int) -> list[tuple[int, dict]]:
|
||||||
|
"""Verifizierte Beispiele des Atoms in VERBATIM-Form (code/tabelle/diagramm) —
|
||||||
|
(artefakt_id, inhalt). Woven-Formen (text/mathe) gehen über `_beispiel`."""
|
||||||
|
out = []
|
||||||
|
for r in db.query("SELECT id, inhalt FROM artefakte WHERE atom_id=? AND typ='beispiel'"
|
||||||
|
" AND status='verifiziert'", (atom_id,)):
|
||||||
|
inh = db.uj(r["inhalt"], {})
|
||||||
|
if inh.get("form") in _VERBATIM_FORMEN:
|
||||||
|
out.append((r["id"], inh))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _beispiel_render(inh: dict) -> str:
|
||||||
|
"""Verbatim-Beispiel → Markdown (Code-Fence / Tabelle / Mermaid-Fence)."""
|
||||||
|
form = inh.get("form")
|
||||||
|
if form == "code" and inh.get("code"):
|
||||||
|
return f"```{inh.get('sprache', '')}\n{inh['code']}\n```"
|
||||||
|
if form == "tabelle" and inh.get("tabelle"):
|
||||||
|
return inh["tabelle"]
|
||||||
|
if form == "diagramm" and inh.get("mermaid"):
|
||||||
|
return f"```mermaid\n{inh['mermaid']}\n```"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _marker_platzieren(b: dict, lang: str) -> str:
|
||||||
|
"""Verbatim-Marker DETERMINISTISCH setzen (der Writer tut es nicht mehr — er
|
||||||
|
ignorierte die Regel, 2/13 platziert). Beispiel-Marker hinter den Atom-Absatz
|
||||||
|
(atom-genau via Atom-Marker); Diagramme (baustein-scoped) ans Section-Ende.
|
||||||
|
Idempotent: alte Verbatim-Marker erst raus."""
|
||||||
|
lang = _MARKER_BEISPIEL.sub("", _MARKER_DIAGRAMM.sub("", lang))
|
||||||
|
je_atom = {}
|
||||||
|
for a in _atome_von(b["id"]):
|
||||||
|
vs = _beispiel_verbatim(a["id"])
|
||||||
|
if vs:
|
||||||
|
je_atom[a["id"]] = vs[0][0] # erstes Verbatim-Beispiel des Atoms
|
||||||
|
if je_atom:
|
||||||
|
out, pending = [], None
|
||||||
|
for z in lang.split("\n"):
|
||||||
|
out.append(z)
|
||||||
|
m = _MARKER.search(z)
|
||||||
|
if m and int(m.group(1)) in je_atom:
|
||||||
|
pending = je_atom.pop(int(m.group(1)))
|
||||||
|
elif pending is not None and z.strip() == "": # Absatzende → Marker als Block
|
||||||
|
out += [f"<!-- beispiel: {pending} -->", ""]
|
||||||
|
pending = None
|
||||||
|
if pending is not None:
|
||||||
|
out += ["", f"<!-- beispiel: {pending} -->"]
|
||||||
|
lang = "\n".join(out)
|
||||||
|
diags = _diagramme_von(b["id"])
|
||||||
|
if diags:
|
||||||
|
block = "\n".join(f"<!-- diagramm: {d['id']} -->" for d in diags)
|
||||||
|
lang = lang.rstrip() + "\n\n" + block
|
||||||
|
return lang
|
||||||
|
|
||||||
|
|
||||||
|
def _beispiel_einsetzen(baustein_id: int, lang: str) -> str:
|
||||||
|
"""Assembly: `<!-- beispiel: id -->` → gerendertes Verbatim-Beispiel; tote Marker weg."""
|
||||||
|
if "<!-- beispiel:" not in lang and "<!--beispiel:" not in lang:
|
||||||
|
return lang
|
||||||
|
je_id = {aid: inh for a in _atome_von(baustein_id)
|
||||||
|
for aid, inh in _beispiel_verbatim(a["id"])}
|
||||||
|
|
||||||
|
def sub(m: re.Match) -> str:
|
||||||
|
inh = je_id.get(int(m.group(1)))
|
||||||
|
return _beispiel_render(inh) if inh else ""
|
||||||
|
|
||||||
|
return _MARKER_BEISPIEL.sub(sub, lang)
|
||||||
|
|
||||||
|
|
||||||
def _diagramme_von(baustein_id: int) -> list[dict]:
|
def _diagramme_von(baustein_id: int) -> list[dict]:
|
||||||
@@ -76,13 +150,6 @@ def _diagramme_von(baustein_id: int) -> list[dict]:
|
|||||||
" ORDER BY id", (baustein_id,))
|
" ORDER BY id", (baustein_id,))
|
||||||
|
|
||||||
|
|
||||||
def _diagramm_angebot(baustein_id: int) -> str:
|
|
||||||
"""Verfügbare Diagramme für den Writer — er platziert den Marker beim Absatz."""
|
|
||||||
zeilen = [f"<!-- diagramm: {d['id']} --> — {_DIAGRAMM_BESCHR.get(d['typ'], d['typ'])}"
|
|
||||||
for d in _diagramme_von(baustein_id)]
|
|
||||||
return "\n".join(zeilen) or "(keine)"
|
|
||||||
|
|
||||||
|
|
||||||
def _diagramm_einsetzen(baustein_id: int, lang: str) -> str:
|
def _diagramm_einsetzen(baustein_id: int, lang: str) -> str:
|
||||||
"""Assembly: `<!-- diagramm: id -->` → ```mermaid-Fence des verifizierten
|
"""Assembly: `<!-- diagramm: id -->` → ```mermaid-Fence des verifizierten
|
||||||
Diagramms. Tote/unverifizierte Marker fallen ersatzlos weg."""
|
Diagramms. Tote/unverifizierte Marker fallen ersatzlos weg."""
|
||||||
@@ -117,15 +184,14 @@ def _zitate(atom_id: int, max_n: int = 3) -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def _beispiel(atom_id: int) -> str:
|
def _beispiel(atom_id: int) -> str:
|
||||||
|
"""Nur die WOVEN-Formen (text/mathe) als Prosa für den Writer. Verbatim-Formen
|
||||||
|
(code/tabelle/diagramm) kommen über den `<!-- beispiel: id -->`-Marker rein."""
|
||||||
row = db.one("SELECT inhalt FROM artefakte WHERE atom_id=? AND typ='beispiel'"
|
row = db.one("SELECT inhalt FROM artefakte WHERE atom_id=? AND typ='beispiel'"
|
||||||
" AND status='verifiziert' LIMIT 1", (atom_id,))
|
" AND status='verifiziert' LIMIT 1", (atom_id,))
|
||||||
return db.uj(row["inhalt"], {}).get("text", "") if row else ""
|
if not row:
|
||||||
|
return ""
|
||||||
|
inh = db.uj(row["inhalt"], {})
|
||||||
def _fragen_pool(atom_id: int, max_n: int = 2) -> list[str]:
|
return inh.get("text", "") if inh.get("form", "text") in ("text", "mathe") else ""
|
||||||
rows = db.query("SELECT inhalt FROM artefakte WHERE atom_id=? AND typ='flashcard'"
|
|
||||||
" AND status='verifiziert' LIMIT ?", (atom_id, max_n))
|
|
||||||
return [f for r in rows if (f := db.uj(r["inhalt"], {}).get("frage", "").strip())]
|
|
||||||
|
|
||||||
|
|
||||||
def _atom_paket(a: dict) -> str:
|
def _atom_paket(a: dict) -> str:
|
||||||
@@ -135,9 +201,6 @@ def _atom_paket(a: dict) -> str:
|
|||||||
f"Definition: {a['definition']}\nBelege (VERBATIM zitierbar):\n{zitate}")
|
f"Definition: {a['definition']}\nBelege (VERBATIM zitierbar):\n{zitate}")
|
||||||
if beispiel:
|
if beispiel:
|
||||||
teil += f"\nVerifiziertes Beispiel (Werte wörtlich übernehmen):\n{beispiel}"
|
teil += f"\nVerifiziertes Beispiel (Werte wörtlich übernehmen):\n{beispiel}"
|
||||||
fragen = _fragen_pool(a["id"])
|
|
||||||
if fragen:
|
|
||||||
teil += "\nFRAGEN-POOL:\n" + "\n".join(f"- {f}" for f in fragen)
|
|
||||||
return teil
|
return teil
|
||||||
|
|
||||||
|
|
||||||
@@ -170,6 +233,25 @@ def _anknuepf_kontext(b: dict) -> str:
|
|||||||
|
|
||||||
# ── Stages ────────────────────────────────────────────────────────────────────
|
# ── Stages ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _split_writer(text: str) -> tuple[str, str]:
|
||||||
|
"""Delimited-Ausgabe des Writers/Fix parsen: ===LANG===… / ===KOMPAKT===….
|
||||||
|
Grund: rohe LaTeX-Backslashes im Text sind invalides JSON (34 % Parse-Fehler).
|
||||||
|
Robust: Blöcke unabhängig von der Reihenfolge; fehlt der Delimiter, jsonx-Fallback
|
||||||
|
(alt-JSON), sonst ganzer Text = lang."""
|
||||||
|
def block(name: str) -> str | None:
|
||||||
|
m = re.search(rf"===\s*{name}\s*===\s*(.*?)(?=\n===\s*[A-ZÄÖÜ]+\s*===|$)",
|
||||||
|
text or "", re.S)
|
||||||
|
return m.group(1).strip() if m else None
|
||||||
|
|
||||||
|
lang, kompakt = block("LANG"), block("KOMPAKT")
|
||||||
|
if lang is not None or kompakt is not None:
|
||||||
|
return kompakt or "", lang or ""
|
||||||
|
daten = jsonx.parse(text or "") # Fallback: altes JSON-Format
|
||||||
|
if isinstance(daten, dict):
|
||||||
|
return str(daten.get("kompakt", "")), str(daten.get("lang", ""))
|
||||||
|
return "", (text or "").strip()
|
||||||
|
|
||||||
|
|
||||||
async def _stage_writer(ctx: llm.Kontext, b: dict) -> str:
|
async def _stage_writer(ctx: llm.Kontext, b: dict) -> str:
|
||||||
atome = _atome_von(b["id"])
|
atome = _atome_von(b["id"])
|
||||||
ziel = db.one("SELECT text FROM lernziele WHERE id=?", (b["ziel_id"],)) or {"text": b["titel"]}
|
ziel = db.one("SELECT text FROM lernziele WHERE id=?", (b["ziel_id"],)) or {"text": b["titel"]}
|
||||||
@@ -178,20 +260,22 @@ async def _stage_writer(ctx: llm.Kontext, b: dict) -> str:
|
|||||||
"atome": "\n\n".join(_atom_paket(a) for a in atome),
|
"atome": "\n\n".join(_atom_paket(a) for a in atome),
|
||||||
"min_woerter": lo, "max_woerter": hi,
|
"min_woerter": lo, "max_woerter": hi,
|
||||||
"durchgang": DURCHGANG.get(b["level"], DURCHGANG["M"]),
|
"durchgang": DURCHGANG.get(b["level"], DURCHGANG["M"]),
|
||||||
"kontext": _anknuepf_kontext(b) or "(nichts — erster Durchgang)",
|
"kontext": _anknuepf_kontext(b) or "(nichts — erster Durchgang)"}
|
||||||
"diagramme": _diagramm_angebot(b["id"])}
|
|
||||||
res = await llm.call(ctx, stage="writer", template="Guide-Writer", werte=werte,
|
res = await llm.call(ctx, stage="writer", template="Guide-Writer", werte=werte,
|
||||||
role="guide", n=len(atome), item=f"b{b['id']}", erwartet=dict)
|
role="guide", n=len(atome), item=f"b{b['id']}", erwartet=str)
|
||||||
if res is None:
|
if res is None:
|
||||||
return "writer" # Stage bleibt, nächster Lauf versucht erneut
|
return "writer" # Stage bleibt, nächster Lauf versucht erneut
|
||||||
kompakt, lang = str(res.get("kompakt", "")), str(res.get("lang", ""))
|
kompakt, lang = _split_writer(res)
|
||||||
fehlend = _marker_fehlend(lang, atome)
|
fehlend = _marker_fehlend(lang, atome)
|
||||||
if fehlend: # ein gezielter Zweitversuch mit explizitem Mangel
|
if fehlend: # ein gezielter Zweitversuch mit explizitem Mangel
|
||||||
res2 = await llm.call(ctx, stage="writer2", template="Guide-Writer",
|
res2 = await llm.call(ctx, stage="writer2", template="Guide-Writer",
|
||||||
schritt="writer", role="guide", n=len(atome),
|
schritt="writer", role="guide", n=len(atome),
|
||||||
item=f"b{b['id']}-2", werte=werte, erwartet=dict)
|
item=f"b{b['id']}-2", werte=werte, erwartet=str)
|
||||||
if res2 and not _marker_fehlend(str(res2.get("lang", "")), atome):
|
if res2:
|
||||||
kompakt, lang = str(res2.get("kompakt", "")), str(res2.get("lang", ""))
|
k2, l2 = _split_writer(res2)
|
||||||
|
if not _marker_fehlend(l2, atome):
|
||||||
|
kompakt, lang = k2, l2
|
||||||
|
lang = _marker_platzieren(b, lang) # Beispiel-/Diagramm-Marker deterministisch setzen
|
||||||
db.update("sections", "baustein_id", b["id"], text_kompakt=kompakt, text_lang=lang)
|
db.update("sections", "baustein_id", b["id"], text_kompakt=kompakt, text_lang=lang)
|
||||||
return "pruefer"
|
return "pruefer"
|
||||||
|
|
||||||
@@ -378,7 +462,9 @@ def _vorwaertsverweise(topic: str, b: dict, lang: str) -> list[str]:
|
|||||||
if je_baustein.get(a["bid"], 0) <= mein_kapitel:
|
if je_baustein.get(a["bid"], 0) <= mein_kapitel:
|
||||||
continue
|
continue
|
||||||
t = textkit.norm(a["titel"])
|
t = textkit.norm(a["titel"])
|
||||||
if len(t) < 6 and len(t.split()) < 2:
|
# nur SIGNIFIKANTE Titel flaggen: ≥2 Token UND ≥8 Zeichen. Kurze Fachwörter
|
||||||
|
# (oft Homonyme/gängige Wörter) produzierten unschließbare Dauer-Flags.
|
||||||
|
if len(t.split()) < 2 or len(t) < 8:
|
||||||
continue
|
continue
|
||||||
if re.search(rf"(?<![a-zäöüß0-9]){re.escape(t)}(?![a-zäöüß0-9])", text):
|
if re.search(rf"(?<![a-zäöüß0-9]){re.escape(t)}(?![a-zäöüß0-9])", text):
|
||||||
treffer.append(a["titel"])
|
treffer.append(a["titel"])
|
||||||
@@ -428,13 +514,14 @@ async def _stage_fix(ctx: llm.Kontext, b: dict) -> str:
|
|||||||
werte={"kompakt": sec["text_kompakt"], "lang": sec["text_lang"],
|
werte={"kompakt": sec["text_kompakt"], "lang": sec["text_lang"],
|
||||||
"auftraege": "\n".join(f"- {a}" for a in auftraege),
|
"auftraege": "\n".join(f"- {a}" for a in auftraege),
|
||||||
"fakten": fakten},
|
"fakten": fakten},
|
||||||
erwartet=dict)
|
erwartet=str)
|
||||||
kritisch = any(a.startswith("KRITISCH") for a in auftraege)
|
kritisch = any(a.startswith("KRITISCH") for a in auftraege)
|
||||||
if res:
|
if res:
|
||||||
lang = str(res.get("lang", ""))
|
kompakt, lang = _split_writer(res)
|
||||||
if not _marker_fehlend(lang, atome): # Fix darf die Marker-Invariante nie brechen
|
if not _marker_fehlend(lang, atome): # Fix darf die Marker-Invariante nie brechen
|
||||||
|
lang = _marker_platzieren(b, lang) # Marker nach dem Rewrite neu setzen
|
||||||
db.update("sections", "baustein_id", b["id"],
|
db.update("sections", "baustein_id", b["id"],
|
||||||
text_kompakt=str(res.get("kompakt", "")), text_lang=lang, befunde=db.j([]))
|
text_kompakt=kompakt, text_lang=lang, befunde=db.j([]))
|
||||||
if kritisch: # genau EIN Re-Check; Rest fängt die Ebenen-QA
|
if kritisch: # genau EIN Re-Check; Rest fängt die Ebenen-QA
|
||||||
await _stage_pruefer(ctx, b, tag="-re")
|
await _stage_pruefer(ctx, b, tag="-re")
|
||||||
return "done"
|
return "done"
|
||||||
@@ -496,23 +583,6 @@ async def bauen(ctx: llm.Kontext) -> None:
|
|||||||
await _kapitel_intros(ctx)
|
await _kapitel_intros(ctx)
|
||||||
|
|
||||||
|
|
||||||
def _pruefe_dich(baustein_id: int, lang: str) -> list[dict]:
|
|
||||||
"""Antworten zu den „Prüfe dich"-Fragen: die Fragen stammen wörtlich aus dem
|
|
||||||
Flashcard-Pool — Matching gegen den Langtext liefert die passenden Karten
|
|
||||||
(Retrieval mit Feedback wirkt ~doppelt so stark wie ohne)."""
|
|
||||||
out = []
|
|
||||||
lang_norm = textkit.norm(lang) # Case/Whitespace-tolerant — ein Stil-Fix an der
|
|
||||||
for a in _atome_von(baustein_id): # Frage ließ das rohe Substring-Matching scheitern
|
|
||||||
for r in db.query("SELECT inhalt FROM artefakte WHERE atom_id=? AND"
|
|
||||||
" typ='flashcard' AND status='verifiziert'", (a["id"],)):
|
|
||||||
fc = db.uj(r["inhalt"], {})
|
|
||||||
frage = str(fc.get("frage", "")).strip()
|
|
||||||
antwort = str(fc.get("antwort", "")).strip()
|
|
||||||
if frage and antwort and textkit.norm(frage) in lang_norm:
|
|
||||||
out.append({"frage": frage, "antwort": antwort})
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def kapitel_struktur(topic: str) -> list[dict]:
|
def kapitel_struktur(topic: str) -> list[dict]:
|
||||||
"""Kapitel (Struktur-Ebene) entlang der Baustein-Ordnung; die Segmente sind
|
"""Kapitel (Struktur-Ebene) entlang der Baustein-Ordnung; die Segmente sind
|
||||||
kontiguierlich, also wechselt H2 genau an den Kapitel-Grenzen."""
|
kontiguierlich, also wechselt H2 genau an den Kapitel-Grenzen."""
|
||||||
@@ -534,15 +604,16 @@ def kapitel_struktur(topic: str) -> list[dict]:
|
|||||||
"titel": k.get("titel", "Weitere Themen"),
|
"titel": k.get("titel", "Weitere Themen"),
|
||||||
"intro": k.get("intro", ""),
|
"intro": k.get("intro", ""),
|
||||||
"level": k.get("level", b["level"]), "sections": []})
|
"level": k.get("level", b["level"]), "sections": []})
|
||||||
# Diagramm-Marker gegen den ```mermaid-Fence tauschen (Assembly-Schritt);
|
# Diagramm- und Beispiel-Marker gegen ihre Fences/Tabellen tauschen (Assembly);
|
||||||
# Atom-Marker bleiben — die strippt erst das Frontend.
|
# Atom-Marker bleiben — die strippt erst das Frontend.
|
||||||
lang = _diagramm_einsetzen(b["id"], sec["text_lang"]) if sec else ""
|
lang = ""
|
||||||
|
if sec:
|
||||||
|
lang = _beispiel_einsetzen(b["id"], _diagramm_einsetzen(b["id"], sec["text_lang"]))
|
||||||
kapitel[-1]["sections"].append({
|
kapitel[-1]["sections"].append({
|
||||||
"baustein": b["id"], "titel": b["titel"],
|
"baustein": b["id"], "titel": b["titel"],
|
||||||
"ziel": ziele.get(b["ziel_id"], {}).get("text", ""),
|
"ziel": ziele.get(b["ziel_id"], {}).get("text", ""),
|
||||||
"kompakt": sec["text_kompakt"] if sec else "",
|
"kompakt": sec["text_kompakt"] if sec else "",
|
||||||
"lang": lang,
|
"lang": lang})
|
||||||
"fragen": _pruefe_dich(b["id"], lang) if lang else []})
|
|
||||||
# NULL-kapitel_id durch eindeutige synthetische Keys ersetzen — mehrere
|
# NULL-kapitel_id durch eindeutige synthetische Keys ersetzen — mehrere
|
||||||
# kapitellose Gruppen kollidierten sonst im Vue-:key (null == null).
|
# kapitellose Gruppen kollidierten sonst im Vue-:key (null == null).
|
||||||
for i, kap in enumerate(kapitel):
|
for i, kap in enumerate(kapitel):
|
||||||
@@ -593,6 +664,12 @@ def messen(ctx: llm.Kontext) -> list[dict]:
|
|||||||
befunde.append({"art": "diagramm_marker_tot", "item": str(b["id"]),
|
befunde.append({"art": "diagramm_marker_tot", "item": str(b["id"]),
|
||||||
"detail": f"Stray Diagramm-Marker <!-- diagramm: {m} -->"
|
"detail": f"Stray Diagramm-Marker <!-- diagramm: {m} -->"
|
||||||
f" entfernen (kein solches verifiziertes Diagramm)"})
|
f" entfernen (kein solches verifiziertes Diagramm)"})
|
||||||
|
gueltige_b = {aid for a in atome for aid, _ in _beispiel_verbatim(a["id"])}
|
||||||
|
for m in _MARKER_BEISPIEL.findall(lang):
|
||||||
|
if int(m) not in gueltige_b: # Marker ohne verifiziertes Verbatim-Beispiel
|
||||||
|
befunde.append({"art": "beispiel_marker_tot", "item": str(b["id"]),
|
||||||
|
"detail": f"Stray Beispiel-Marker <!-- beispiel: {m} -->"
|
||||||
|
f" entfernen (kein solches verifiziertes Beispiel)"})
|
||||||
for auftrag in _det_auftraege(ctx.topic, b, lang, sec["text_kompakt"], qa=True):
|
for auftrag in _det_auftraege(ctx.topic, b, lang, sec["text_kompakt"], qa=True):
|
||||||
befunde.append({"art": "det_check", "item": str(b["id"]),
|
befunde.append({"art": "det_check", "item": str(b["id"]),
|
||||||
"detail": auftrag[:300]})
|
"detail": auftrag[:300]})
|
||||||
@@ -611,7 +688,7 @@ def messen(ctx: llm.Kontext) -> list[dict]:
|
|||||||
befunde.append({"art": "ziel_ohne_anker", "item": str(b["id"]),
|
befunde.append({"art": "ziel_ohne_anker", "item": str(b["id"]),
|
||||||
"detail": ziel["text"][:120]})
|
"detail": ziel["text"][:120]})
|
||||||
lo, hi = _laenge_band(len(atome))
|
lo, hi = _laenge_band(len(atome))
|
||||||
w = len(lang.split())
|
w = len(_ohne_marker(lang).split()) # marker-frei — konsistent mit dem Fix-Band
|
||||||
if not lo <= w <= hi * 1.25: # QA-Band weiter als das Fix-Band (Lektion 75)
|
if not lo <= w <= hi * 1.25: # QA-Band weiter als das Fix-Band (Lektion 75)
|
||||||
befunde.append({"art": "laenge", "item": str(b["id"]), "detail": f"{w} Wörter"})
|
befunde.append({"art": "laenge", "item": str(b["id"]), "detail": f"{w} Wörter"})
|
||||||
for t in _vorwaertsverweise(ctx.topic, b, lang):
|
for t in _vorwaertsverweise(ctx.topic, b, lang):
|
||||||
@@ -703,12 +780,21 @@ async def reparieren(ctx: llm.Kontext, befunde: list[dict]) -> bool:
|
|||||||
betroffen.setdefault(b_id, []).append(b)
|
betroffen.setdefault(b_id, []).append(b)
|
||||||
if not betroffen:
|
if not betroffen:
|
||||||
return False
|
return False
|
||||||
|
geroutet: list[int] = []
|
||||||
for b_id, liste in betroffen.items():
|
for b_id, liste in betroffen.items():
|
||||||
arten = [x["art"] for x in liste]
|
arten = [x["art"] for x in liste]
|
||||||
# ein fix_offen-Detail mit KRITISCH-Präfix ist ebenfalls kritisch
|
# ein fix_offen-Detail mit KRITISCH-Präfix ist ebenfalls kritisch
|
||||||
kritisch_detail = any(x["art"] == "fix_offen"
|
kritisch_detail = any(x["art"] == "fix_offen"
|
||||||
and str(x["detail"]).startswith("KRITISCH") for x in liste)
|
and str(x["detail"]).startswith("KRITISCH") for x in liste)
|
||||||
if any(a in KRITISCH or a == "section_fehlt" for a in arten) or kritisch_detail:
|
kritisch = any(a in KRITISCH or a == "section_fehlt" for a in arten) or kritisch_detail
|
||||||
|
if not kritisch:
|
||||||
|
# Fix-Cap: eine Section, deren Fix K-mal nichts geändert hat, wird
|
||||||
|
# eingefroren — kein weiteres Routing der Stil-/Längen-/vorwaerts-
|
||||||
|
# Befunde. Rest-Schuld (Gewicht 0.5) akzeptiert; bremst den 617-Churn.
|
||||||
|
sec = db.one("SELECT fix_versuche FROM sections WHERE baustein_id=?", (b_id,))
|
||||||
|
if sec and sec["fix_versuche"] >= FIX_MAX_VERSUCHE:
|
||||||
|
continue
|
||||||
|
if kritisch:
|
||||||
if "section_fehlt" in arten:
|
if "section_fehlt" in arten:
|
||||||
db.execute("UPDATE sections SET stage='writer' WHERE baustein_id=?", (b_id,))
|
db.execute("UPDATE sections SET stage='writer' WHERE baustein_id=?", (b_id,))
|
||||||
else:
|
else:
|
||||||
@@ -730,6 +816,15 @@ async def reparieren(ctx: llm.Kontext, befunde: list[dict]) -> bool:
|
|||||||
auftraege = list(dict.fromkeys(vorhanden + neu))
|
auftraege = list(dict.fromkeys(vorhanden + neu))
|
||||||
db.update("sections", "baustein_id", b_id, stage="fix", befunde=db.j(auftraege))
|
db.update("sections", "baustein_id", b_id, stage="fix", befunde=db.j(auftraege))
|
||||||
db.update("bausteine", "id", b_id, status="repair")
|
db.update("bausteine", "id", b_id, status="repair")
|
||||||
vorher = {b_id: _text_sig(b_id) for b_id in betroffen}
|
geroutet.append(b_id)
|
||||||
|
vorher = {b_id: _text_sig(b_id) for b_id in geroutet}
|
||||||
await bauen(ctx)
|
await bauen(ctx)
|
||||||
return any(vorher[b_id] != _text_sig(b_id) for b_id in betroffen)
|
bewegt = False
|
||||||
|
for b_id in geroutet: # Fix-Cap-Zähler pflegen: Änderung → reset, sonst +1
|
||||||
|
if _text_sig(b_id) != vorher[b_id]:
|
||||||
|
db.execute("UPDATE sections SET fix_versuche=0 WHERE baustein_id=?", (b_id,))
|
||||||
|
bewegt = True
|
||||||
|
else:
|
||||||
|
db.execute("UPDATE sections SET fix_versuche=fix_versuche+1 WHERE baustein_id=?",
|
||||||
|
(b_id,))
|
||||||
|
return bewegt
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import embedding
|
|||||||
import korpus
|
import korpus
|
||||||
import llm
|
import llm
|
||||||
import textkit
|
import textkit
|
||||||
from config import (ANKER_OVERLAP_MERGE, LUECKEN_RUNDEN_MAX, MERGE_KANDIDAT_COS,
|
from config import (ANKER_OVERLAP_MERGE, BRAUCHT_KANDIDATEN, LUECKEN_RUNDEN_MAX,
|
||||||
MERGE_KANDIDAT_JACCARD,
|
MERGE_KANDIDAT_COS, MERGE_KANDIDAT_JACCARD,
|
||||||
MERGE_PANEL, READER_JE_ABSCHNITT, ZIELE_CHUNK_ATOME)
|
MERGE_PANEL, READER_JE_ABSCHNITT, ZIELE_CHUNK_ATOME)
|
||||||
|
|
||||||
log = logging.getLogger("creator2.inventar")
|
log = logging.getLogger("creator2.inventar")
|
||||||
@@ -266,6 +266,101 @@ def _kanten_aufloesen(topic: str) -> None:
|
|||||||
" VALUES(?,?,?,'braucht')", (topic, a["id"], ziel))
|
" VALUES(?,?,?,'braucht')", (topic, a["id"], ziel))
|
||||||
|
|
||||||
|
|
||||||
|
def _braucht_unaufgeloest(atome: list[dict]) -> list[tuple[dict, str]]:
|
||||||
|
"""(Atom, braucht-Titel), die _kanten_aufloesen (norm/kern) NICHT traf. Selbe
|
||||||
|
Auflösungslogik — was sie fand, ist erledigt und bleibt außen vor."""
|
||||||
|
je_norm = {textkit.norm(a["titel"]): a["id"] for a in atome}
|
||||||
|
je_kern: dict[str, int] = {}
|
||||||
|
for a in atome:
|
||||||
|
if kern := textkit.titel_kern(a["titel"]):
|
||||||
|
je_kern.setdefault(kern, a["id"])
|
||||||
|
offen = []
|
||||||
|
for a in atome:
|
||||||
|
for titel in db.uj(a["braucht"]):
|
||||||
|
ziel = je_norm.get(textkit.norm(titel))
|
||||||
|
if not ziel and (kern := textkit.titel_kern(titel)):
|
||||||
|
ziel = je_kern.get(kern)
|
||||||
|
if not ziel or ziel == a["id"]:
|
||||||
|
offen.append((a, titel))
|
||||||
|
return offen
|
||||||
|
|
||||||
|
|
||||||
|
def _trigramm(s: str) -> set[str]:
|
||||||
|
s = textkit.norm(s)
|
||||||
|
return {s[i:i + 3] for i in range(len(s) - 2)} or {s}
|
||||||
|
|
||||||
|
|
||||||
|
def _tri_dice(a: str, b: str) -> float:
|
||||||
|
"""Zeichen-Trigramm-Dice — fuzzy Titel-Ähnlichkeit, die geteilte Wortstämme UND
|
||||||
|
Kopfnomen erfasst („Approximationsalgorithmus" ~ „Approximativer Algorithmus")."""
|
||||||
|
A, B = _trigramm(a), _trigramm(b)
|
||||||
|
return 2 * len(A & B) / (len(A) + len(B)) if A or B else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _braucht_kandidaten(titel: str, atome: list[dict], selbst_id: int, cap: int) -> list[dict]:
|
||||||
|
"""Expliziter Kandidatengenerator für Kompositum↔Expansion (Lektion 28: Cosinus
|
||||||
|
~0.53, Embedding schlägt sie nie vor). Zulassung (hohe Trefferquote): ein
|
||||||
|
signifikantes Token (≥5 Zeichen) ist Substring des anderen normierten Titels
|
||||||
|
(„algorithmus" ⊂ „approximationsalgorithmus"). Rang nach Trigramm-Dice — der
|
||||||
|
ganze Titel zählt, nicht nur das Kopfnomen, so steht der Grundbegriff vor
|
||||||
|
generischen Distraktoren („Algorithmus"). Der Judge trennt dann fein."""
|
||||||
|
nt = textkit.norm(titel)
|
||||||
|
bt = {t for t in textkit.tokens(titel) if len(t) >= 5}
|
||||||
|
treffer = []
|
||||||
|
for a in atome:
|
||||||
|
if a["id"] == selbst_id:
|
||||||
|
continue
|
||||||
|
na = textkit.norm(a["titel"])
|
||||||
|
at = {t for t in textkit.tokens(a["titel"]) if len(t) >= 5}
|
||||||
|
if {t for t in at if t in nt} | {t for t in bt if t in na}:
|
||||||
|
treffer.append((_tri_dice(titel, a["titel"]), a["id"], a))
|
||||||
|
treffer.sort(key=lambda x: (-x[0], x[1]))
|
||||||
|
return [a for _, _, a in treffer[:cap]]
|
||||||
|
|
||||||
|
|
||||||
|
async def _braucht_fallback(ctx: llm.Kontext) -> None:
|
||||||
|
"""braucht-Titel, die norm/kern nicht auflösten, per Judge auf Atome mappen.
|
||||||
|
Deterministischer Substring-Prefilter (Kandidaten) → EIN Judge (n=1) als
|
||||||
|
Präzisions-Gate. braucht-Kanten sind reversibel/billiger als Merges → kein Panel
|
||||||
|
(Lektion 78). Im Zweifel null: eine Falsch-Kante verschiebt Ordnung + Level.
|
||||||
|
Idempotent (INSERT OR IGNORE); läuft nur in bauen, damit ein Bausteine-Reset ihn
|
||||||
|
ohne Re-Extraktion mitnimmt."""
|
||||||
|
topic = ctx.topic
|
||||||
|
atome = aktive_atome(topic)
|
||||||
|
offen = _braucht_unaufgeloest(atome)
|
||||||
|
if not offen:
|
||||||
|
return
|
||||||
|
angaben = [(a, titel, kand) for a, titel in offen
|
||||||
|
if (kand := _braucht_kandidaten(titel, atome, a["id"], BRAUCHT_KANDIDATEN))]
|
||||||
|
if not angaben:
|
||||||
|
log.info("braucht-Fallback: %d unaufgelöste Titel, keine Kandidaten", len(offen))
|
||||||
|
return
|
||||||
|
geloest = 0
|
||||||
|
|
||||||
|
async def chunk_loesen(chunk: list) -> None:
|
||||||
|
nonlocal geloest
|
||||||
|
liste = "\n\n".join(
|
||||||
|
f"ANGABE {n}: Voraussetzung „{titel}“ (gebraucht von Atom {a['id']}"
|
||||||
|
f" „{a['titel']}“)\nKandidaten:\n"
|
||||||
|
+ "\n".join(f" - atom {k['id']}: {k['titel']} — {k['definition']}" for k in kand)
|
||||||
|
for n, (a, titel, kand) in enumerate(chunk, 1))
|
||||||
|
res = await llm.call(ctx, stage="braucht", template="Braucht-Aufloesung",
|
||||||
|
werte={"angaben": liste}, role="judge",
|
||||||
|
n=len(chunk), item=f"br{chunk[0][0]['id']}", erwartet=list)
|
||||||
|
wahl = {e.get("phrase"): e.get("atom") for e in res or [] if isinstance(e, dict)}
|
||||||
|
for n, (a, titel, kand) in enumerate(chunk, 1):
|
||||||
|
zid = wahl.get(n)
|
||||||
|
if zid in {k["id"] for k in kand} and zid != a["id"]:
|
||||||
|
db.execute("INSERT OR IGNORE INTO kanten(topic, von_atom, zu_atom, art)"
|
||||||
|
" VALUES(?,?,?,'braucht')", (topic, a["id"], zid))
|
||||||
|
geloest += 1
|
||||||
|
|
||||||
|
chunks = [angaben[i:i + PAAR_CHUNK] for i in range(0, len(angaben), PAAR_CHUNK)]
|
||||||
|
await llm.alle(chunk_loesen(c) for c in chunks)
|
||||||
|
log.info("braucht-Fallback: %d/%d Titel aufgelöst (%d ohne Kandidaten)",
|
||||||
|
geloest, len(offen), len(offen) - len(angaben))
|
||||||
|
|
||||||
|
|
||||||
# ── Soll-Zuordnung ────────────────────────────────────────────────────────────
|
# ── Soll-Zuordnung ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async def _soll_zuordnen(ctx: llm.Kontext, nur_offene: bool = True) -> None:
|
async def _soll_zuordnen(ctx: llm.Kontext, nur_offene: bool = True) -> None:
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ GEWICHTE = {
|
|||||||
"det_check": 0.5, "fix_offen": 1.5,
|
"det_check": 0.5, "fix_offen": 1.5,
|
||||||
"diagramm_parse_fehler": 3.0, "diagramm_ungegroundet": 3.0,
|
"diagramm_parse_fehler": 3.0, "diagramm_ungegroundet": 3.0,
|
||||||
"diagramm_unverifiziert": 0.5, "diagramm_marker_tot": 3.0,
|
"diagramm_unverifiziert": 0.5, "diagramm_marker_tot": 3.0,
|
||||||
|
"beispiel_marker_tot": 3.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import logging
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
import db
|
import db
|
||||||
|
import inventar
|
||||||
import llm
|
import llm
|
||||||
from config import (BAUSTEIN_MAX_ATOME, BAUSTEIN_MIN_ATOME, DURCHGANG,
|
from config import (BAUSTEIN_MAX_ATOME, BAUSTEIN_MIN_ATOME, DURCHGANG,
|
||||||
KAPITEL_BAUSTEINE, LEVEL_RANG, ZIELE_CHUNK_ATOME)
|
KAPITEL_BAUSTEINE, LEVEL_RANG, ZIELE_CHUNK_ATOME)
|
||||||
@@ -465,6 +466,8 @@ async def _kapitel_bilden(ctx: llm.Kontext) -> None:
|
|||||||
async def bauen(ctx: llm.Kontext) -> None:
|
async def bauen(ctx: llm.Kontext) -> None:
|
||||||
ctx.ebene = EBENE
|
ctx.ebene = EBENE
|
||||||
await _ziele_bilden(ctx)
|
await _ziele_bilden(ctx)
|
||||||
|
await inventar._braucht_fallback(ctx) # Kompositum↔Expansion-braucht-Titel nachziehen,
|
||||||
|
# bevor Zyklen/Level auf dem Graph rechnen
|
||||||
await _zyklen_brechen(ctx)
|
await _zyklen_brechen(ctx)
|
||||||
await _level_kalibrieren(ctx) # VOR dem Schnitt: Level bestimmt die Bausteine
|
await _level_kalibrieren(ctx) # VOR dem Schnitt: Level bestimmt die Bausteine
|
||||||
_level_konflikte_loesen(ctx.topic)
|
_level_konflikte_loesen(ctx.topic)
|
||||||
|
|||||||
@@ -46,14 +46,6 @@ function html(s) {
|
|||||||
return htmlCache.get(key)
|
return htmlCache.get(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// „Prüfe dich"-Antworten ebenfalls cachen — sonst rechnet jeder Scroll-Tick
|
|
||||||
// Markdown+KaTeX für alle sichtbaren Antworten neu (genau der Kostenpfad oben).
|
|
||||||
function htmlAntwort(s, fi) {
|
|
||||||
const key = `a|${s.baustein}|${fi}`
|
|
||||||
if (!htmlCache.has(key)) htmlCache.set(key, render(s.fragen[fi].antwort))
|
|
||||||
return htmlCache.get(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mermaid rendert asynchron ins DOM — nach jedem Einblenden (Scroll/Level/Ansicht)
|
// Mermaid rendert asynchron ins DOM — nach jedem Einblenden (Scroll/Level/Ansicht)
|
||||||
// die noch nicht verarbeiteten ```mermaid-Blöcke im sichtbaren Bereich rendern.
|
// die noch nicht verarbeiteten ```mermaid-Blöcke im sichtbaren Bereich rendern.
|
||||||
async function renderMermaid() {
|
async function renderMermaid() {
|
||||||
@@ -124,7 +116,7 @@ watch([level, ansicht, daten], () => {
|
|||||||
nextTick(() => { if (scrollEl.value) scrollEl.value.scrollTop = 0 })
|
nextTick(() => { if (scrollEl.value) scrollEl.value.scrollTop = 0 })
|
||||||
})
|
})
|
||||||
|
|
||||||
// Lesezeit des SICHTBAREN Umfangs: 150 Wörter/min (technischer Text)
|
// Lesezeit des SICHTBAREN Umfangs: 100 Wörter/min (technischer Text)
|
||||||
// + 5 s je Display-Formel (Medium ~265 wpm gilt nicht für Mathe)
|
// + 5 s je Display-Formel (Medium ~265 wpm gilt nicht für Mathe)
|
||||||
const stats = computed(() => {
|
const stats = computed(() => {
|
||||||
const je = anzeige.value.map((kap) => {
|
const je = anzeige.value.map((kap) => {
|
||||||
@@ -135,7 +127,7 @@ const stats = computed(() => {
|
|||||||
woerter += st.woerter
|
woerter += st.woerter
|
||||||
formeln += st.displayFormeln
|
formeln += st.displayFormeln
|
||||||
}
|
}
|
||||||
return { woerter, minuten: woerter / 150 + (formeln * 5) / 60 }
|
return { woerter, minuten: woerter / 100 + (formeln * 5) / 60 }
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
je,
|
je,
|
||||||
@@ -219,13 +211,6 @@ function onScroll(e) {
|
|||||||
<div v-for="s in kap.sections" :key="s.baustein" class="guide-section">
|
<div v-for="s in kap.sections" :key="s.baustein" class="guide-section">
|
||||||
<h3 style="margin-top: 0">{{ s.titel }}</h3>
|
<h3 style="margin-top: 0">{{ s.titel }}</h3>
|
||||||
<div :class="ansicht === 'kompakt' ? 'kompakt' : 'markdown'" v-html="html(s)"></div>
|
<div :class="ansicht === 'kompakt' ? 'kompakt' : 'markdown'" v-html="html(s)"></div>
|
||||||
<details v-if="ansicht === 'erklaerend' && s.fragen?.length" class="antworten">
|
|
||||||
<summary>Antworten zu „Prüfe dich"</summary>
|
|
||||||
<div v-for="(f, fi) in s.fragen" :key="fi" class="antwort">
|
|
||||||
<div class="frage">{{ f.frage }}</div>
|
|
||||||
<div v-html="htmlAntwort(s, fi)"></div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="kapitel-platzhalter" :style="{ height: hoehe(i) + 'px' }"></div>
|
<div v-else class="kapitel-platzhalter" :style="{ height: hoehe(i) + 'px' }"></div>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const MARKER = /<!--\s*atom:.*?-->/g
|
|||||||
export function lesestat(text) {
|
export function lesestat(text) {
|
||||||
let displayFormeln = 0
|
let displayFormeln = 0
|
||||||
const t = (text || '')
|
const t = (text || '')
|
||||||
.replace(/```mermaid[\s\S]*?```/g, ' ') // Diagramm-Quelltext zählt nicht als Text
|
.replace(/```[\s\S]*?```/g, ' ') // Code-/Diagramm-Fences zählen nicht als Text
|
||||||
.replace(MARKER, ' ')
|
.replace(MARKER, ' ')
|
||||||
.replace(/\$\$[\s\S]+?\$\$/g, () => { displayFormeln += 1; return ' ' })
|
.replace(/\$\$[\s\S]+?\$\$/g, () => { displayFormeln += 1; return ' ' })
|
||||||
.replace(/\\\[[\s\S]+?\\\]/g, () => { displayFormeln += 1; return ' ' })
|
.replace(/\\\[[\s\S]+?\\\]/g, () => { displayFormeln += 1; return ' ' })
|
||||||
|
|||||||
@@ -232,8 +232,10 @@ body.nav-zu .hauptbereich { padding-top: 38px; }
|
|||||||
.markdown blockquote p { margin: 4px 0; }
|
.markdown blockquote p { margin: 4px 0; }
|
||||||
.markdown :is(code):not(.hljs) { background: var(--karte); padding: 1px 5px; border-radius: 4px; }
|
.markdown :is(code):not(.hljs) { background: var(--karte); padding: 1px 5px; border-radius: 4px; }
|
||||||
.markdown pre { background: var(--code-bg); padding: 10px; border-radius: 6px; overflow-x: auto; }
|
.markdown pre { background: var(--code-bg); padding: 10px; border-radius: 6px; overflow-x: auto; }
|
||||||
.markdown table { border-collapse: collapse; }
|
.markdown table { border-collapse: collapse; display: block; overflow-x: auto; max-width: 100%; }
|
||||||
.markdown td, .markdown th { border: 1px solid var(--rand); padding: 4px 10px; }
|
.markdown td, .markdown th { border: 1px solid var(--rand); padding: 4px 10px; }
|
||||||
|
.markdown thead th { background: var(--karte); text-align: left; }
|
||||||
|
.markdown tbody tr:nth-child(even) { background: color-mix(in srgb, var(--karte) 45%, transparent); }
|
||||||
|
|
||||||
.flash {
|
.flash {
|
||||||
background: var(--panel); border: 1px solid var(--rand); border-radius: 10px;
|
background: var(--panel); border: 1px solid var(--rand); border-radius: 10px;
|
||||||
|
|||||||
@@ -12,5 +12,8 @@ BELEGE (einzige erlaubte Faktenbasis — nichts erfinden):
|
|||||||
|
|
||||||
Antworte NUR mit JSON (keine Code-Fences). Für flashcard:
|
Antworte NUR mit JSON (keine Code-Fences). Für flashcard:
|
||||||
{{"frage": "…", "antwort": "…"}}
|
{{"frage": "…", "antwort": "…"}}
|
||||||
Für beispiel:
|
Für beispiel: BEHALTE die Form aus dem Artefakt oben und gib die passenden Felder zurück:
|
||||||
{{"text": "…"}}
|
- form "text"/"mathe": {{"text": "…"}}
|
||||||
|
- form "code": {{"sprache": "python", "code": "…"}}
|
||||||
|
- form "tabelle": {{"tabelle": "| … | … |\n|---|---|\n| … | … |"}}
|
||||||
|
- form "diagramm": {{"spec": {{"knoten": […], "kanten": […]}}}}
|
||||||
|
|||||||
@@ -4,10 +4,21 @@ Unten Atome eines Lernthemas (Titel, Definition, Beleg-Zitate).
|
|||||||
Erzeuge pro Atom:
|
Erzeuge pro Atom:
|
||||||
- 2 Flashcards (typ "flashcard"): eine Frage + präzise Antwort. Die Antwort stützt sich
|
- 2 Flashcards (typ "flashcard"): eine Frage + präzise Antwort. Die Antwort stützt sich
|
||||||
NUR auf Definition und Belege — nichts erfinden, nicht neu recherchieren.
|
NUR auf Definition und Belege — nichts erfinden, nicht neu recherchieren.
|
||||||
- 1 Worked Example (typ "beispiel", Feld "text"): ein EINFACHES durchgerechnetes
|
- Ggf. 1 Worked Example (typ "beispiel"): die kleinste Instanz, die das Atom klärt.
|
||||||
Beispiel — die kleinste Instanz, die das Atom klärt. Keine mehrstufigen oder
|
Alle Werte/Aussagen aus den Belegen ableitbar. Concreteness Fading: erst konkret,
|
||||||
verschachtelten Konstruktionen. Alle Werte/Aussagen müssen aus den Belegen ableitbar
|
dann kurz verallgemeinern. Auswahl EINDEUTIG und MINIMAL — keine Oberflächen-
|
||||||
sein. Wenn die Belege KEIN sinnvolles einfaches Beispiel hergeben, weglassen.
|
Überlappung zwischen Aufgabe und Lösung (sonst Fehlvorstellungen).
|
||||||
|
|
||||||
|
OB und WELCHE Form richtet sich nach dem Atom-Typ [in eckigen Klammern]:
|
||||||
|
- [verfahren] → FAST IMMER ein Beispiel. Form "code" (Felder "sprache" ∈
|
||||||
|
{{python, javascript}}, "code") bei Programmier-/Algorithmus-Konzepten, sonst
|
||||||
|
"text" als durchgerechneter Schritt-für-Schritt-Ablauf.
|
||||||
|
- [begriff] → NUR wenn abstrakt/schwer greifbar. Dann "text"/"mathe" (konkrete
|
||||||
|
Instanz) oder "tabelle" (Fälle/Vergleich). Simpler, selbsterklärender Begriff → KEIN Beispiel.
|
||||||
|
- [aussage] → NUR wenn nicht trivial. Dann "mathe"/"text" (Anwendung an Mini-Instanz)
|
||||||
|
oder "diagramm" (Feld "spec": {{knoten:[…], kanten:[…]}}) für Automat/Ablauf/Baum.
|
||||||
|
Nur EINE Form. Im Zweifel WEGLASSEN — ein schwaches/redundantes Beispiel schadet
|
||||||
|
(Forschung: die Auswahl ist kritisch). Code selbsterklärend, ohne I/O-Tricks.
|
||||||
|
|
||||||
Regeln:
|
Regeln:
|
||||||
- Atom-ids wörtlich übernehmen, keine erfinden, keine Atome auslassen (außer Beispiel s. o.).
|
- Atom-ids wörtlich übernehmen, keine erfinden, keine Atome auslassen (außer Beispiel s. o.).
|
||||||
@@ -23,4 +34,4 @@ ATOME:
|
|||||||
|
|
||||||
Antworte NUR mit JSON (keine Code-Fences, keine Prosa, KEINE Datei):
|
Antworte NUR mit JSON (keine Code-Fences, keine Prosa, KEINE Datei):
|
||||||
[{{"atom": 12, "typ": "flashcard", "frage": "…", "antwort": "…"}},
|
[{{"atom": 12, "typ": "flashcard", "frage": "…", "antwort": "…"}},
|
||||||
{{"atom": 12, "typ": "beispiel", "text": "…"}}]
|
{{"atom": 12, "typ": "beispiel", "form": "text", "text": "…"}}]
|
||||||
|
|||||||
23
templates/Braucht-Aufloesung.md
Normal file
23
templates/Braucht-Aufloesung.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<!-- template:Braucht-Aufloesung -->
|
||||||
|
Unten Voraussetzungs-Angaben aus Lern-Atomen. Jede nennt eine PHRASE (ein
|
||||||
|
Konzept, das ein Atom voraussetzt) und eine Liste KANDIDATEN-Atome (id, Titel,
|
||||||
|
Definition), die dieses Konzept meinen KÖNNTEN.
|
||||||
|
|
||||||
|
Entscheide je Angabe: Welches Kandidaten-Atom DEFINIERT genau das Konzept, das die
|
||||||
|
Phrase bezeichnet? Antworte mit dessen `atom`-id — oder `null`, wenn keines eindeutig
|
||||||
|
passt.
|
||||||
|
|
||||||
|
Regeln:
|
||||||
|
- Es geht um KONZEPT-Gleichheit, nicht Wortgleichheit. „Approximationsalgorithmus"
|
||||||
|
(Kompositum) und „Approximativer Algorithmus" (Phrase) sind dasselbe Konzept.
|
||||||
|
- Wähle das Atom, das die Phrase als eigenen GEGENSTAND definiert — nicht ein Atom,
|
||||||
|
das sie nur benutzt oder einen Spezialfall behandelt.
|
||||||
|
- Genau EIN Treffer je Phrase. Passt keiner klar, oder passen mehrere gleich gut →
|
||||||
|
`null`. Eine falsche Kante verschiebt Reihenfolge und Level; im Zweifel `null`.
|
||||||
|
- Antworte für JEDE Angabe.
|
||||||
|
|
||||||
|
ANGABEN:
|
||||||
|
{angaben}
|
||||||
|
|
||||||
|
Antworte NUR mit JSON (keine Code-Fences):
|
||||||
|
[{{"phrase": 1, "atom": 2065}}, {{"phrase": 2, "atom": null}}]
|
||||||
@@ -16,13 +16,18 @@ SECTION (lang):
|
|||||||
Regeln:
|
Regeln:
|
||||||
- Alle Marker-Zeilen (<!-- atom: … -->) EXAKT erhalten bzw. fehlende exakt einfügen —
|
- Alle Marker-Zeilen (<!-- atom: … -->) EXAKT erhalten bzw. fehlende exakt einfügen —
|
||||||
je Atom genau eine, unsichtbar, keine Überschrift.
|
je Atom genau eine, unsichtbar, keine Überschrift.
|
||||||
- Struktur erhalten: `####`-Zwischenüberschriften, „**Kernpunkte:**"- und
|
- Struktur erhalten: `####`-Zwischenüberschriften; andere Überschriften-Ebenen
|
||||||
„**Prüfe dich:**"-Block am Ende; andere Überschriften-Ebenen bleiben verboten.
|
bleiben verboten. Kein Kernpunkte-/Prüfe-dich-Block.
|
||||||
- Mathe in KaTeX (`$…$` / `$$…$$`) setzen bzw. belassen.
|
- Mathe in KaTeX (`$…$` / `$$…$$`) setzen bzw. belassen.
|
||||||
- Nur die beauftragten Stellen ändern; den Rest wörtlich erhalten.
|
- Nur die beauftragten Stellen ändern; den Rest wörtlich erhalten.
|
||||||
- Frei und didaktisch formulieren (Deutsch); jede Zahl/Behauptung muss aus der
|
- Frei und didaktisch formulieren (Deutsch); jede Zahl/Behauptung muss aus der
|
||||||
Faktenbasis ableitbar sein. Keine Aufgabennummern/Punktzahlen/„Zeigen Sie…".
|
Faktenbasis ableitbar sein. Keine Aufgabennummern/Punktzahlen/„Zeigen Sie…".
|
||||||
- Nichts erfinden; Korrekturen ausschließlich aus der Faktenbasis.
|
- Nichts erfinden; Korrekturen ausschließlich aus der Faktenbasis.
|
||||||
|
|
||||||
Antworte NUR mit JSON (keine Code-Fences):
|
Antworte in GENAU ZWEI Blöcken mit diesen Trennern (KEIN JSON, keine Code-Fences).
|
||||||
{{"kompakt": "…", "lang": "…"}}
|
LaTeX-Backslashes bleiben roh (kein Escaping):
|
||||||
|
===LANG===
|
||||||
|
<der korrigierte Fließtext>
|
||||||
|
===KOMPAKT===
|
||||||
|
<die korrigierten Stichpunkte>
|
||||||
|
=====
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ BEREITS BEHANDELT (frühere Durchgänge — NICHT neu erklären, nur bei Bedarf
|
|||||||
einem Halbsatz anknüpfen; dafür KEINE Marker setzen):
|
einem Halbsatz anknüpfen; dafür KEINE Marker setzen):
|
||||||
{kontext}
|
{kontext}
|
||||||
|
|
||||||
Unten die Atome mit Marker, Definition, Belegen, ggf. Beispiel und Fragen-Pool.
|
Unten die Atome mit Marker, Definition, Belegen und ggf. Beispiel.
|
||||||
|
|
||||||
Regeln (hart):
|
Regeln (hart):
|
||||||
1. LEHREN, NICHT ABSCHREIBEN: Formuliere frei und didaktisch in eigenen Worten —
|
1. LEHREN, NICHT ABSCHREIBEN: Formuliere frei und didaktisch in eigenen Worten —
|
||||||
@@ -38,9 +38,8 @@ Regeln (hart):
|
|||||||
mehrstufige Beispiele weglassen oder auf ihren Kern vereinfachen.
|
mehrstufige Beispiele weglassen oder auf ihren Kern vereinfachen.
|
||||||
- Kohäsion: Jeder Absatz knüpft explizit an den vorigen an (Konnektor oder
|
- Kohäsion: Jeder Absatz knüpft explizit an den vorigen an (Konnektor oder
|
||||||
Übergangssatz). Fachbegriffe konsistent wiederverwenden — keine Synonym-Variation.
|
Übergangssatz). Fachbegriffe konsistent wiederverwenden — keine Synonym-Variation.
|
||||||
- Abschluss: „**Kernpunkte:**" mit 3–5 Stichpunkten (nur Verdichtung, KEINE neuen
|
- Kein Zusammenfassungs- oder Frageblock am Ende — die Section endet mit dem
|
||||||
Formulierungen) und „**Prüfe dich:**" mit genau 2 Fragen aus dem FRAGEN-POOL
|
letzten inhaltlichen Absatz.
|
||||||
(wörtlich übernehmen, ohne Antworten).
|
|
||||||
4. MATHE: Mathematische Ausdrücke in KaTeX setzen: inline `$…$`, abgesetzt `$$…$$`
|
4. MATHE: Mathematische Ausdrücke in KaTeX setzen: inline `$…$`, abgesetzt `$$…$$`
|
||||||
(z. B. `$0^{{2n}}1^{{2n}}$` statt roher Notation). NIE nackte LaTeX-Befehle
|
(z. B. `$0^{{2n}}1^{{2n}}$` statt roher Notation). NIE nackte LaTeX-Befehle
|
||||||
ohne Delimiter in den Fließtext (`\leq`, `\text{{…}}`, Mengenklammern brauchen
|
ohne Delimiter in den Fließtext (`\leq`, `\text{{…}}`, Mengenklammern brauchen
|
||||||
@@ -55,6 +54,11 @@ Regeln (hart):
|
|||||||
auf schmalen Bildschirmen aus dem Text.
|
auf schmalen Bildschirmen aus dem Text.
|
||||||
PDF-Artefakte aus den Belegen in saubere Notation übersetzen: „u 6= v" ist
|
PDF-Artefakte aus den Belegen in saubere Notation übersetzen: „u 6= v" ist
|
||||||
$u \neq v$, „V 0" ist $V'$ — du lehrst, du zitierst nicht.
|
$u \neq v$, „V 0" ist $V'$ — du lehrst, du zitierst nicht.
|
||||||
|
JEDE Variable/jedes Symbol bei ERSTER Nutzung erklären (was ist `$m$`? etwa
|
||||||
|
„die Anzahl der Maschinen"). Konkrete Ergebnisse mit Variablen aus anderen Atomen
|
||||||
|
(z. B. eine Schranke $2-1/m$) nur zitieren, wenn deren Setting UND Variablen hier
|
||||||
|
eingeführt sind — sonst weglassen oder das Setting zuerst in einem Halbsatz nennen.
|
||||||
|
Kein nacktes Symbol, das der Leser dieses Durchgangs nicht kennt.
|
||||||
5. LÄNGE: „lang" hat {min_woerter}–{max_woerter} Wörter. Die Obergrenze ist ein HARTES
|
5. LÄNGE: „lang" hat {min_woerter}–{max_woerter} Wörter. Die Obergrenze ist ein HARTES
|
||||||
Limit, kein Zielwert. Minimal schlägt ausführlich: jeder Satz zahlt aufs Lernziel
|
Limit, kein Zielwert. Minimal schlägt ausführlich: jeder Satz zahlt aufs Lernziel
|
||||||
ein, kein Fülltext, keine Wiederholungen.
|
ein, kein Fülltext, keine Wiederholungen.
|
||||||
@@ -63,17 +67,17 @@ Regeln (hart):
|
|||||||
7. Keine Vorgriffe auf Stoff, der nicht in den Atomen unten steht — auch nicht
|
7. Keine Vorgriffe auf Stoff, der nicht in den Atomen unten steht — auch nicht
|
||||||
auf spätere Durchgänge. Unbekannte Begriffe beim ersten Auftreten kurz mit
|
auf spätere Durchgänge. Unbekannte Begriffe beim ersten Auftreten kurz mit
|
||||||
Bekanntem einordnen, nie vertrösten („dazu später mehr" ist VERBOTEN).
|
Bekanntem einordnen, nie vertrösten („dazu später mehr" ist VERBOTEN).
|
||||||
8. DIAGRAMME: Unten stehen fertige Diagramme (Marker + Beschreibung). Setze den
|
(Code-/Tabellen-/Diagramm-Beispiele werden NACH dir automatisch beim passenden
|
||||||
passenden `<!-- diagramm: id -->`-Marker in eine EIGENE Zeile direkt bei dem
|
Atom eingefügt — du musst KEINE Beispiel-Marker setzen. Text-Beispiele unter den
|
||||||
Absatz, der die gezeigte Relation einführt (Nähe zum Text). Nur die
|
Atomen webst du wie gehabt in die Prosa ein.)
|
||||||
angebotenen Marker verwenden, keine erfinden, jeden höchstens einmal. Passt
|
|
||||||
ein Diagramm nicht zum Fluss, lass seinen Marker weg. „(keine)" → ignorieren.
|
|
||||||
|
|
||||||
VERFÜGBARE DIAGRAMME:
|
|
||||||
{diagramme}
|
|
||||||
|
|
||||||
ATOME:
|
ATOME:
|
||||||
{atome}
|
{atome}
|
||||||
|
|
||||||
Antworte NUR mit JSON (keine Code-Fences, keine Prosa, KEINE Datei):
|
Antworte in GENAU ZWEI Blöcken mit diesen Trennern (KEIN JSON, keine Code-Fences,
|
||||||
{{"kompakt": "…", "lang": "…"}}
|
keine Datei). LaTeX-Backslashes bleiben roh (kein Escaping):
|
||||||
|
===LANG===
|
||||||
|
<der ausführliche Fließtext mit Markern und Formeln>
|
||||||
|
===KOMPAKT===
|
||||||
|
<die 2–3 Stichpunkte>
|
||||||
|
=====
|
||||||
|
|||||||
@@ -79,3 +79,104 @@ async def test_guard_verwirft_kandidat_und_repair_kann_nachlegen():
|
|||||||
# verworfene zählen nicht als versorgt: Repair generiert für das Atom nach
|
# verworfene zählen nicht als versorgt: Repair generiert für das Atom nach
|
||||||
chunks = artefakte._chunks(topic, nur_ohne=True)
|
chunks = artefakte._chunks(topic, nur_ohne=True)
|
||||||
assert [x["id"] for c in chunks for x in c] == [a]
|
assert [x["id"] for c in chunks for x in c] == [a]
|
||||||
|
|
||||||
|
|
||||||
|
def test_tabelle_struktur_check():
|
||||||
|
# wohlgeformt
|
||||||
|
ok = "| A | B |\n|---|---|\n| 1 | 2 |"
|
||||||
|
assert artefakte._tabelle_ok(ok)
|
||||||
|
# keine Datenzeile / kein Trenner / inkonsistente Spalten
|
||||||
|
assert not artefakte._tabelle_ok("| A | B |\n|---|---|")
|
||||||
|
assert not artefakte._tabelle_ok("A B\n1 2\n3 4")
|
||||||
|
assert not artefakte._tabelle_ok("| A | B |\n|---|---|\n| 1 |")
|
||||||
|
|
||||||
|
|
||||||
|
def test_referenziert_quelle_ignoriert_code():
|
||||||
|
# „aufgabe 3" in Code/Kommentar darf NICHT als Quellen-Referenz matchen
|
||||||
|
inhalt = {"form": "code", "text": "", "code": "# aufgabe 3: sum\nprint(sum([1,2]))",
|
||||||
|
"sprache": "python", "tabelle": "", "mermaid": ""}
|
||||||
|
assert not artefakte._referenziert_quelle(inhalt)
|
||||||
|
# in der Prosa (text) matcht es weiterhin
|
||||||
|
assert artefakte._referenziert_quelle({"text": "siehe aufgabe 3", "frage": "", "antwort": ""})
|
||||||
|
|
||||||
|
|
||||||
|
def test_formen_gate_verwirft_kaputte_tabelle():
|
||||||
|
topic = topic_anlegen("formgate")
|
||||||
|
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
|
||||||
|
status="neu", braucht=db.j([]))
|
||||||
|
gut = db.insert("artefakte", atom_id=a, typ="beispiel", status="kandidat",
|
||||||
|
inhalt=db.j({"form": "tabelle", "tabelle": "| A | B |\n|---|---|\n| 1 | 2 |"}))
|
||||||
|
schlecht = db.insert("artefakte", atom_id=a, typ="beispiel", status="kandidat",
|
||||||
|
inhalt=db.j({"form": "tabelle", "tabelle": "kaputt ohne pipes"}))
|
||||||
|
artefakte._formen_gate(topic)
|
||||||
|
assert db.one("SELECT status FROM artefakte WHERE id=?", (gut,))["status"] == "kandidat"
|
||||||
|
assert db.one("SELECT status FROM artefakte WHERE id=?", (schlecht,))["status"] == "verworfen"
|
||||||
|
|
||||||
|
|
||||||
|
def test_beispiel_einsetzen_tabelle():
|
||||||
|
import guide
|
||||||
|
topic = topic_anlegen("bsp-tab")
|
||||||
|
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([]))
|
||||||
|
md = "| A | B |\n|---|---|\n| 1 | 2 |"
|
||||||
|
art = db.insert("artefakte", atom_id=a, typ="beispiel", status="verifiziert",
|
||||||
|
inhalt=db.j({"form": "tabelle", "tabelle": md}))
|
||||||
|
lang = f"Text.\n\n<!-- beispiel: {art} -->\n\nMehr."
|
||||||
|
out = guide._beispiel_einsetzen(b, lang)
|
||||||
|
assert md in out and "<!-- beispiel:" not in out
|
||||||
|
# toter Marker verschwindet
|
||||||
|
assert "<!-- beispiel:" not in guide._beispiel_einsetzen(b, "x\n<!-- beispiel: 999 -->\ny")
|
||||||
|
|
||||||
|
|
||||||
|
def test_code_syntax_gate():
|
||||||
|
assert artefakte._code_ok("python", "x = sum([1, 2])\nprint(x)")
|
||||||
|
assert not artefakte._code_ok("python", "def f(: pass") # SyntaxError
|
||||||
|
assert artefakte._code_ok("javascript", "const x = [1,2].map(v => v*2)")
|
||||||
|
assert not artefakte._code_ok("javascript", "const x = ;;;(")
|
||||||
|
assert not artefakte._code_ok("python", "") # leer
|
||||||
|
assert artefakte._code_ok("ruby", "puts 1") # unbekannt → kein Gate
|
||||||
|
|
||||||
|
|
||||||
|
def test_formen_gate_verwirft_kaputten_code():
|
||||||
|
topic = topic_anlegen("codegate")
|
||||||
|
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
|
||||||
|
status="neu", braucht=db.j([]))
|
||||||
|
gut = db.insert("artefakte", atom_id=a, typ="beispiel", status="kandidat",
|
||||||
|
inhalt=db.j({"form": "code", "sprache": "python", "code": "print(1+1)"}))
|
||||||
|
schlecht = db.insert("artefakte", atom_id=a, typ="beispiel", status="kandidat",
|
||||||
|
inhalt=db.j({"form": "code", "sprache": "python", "code": "def f(: pass"}))
|
||||||
|
artefakte._formen_gate(topic)
|
||||||
|
assert db.one("SELECT status FROM artefakte WHERE id=?", (gut,))["status"] == "kandidat"
|
||||||
|
assert db.one("SELECT status FROM artefakte WHERE id=?", (schlecht,))["status"] == "verworfen"
|
||||||
|
|
||||||
|
|
||||||
|
def test_formen_gate_diagramm_beispiel():
|
||||||
|
topic = topic_anlegen("diagbsp")
|
||||||
|
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
|
||||||
|
status="neu", braucht=db.j([]))
|
||||||
|
spec = {"knoten": [{"id": "q0", "label": "Start"}, {"id": "q1", "label": "Ende"}],
|
||||||
|
"kanten": [{"von": "q0", "zu": "q1", "label": "a"}]}
|
||||||
|
d = db.insert("artefakte", atom_id=a, typ="beispiel", status="kandidat",
|
||||||
|
inhalt=db.j({"form": "diagramm", "spec": spec}))
|
||||||
|
leer = db.insert("artefakte", atom_id=a, typ="beispiel", status="kandidat",
|
||||||
|
inhalt=db.j({"form": "diagramm", "spec": {"knoten": []}}))
|
||||||
|
artefakte._formen_gate(topic)
|
||||||
|
d_row = db.one("SELECT status, inhalt FROM artefakte WHERE id=?", (d,))
|
||||||
|
assert d_row["status"] == "kandidat"
|
||||||
|
assert db.uj(d_row["inhalt"])["mermaid"].startswith("flowchart") # kompiliert
|
||||||
|
assert db.one("SELECT status FROM artefakte WHERE id=?", (leer,))["status"] == "verworfen"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_guide_messen_toter_beispiel_marker():
|
||||||
|
import guide
|
||||||
|
topic = topic_anlegen("bspqa")
|
||||||
|
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([]))
|
||||||
|
db.insert("lernziele", topic=topic, id=1, text="Z", status="aktiv")
|
||||||
|
db.insert("sections", baustein_id=b, stage="done", text_kompakt="- k",
|
||||||
|
text_lang=f"<!-- atom: {a} | X -->\n<!-- beispiel: 777 -->\n" + "Wort " * 60)
|
||||||
|
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
|
||||||
|
ctx.ebene = "guide"
|
||||||
|
assert "beispiel_marker_tot" in {x["art"] for x in guide.messen(ctx)}
|
||||||
|
|||||||
@@ -333,22 +333,6 @@ def test_stil_kein_fehlalarm_deutsch():
|
|||||||
assert not any("englische Passage" in x for x in guide._stil_auftraege(text, "Langtext"))
|
assert not any("englische Passage" in x for x in guide._stil_auftraege(text, "Langtext"))
|
||||||
|
|
||||||
|
|
||||||
def test_pruefe_dich_norm_match():
|
|
||||||
"""Frage-Matching ist Case/Whitespace-tolerant (Fix darf umformatieren)."""
|
|
||||||
import db
|
|
||||||
import guide
|
|
||||||
from conftest import topic_anlegen
|
|
||||||
topic = topic_anlegen("pruefe")
|
|
||||||
ziel = db.insert("lernziele", topic=topic, text="Z", status="aktiv")
|
|
||||||
b_id = db.insert("bausteine", topic=topic, ziel_id=ziel, titel="B", ord=0, status="neu")
|
|
||||||
a = db.insert("atome", topic=topic, titel="A", typ="begriff", definition="d",
|
|
||||||
status="neu", baustein_id=b_id, braucht=db.j([]))
|
|
||||||
db.insert("artefakte", atom_id=a, typ="flashcard", status="verifiziert",
|
|
||||||
inhalt=db.j({"frage": "Was ist ein Graph?", "antwort": "Knoten und Kanten."}))
|
|
||||||
fragen = guide._pruefe_dich(b_id, "Prüfe dich: Was ist ein Graph?")
|
|
||||||
assert len(fragen) == 1 and fragen[0]["antwort"] == "Knoten und Kanten."
|
|
||||||
|
|
||||||
|
|
||||||
def test_katex_gate_und_markdown_hygiene(monkeypatch):
|
def test_katex_gate_und_markdown_hygiene(monkeypatch):
|
||||||
import guide
|
import guide
|
||||||
monkeypatch.delenv("CREATOR_FAKE_AGENTS", raising=False) # Gate echt laufen lassen
|
monkeypatch.delenv("CREATOR_FAKE_AGENTS", raising=False) # Gate echt laufen lassen
|
||||||
@@ -365,3 +349,42 @@ def test_katex_gate_und_markdown_hygiene(monkeypatch):
|
|||||||
assert "literales" in auftraege
|
assert "literales" in auftraege
|
||||||
lang_formel = "Es gilt $" + "x + ".join(["y"] * 30) + "$ hier."
|
lang_formel = "Es gilt $" + "x + ".join(["y"] * 30) + "$ hier."
|
||||||
assert "überlange Inline-Formel" in " ".join(guide._mathe_auftraege(lang_formel, "T"))
|
assert "überlange Inline-Formel" in " ".join(guide._mathe_auftraege(lang_formel, "T"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_split_writer_delimited_und_fallback():
|
||||||
|
import guide
|
||||||
|
# delimited: roher LaTeX-Backslash bleibt (kein JSON-Escaping)
|
||||||
|
k, l = guide._split_writer("===LANG===\nText $a \\leq b$\n===KOMPAKT===\n- p")
|
||||||
|
assert l == "Text $a \\leq b$" and k == "- p"
|
||||||
|
# umgekehrte Reihenfolge der Blöcke
|
||||||
|
k2, l2 = guide._split_writer("===KOMPAKT===\n- x\n===LANG===\nY")
|
||||||
|
assert l2 == "Y" and k2 == "- x"
|
||||||
|
# Fallback: altes JSON
|
||||||
|
k3, l3 = guide._split_writer('{"kompakt": "- a", "lang": "b"}')
|
||||||
|
assert l3 == "b" and k3 == "- a"
|
||||||
|
# Fallback: nackter Text → alles lang
|
||||||
|
assert guide._split_writer("nur text")[1] == "nur text"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fix_cap_friert_section_ein(monkeypatch):
|
||||||
|
"""Nach FIX_MAX_VERSUCHE erfolglosen Fixes (Text unverändert) wird die Section
|
||||||
|
für Stil-Befunde eingefroren → reparieren meldet bewegt=False (Stillstand)."""
|
||||||
|
import db
|
||||||
|
import fake_agents
|
||||||
|
import guide
|
||||||
|
import llm
|
||||||
|
from conftest import run_anlegen, topic_anlegen
|
||||||
|
topic = topic_anlegen("fixcap")
|
||||||
|
run = run_anlegen(topic)
|
||||||
|
ziel = db.insert("lernziele", topic=topic, text="Kann X", status="aktiv")
|
||||||
|
b = db.insert("bausteine", topic=topic, ziel_id=ziel, titel="B", ord=0, status="neu")
|
||||||
|
a = db.insert("atome", topic=topic, titel="A", typ="begriff", definition="d",
|
||||||
|
status="neu", baustein_id=b, braucht=db.j([]))
|
||||||
|
lang = f"<!-- atom: {a} | A -->\n" + "Wort " * 60
|
||||||
|
db.insert("sections", baustein_id=b, stage="fix", text_lang=lang, text_kompakt="- p",
|
||||||
|
befunde=db.j([]), fix_versuche=guide.FIX_MAX_VERSUCHE) # schon am Cap
|
||||||
|
ctx = llm.Kontext(run, topic, "minimax")
|
||||||
|
ctx.ebene = "guide"
|
||||||
|
bewegt = await guide.reparieren(
|
||||||
|
ctx, [{"art": "det_check", "item": str(b), "detail": "Kürzen"}])
|
||||||
|
assert bewegt is False # eingefroren → nicht geroutet → kein Fortschritt
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Ebene 3.5 Diagramme: deterministische DAG-Diagramme (kein LLM), Spec→Mermaid."""
|
"""Ebene 3.5 Diagramme: LLM-Diagramme (Automat/Ablauf/Baum), Spec→Mermaid, Assembly."""
|
||||||
|
|
||||||
import db
|
import db
|
||||||
import diagramme
|
import diagramme
|
||||||
@@ -21,7 +21,7 @@ def _baustein_mit_kette(topic, n=4):
|
|||||||
|
|
||||||
|
|
||||||
def test_spec_zu_mermaid_flowchart():
|
def test_spec_zu_mermaid_flowchart():
|
||||||
spec = {"typ": "dependency",
|
spec = {"typ": "flow",
|
||||||
"knoten": [{"id": "1", "label": "A"}, {"id": "2", "label": 'B "x" [y]'}],
|
"knoten": [{"id": "1", "label": "A"}, {"id": "2", "label": 'B "x" [y]'}],
|
||||||
"kanten": [{"von": "1", "zu": "2", "label": ""}]}
|
"kanten": [{"von": "1", "zu": "2", "label": ""}]}
|
||||||
out = diagramme._spec_zu_mermaid(spec)
|
out = diagramme._spec_zu_mermaid(spec)
|
||||||
@@ -30,25 +30,11 @@ def test_spec_zu_mermaid_flowchart():
|
|||||||
assert '"' not in out.split("\n", 1)[1].replace('["', "").replace('"]', "") # Label entschärft
|
assert '"' not in out.split("\n", 1)[1].replace('["', "").replace('"]', "") # Label entschärft
|
||||||
|
|
||||||
|
|
||||||
async def test_dag_diagramm_wird_gebaut_und_verifiziert():
|
async def test_fake_judge_nein_keine_diagramme():
|
||||||
topic = topic_anlegen("diag-dag")
|
|
||||||
b, atome = _baustein_mit_kette(topic, n=4) # 3 Kanten ≥ DIAGRAMM_MIN_KANTEN
|
|
||||||
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
|
|
||||||
await diagramme.bauen(ctx)
|
|
||||||
d = db.one("SELECT * FROM diagramme WHERE baustein_id=?", (b,))
|
|
||||||
assert d and d["status"] == "verifiziert" and d["quelle"] == "dag"
|
|
||||||
assert d["mermaid"].startswith("flowchart TD")
|
|
||||||
assert all(f"n{a}" in d["mermaid"] for a in atome) # jeder Knoten drin
|
|
||||||
assert diagramme.messen(ctx) == [] # keine offenen Kandidaten
|
|
||||||
assert diagramme.gate(ctx) is None
|
|
||||||
|
|
||||||
|
|
||||||
async def test_wenig_kanten_kein_diagramm():
|
|
||||||
topic = topic_anlegen("diag-leer")
|
topic = topic_anlegen("diag-leer")
|
||||||
b, _ = _baustein_mit_kette(topic, n=2) # nur 1 Kante < Schwelle
|
b, _ = _baustein_mit_kette(topic, n=2)
|
||||||
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
|
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
|
||||||
await diagramme.bauen(ctx)
|
await diagramme.bauen(ctx) # Fake-Judge sagt noetig=False → nur 'kein'-Merkzeile
|
||||||
# kein GEBAUTES Diagramm (Fake-Judge sagt noetig=False → nur eine 'kein'-Merkzeile)
|
|
||||||
assert db.query("SELECT id FROM diagramme WHERE topic=? AND status='verifiziert'",
|
assert db.query("SELECT id FROM diagramme WHERE topic=? AND status='verifiziert'",
|
||||||
(topic,)) == []
|
(topic,)) == []
|
||||||
assert db.one("SELECT status FROM diagramme WHERE baustein_id=?", (b,))["status"] == "kein"
|
assert db.one("SELECT status FROM diagramme WHERE baustein_id=?", (b,))["status"] == "kein"
|
||||||
@@ -97,12 +83,12 @@ async def test_marker_wird_zu_fence():
|
|||||||
"""Assembly: platzierter <!-- diagramm: id --> → ```mermaid-Fence."""
|
"""Assembly: platzierter <!-- diagramm: id --> → ```mermaid-Fence."""
|
||||||
import guide
|
import guide
|
||||||
topic = topic_anlegen("diag-place")
|
topic = topic_anlegen("diag-place")
|
||||||
b, _ = _baustein_mit_kette(topic, n=4)
|
b, _ = _baustein_mit_kette(topic, n=2)
|
||||||
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
|
mermaid = "flowchart TD\n n1[\"A\"]\n n2[\"B\"]\n n1 --> n2"
|
||||||
await diagramme.bauen(ctx)
|
d = db.insert("diagramme", topic=topic, baustein_id=b, typ="flow", quelle="llm",
|
||||||
d = db.one("SELECT id FROM diagramme WHERE baustein_id=?", (b,))
|
spec=db.j({}), mermaid=mermaid, status="verifiziert")
|
||||||
db.insert("sections", baustein_id=b, stage="done", text_kompakt="- k",
|
db.insert("sections", baustein_id=b, stage="done", text_kompakt="- k",
|
||||||
text_lang=f"Text.\n\n<!-- diagramm: {d['id']} -->\n\nMehr Text.")
|
text_lang=f"Text.\n\n<!-- diagramm: {d} -->\n\nMehr Text.")
|
||||||
lang = guide.kapitel_struktur(topic)[0]["sections"][0]["lang"]
|
lang = guide.kapitel_struktur(topic)[0]["sections"][0]["lang"]
|
||||||
assert "```mermaid" in lang and "flowchart TD" in lang
|
assert "```mermaid" in lang and "flowchart TD" in lang
|
||||||
assert "<!-- diagramm:" not in lang # Marker ersetzt
|
assert "<!-- diagramm:" not in lang # Marker ersetzt
|
||||||
@@ -128,3 +114,23 @@ async def test_guide_messen_meldet_toten_marker():
|
|||||||
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
|
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
|
||||||
ctx.ebene = "guide"
|
ctx.ebene = "guide"
|
||||||
assert "diagramm_marker_tot" in {x["art"] for x in guide.messen(ctx)}
|
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
|
||||||
|
|||||||
@@ -39,7 +39,11 @@ def _pruefe_endzustand(topic, run_id):
|
|||||||
assert "| E -->" not in md and "| M -->" not in md # Marker tragen kein Level mehr
|
assert "| E -->" not in md and "| M -->" not in md # Marker tragen kein Level mehr
|
||||||
# Kapitel-Struktur: H2 = Kapitel (Struktur-Ebene, mit Intro), H3 = Baustein
|
# Kapitel-Struktur: H2 = Kapitel (Struktur-Ebene, mit Intro), H3 = Baustein
|
||||||
assert "\n## " in "\n" + md and "### " in md
|
assert "\n## " in "\n" + md and "### " in md
|
||||||
assert "**Kernpunkte:**" in md and "**Prüfe dich:**" in md
|
assert "**Kernpunkte:**" not in md and "**Prüfe dich:**" not in md # Blöcke entfernt
|
||||||
|
# Typisiertes Beispiel: die Fake-Welt liefert Tabellen-Beispiele → verbatim im Guide,
|
||||||
|
# Marker durch die Assembly ersetzt (kein toter Beispiel-Marker übrig)
|
||||||
|
assert "| Fall | Ergebnis |" in md
|
||||||
|
assert "<!-- beispiel:" not in md
|
||||||
kaps = db.query("SELECT * FROM kapitel WHERE topic=? ORDER BY ord", (topic,))
|
kaps = db.query("SELECT * FROM kapitel WHERE topic=? ORDER BY ord", (topic,))
|
||||||
assert kaps, "keine Kapitel gebildet"
|
assert kaps, "keine Kapitel gebildet"
|
||||||
for k in kaps:
|
for k in kaps:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Struktur-Ebene: Topo-Sortierung, Zyklenbruch, Band-Schnitt — mit synthetischen Atomen."""
|
"""Struktur-Ebene: Topo-Sortierung, Zyklenbruch, Band-Schnitt — mit synthetischen Atomen."""
|
||||||
|
|
||||||
import db
|
import db
|
||||||
|
import inventar
|
||||||
import llm
|
import llm
|
||||||
import struktur
|
import struktur
|
||||||
from conftest import run_anlegen, topic_anlegen
|
from conftest import run_anlegen, topic_anlegen
|
||||||
@@ -224,6 +225,30 @@ def test_level_konflikt_absenkung():
|
|||||||
assert not [b for b in struktur.messen(ctx) if b["art"] == "level_konflikt"]
|
assert not [b for b in struktur.messen(ctx) if b["art"] == "level_konflikt"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_braucht_fallback_kompositum():
|
||||||
|
# braucht-Titel "Approximationsalgorithmus" (Kompositum) trifft weder norm noch
|
||||||
|
# titel_kern das Atom "Approximativer Algorithmus" (Phrase). Der Fallback-Judge
|
||||||
|
# legt die Kante; _level_konflikte_loesen senkt die Voraussetzung von M auf E.
|
||||||
|
topic = topic_anlegen("brfall")
|
||||||
|
run = run_anlegen(topic)
|
||||||
|
ziel = db.insert("lernziele", topic=topic, text="Kann X", status="aktiv")
|
||||||
|
a = db.insert("atome", topic=topic, titel="Absolute Güte", typ="begriff",
|
||||||
|
definition="d", level="E", status="neu", ziel_id=ziel,
|
||||||
|
braucht=db.j(["Approximationsalgorithmus"]))
|
||||||
|
b = db.insert("atome", topic=topic, titel="Approximativer Algorithmus", typ="begriff",
|
||||||
|
definition="d", level="M", status="neu", ziel_id=ziel, braucht=db.j([]))
|
||||||
|
ctx = llm.Kontext(run, topic, "minimax")
|
||||||
|
ctx.ebene = "struktur"
|
||||||
|
inventar._kanten_aufloesen(topic) # deterministisch: verfehlt das Kompositum
|
||||||
|
assert db.query("SELECT * FROM kanten WHERE topic=? AND von_atom=?", (topic, a)) == []
|
||||||
|
await inventar._braucht_fallback(ctx)
|
||||||
|
kante = db.one("SELECT * FROM kanten WHERE topic=? AND von_atom=? AND zu_atom=?",
|
||||||
|
(topic, a, b))
|
||||||
|
assert kante and kante["art"] == "braucht"
|
||||||
|
struktur._level_konflikte_loesen(topic)
|
||||||
|
assert db.one("SELECT level FROM atome WHERE id=?", (b,))["level"] == "E"
|
||||||
|
|
||||||
|
|
||||||
async def test_level_split_ordnung_kapitel():
|
async def test_level_split_ordnung_kapitel():
|
||||||
# Ein Ziel mit 4×E + 4×M → zwei Bausteine mit level, E-ord < M-ord,
|
# Ein Ziel mit 4×E + 4×M → zwei Bausteine mit level, E-ord < M-ord,
|
||||||
# Kapitel je Durchgang, kein level_mix/kapitel_level
|
# Kapitel je Durchgang, kein level_mix/kapitel_level
|
||||||
|
|||||||
Reference in New Issue
Block a user