"""Fake-Welt: deterministische Agenten-Antworten ohne LLM (PLANER_FAKE_AGENTS=1). Creator-Muster: eine Welt beantwortet Calls anhand von agent_key-Mustern und kann Störungen einspielen ({muster, modus, mal}) — damit testet die Fake-E2E den kompletten Pfad inkl. Retry/Dead-Letter/Pause in Sekunden. """ import re # Modul-globale Welt für den ENV-Weg; Tests setzen ihre eigene via set_welt(). _WELT = None def set_welt(welt) -> None: global _WELT _WELT = welt class Welt: def __init__(self): self.calls: list[str] = [] # Auditspur aller agent_keys self.stoerungen: list[dict] = [] # {muster, modus: fehler|garbage|infra, mal, [antwort]} self.antworten: list[tuple[str, object]] = [] # (regex auf agent_key, fn(prompt) -> str) def antwort_auf(self, muster: str, fn) -> None: self.antworten.append((muster, fn)) def _stoerung(self, agent_key: str) -> tuple[int, str, str] | None: for s in self.stoerungen: if s.get("mal", 0) > 0 and re.search(s["muster"], agent_key): s["mal"] -= 1 modus = s.get("modus", "fehler") if modus == "fehler": return 1, "", "fake-stoerung" if modus == "garbage": return 0, "### kaputtes {fragment", "" if modus == "infra": from agents import AgentInfraError raise AgentInfraError("fake-infra-stoerung") if modus == "antwort": return 0, s.get("antwort", ""), "" return None async def respond(self, agent_key: str, prompt: str, capabilities: str) -> tuple[int, str, str]: self.calls.append(agent_key) gestoert = self._stoerung(agent_key) if gestoert is not None: return gestoert for muster, fn in self.antworten: if re.search(muster, agent_key): return 0, fn(prompt), "" return 1, "", f"fake: keine Antwort für {agent_key}" async def respond(agent_key: str, prompt: str, capabilities: str) -> tuple[int, str, str]: global _WELT if _WELT is None: _WELT = Welt() standard_antworten(_WELT) return await _WELT.respond(agent_key, prompt, capabilities) # --- Standard-Antworten: mechanisch erzeugte Ausgaben, die die ECHTEN Gates bestehen --- def _quelle_aus(prompt: str) -> tuple[str, str]: """(datei, quelltext) aus einem Scan-Prompt (Template-Struktur).""" m = re.search(r"Ausschnitt aus `(.+?)`\n\n```python\n(.*?)\n?```", prompt, re.S) return (m.group(1), m.group(2)) if m else ("", "") def _symbol_liste(prompt: str, kopf: str) -> list[str]: """`- symbol`-Liste direkt nach der Zeile mit `kopf` (Leerzeilen dazwischen erlaubt).""" idx = prompt.find(kopf) if idx < 0: return [] out: list[str] = [] for zeile in prompt[idx:].splitlines()[1:]: z = zeile.strip() if z.startswith("- "): if "(keine)" not in z: out.append(z[2:].strip()) elif z: break # erste andere Zeile beendet die Liste return out def _einheit_fuer(datei: str, symbol: str, quelle: str) -> str: """Eine gültige Einheit: Beleg = die wörtliche Definitionszeile aus der Quelle.""" beleg = next((z.strip() for z in quelle.splitlines() if re.match(rf"\s*(?:async\s+)?(?:def|class)\s+{re.escape(symbol)}\b", z) or re.match(rf"{re.escape(symbol)}\s*[:=]", z)), "") beleg = beleg[:60].replace('"', "'") return (f"## {datei}::{symbol}\n" f"beschreibung: Fake-Verantwortung von {symbol}.\n" f"input: Fake-Input\noutput: Fake-Output\n" f"entscheidungen:\n- Fake-Entscheidung zu {symbol}.\n beleg: \"{beleg}\"\n" f"kanten:\n") def _fake_schneiden(prompt: str, lasse_aus: set[str]) -> str: datei, quelle = _quelle_aus(prompt) symbole = [s for s in _symbol_liste(prompt, "MÜSSEN alle abgedeckt sein") if s not in lasse_aus] if not symbole: # Datei ohne def/class: erste Konstante als Einheit m = re.search(r"^(\w+)\s*[:=]", quelle, re.M) symbole = [m.group(1)] if m else [] return "\n".join(_einheit_fuer(datei, s, quelle) for s in symbole) def _fake_nachfass(prompt: str) -> str: datei, quelle = _quelle_aus(prompt) fehlend = _symbol_liste(prompt, "zugeordnet sind:") return "\n".join(_einheit_fuer(datei, s, quelle) for s in fehlend) def _ids_aus_uebersicht(prompt: str) -> list[str]: return re.findall(r"^- (\S+::\S+):", prompt, re.M) def _projekt_aus(prompt: str) -> str: m = re.search(r"Einheiten von `(.+?)`", prompt) return m.group(1) if m else "projekt" def _fake_features(prompt: str) -> str: ids = _ids_aus_uebersicht(prompt) p = _projekt_aus(prompt) return (f"# Kern: {p}\nFake-Kern in zwei Sätzen. Mehr Fake.\n\n" f"tragende-einheiten: {ids[0]}\n\n" f"# Bereich: fake-bereich\nbeschreibung: Ein Fake-Bereich.\n\n" f"## Feature: Fake-Feature [kern]\nbeschreibung: Kann alles Fake.\n\n" f"### Kann fake arbeiten\neinheiten: {', '.join(ids)}\n") def _fake_architektur(prompt: str) -> str: ids = _ids_aus_uebersicht(prompt) p = _projekt_aus(prompt) return (f"# Architektur: {p}\n\n## Komponente: fake-komponente\n" f"beschreibung: Trägt alles.\neinheiten: {', '.join(ids)}\nbeziehungen:\n") def _fake_flows(prompt: str) -> str: ids = _ids_aus_uebersicht(prompt) p = _projekt_aus(prompt) schritte = "\n".join(f"{i+1}. Fake-Schritt {i+1} — {eid}" for i, eid in enumerate(ids[:3])) return f"# Flows: {p}\n\n## Flow: fake-ablauf\n{schritte}\n" def standard_antworten(welt: Welt, lasse_aus: set[str] | None = None) -> None: """Registriert gültige Fake-Antworten für alle Scan-Rollen. `lasse_aus`: Symbole, die der Erst-Scan absichtlich auslässt (übt den Nachfass-Pfad).""" aus = lasse_aus or set() welt.antwort_auf(r"-schneiden-", lambda pr: _fake_schneiden(pr, aus)) welt.antwort_auf(r"-nachfass-", _fake_nachfass) welt.antwort_auf(r"-sichten-sicht-features", _fake_features) welt.antwort_auf(r"-sichten-sicht-architektur", _fake_architektur) welt.antwort_auf(r"-sichten-sicht-flows", _fake_flows)