update
This commit is contained in:
117
backend/pruefung.py
Normal file
117
backend/pruefung.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""Prüft die Karten eines Projekts gegen den Stil-Katalog.
|
||||
|
||||
Ein Lauf je Projekt, Zustand nur im Speicher — er überlebt keinen Serverneustart.
|
||||
Je Karte eine Anfrage, höchstens PRUEFUNG_PARALLEL gleichzeitig; Probleme erscheinen,
|
||||
sobald die jeweilige Karte geprüft ist.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
|
||||
import agents
|
||||
import config
|
||||
import features
|
||||
|
||||
# projektname -> {"laeuft": bool, "fertig": int, "gesamt": int,
|
||||
# "probleme": [...], "meldung": str}
|
||||
_laeufe = {}
|
||||
|
||||
SYSTEM = """Du prüfst Karten einer Feature-Verwaltung gegen einen Stil-Katalog.
|
||||
|
||||
{stil}
|
||||
|
||||
Du bekommst eine Karte als Markdown. Melde jede Abweichung vom Katalog.
|
||||
Antworte ausschließlich mit JSON:
|
||||
{{"probleme": [{{"feld": "...", "funktion": "...", "original": "...",
|
||||
"problem": "...", "vorschlag": "..."}}]}}
|
||||
|
||||
- feld: titel, ziel, regeln oder testen
|
||||
- funktion: Titel der Funktion, zu der die Stelle gehört; "" für die Karte selbst
|
||||
- original: der fehlerhafte Text wortwörtlich; bei regeln und testen genau eine Zeile ohne führendes "- "
|
||||
- problem: die verletzte Vorgabe in höchstens 8 Wörtern
|
||||
- vorschlag: der korrigierte Text, sonst nichts
|
||||
|
||||
Keine Probleme gefunden: {{"probleme": []}}
|
||||
Erfinde nichts und melde nur, was der Katalog verlangt."""
|
||||
|
||||
|
||||
def status(projekt: str) -> dict:
|
||||
lauf = _laeufe.get(projekt)
|
||||
if not lauf:
|
||||
return {"laeuft": False, "fertig": 0, "gesamt": 0, "probleme": [], "meldung": ""}
|
||||
return lauf
|
||||
|
||||
|
||||
def _ohne_strich(text: str) -> str:
|
||||
return re.sub(r"^[-*]\s*", "", (text or "").strip())
|
||||
|
||||
|
||||
def _antwort_lesen(text: str) -> list[dict]:
|
||||
"""Holt das JSON aus der Antwort, auch wenn es in ```-Zäunen steckt.
|
||||
Listenzeilen kommen mal mit, mal ohne „- " zurück — hier vereinheitlicht,
|
||||
damit die Anzeige die Stelle wiederfindet."""
|
||||
treffer = re.search(r"\{.*\}", text, re.S)
|
||||
if not treffer:
|
||||
raise ValueError("Antwort enthält kein JSON")
|
||||
daten = json.loads(treffer.group(0))
|
||||
probleme = []
|
||||
for p in daten.get("probleme") or []:
|
||||
if not isinstance(p, dict) or not p.get("original"):
|
||||
continue
|
||||
p["original"] = _ohne_strich(p["original"])
|
||||
p["vorschlag"] = _ohne_strich(p.get("vorschlag"))
|
||||
p["funktion"] = (p.get("funktion") or "").strip()
|
||||
probleme.append(p)
|
||||
return probleme
|
||||
|
||||
|
||||
async def _pruefe_karte(stil: str, feature: dict) -> list[dict]:
|
||||
text = await agents.frage(SYSTEM.format(stil=stil), feature["roh"])
|
||||
probleme = _antwort_lesen(text)
|
||||
for p in probleme:
|
||||
p["feature"] = feature["id"]
|
||||
return probleme
|
||||
|
||||
|
||||
async def _lauf(projekt: str, liste: list[dict]):
|
||||
lauf = _laeufe[projekt]
|
||||
stil = config.STIL_DATEI.read_text()
|
||||
schranke = asyncio.Semaphore(config.PRUEFUNG_PARALLEL)
|
||||
|
||||
async def eine(feature):
|
||||
async with schranke:
|
||||
if _laeufe.get(projekt) is not lauf:
|
||||
return # verworfen (Panel geschlossen) — keine weitere Anfrage
|
||||
try:
|
||||
lauf["probleme"].extend(await _pruefe_karte(stil, feature))
|
||||
except Exception as e:
|
||||
lauf["meldung"] = str(e)
|
||||
lauf["fertig"] += 1
|
||||
|
||||
try:
|
||||
await asyncio.gather(*(eine(f) for f in liste))
|
||||
finally:
|
||||
lauf["laeuft"] = False
|
||||
|
||||
|
||||
async def starte(projekt: str) -> dict:
|
||||
"""Startet einen Lauf im Hintergrund; ein laufender wird nicht verdoppelt."""
|
||||
if status(projekt)["laeuft"]:
|
||||
raise ValueError("Prüfung läuft bereits")
|
||||
agents.pruefe_konfiguration() # früher Stopp statt halbem Lauf
|
||||
liste = features.liste(projekt)
|
||||
_laeufe[projekt] = {
|
||||
"laeuft": True,
|
||||
"fertig": 0,
|
||||
"gesamt": len(liste),
|
||||
"probleme": [],
|
||||
"meldung": "",
|
||||
}
|
||||
asyncio.create_task(_lauf(projekt, liste))
|
||||
return _laeufe[projekt]
|
||||
|
||||
|
||||
def beende(projekt: str):
|
||||
"""Ergebnis verwerfen (Panel schließen)."""
|
||||
_laeufe.pop(projekt, None)
|
||||
Reference in New Issue
Block a user