update
This commit is contained in:
196
backend/guide.py
196
backend/guide.py
@@ -50,7 +50,51 @@ def _katex_fehler(formeln: list[dict]) -> list[str]:
|
||||
|
||||
EBENE = "guide"
|
||||
_MARKER = re.compile(r"<!--\s*atom:\s*(\d+)\s*\|")
|
||||
_MARKER_VOLL = re.compile(r"<!--\s*atom:[^>]*-->") # ganze Marker-Zeile (für Text-Checks)
|
||||
# ganze Marker-Zeile — non-greedy statt [^>]*, sonst bricht ein „>" im Atom-Titel
|
||||
# den Match und die Marker-Zeile bliebe in den Text-Checks stehen (Marker sind
|
||||
# einzeilig, also kein DOTALL). Muster spiegelt frontend/src/markdown.js.
|
||||
_MARKER_VOLL = re.compile(r"<!--\s*atom:.*?-->")
|
||||
_MARKER_DIAGRAMM = re.compile(r"<!--\s*diagramm:\s*(\d+)\s*-->")
|
||||
|
||||
# Kurzbeschreibung je Diagramm-Typ (Writer-Angebot)
|
||||
_DIAGRAMM_BESCHR = {"dependency": "Abhängigkeitsgraph der Begriffe dieses Bausteins",
|
||||
"state": "Zustandsdiagramm/Automat", "flow": "Ablaufdiagramm",
|
||||
"tree": "Baumstruktur"}
|
||||
|
||||
|
||||
def _ohne_marker(text: str) -> str:
|
||||
"""Marker-Zeilen entfernen: sie sind Backend-Schnittstelle, kein Fließtext.
|
||||
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"
|
||||
unfixierbare Aufträge (der Fix darf Marker nie ändern → Endlos-Loop).
|
||||
Diagramm-Marker gehören ebenso raus."""
|
||||
return _MARKER_DIAGRAMM.sub("", _MARKER_VOLL.sub("", text))
|
||||
|
||||
|
||||
def _diagramme_von(baustein_id: int) -> list[dict]:
|
||||
return db.query("SELECT * FROM diagramme WHERE baustein_id=? AND status='verifiziert'"
|
||||
" 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:
|
||||
"""Assembly: `<!-- diagramm: id -->` → ```mermaid-Fence des verifizierten
|
||||
Diagramms. Tote/unverifizierte Marker fallen ersatzlos weg."""
|
||||
if "<!-- diagramm:" not in lang and "<!--diagramm:" not in lang:
|
||||
return lang
|
||||
je_id = {d["id"]: d for d in _diagramme_von(baustein_id)}
|
||||
|
||||
def sub(m: re.Match) -> str:
|
||||
d = je_id.get(int(m.group(1)))
|
||||
return f"```mermaid\n{d['mermaid']}\n```" if d and d["mermaid"] else ""
|
||||
|
||||
return _MARKER_DIAGRAMM.sub(sub, lang)
|
||||
|
||||
# Kritische Befund-Arten routen zurück auf den Prüfer, Stil-Arten nur in den Fix
|
||||
# (vorwaerts ist Stil: ein Halbsatz-Fix, kein Fakten-Problem)
|
||||
@@ -134,7 +178,8 @@ async def _stage_writer(ctx: llm.Kontext, b: dict) -> str:
|
||||
"atome": "\n\n".join(_atom_paket(a) for a in atome),
|
||||
"min_woerter": lo, "max_woerter": hi,
|
||||
"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,
|
||||
role="guide", n=len(atome), item=f"b{b['id']}", erwartet=dict)
|
||||
if res is None:
|
||||
@@ -166,8 +211,9 @@ _FLOSKEL = re.compile(r"später (mehr|noch (ausführlicher|genauer|vertieft))"
|
||||
r"|späteren (Kapitel|Abschnitt|Durchgang)", re.IGNORECASE)
|
||||
_LEAK = re.compile(r"\b(Satz|Lemma|Korollar|Bemerkung|Transformation|Definition)"
|
||||
r"\s+[IVX]*\d+\.\d+|\bZeigen Sie\b")
|
||||
_EN_STOP = frozenset("the and of is that this are with for as by be from which were"
|
||||
" has have not can will or an it its when each such".split())
|
||||
# ohne deutsche Homographen (an/will/not/or/is) — die flaggten deutschen Text
|
||||
_EN_STOP = frozenset("the and of that this are with for as by be from which were"
|
||||
" has have can its when each such".split())
|
||||
|
||||
|
||||
def _stil_auftraege(text: str, wo: str) -> list[str]:
|
||||
@@ -177,7 +223,7 @@ def _stil_auftraege(text: str, wo: str) -> list[str]:
|
||||
und unübersetzte englische Passagen (Lernen: 13 Passagen aus EN-Quellen).
|
||||
Marker-Zeilen sind Schnittstelle, kein Text — vorher strippen, sonst
|
||||
erzeugen Atom-TITEL wie „Satz 6.26“ unfixierbare Aufträge (Token-Loop)."""
|
||||
text = _MARKER_VOLL.sub("", text)
|
||||
text = _ohne_marker(text)
|
||||
auftraege = []
|
||||
for m in sorted({m.group(0) for m in _FLOSKEL.finditer(text)}):
|
||||
auftraege.append(f"{wo}: Vertröstungs-Floskel („{m}“) streichen — den Begriff"
|
||||
@@ -189,7 +235,11 @@ def _stil_auftraege(text: str, wo: str) -> list[str]:
|
||||
f" eigenständig formulieren.")
|
||||
for absatz in text.split("\n\n"):
|
||||
toks = re.findall(r"[a-zA-Z']+", absatz.lower())
|
||||
if len(toks) >= 12 and sum(t in _EN_STOP for t in toks) / len(toks) > 0.15:
|
||||
treffer = {t for t in toks if t in _EN_STOP} # verschiedene Stopwörter
|
||||
# ≥3 VERSCHIEDENE Treffer zusätzlich zur Quote — ein deutscher Satz mit
|
||||
# zufällig zwei englischen Wörtern reicht nicht mehr für einen Fehlalarm
|
||||
if len(toks) >= 12 and len(treffer) >= 3 \
|
||||
and sum(t in _EN_STOP for t in toks) / len(toks) > 0.15:
|
||||
kurz = " ".join(absatz.split())[:60]
|
||||
auftraege.append(f"{wo}: englische Passage („{kurz}…“) ins Deutsche"
|
||||
f" übersetzen — etablierte Fachtermini dürfen bleiben.")
|
||||
@@ -199,7 +249,10 @@ def _stil_auftraege(text: str, wo: str) -> list[str]:
|
||||
def _mathe_auftraege(text: str, wo: str) -> list[str]:
|
||||
"""Deterministische Mathe-Hygiene je Fassung: $-Parität, nackte LaTeX-Befehle,
|
||||
Klartext-Formelreste (x_i, u_{3m}, 2^(…) — aak Lauf 19: 8 Kompakt-/6
|
||||
Lang-Fassungen betroffen)."""
|
||||
Lang-Fassungen betroffen). Marker vorher strippen — Atom-Titel wie „a_n" oder
|
||||
„A*-Suche" trafen sonst die Formelreste-Checks unfixierbar (der Fix darf den
|
||||
Marker nicht ändern → Endlos-Loop)."""
|
||||
text = _ohne_marker(text)
|
||||
auftraege = []
|
||||
for nr, absatz in enumerate(text.split("\n\n"), 1):
|
||||
if absatz.count("$") % 2: # ein einzelnes $ zieht Fließtext in die Formel
|
||||
@@ -257,15 +310,16 @@ def _det_auftraege(topic: str, b: dict, lang: str, kompakt: str = "",
|
||||
f" (Kontext-Atom?) — Marker-Zeile ersatzlos entfernen."
|
||||
for i in _marker_fremd_ids(lang, atome)]
|
||||
lo, hi = _laenge_band(len(atome))
|
||||
woerter = len(lang.split())
|
||||
woerter = len(_ohne_marker(lang).split()) # Marker-Tokens zählen nicht mit
|
||||
if woerter > hi:
|
||||
auftraege.append(f"Kürzen auf höchstens {hi} Wörter (aktuell {woerter}).")
|
||||
elif woerter < lo:
|
||||
auftraege.append(f"Ausbauen auf mindestens {lo} Wörter (aktuell {woerter}).")
|
||||
# Auftrag an den Detektor angleichen: der prüft nur Titel-PRÄSENZ, also kann
|
||||
# „erklären + behalten" den Befund nie schließen — nur Entfernen/Umschreiben.
|
||||
auftraege += [f"Vorwärtsverweis „{t}“ (kommt erst in einem späteren Kapitel):"
|
||||
f" beim ersten Auftreten mit bereits eingeführten Begriffen kurz"
|
||||
f" erklären oder die Verwendung entfernen — nie unerklärt lassen,"
|
||||
f" NICHT vertrösten."
|
||||
f" den späteren Fachbegriff entfernen oder die Stelle ohne ihn"
|
||||
f" umschreiben (mit bereits eingeführten Begriffen)."
|
||||
for t in _vorwaertsverweise(topic, b, lang)]
|
||||
zitate = sum(1 for z in lang.splitlines() if z.lstrip().startswith(">"))
|
||||
if zitate: # Lehren statt Abschreiben: Rohzitate gehören nicht in den Lehrtext
|
||||
@@ -316,7 +370,7 @@ def _vorwaertsverweise(topic: str, b: dict, lang: str) -> list[str]:
|
||||
db.query("SELECT * FROM kapitel WHERE topic=?", (topic,))}
|
||||
je_baustein = {bb["id"]: kap_ord.get(bb["kapitel_id"], 0) for bb in _bausteine(topic)}
|
||||
mein_kapitel = je_baustein.get(b["id"], 0)
|
||||
text = textkit.norm(_MARKER_VOLL.sub("", lang))
|
||||
text = textkit.norm(_ohne_marker(lang))
|
||||
treffer = []
|
||||
for a in db.query(
|
||||
"SELECT a.titel, bb.id AS bid FROM atome a JOIN bausteine bb ON a.baustein_id=bb.id"
|
||||
@@ -336,6 +390,12 @@ async def _stage_pruefer(ctx: llm.Kontext, b: dict, tag: str = "") -> str:
|
||||
atome = _atome_von(b["id"])
|
||||
ziel = db.one("SELECT text FROM lernziele WHERE id=?", (b["ziel_id"],)) or {"text": b["titel"]}
|
||||
auftraege = _det_auftraege(ctx.topic, b, sec["text_lang"], sec["text_kompakt"])
|
||||
# bereits offene KRITISCH-Aufträge übernehmen (z. B. ein aus der Ebenen-QA
|
||||
# bestätigter fachlich_falsch-Claim) — der In-Card-Prüfer findet sie nicht
|
||||
# immer selbst wieder, ohne dies verpuffte der Befund im Ping-Pong.
|
||||
for alt in db.uj(sec["befunde"]):
|
||||
if str(alt).startswith("KRITISCH") and alt not in auftraege:
|
||||
auftraege.append(alt)
|
||||
fakten = "\n\n".join(f"Atom {a['id']} ({a['titel']}): {a['definition']}\n"
|
||||
+ "\n".join(f"> {z}" for z in _zitate(a["id"])) for a in atome)
|
||||
res = await llm.call(ctx, stage=f"pruefer{tag}", template="Guide-Pruefer",
|
||||
@@ -351,6 +411,7 @@ async def _stage_pruefer(ctx: llm.Kontext, b: dict, tag: str = "") -> str:
|
||||
auftraege.append(f"KRITISCH ({art}): {detail}")
|
||||
elif art == "stil" and detail:
|
||||
auftraege.append(detail)
|
||||
auftraege = list(dict.fromkeys(auftraege)) # Duplikate (übernommene + neue) raus
|
||||
db.update("sections", "baustein_id", b["id"], befunde=db.j(auftraege))
|
||||
return "fix" if auftraege else "done"
|
||||
|
||||
@@ -423,7 +484,7 @@ async def _kapitel_intros(ctx: llm.Kontext) -> None:
|
||||
db.update("kapitel", "id", kap["id"], intro=intro)
|
||||
|
||||
offen = db.query("SELECT * FROM kapitel WHERE topic=? AND intro=''", (ctx.topic,))
|
||||
await asyncio.gather(*(einer(k) for k in offen))
|
||||
await llm.alle(einer(k) for k in offen)
|
||||
|
||||
|
||||
async def bauen(ctx: llm.Kontext) -> None:
|
||||
@@ -431,7 +492,7 @@ 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 asyncio.gather(*(_karte(ctx, b) for b in offen))
|
||||
await llm.alle(_karte(ctx, b) for b in offen)
|
||||
await _kapitel_intros(ctx)
|
||||
|
||||
|
||||
@@ -440,13 +501,14 @@ def _pruefe_dich(baustein_id: int, lang: str) -> list[dict]:
|
||||
Flashcard-Pool — Matching gegen den Langtext liefert die passenden Karten
|
||||
(Retrieval mit Feedback wirkt ~doppelt so stark wie ohne)."""
|
||||
out = []
|
||||
for a in _atome_von(baustein_id):
|
||||
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 frage in lang:
|
||||
if frage and antwort and textkit.norm(frage) in lang_norm:
|
||||
out.append({"frage": frage, "antwort": antwort})
|
||||
return out
|
||||
|
||||
@@ -461,18 +523,31 @@ def kapitel_struktur(topic: str) -> list[dict]:
|
||||
for b in _bausteine(topic):
|
||||
sec = db.one("SELECT * FROM sections WHERE baustein_id=?", (b["id"],))
|
||||
k_id = b["kapitel_id"] if b["kapitel_id"] in kaps else None
|
||||
if not kapitel or kapitel[-1]["kapitel_id"] != k_id:
|
||||
# neue Gruppe bei Kapitelwechsel — bei einer kapitellosen (NULL-)Gruppe
|
||||
# zusätzlich bei Level-Wechsel, sonst mischt ein „Weitere Themen"-Block
|
||||
# E-, M- und S-Bausteine und erbt fälschlich das Level des ersten.
|
||||
neu = (not kapitel or kapitel[-1]["_kid"] != k_id
|
||||
or (k_id is None and kapitel[-1]["level"] != b["level"]))
|
||||
if neu:
|
||||
k = kaps.get(k_id, {})
|
||||
kapitel.append({"kapitel_id": k_id, "titel": k.get("titel", "Weitere Themen"),
|
||||
kapitel.append({"kapitel_id": k_id, "_kid": k_id,
|
||||
"titel": k.get("titel", "Weitere Themen"),
|
||||
"intro": k.get("intro", ""),
|
||||
"level": k.get("level", b["level"]), "sections": []})
|
||||
lang = sec["text_lang"] if sec else ""
|
||||
# Diagramm-Marker gegen den ```mermaid-Fence tauschen (Assembly-Schritt);
|
||||
# Atom-Marker bleiben — die strippt erst das Frontend.
|
||||
lang = _diagramm_einsetzen(b["id"], sec["text_lang"]) if sec else ""
|
||||
kapitel[-1]["sections"].append({
|
||||
"baustein": b["id"], "titel": b["titel"],
|
||||
"ziel": ziele.get(b["ziel_id"], {}).get("text", ""),
|
||||
"kompakt": sec["text_kompakt"] if sec else "",
|
||||
"lang": lang,
|
||||
"fragen": _pruefe_dich(b["id"], lang) if lang else []})
|
||||
# NULL-kapitel_id durch eindeutige synthetische Keys ersetzen — mehrere
|
||||
# kapitellose Gruppen kollidierten sonst im Vue-:key (null == null).
|
||||
for i, kap in enumerate(kapitel):
|
||||
if kap.pop("_kid") is None:
|
||||
kap["kapitel_id"] = f"frei-{i}"
|
||||
return kapitel
|
||||
|
||||
|
||||
@@ -512,9 +587,20 @@ def messen(ctx: llm.Kontext) -> list[dict]:
|
||||
for i in _marker_fremd_ids(lang, atome):
|
||||
befunde.append({"art": "marker_fremd", "item": str(b["id"]),
|
||||
"detail": f"Atom {i}"})
|
||||
gueltige_d = {d["id"] for d in _diagramme_von(b["id"])}
|
||||
for m in _MARKER_DIAGRAMM.findall(lang):
|
||||
if int(m) not in gueltige_d: # Marker ohne verifiziertes Diagramm → entfernen
|
||||
befunde.append({"art": "diagramm_marker_tot", "item": str(b["id"]),
|
||||
"detail": f"Stray Diagramm-Marker <!-- diagramm: {m} -->"
|
||||
f" entfernen (kein solches verifiziertes Diagramm)"})
|
||||
for auftrag in _det_auftraege(ctx.topic, b, lang, sec["text_kompakt"], qa=True):
|
||||
befunde.append({"art": "det_check", "item": str(b["id"]),
|
||||
"detail": auftrag[:300]})
|
||||
# Rest-Aufträge eines gescheiterten Fix (stage bleibt „done", befunde nicht
|
||||
# geleert) sichtbar machen — sonst passiert eine bekannte Lücke das Gate.
|
||||
for auftrag in db.uj(sec["befunde"]):
|
||||
befunde.append({"art": "fix_offen", "item": str(b["id"]),
|
||||
"detail": str(auftrag)[:300]})
|
||||
ziel = db.one("SELECT text FROM lernziele WHERE id=?", (b["ziel_id"],))
|
||||
if ziel:
|
||||
noetig = _stopfrei(ziel["text"])
|
||||
@@ -541,9 +627,15 @@ async def messen_llm(ctx: llm.Kontext) -> list[dict]:
|
||||
import hashlib
|
||||
ctx.ebene = EBENE
|
||||
befunde = []
|
||||
# topic-scoped (nicht run-scoped): ein in Run N bestätigter Fakten-Fehler bleibt
|
||||
# nach Pause/Resume (Run N+1) sichtbar, sonst greift der qa_hash-Skip fälschlich.
|
||||
offene_falsch = {b["item"] for b in db.query(
|
||||
"SELECT item FROM befunde WHERE run_id=? AND ebene=? AND art='fachlich_falsch'"
|
||||
" AND status='offen'", (ctx.run_id, EBENE))}
|
||||
"SELECT b.item FROM befunde b JOIN runs r ON r.id=b.run_id"
|
||||
" WHERE r.topic=? AND b.ebene=? AND b.art='fachlich_falsch' AND b.status='offen'",
|
||||
(ctx.topic, EBENE))}
|
||||
|
||||
def hash_setzen(bid: int, h: str) -> None:
|
||||
db.execute("UPDATE sections SET qa_hash=? WHERE baustein_id=?", (h, bid))
|
||||
|
||||
async def eine(b: dict) -> None:
|
||||
sec = db.one("SELECT * FROM sections WHERE baustein_id=?", (b["id"],))
|
||||
@@ -552,16 +644,20 @@ async def messen_llm(ctx: llm.Kontext) -> list[dict]:
|
||||
h = hashlib.sha256(sec["text_lang"].encode()).hexdigest()[:12]
|
||||
if h == sec["qa_hash"] and str(b["id"]) not in offene_falsch:
|
||||
return # unverändert und sauber — kein Re-Check
|
||||
db.execute("UPDATE sections SET qa_hash=? WHERE baustein_id=?", (h, b["id"]))
|
||||
# qa_hash ERST nach vollständiger Prüfung setzen — bei Call-/Panel-Ausfall
|
||||
# bleibt die Section ungeprüft (fail-closed statt fail-open).
|
||||
atome = _atome_von(b["id"])
|
||||
fakten = "\n".join(f"- {a['titel']}: " + " | ".join(_zitate(a["id"], 2)) for a in atome)
|
||||
verdacht = await llm.call(ctx, stage="qa_falsch", template="QA-Guide-Falsch",
|
||||
schritt="qa_judge", role="judge", item=f"b{b['id']}",
|
||||
werte={"fakten": fakten, "text": sec["text_lang"]},
|
||||
erwartet=list)
|
||||
claims = [str(c.get("claim", "")).strip() for c in verdacht or []
|
||||
if verdacht is None:
|
||||
return # Verdachts-Call ausgefallen → nächste Runde erneut, kein Hash
|
||||
claims = [str(c.get("claim", "")).strip() for c in verdacht
|
||||
if isinstance(c, dict) and c.get("claim")]
|
||||
if not claims:
|
||||
hash_setzen(b["id"], h) # sauber geprüft
|
||||
return
|
||||
stimmen = await llm.panel(ctx, 2, stage="qa_falsch_check", template="QA-Guide-Falsch-Check",
|
||||
schritt="qa_judge", role="judge", item=f"b{b['id']}",
|
||||
@@ -569,20 +665,34 @@ async def messen_llm(ctx: llm.Kontext) -> list[dict]:
|
||||
"claims": "\n".join(f"{i + 1}: {c}"
|
||||
for i, c in enumerate(claims))},
|
||||
erwartet=list)
|
||||
if not stimmen:
|
||||
return # Panel-Ausfall → kein Hash, nächste Runde prüft erneut
|
||||
for i, claim in enumerate(claims, 1):
|
||||
ja = sum(1 for s in stimmen for e in s
|
||||
if isinstance(e, dict) and e.get("claim") == i and e.get("falsch"))
|
||||
if ja >= 2:
|
||||
befunde.append({"art": "fachlich_falsch", "item": str(b["id"]),
|
||||
"detail": claim[:200]})
|
||||
hash_setzen(b["id"], h) # vollständig geprüft
|
||||
|
||||
await asyncio.gather(*(eine(b) for b in _bausteine(ctx.topic)))
|
||||
await llm.alle(eine(b) for b in _bausteine(ctx.topic))
|
||||
return befunde
|
||||
|
||||
|
||||
def _text_sig(b_id: int) -> str:
|
||||
import hashlib
|
||||
s = db.one("SELECT text_kompakt, text_lang FROM sections WHERE baustein_id=?", (b_id,))
|
||||
if not s:
|
||||
return ""
|
||||
roh = (s["text_kompakt"] or "") + "\x00" + (s["text_lang"] or "")
|
||||
return hashlib.sha256(roh.encode()).hexdigest()[:16]
|
||||
|
||||
|
||||
async def reparieren(ctx: llm.Kontext, befunde: list[dict]) -> bool:
|
||||
"""Kritische Befunde → zurück auf pruefer (voller Kontext + Re-Check);
|
||||
Stil-Befunde → direkt in den Fix (genau ein Rewrite)."""
|
||||
Stil-Befunde → direkt in den Fix (genau ein Rewrite). Rückgabe ist ehrlich:
|
||||
True nur, wenn sich Section-Text tatsächlich geändert hat — sonst erkennt
|
||||
auto_loop nie Stillstand und dreht alle 10 Iterationen wirkungslos durch."""
|
||||
ctx.ebene = EBENE
|
||||
betroffen: dict[int, list[dict]] = {}
|
||||
for b in befunde:
|
||||
@@ -591,17 +701,35 @@ async def reparieren(ctx: llm.Kontext, befunde: list[dict]) -> bool:
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
betroffen.setdefault(b_id, []).append(b)
|
||||
if not betroffen:
|
||||
return False
|
||||
for b_id, liste in betroffen.items():
|
||||
arten = [x["art"] for x in liste]
|
||||
if any(a in KRITISCH or a == "section_fehlt" for a in arten):
|
||||
stage = "writer" if "section_fehlt" in arten else "pruefer"
|
||||
db.execute("UPDATE sections SET stage=? WHERE baustein_id=?", (stage, b_id))
|
||||
# ein fix_offen-Detail mit KRITISCH-Präfix ist ebenfalls kritisch
|
||||
kritisch_detail = any(x["art"] == "fix_offen"
|
||||
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:
|
||||
if "section_fehlt" in arten:
|
||||
db.execute("UPDATE sections SET stage='writer' WHERE baustein_id=?", (b_id,))
|
||||
else:
|
||||
# KRITISCH-Details in die Spalte schreiben, damit _stage_pruefer sie
|
||||
# sieht (der bestätigte Claim erreicht sonst weder Prüfer noch Fix)
|
||||
vorhanden = db.uj(db.one("SELECT befunde FROM sections WHERE baustein_id=?",
|
||||
(b_id,))["befunde"])
|
||||
neu = [f"KRITISCH ({x['art']}): {x['detail']}" for x in liste
|
||||
if x["art"] in KRITISCH]
|
||||
zusammen = list(dict.fromkeys(vorhanden + neu))
|
||||
db.update("sections", "baustein_id", b_id, stage="pruefer",
|
||||
befunde=db.j(zusammen))
|
||||
else:
|
||||
# Detail mitgeben — „Behebe: vorwaerts“ ohne den Begriff ließ den Fix
|
||||
# raten (aak: 371 kumulierte vorwaerts-Befunde über 10 Iterationen)
|
||||
auftraege = [f"Behebe ({x['art']}): {x['detail']}" for x in liste]
|
||||
# bestehende Fix-Aufträge (fix_offen) behalten, Stil-Details anhängen
|
||||
vorhanden = db.uj(db.one("SELECT befunde FROM sections WHERE baustein_id=?",
|
||||
(b_id,))["befunde"])
|
||||
neu = [f"Behebe ({x['art']}): {x['detail']}" for x in liste
|
||||
if x["art"] != "fix_offen"]
|
||||
auftraege = list(dict.fromkeys(vorhanden + neu))
|
||||
db.update("sections", "baustein_id", b_id, stage="fix", befunde=db.j(auftraege))
|
||||
db.update("bausteine", "id", b_id, status="repair")
|
||||
if betroffen:
|
||||
await bauen(ctx)
|
||||
return bool(betroffen)
|
||||
vorher = {b_id: _text_sig(b_id) for b_id in betroffen}
|
||||
await bauen(ctx)
|
||||
return any(vorher[b_id] != _text_sig(b_id) for b_id in betroffen)
|
||||
|
||||
Reference in New Issue
Block a user