317 lines
13 KiB
Python
317 lines
13 KiB
Python
"""Deterministische Fake-Welt (CREATOR_FAKE=1): ersetzt LLM, Suche und Laden.
|
|
Die Fakes LESEN den Prompt — Zitate kommen nur, wenn sie wörtlich im gelieferten
|
|
Text stehen (Zitat-Gate wird mitgetestet). Präparierte Eigenschaften:
|
|
- Dublette über 2 Quellen (HTTP ↔ Hypertext Transfer Protocol, Akronym-Kanal)
|
|
- Soll-Punkt „Fehlerbehandlung" ohne Atom (erst die Web-Nachrecherche deckt ihn)
|
|
- Kapitel-übergreifende Satz-Dopplung, die der Fix per Verweis ersetzt
|
|
Fehlender Handler → harter Fehler (nie still raten)."""
|
|
import re
|
|
|
|
from . import skills
|
|
|
|
FUELL = ("Dieser Absatz liefert zusätzlichen Zusammenhang für Lernende. " * 4).strip()
|
|
|
|
S1 = "Ein Protokoll regelt den Austausch von Nachrichten zwischen zwei Partnern im Netz."
|
|
S2 = "HTTP steuert die Übertragung von Webseiten zwischen Browser und Server."
|
|
S3 = "Das Hypertext Transfer Protocol überträgt Inhalte im World Wide Web."
|
|
S4 = "Kommunikation braucht klare Regeln für beide Seiten."
|
|
S5 = ("Beim Wiederholungsmuster wird eine fehlgeschlagene Anfrage nach kurzer "
|
|
"Wartezeit erneut gesendet.")
|
|
S6A = "Ohne saubere Fehlerbehandlung scheitern Systeme im Alltag."
|
|
S7A = "Ein Vorteil ist bessere Konzentration bei der Arbeit."
|
|
S7B = "Ein weiterer Vorteil ist weniger Stress im Alltag."
|
|
S6B = "Fehlerbehandlung entscheidet über die Zuverlässigkeit eines Systems."
|
|
DUP = ("Fehler lassen sich nie ganz vermeiden, entscheidend ist der richtige "
|
|
"Umgang mit ihnen.")
|
|
|
|
P1 = "Grundlagen der Protokolle und Regeln erklären"
|
|
P2 = "Das HTTP-Protokoll verstehen"
|
|
P3 = "Fehlerbehandlung beherrschen"
|
|
|
|
QUELLEN = {
|
|
"https://beispiel.de/grundlagen":
|
|
f"Einführung in das Thema.\n\n{FUELL}\n\n{S1}\n\n{FUELL}\n\n{S2}\n\n"
|
|
f"{FUELL}\n\n{S6A}\n\n{FUELL}",
|
|
"https://beispiel.de/vertiefung":
|
|
f"Vertiefende Betrachtung.\n\n{FUELL}\n\n{S4}\n\n{FUELL}\n\n{S3}\n\n"
|
|
f"{FUELL}\n\n{S6B}\n\n{S7A} {S7B}\n\n{FUELL}",
|
|
"https://beispiel.de/fehler":
|
|
f"Umgang mit Fehlern.\n\n{FUELL}\n\n{S5}\n\n{FUELL}\n\n{FUELL}",
|
|
}
|
|
|
|
SOLL = [(P1, [S1, S4]), (P2, [S2, S3]), (P3, [S6A, S6B])]
|
|
ATOME = [ # (titel, typ, definition, beleg-satz)
|
|
("Protokoll", "begriff",
|
|
"Ein Protokoll ist die Vereinbarung, nach der zwei Partner Nachrichten austauschen.", S1),
|
|
("HTTP", "begriff",
|
|
"HTTP ist das Protokoll für die Übertragung von Webseiten.", S2),
|
|
("Hypertext Transfer Protocol", "begriff",
|
|
"Das Hypertext Transfer Protocol dient der Übertragung von Inhalten im Web.", S3),
|
|
("Kommunikationsregeln", "aussage",
|
|
"Erfolgreiche Kommunikation setzt klare, beidseitig bekannte Regeln voraus.", S4),
|
|
("Vorteil Konzentration", "aussage",
|
|
"Die Methode verbessert die Konzentration bei der Arbeit.", S7A),
|
|
("Vorteil Stressabbau", "aussage",
|
|
"Die Methode senkt den empfundenen Stress im Alltag.", S7B),
|
|
("Wiederholungsmuster", "verfahren",
|
|
"Beim Wiederholungsmuster wird eine fehlgeschlagene Anfrage nach Wartezeit wiederholt.", S5),
|
|
]
|
|
_ATOM_SOLL = {"Protokoll": P1, "Kommunikationsregeln": P1, "HTTP": P2,
|
|
"Hypertext Transfer Protocol": P2, "Wiederholungsmuster": P3,
|
|
"Vorteil Konzentration": P1, "Vorteil Stressabbau": P1}
|
|
_DUBLETTEN = {frozenset({"HTTP", "Hypertext Transfer Protocol"})}
|
|
_ENTHALTEN = {frozenset({"Basisregeln", "Basisregeln und Sonderfaelle"})}
|
|
|
|
|
|
def suche(query: str) -> list[dict]:
|
|
q = query.casefold()
|
|
if "fehlerbehandlung" in q:
|
|
return [{"titel": "Fehlerbehandlung", "url": "https://beispiel.de/fehler",
|
|
"backend": "fake"}]
|
|
if "r2" in q or " v2" in q:
|
|
return [] # Runde 2: nichts Neues → Sättigung
|
|
if "ueberblick" in q:
|
|
return [{"titel": "Grundlagen", "url": "https://beispiel.de/grundlagen",
|
|
"backend": "fake"}]
|
|
if "tutorial" in q:
|
|
return [{"titel": "Vertiefung", "url": "https://beispiel.de/vertiefung",
|
|
"backend": "fake"}]
|
|
return []
|
|
|
|
|
|
def laden(url: str) -> str:
|
|
return QUELLEN.get(url, "")
|
|
|
|
|
|
def _abschnitt(prompt: str, marke: str) -> str:
|
|
m = re.search(rf"{marke}:\n(.*?)(?=\n\n[A-ZÄÖÜ\-]{{3,}}[^a-z]*:\n|\nAusgabeformat|\Z)",
|
|
prompt, re.DOTALL)
|
|
return m.group(1) if m else prompt
|
|
|
|
|
|
def _ids_liste(text: str) -> list[tuple[int, str]]:
|
|
return [(int(m.group(1)), m.group(2).strip())
|
|
for m in re.finditer(r"^\[(\d+)\]\s*(.+)$", text, re.MULTILINE)]
|
|
|
|
|
|
# ── Handler je Aufgabe ──
|
|
|
|
def _nummerierte_abschnitte(p: str) -> dict[int, str]:
|
|
"""`=== ABSCHNITT n ===`-Blöcke aus dem Bündel-Prompt lesen."""
|
|
return {int(m.group(1)): m.group(2) for m in re.finditer(
|
|
r"=== ABSCHNITT (\d+) ===\n(.*?)(?=\n\n=== ABSCHNITT |\n\nRegeln:|\Z)",
|
|
p, re.DOTALL)}
|
|
|
|
|
|
def _recherche_plan(p: str) -> str:
|
|
runde = re.search(r"\(Runde (\d+)\)", p)
|
|
r = runde.group(1) if runde else "1"
|
|
m = re.search(r"BLICKWINKEL:\n(.*?)\n\nBereits", p, re.DOTALL)
|
|
lenses = re.findall(r"^- (\w+)$", m.group(1) if m else "", re.MULTILINE)
|
|
return "\n".join(f"===QUERY===\nLENS: {l}\nTEXT: thema {l} r{r} nr{i}"
|
|
for l in (lenses or ["x"]) for i in (1, 2))
|
|
|
|
|
|
def _soll_extraktion(p: str) -> str:
|
|
out = []
|
|
for i, text in _nummerierte_abschnitte(p).items():
|
|
n = 0
|
|
for punkt, belege in SOLL:
|
|
for beleg in belege:
|
|
if beleg in text:
|
|
out.append(f"===PUNKT===\nABSCHNITT: {i}\nTEXT: {punkt}\n"
|
|
f"BELEG: {beleg}")
|
|
n += 1
|
|
if not n:
|
|
out.append(f"===LEER===\nABSCHNITT: {i}")
|
|
return "\n".join(out)
|
|
|
|
|
|
def _soll_konsens(p: str) -> str:
|
|
gruppen: dict[str, list[int]] = {}
|
|
for i, text in _ids_liste(_abschnitt(p, "KANDIDATEN")):
|
|
gruppen.setdefault(text, []).append(i)
|
|
return "\n".join(f"===GRUPPE===\nIDS: {','.join(map(str, ids))}\nTEXT: {text}"
|
|
for text, ids in gruppen.items())
|
|
|
|
|
|
def _atom_extraktion(p: str) -> str:
|
|
out = []
|
|
for i, text in _nummerierte_abschnitte(p).items():
|
|
n = 0
|
|
for titel, typ, definition, satz in ATOME:
|
|
if satz in text:
|
|
out.append(f"===ATOM===\nABSCHNITT: {i}\nTITEL: {titel}\n"
|
|
f"TYP: {typ}\nDEFINITION: {definition}\nZITAT: {satz}")
|
|
n += 1
|
|
if not n:
|
|
out.append(f"===LEER===\nABSCHNITT: {i}")
|
|
return "\n".join(out)
|
|
|
|
|
|
def _anker_fix(p: str) -> str:
|
|
return "\n".join(f"===FIX===\nID: {i}\nZITAT: VERWERFEN"
|
|
for i, _ in _ids_liste(_abschnitt(p, "ATOME")))
|
|
|
|
|
|
def _dedup_urteil(p: str) -> str:
|
|
out = []
|
|
for m in re.finditer(r"PAAR (\d+):\nA\[\d+\] ([^:]+):.*?\nB\[\d+\] ([^:]+):",
|
|
p, re.DOTALL):
|
|
nr, ta, tb = m.group(1), m.group(2).strip(), m.group(3).strip()
|
|
paar = frozenset({ta, tb})
|
|
urteil = ("dublette" if paar in _DUBLETTEN
|
|
else "enthalten" if paar in _ENTHALTEN else "verschieden")
|
|
out.append(f"===URTEIL===\nPAAR: {nr}\nURTEIL: {urteil}\nGRUND: fake")
|
|
return "\n".join(out) or "===URTEIL===\nPAAR: 1\nURTEIL: verschieden\nGRUND: leer"
|
|
|
|
|
|
def _soll_zuordnung(p: str) -> str:
|
|
soll_map = {text: i for i, text in _ids_liste(_abschnitt(p, "SOLL-PUNKTE"))}
|
|
out = []
|
|
for atom_id, zeile in _ids_liste(_abschnitt(p, "ATOME")):
|
|
titel = zeile.split(":")[0].strip()
|
|
punkt = _ATOM_SOLL.get(titel, "")
|
|
ziel = soll_map.get(punkt)
|
|
out.append(f"===ZUORDNUNG===\nATOM: {atom_id}\n"
|
|
f"SOLL: {ziel if ziel is not None else 'KEINER'}")
|
|
return "\n".join(out)
|
|
|
|
|
|
def _luecke_nachextraktion(p: str) -> str:
|
|
out = []
|
|
for m in re.finditer(r"=== LUECKE ===\nSOLL: (\d+)\nPUNKT: .*?\nFENSTER:\n"
|
|
r"(.*?)(?=\n\n=== LUECKE ===|\n\nRegeln:|\Z)",
|
|
p, re.DOTALL):
|
|
sid, fenster = m.group(1), m.group(2)
|
|
if S5 in fenster:
|
|
t, typ, d, s = next(a for a in ATOME if a[3] == S5)
|
|
out.append(f"===ATOM===\nSOLL: {sid}\nTITEL: {t}\nTYP: {typ}\n"
|
|
f"DEFINITION: {d}\nZITAT: {s}")
|
|
else:
|
|
out.append(f"===NICHTS===\nSOLL: {sid}")
|
|
return "\n".join(out)
|
|
|
|
|
|
def _verdichtung(p: str) -> str:
|
|
vorteile, andere = [], []
|
|
for i, zeile in _ids_liste(_abschnitt(p, "ATOME")):
|
|
(vorteile if "Vorteil" in zeile else andere).append(str(i))
|
|
out = []
|
|
if vorteile:
|
|
out.append(f"===EINHEIT===\nIDS: {','.join(vorteile)}")
|
|
out += [f"===EINHEIT===\nIDS: {i}" for i in andere]
|
|
return "\n".join(out)
|
|
|
|
|
|
def _lernziele(p: str) -> str:
|
|
out = []
|
|
for m in re.finditer(r"=== SOLL (\d+) ===\nPUNKT: (.*?)\nATOME:\n"
|
|
r"(.*?)(?=\n\n=== SOLL |\n\nRegeln:|\Z)", p, re.DOTALL):
|
|
i, punkt = m.group(1), m.group(2)
|
|
ids = [str(x) for x, _ in _ids_liste(m.group(3))]
|
|
out.append(f"===ZIEL===\nSOLL: {i}\nTITEL: {punkt.split()[0][:30]}\n"
|
|
f"TEXT: Der Lernende kann {punkt} anwenden.\n"
|
|
f"ATOME: {','.join(ids)}")
|
|
return "\n".join(out)
|
|
|
|
|
|
def _bausteine(p: str) -> str:
|
|
ids = [str(i) for i, _ in _ids_liste(_abschnitt(p, "ATOME (in Quellreihenfolge)"))]
|
|
return f"===BAUSTEIN===\nTITEL: Baustein\nATOME: {','.join(ids)}"
|
|
|
|
|
|
def _themen_schnitt(p: str) -> str:
|
|
fehler, rest = [], []
|
|
for i, text in _ids_liste(_abschnitt(p, "SOLL-PUNKTE")):
|
|
(fehler if "Fehlerbehandlung" in text else rest).append(str(i))
|
|
out = []
|
|
if rest:
|
|
out.append(f"===KAPITEL===\nTITEL: Grundlagen\nPUNKTE: {','.join(rest)}")
|
|
if fehler:
|
|
out.append(f"===KAPITEL===\nTITEL: Fehlerbehandlung\nPUNKTE: {','.join(fehler)}")
|
|
return "\n".join(out)
|
|
|
|
|
|
def _ordnung(p: str) -> str:
|
|
prior = re.search(r"übernehmen\): ([\d,]+)", p)
|
|
return f"===ORDNUNG===\nIDS: {prior.group(1) if prior else ''}"
|
|
|
|
|
|
def _writer(p: str) -> str:
|
|
atome = re.findall(r"<!-- atom: (\d+) -->\n\*\*(.+?)\*\* — (.+)", p)
|
|
teile = []
|
|
for aid, titel, definition in atome:
|
|
# Erster Satz bewusst KEIN Definitions-Echo (semantischer det-Check)
|
|
teile.append(f"<!-- atom: {aid} -->\n\nSchauen wir uns das Konzept "
|
|
f"nun genauer an. {definition} "
|
|
f"{FUELL} Damit ist {titel} eingeordnet.")
|
|
teile.append(DUP) # gepflanzte Dopplung — Redundanz-Check muss sie finden
|
|
return "===TEXT===\n" + "\n\n".join(teile)
|
|
|
|
|
|
def _kapitel_intro(p: str) -> str:
|
|
kap = re.search(r"Kapitel „(.+?)\"", p)
|
|
return (f"===INTRO===\nDieses Kapitel behandelt {kap.group(1) if kap else 'X'}. "
|
|
"Es baut auf dem bisher Gelernten auf.")
|
|
|
|
|
|
def _guide_pruefung(p: str) -> str:
|
|
ids = re.findall(r"^=== SECTION (\d+) ===$", p, re.MULTILINE)
|
|
return "\n".join(f"===OK===\nSECTION: {i}"
|
|
for i in dict.fromkeys(ids)) or "===OK===\nSECTION: 0"
|
|
|
|
|
|
def _guide_fix(p: str) -> str:
|
|
section = _abschnitt(p, "SECTION")
|
|
zeilen = [z for z in section.splitlines()
|
|
if "Fehler lassen sich nie ganz vermeiden" not in z]
|
|
zeilen.append("Wie bereits bei den Grundlagen beschrieben, zählt der "
|
|
"Umgang mit Fehlern.")
|
|
return "===TEXT===\n" + "\n".join(zeilen).strip()
|
|
|
|
|
|
def _redundanz_urteil(p: str) -> str:
|
|
out = []
|
|
for m in re.finditer(r"PAAR (\d+):\nKapitel A: (.*)\nKapitel B: (.*)", p):
|
|
redundant = "Fehler lassen sich nie ganz vermeiden" in m.group(2)
|
|
out.append(f"===URTEIL===\nPAAR: {m.group(1)}\n"
|
|
f"URTEIL: {'redundant' if redundant else 'didaktisch_gewollt'}")
|
|
return "\n".join(out) or "===URTEIL===\nPAAR: 1\nURTEIL: didaktisch_gewollt"
|
|
|
|
|
|
_HANDLER = {
|
|
"recherche-plan": _recherche_plan,
|
|
"soll-extraktion": _soll_extraktion,
|
|
"soll-konsens": _soll_konsens,
|
|
"soll-beleg": lambda p: "===BELEG===\nID: 0\nBELEG: KEIN BELEG",
|
|
"quellen-relevanz": lambda p: "===RELEVANT===\nIDS: " + ",".join(
|
|
m for m in re.findall(r"^\[(\d+)\]", p, re.MULTILINE)),
|
|
"atom-extraktion": _atom_extraktion,
|
|
"anker-fix": _anker_fix,
|
|
"dedup-urteil": _dedup_urteil,
|
|
"soll-zuordnung": _soll_zuordnung,
|
|
"luecke-nachextraktion": _luecke_nachextraktion,
|
|
"luecke-freispruch": lambda p: "===URTEIL===\nURTEIL: unerfuellbar\nGRUND: fake",
|
|
"verdichtung": _verdichtung,
|
|
"lernziele": _lernziele,
|
|
"bausteine": _bausteine,
|
|
"themen-schnitt": _themen_schnitt,
|
|
"ordnung": _ordnung,
|
|
"writer": _writer,
|
|
"kapitel-intro": _kapitel_intro,
|
|
"guide-pruefung": _guide_pruefung,
|
|
"guide-fix": _guide_fix,
|
|
"redundanz-urteil": _redundanz_urteil,
|
|
"klaerung": lambda p: "===URTEIL===\nURTEIL: kein_mangel\nGRUND: fake",
|
|
}
|
|
|
|
|
|
def antwort(prompt: str) -> tuple[int, str, str]:
|
|
"""→ (rc, text, err). Fehlender Handler = harter Fehler, nie still raten."""
|
|
name = skills.aufgaben_name(prompt)
|
|
handler = _HANDLER.get(name)
|
|
if handler is None:
|
|
return 1, "", f"kein Fake-Handler für Aufgabe '{name}'"
|
|
return 0, handler(prompt), ""
|