"""Deterministische Fake-Antworten auf run_agent-Ebene (CREATOR_FAKE_AGENTS=1): kompletter Generierungspfad in Sekunden, ohne LLM. Die Fakes erkennen das Template am Marker `` 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", "braucht": []}, {"titel": "Potenzmengenkonstruktion", "typ": "verfahren", "definition": SATZ_POTENZ, "zitat": SATZ_POTENZ, "level": "M", "braucht": ["Deterministischer endlicher Automat"]}, {"titel": "Pumping-Lemma", "typ": "aussage", "definition": SATZ_PUMPING, "zitat": SATZ_PUMPING, "level": "S", "braucht": ["Deterministischer endlicher Automat"]}, ] PUNKTE = [("Endliche Automaten", (SATZ_DFA, SATZ_POTENZ)), ("Nachweis von Nicht-Regularität", (SATZ_PUMPING,))] _TEMPLATE = re.compile(r"") 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 _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 _kapitel_schnitt(prompt: str): ids = [int(i) for i in re.findall(r"^(\d+): ", _text_nach(prompt, "BAUSTEINE:"), re.MULTILINE)] grenzen = [ids[min(i + 1, len(ids) - 1)] for i in range(0, len(ids), 2)] return [{"titel": f"Themenblock {n + 1}", "bis": g} for n, g in enumerate(dict.fromkeys(grenzen))] def _zyklus(prompt: str): m = re.search(r"^(\d+)→(\d+):", _text_nach(prompt, "ZYKLUS:"), re.MULTILINE) return {"von": int(m.group(1)), "zu": int(m.group(2))} if m else {} def _braucht_aufloesung(prompt: str): # je ANGABE den Kandidaten mit den meisten geteilten Tokens (Substring, ≥4 Zeichen) # wählen, sonst null — spiegelt den Substring-Prefilter der Auflösung. def toks(s): return {t for t in re.findall(r"\w+", s.lower()) if len(t) >= 4} out = [] bloecke = re.split(r"ANGABE (\d+):", prompt)[1:] for i in range(0, len(bloecke), 2): n, body = int(bloecke[i]), bloecke[i + 1] mp = re.search(r"Voraussetzung „([^“]+)", body) ph = mp.group(1) if mp else "" pl, pt = ph.lower(), toks(ph) ranked = [] for kid, ktitel in re.findall(r"- atom (\d+): (.+?) —", body): geteilt = {t for t in toks(ktitel) if t in pl} | {t for t in pt if t in ktitel.lower()} if geteilt: ranked.append((-len(geteilt), len(ktitel), int(kid))) ranked.sort() out.append({"phrase": n, "atom": ranked[0][2] if ranked else None}) return out def _writer(prompt: str): ziel = re.search(r"Lernziel: (.+)", prompt) marker = re.findall(r"MARKER \(exakt so übernehmen\): ()", 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 _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, "Lernziele": _lernziele, "Kapitel-Intro": _kapitel_intro, "Kapitel-Schnitt": _kapitel_schnitt, "Level-Kalibrierung": _level_kalibrierung, "Kanten-Zyklus": _zyklus, "Braucht-Aufloesung": _braucht_aufloesung, "Guide-Writer": _writer, "Guide-Pruefer": _pruefer, "Guide-Fix": _fix, "QA-Guide-Falsch": _qa_falsch, "QA-Guide-Falsch-Check": _qa_falsch_check, }