update
This commit is contained in:
@@ -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;
|
||||
sonst ein Fix + Re-Verify durch einen Judge; danach verifiziert oder verworfen."""
|
||||
|
||||
import ast
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
import db
|
||||
import diagramme
|
||||
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")
|
||||
|
||||
@@ -25,8 +29,14 @@ _QUELLEN_REFERENZ = re.compile(
|
||||
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:
|
||||
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:
|
||||
@@ -60,6 +70,21 @@ def _chunks(topic: str, nur_ohne: bool) -> list[list[dict]]:
|
||||
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:
|
||||
bloecke = "\n\n".join(_atom_block(a) for a in chunk)
|
||||
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()
|
||||
if atom_id not in gueltig or typ not in ("flashcard", "beispiel"):
|
||||
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):
|
||||
log.info("Artefakt zu Atom %s nicht übernommen: referenziert die Quelle", atom_id)
|
||||
continue
|
||||
@@ -129,7 +157,16 @@ async def _fixen(ctx: llm.Kontext, k: dict, maengel: list[str], atom: dict) -> N
|
||||
"belege": _zitate(k["atom_id"])},
|
||||
erwartet=dict)
|
||||
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):
|
||||
db.update("artefakte", "id", k["id"], status="verworfen")
|
||||
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")
|
||||
|
||||
|
||||
_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:
|
||||
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user