This commit is contained in:
Team3
2026-09-27 21:36:33 +02:00
commit 498fe2220a
34 changed files with 3608 additions and 0 deletions

169
backend/state.py Normal file
View File

@@ -0,0 +1,169 @@
"""Zustand je Thema: storage/<name>/{thema.json,state.json,log.jsonl}. Alles im Speicher, bei Änderung auf Platte."""
import json
import re
import shutil
from datetime import datetime, timezone
from pathlib import Path
from . import config
_cache: dict[str, dict] = {}
def jetzt() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
def name_ok(name: str) -> bool:
return bool(name) and name.strip() == name and "/" not in name and ".." not in name and len(name) < 120
def verzeichnis(name: str) -> Path:
return config.STORAGE / name
def _lesen(pfad: Path, standard):
try:
return json.loads(pfad.read_text())
except (OSError, ValueError):
return standard
def _schreiben(pfad: Path, daten) -> None:
pfad.parent.mkdir(parents=True, exist_ok=True)
tmp = pfad.with_suffix(pfad.suffix + ".tmp")
tmp.write_text(json.dumps(daten, ensure_ascii=False, indent=1))
tmp.replace(pfad)
def leerer_zustand() -> dict:
return {"status": "neu", "grund": "", "stufe": "", "deutung": None,
"stufen": [{"id": s, "titel": t, "status": "offen", "fertig": 0, "gesamt": 0,
"kosten_usd": 0.0, "tokens": 0, "gestartet": "", "beendet": ""} for s, t in config.STUFEN],
"kosten_usd": 0.0, "tokens": 0, "agenten": [], "ereignisse": []}
def themen() -> list[dict]:
aus = []
for d in sorted(config.STORAGE.glob("*/thema.json")):
t = _lesen(d, None)
if t:
aus.append(t)
return aus
def anlegen(name: str, hinweise: str) -> dict:
v = verzeichnis(name)
if v.exists():
raise ValueError("Thema existiert schon")
for u in ("quellen", "buch", "berichte", "arbeit"):
(v / u).mkdir(parents=True)
t = {"name": name, "titel": name, "hinweise": hinweise.strip(), "angelegt": jetzt()}
_schreiben(v / "thema.json", t)
_schreiben(v / "state.json", leerer_zustand())
return t
def loeschen(name: str) -> None:
_cache.pop(name, None)
shutil.rmtree(verzeichnis(name), ignore_errors=True)
def zuruecksetzen(name: str) -> None:
v = verzeichnis(name)
t = _lesen(v / "thema.json", None)
_cache.pop(name, None)
for u in ("quellen", "buch", "berichte", "arbeit"):
shutil.rmtree(v / u, ignore_errors=True)
(v / u).mkdir()
for f in ("deutung.json", "log.jsonl", "state.json"):
(v / f).unlink(missing_ok=True)
_schreiben(v / "state.json", leerer_zustand())
if t:
_schreiben(v / "thema.json", t)
def thema(name: str) -> dict | None:
return _lesen(verzeichnis(name) / "thema.json", None)
def zustand(name: str) -> dict:
if name not in _cache:
st = _lesen(verzeichnis(name) / "state.json", None) or leerer_zustand()
# Ein Lauf, der beim Serverstart noch als „laeuft“ gespeichert ist, ist tot.
if st.get("status") == "laeuft":
st["status"] = "pausiert"
st["grund"] = "Server wurde beendet – mit „Fortsetzen“ weitermachen"
for s in st["stufen"]:
if s["status"] == "laeuft":
s["status"] = "offen"
for a in st["agenten"]:
if a["status"] == "laeuft":
a["status"] = "abgebrochen"
_cache[name] = st
return _cache[name]
def speichern(name: str) -> None:
if name in _cache:
_schreiben(verzeichnis(name) / "state.json", _cache[name])
def deutung(name: str) -> dict | None:
return _lesen(verzeichnis(name) / "deutung.json", None)
def stufe(name: str, sid: str) -> dict:
return next(s for s in zustand(name)["stufen"] if s["id"] == sid)
def ereignis(name: str, agent: str, art: str, text: str) -> None:
st = zustand(name)
e = {"zeit": jetzt(), "agent": agent, "art": art, "text": text[:300]}
st["ereignisse"].append(e)
del st["ereignisse"][:-200]
try:
with open(verzeichnis(name) / "log.jsonl", "a") as f:
f.write(json.dumps(e, ensure_ascii=False) + "\n")
except OSError:
pass
def agent_eintrag(name: str, agent_id: str, stufe_id: str, titel: str) -> dict:
st = zustand(name)
st["agenten"] = [a for a in st["agenten"] if a["id"] != agent_id]
a = {"id": agent_id, "stufe": stufe_id, "name": titel, "status": "laeuft", "kosten_usd": 0.0, "tokens": 0,
"werkzeuge": 0, "gestartet": jetzt(), "beendet": "", "bericht": ""}
st["agenten"].append(a)
speichern(name)
return a
def agent_fertig(name: str, a: dict, status: str, bericht: str, kosten: float, tokens: int) -> None:
st = zustand(name)
a.update(status=status, bericht=bericht, kosten_usd=round(kosten, 4), tokens=tokens, beendet=jetzt())
s = stufe(name, a["stufe"])
s["kosten_usd"] = round(s["kosten_usd"] + kosten, 4)
s["tokens"] += tokens
st["kosten_usd"] = round(st["kosten_usd"] + kosten, 4)
st["tokens"] += tokens
speichern(name)
def bericht_schreiben(name: str, agent_id: str, text: str) -> None:
p = verzeichnis(name) / "berichte" / f"{agent_id}.md"
p.parent.mkdir(exist_ok=True)
p.write_text(text.strip() + "\n")
def bericht_lesen(name: str, agent_id: str) -> str:
p = verzeichnis(name) / "berichte" / f"{agent_id}.md"
return p.read_text() if p.exists() else ""
def slug(text: str) -> str:
t = text.lower()
for a, b in (("ä", "ae"), ("ö", "oe"), ("ü", "ue"), ("ß", "ss")):
t = t.replace(a, b)
t = re.sub(r"[^a-z0-9]+", "-", t).strip("-")
return t[:60] or "kapitel"