This commit is contained in:
team3
2026-07-13 01:45:45 +02:00
parent 8cedcc87ec
commit 698bba6ccf
8 changed files with 82 additions and 31 deletions

View File

@@ -248,12 +248,17 @@ def _beispiel_pruefen(inh: dict) -> tuple[bool, dict]:
return True, inh
def _formen_gate(topic: str) -> None:
def _formen_gate(topic: str, atom_ids: list[int] | None = None) -> 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,)):
(fail-closed für den Inhalt; ein fehlendes Beispiel ist erlaubt, optional).
atom_ids: optional auf einen Chunk beschränken (Pipeline pro Kette)."""
sql = ("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'")
params: tuple = (topic,)
if atom_ids:
sql += f" AND ar.atom_id IN ({','.join('?' * len(atom_ids))})"
params += tuple(atom_ids)
for b in db.query(sql, params):
inh = db.uj(b["inhalt"], {})
ok, inh_neu = _beispiel_pruefen(inh)
if not ok:
@@ -262,13 +267,22 @@ def _formen_gate(topic: str) -> None:
db.update("artefakte", "id", b["id"], inhalt=db.j(inh_neu))
async def _kette(ctx: llm.Kontext, chunk: list[dict]) -> None:
"""gen→gate→verify EINES Chunks verkettet — der Verify braucht nur die eigenen
Kandidaten, nicht die Generierung fremder Chunks (Stage-Barriere kostete
Wall-Clock: 1 Nachzügler blockierte alle Verifies)."""
ohne = [a for a in chunk if not db.one(
"SELECT id FROM artefakte WHERE atom_id=? AND typ='flashcard'"
" AND status!='verworfen' LIMIT 1", (a["id"],))]
if ohne:
await _generieren(ctx, ohne)
_formen_gate(ctx.topic, [a["id"] for a in chunk])
await _verifizieren(ctx, chunk)
async def bauen(ctx: llm.Kontext) -> None:
ctx.ebene = EBENE
neu = _chunks(ctx.topic, nur_ohne=True)
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)
await llm.alle(_verifizieren(ctx, c) for c in alle)
await llm.alle(_kette(ctx, c) for c in _chunks(ctx.topic, nur_ohne=False))
def messen(ctx: llm.Kontext) -> list[dict]:

View File

@@ -137,6 +137,10 @@ def _init_schema(con: sqlite3.Connection) -> None:
con.execute(zusatz)
except sqlite3.OperationalError:
pass # Spalte existiert schon
# Status-Marker entfernter Ebenen (diagramme/fehlersuche) zurückrollen — sonst
# kennt _ORDNUNG den Status nicht und der Resume fiele fälschlich auf korpus zurück.
con.execute("UPDATE topics SET status='struktur_fertig'"
" WHERE status IN ('diagramme_fertig', 'fehlersuche_fertig')")
con.commit()

View File

@@ -560,8 +560,8 @@ async def bauen(ctx: llm.Kontext) -> None:
offen = [b for b in _bausteine(ctx.topic) if b["status"] != "fertig"
or (db.one("SELECT stage FROM sections WHERE baustein_id=?", (b["id"],)) or
{"stage": "writer"})["stage"] != "done"]
await llm.alle(_karte(ctx, b) for b in offen)
await _kapitel_intros(ctx)
# Intros brauchen nur Kapitel+Ziele (keine Sections) → parallel zu den Karten
await llm.alle([*(_karte(ctx, b) for b in offen), _kapitel_intros(ctx)])
def kapitel_struktur(topic: str) -> list[dict]:

View File

@@ -6,6 +6,24 @@ import re
_FENCE = re.compile(r"```(?:json)?\s*(.*?)```", re.DOTALL)
# LaTeX in JSON-Strings ("$\in$", "\Sigma") ist ein INVALIDES Escape → json.loads
# wirft (kostete bis zu 29 % Parse-Fehler bei Mathe-Stages). Nur ungültige Escapes
# verdoppeln: gültige (\" \\ \/ \b \f \n \r \t \uXXXX) bleiben; \u ohne 4
# Hex-Ziffern (\underline) zählt als ungültig.
_UNGUELTIGES_ESCAPE = re.compile(r'\\(?![\"\\/bfnrt]|u[0-9a-fA-F]{4})')
def _loads(s: str):
"""json.loads mit EINER Reparaturstufe für rohe LaTeX-Backslashes. Valides
JSON durchläuft unverändert (Reparatur nur im Fehlerfall)."""
try:
return json.loads(s)
except ValueError:
try:
return json.loads(_UNGUELTIGES_ESCAPE.sub(r"\\\\", s))
except ValueError:
return None
def parse(text: str):
"""→ Objekt oder None. Nie werfen — der Aufrufer entscheidet über Retry.
@@ -54,11 +72,8 @@ def _ein_kandidat(text: str):
elif c == end_ch:
depth -= 1
if depth == 0:
try:
return json.loads(text[start:i + 1])
except ValueError:
break
try:
return json.loads(text)
except ValueError:
return None
daten = _loads(text[start:i + 1])
if daten is not None:
return daten
break
return _loads(text)

View File

@@ -134,6 +134,11 @@ async def call(ctx: Kontext, *, stage: str, template: str, werte: dict,
status = "ok"
return res.text
daten = jsonx.parse(res.text)
# Modelle lassen bei EINEM Element die Array-Klammern gern weg —
# ein valides Einzel-Dict zählt als 1-elementige Liste (sonst
# brannte jeder 1-Item-Chunk 3 Neuversuche durch).
if erwartet is list and isinstance(daten, dict) and daten:
daten = [daten] # leeres {} bleibt „falscher Typ" (= Ausfall)
if daten is not None and isinstance(daten, erwartet):
status = "ok"
return daten