This commit is contained in:
team3
2026-07-10 16:17:29 +02:00
parent 0a41166cca
commit 718c18523d
10 changed files with 297 additions and 16 deletions

View File

@@ -104,7 +104,31 @@ def _marker_fehlend(text: str, atome: list[dict]) -> list[int]:
return [a["id"] for a in atome if a["id"] not in da]
def _det_auftraege(topic: str, b: dict, lang: 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)."""
auftraege = []
for nr, absatz in enumerate(text.split("\n\n"), 1):
if absatz.count("$") % 2: # ein einzelnes $ zieht Fließtext in die Formel
auftraege.append(f"{wo}, Absatz {nr}: ungerade Anzahl $-Zeichen — jede"
f" Formel braucht öffnendes UND schließendes $.")
ohne = re.sub(r"\$\$[\s\S]*?\$\$|\$[^$\n]*\$", "", text)
nackt = sorted(set(re.findall(
r"\\(?:times|Sigma|Gamma|subseteq|neq|leq|geq|cup|cap|mid|forall|exists"
r"|mathbb|frac|text|dots|ldots|quad|qquad|bar|setminus)\b", ohne)))
if nackt: # KaTeX rendert nur innerhalb von $…$
auftraege.append(f"{wo}: LaTeX ohne $-Delimiter ({', '.join(nackt[:5])}…):"
f" jede Formel vollständig in $…$ bzw. $$…$$ einschließen.")
reste = sorted(set(re.findall(
r"\w+_\{[^}]*\}|\w+\^\{[^}]*\}|\w+\^\([^)]*\)|\b\w+_[a-z0-9]\b", ohne)))
if reste:
auftraege.append(f"{wo}: Formeln im Klartext ({', '.join(reste[:4])}…):"
f" auch Indizes/Potenzen gehören in $…$ (x_i → $x_i$).")
return auftraege
def _det_auftraege(topic: str, b: dict, lang: str, kompakt: str = "") -> list[str]:
"""Deterministische Checks vor dem Judge: Marker, Länge, Vorwärtsverweise."""
atome = _atome_von(b["id"])
auftraege = [f"KRITISCH: Marker für Atom {i} fehlt — exakt einfügen."
@@ -126,18 +150,9 @@ def _det_auftraege(topic: str, b: dict, lang: str) -> list[str]:
if "6=" in lang:
auftraege.append("PDF-Artefakt „6=“ im Text: gemeint ist Ungleichheit —"
" durch $\\neq$ ersetzen.")
for nr, absatz in enumerate(lang.split("\n\n"), 1):
if absatz.count("$") % 2: # ein einzelnes $ zieht Fließtext in die Formel
auftraege.append(f"Absatz {nr}: ungerade Anzahl $-Zeichen — jede Formel"
f" braucht öffnendes UND schließendes $ (Beispiel-Fehler:"
f" „$[.“ statt „$[$.“).")
ohne_mathe = re.sub(r"\$\$[\s\S]*?\$\$|\$[^$\n]*\$", "", lang)
nackt = sorted(set(re.findall(
r"\\(?:times|Sigma|Gamma|subseteq|neq|leq|geq|cup|cap|mid|forall|exists"
r"|mathbb|frac|text|dots|ldots|quad|qquad|bar|setminus)\b", ohne_mathe)))
if nackt: # KaTeX rendert nur innerhalb von $…$ — nackte Befehle bleiben Rohtext
auftraege.append(f"LaTeX ohne $-Delimiter im Text ({', '.join(nackt[:5])}…):"
f" jede Formel vollständig in $…$ bzw. $$…$$ einschließen.")
auftraege += _mathe_auftraege(lang, "Langtext")
if kompakt:
auftraege += _mathe_auftraege(kompakt, "Kompakt-Fassung")
return auftraege
@@ -168,7 +183,7 @@ async def _stage_pruefer(ctx: llm.Kontext, b: dict, tag: str = "") -> str:
sec = db.one("SELECT * FROM sections WHERE baustein_id=?", (b["id"],))
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"])
auftraege = _det_auftraege(ctx.topic, b, sec["text_lang"], sec["text_kompakt"])
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",

View File

@@ -154,6 +154,11 @@ def state(topic: str):
}
@app.get("/api/health")
def health():
return {"ok": True}
@app.get("/api/topics/{topic}/guide")
def guide_holen(topic: str):
# kein markdown-Feld: das Frontend rendert aus kapitel; Roh-Markdown

7
backend/requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
# Runtime-Abhängigkeiten (torch kommt im Dockerfile als CPU-Build davor)
fastapi
uvicorn[standard]
httpx
ftfy
fuzzysearch
sentence-transformers