init
This commit is contained in:
155
backend/fake_agents.py
Normal file
155
backend/fake_agents.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user