Files
creator2/backend/fake_agents.py
2026-07-14 00:15:03 +02:00

300 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Deterministische Fake-Antworten auf run_agent-Ebene (CREATOR_FAKE_AGENTS=1):
kompletter Generierungspfad in Sekunden, ohne LLM. Die Fakes erkennen das Template
am Marker `<!-- template:Name -->` und LESEN den Prompt (Zitate müssen wörtlich im
gelieferten Text stehen, ids müssen existieren) — sonst testet der E2E-Pfad nichts."""
import json
import re
from agents import AgentErgebnis
# Die Fake-Welt: drei Atome, zwei Soll-Punkte, eine Voraussetzungskette.
SATZ_DFA = "Ein deterministischer endlicher Automat (DFA) besteht aus Zuständen und Übergängen."
SATZ_POTENZ = "Die Potenzmengenkonstruktion wandelt einen NFA in einen DFA um."
SATZ_PUMPING = "Das Pumping-Lemma beweist, dass eine Sprache nicht regulär ist."
FAKE_TEXT = f"{SATZ_DFA}\n\n{SATZ_POTENZ}\n\n{SATZ_PUMPING}\n"
ATOME = [
{"titel": "Deterministischer endlicher Automat", "typ": "begriff",
"definition": SATZ_DFA, "zitat": SATZ_DFA, "level": "E"},
{"titel": "Potenzmengenkonstruktion", "typ": "verfahren",
"definition": SATZ_POTENZ, "zitat": SATZ_POTENZ, "level": "M"},
{"titel": "Pumping-Lemma", "typ": "aussage",
"definition": SATZ_PUMPING, "zitat": SATZ_PUMPING, "level": "S"},
]
PUNKTE = [("Endliche Automaten", (SATZ_DFA, SATZ_POTENZ)),
("Nachweis von Nicht-Regularität", (SATZ_PUMPING,))]
_TEMPLATE = re.compile(r"<!--\s*template:([\w-]+)\s*-->")
def _text_nach(prompt: str, marke: str) -> str:
pos = prompt.find(marke)
return prompt[pos + len(marke):] if pos >= 0 else ""
def _ids(text: str) -> list[int]:
return [int(m) for m in re.findall(r"^(\d+):", text, re.MULTILINE)]
def _ok(daten) -> AgentErgebnis:
return AgentErgebnis(0, json.dumps(daten, ensure_ascii=False), "",
{"input": 100, "output": 50})
async def antwort(key: str, prompt: str, capabilities: str) -> AgentErgebnis:
m = _TEMPLATE.search(prompt)
name = m.group(1) if m else ""
fn = _HANDLER.get(name)
if fn is None:
return AgentErgebnis(1, "", f"fake: kein Handler für Template {name!r}")
return _ok(fn(prompt))
def _recherche(prompt: str):
lens = re.search(r"\(Lens\): \*\*(\w+)\*\*", prompt)
lens = lens.group(1) if lens else "x"
inhalt = FAKE_TEXT + f"\nHinweis der Lens {lens}: " + "Übung macht den Meister. " * 40
return [{"titel": f"Fake-Quelle {lens}", "url": f"https://fake/{lens}", "inhalt": inhalt}]
def _soll(prompt: str):
text = _text_nach(prompt, "QUELLTEXT:")
return [{"punkt": p, "zitat": s[0]} for p, s in PUNKTE if s[0] in text]
def _soll_konsens(prompt: str):
zeilen = re.findall(r"^(\d+): (.+)$", _text_nach(prompt, "KANDIDATEN:"), re.MULTILINE)
gruppen: dict[str, list[int]] = {}
for i, punkt in zeilen:
gruppen.setdefault(punkt.strip(), []).append(int(i))
return [{"punkt": p, "kandidaten": ids} for p, ids in gruppen.items()]
def _soll_beleg(prompt: str):
text = _text_nach(prompt, "QUELLTEXT:")
zeilen = re.findall(r"^(\d+): (.+)$", _text_nach(prompt, "SOLL-PUNKTE:"), re.MULTILINE)
out = []
for i, punkt in zeilen:
for p, saetze in PUNKTE:
if p == punkt.strip() and saetze[0] in text:
out.append({"soll": int(i), "zitat": saetze[0]})
return out
def _extraktion(prompt: str):
text = _text_nach(prompt, "QUELLTEXT:")
return [a for a in ATOME if a["zitat"] in text]
def _merge(prompt: str):
paare = re.findall(r"PAAR (\d+):\nA\(id \d+\): ([^—]+)—.*?\nB\(id \d+\): ([^—]+)—",
prompt, re.DOTALL)
return [{"paar": int(n), "gleich": a.strip() == b.strip()} for n, a, b in paare]
def _zuordnung_map(prompt: str) -> dict[str, int]:
soll = re.findall(r"^(\d+): (.+)$", _text_nach(prompt, "SOLL-PUNKTE"), re.MULTILINE)
out = {}
for i, punkt in soll:
out[punkt.strip()] = int(i)
return out
def _soll_zuordnung(prompt: str):
punkte = _zuordnung_map(prompt)
atome = re.findall(r"^(\d+): (.+?) —", _text_nach(prompt, "ATOME"), re.MULTILINE)
out = []
for i, titel in atome:
t = titel.lower()
punkt = ("Nachweis von Nicht-Regularität" if "pumping" in t else "Endliche Automaten")
out.append({"atom": int(i), "soll": punkte.get(punkt, next(iter(punkte.values()), 0))})
return out
def _soll_stich(prompt: str):
punkte = _zuordnung_map(prompt)
return {"soll": next(iter(punkte.values()), "fremd")}
def _anker_fix_batch(prompt: str):
text = _text_nach(prompt, "QUELLTEXT:")
zeilen = re.findall(r"^(\d+): (.+?) —", _text_nach(prompt, "ATOME:"), re.MULTILINE)
out = []
for i, titel in zeilen:
for a in ATOME:
if a["titel"] == titel.strip() and a["zitat"] in text:
out.append({"atom": int(i), "zitat": a["zitat"]})
return out
def _luecke(prompt: str):
text = _text_nach(prompt, "QUELLTEXT:")
return [{k: a[k] for k in ("titel", "typ", "definition", "zitat")}
for a in ATOME if a["zitat"] in text]
def _artefakt_generate(prompt: str):
# Form je Atom-Typ [in Klammern] wählen (wie die typgesteuerte Regel im Prompt)
paare = re.findall(r"ATOM (\d+):[^\[\n]*\[(\w+)\]", prompt)
out = []
for raw, typ in paare:
i = int(raw)
out += [{"atom": i, "typ": "flashcard", "frage": f"Was besagt Atom {i}?",
"antwort": "Siehe Beleg — deterministische Fake-Antwort."},
{"atom": i, "typ": "flashcard", "frage": f"Wozu dient Atom {i}?",
"antwort": "Zum Lernen — deterministische Fake-Antwort."}]
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
def _artefakt_verify(prompt: str):
ids = [int(i) for i in re.findall(r"ARTEFAKT (\d+)", prompt)]
return [{"artefakt": i, "ok": True, "mangel": ""} for i in ids]
def _artefakt_fix(prompt: str):
return {"frage": "Fix-Frage?", "antwort": "Fix-Antwort.", "text": "Fix-Beispiel."}
def _aussagen_generate(prompt: str):
from config import AUSSAGEN_JE_ATOM
ids = [int(m) for m in re.findall(r"^ATOM (\d+):", _text_nach(prompt, "ATOME:"),
re.MULTILINE)]
out = []
for i in ids:
for p in range(AUSSAGEN_JE_ATOM):
out.append({"atom": i, "wahr": f"Atom {i} gilt (Aspekt {p}).",
"falsch": f"Atom {i} gilt nie (Aspekt {p})."})
return out
def _aussagen_verify(prompt: str):
ids = [int(m) for m in re.findall(r"AUSSAGE (\d+) \[", prompt)]
return [{"aussage": i, "ok": True} for i in ids]
def _lernziele(prompt: str):
punkt = re.search(r"Soll-Punkt: „(.+?)“", prompt) or re.search(r'Soll-Punkt: „(.+?)"', prompt)
name = punkt.group(1) if punkt else "das Thema"
ids = _ids(_text_nach(prompt, "ATOME:"))
return [{"ziel": f"Kann {name} anwenden und erklären", "titel": name, "atome": ids}]
def _kapitel_intro(prompt: str):
punkt = re.search(r"Kapitel „(.+?)“", prompt) or re.search(r'Kapitel „(.+?)"', prompt)
return {"intro": f"Dieses Kapitel behandelt {punkt.group(1) if punkt else 'das Thema'}"
" in aufeinander aufbauenden Abschnitten."}
def _level_kalibrierung(prompt: str):
# deklariertes Level der Fake-Welt zurückgeben (Titel-Match) — der E2E-Pfad
# erzeugt so echte E/M/S-Durchgänge; Unbekanntes wird Standardstoff
zeilen = re.findall(r"^(\d+): (.+?) —", _text_nach(prompt, "ATOME:"), re.MULTILINE)
je_titel = {a["titel"]: a["level"] for a in ATOME}
return [{"atom": int(i), "level": je_titel.get(t.strip(), "M")} for i, t in zeilen]
def _titel_fix(prompt: str):
zeilen = re.findall(r"^(\d+): ", _text_nach(prompt, "ATOME:"), re.MULTILINE)
return [{"atom": int(i), "titel": f"Konzept {i}"} for i in zeilen]
def _themen_schnitt(prompt: str):
# konsekutive 2er-Gruppen; Rest hängt an der letzten Gruppe (nie Singleton,
# damit das Größen-Gate im Code deterministisch passiert)
nummern = [int(i) for i in re.findall(r"^(\d+): ", _text_nach(prompt, "SOLL-PUNKTE:"),
re.MULTILINE)]
gruppen = [nummern[i:i + 2] for i in range(0, len(nummern), 2)]
if len(gruppen) > 1 and len(gruppen[-1]) == 1:
gruppen[-2] += gruppen.pop()
return [{"titel": f"Fake-Thema {n + 1}", "punkte": g} for n, g in enumerate(gruppen)]
def _baustein_ordnung(prompt: str):
nummern = [int(i) for i in re.findall(r"^(\d+): ", _text_nach(prompt, "BAUSTEINE"),
re.MULTILINE)]
return nummern # Identität: Prior-Ordnung ist im Fake die Lehr-Reihenfolge
def _writer(prompt: str):
ziel = re.search(r"Lernziel: (.+)", prompt)
marker = re.findall(r"MARKER \(exakt so übernehmen\): (<!-- atom: \d+ \| (.+?) -->)",
prompt)
defs = re.findall(r"Definition: (.+)", prompt)
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$."]
fuellung = "Daran anknüpfend gehen wir das Schritt für Schritt am Beispiel durch. " * 5
for (mk, titel), d in zip(marker, defs):
# Beispiel-Marker setzt jetzt _marker_platzieren deterministisch,
# nicht mehr der Writer.
absaetze.append(f"#### {titel} verstehen\n{mk}\n{d} {fuellung}")
lang = "\n\n".join(absaetze)
while len(lang.split()) < lo:
lang += "\nNoch ein Übungssatz zur Vertiefung des Gelernten."
return {"kompakt": f"- Kurzfassung in {len(marker)} Schritten\n- alles Wesentliche sitzt",
"lang": lang}
def _pruefer(prompt: str):
return {"befunde": []}
def _pruefer_urteil(prompt: str):
nummern = [int(i) for i in re.findall(r"^(\d+): ", _text_nach(prompt, "OFFENE AUFTRÄGE:"),
re.MULTILINE)]
return [{"auftrag": i, "urteil": "behoben"} for i in nummern]
def _klaerung(prompt: str):
# Default „unbegruendet": schließt Konflikte, Fake-E2E terminiert bei Note 10
nummern = [int(i) for i in re.findall(r"^(\d+): ",
_text_nach(prompt, "ESKALIERTE AUFTRÄGE:"),
re.MULTILINE)]
return [{"auftrag": i, "urteil": "unbegruendet"} for i in nummern]
def _fix(prompt: str):
kompakt = _text_nach(prompt, "SECTION (kompakt):").split("SECTION (lang):")[0].strip()
lang = _text_nach(prompt, "SECTION (lang):").split("Regeln:")[0].strip()
return {"kompakt": kompakt, "lang": lang}
def _qa_falsch(prompt: str):
return []
def _qa_falsch_check(prompt: str):
nummern = [int(i) for i in re.findall(r"^(\d+):", _text_nach(prompt, "CLAIMS:"), re.MULTILINE)]
return [{"claim": i, "falsch": False} for i in nummern]
_HANDLER = {
"Korpus-Recherche": _recherche, "Korpus-Soll": _soll,
"Korpus-Soll-Konsens": _soll_konsens, "Korpus-Soll-Beleg": _soll_beleg,
"Atom-Extraktion": _extraktion, "Atom-Extraktion-Aufgaben": _extraktion,
"Atom-Merge": _merge,
"Atom-Soll-Zuordnung": _soll_zuordnung, "Atom-Soll-Stich": _soll_stich,
"Atom-Anker-Fix-Batch": _anker_fix_batch,
"Atom-Luecke": _luecke, "Atom-Titel-Fix": _titel_fix,
"Soll-Abgedeckt": lambda p: {"abgedeckt": True},
"Artefakt-Generate": _artefakt_generate, "Artefakt-Verify": _artefakt_verify,
"Artefakt-Fix": _artefakt_fix,
"Aussagen-Generate": _aussagen_generate, "Aussagen-Verify": _aussagen_verify,
"Lernziele": _lernziele, "Kapitel-Intro": _kapitel_intro,
"Themen-Schnitt": _themen_schnitt, "Level-Kalibrierung": _level_kalibrierung,
"Baustein-Ordnung": _baustein_ordnung,
"Guide-Writer": _writer, "Guide-Pruefer": _pruefer,
"Guide-Pruefer-Urteil": _pruefer_urteil, "Guide-Konflikt-Klaerung": _klaerung,
"Guide-Fix": _fix,
"QA-Guide-Falsch": _qa_falsch, "QA-Guide-Falsch-Check": _qa_falsch_check,
}