init
This commit is contained in:
98
backend/skills.py
Normal file
98
backend/skills.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""Skill-Loader + Komposition (R5): genau 1 Rolle + 1 Aufgabe + 0..n Stil.
|
||||
Nur die Aufgabe definiert das Ausgabeformat (Konfliktfreiheit per Konstruktion).
|
||||
{{platzhalter}} nur im Aufgabe-Body, str.replace statt format_map.
|
||||
Hashing zweistufig: je Datei sha256[:12], skill_hash über die geordnete Liste."""
|
||||
import hashlib
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from . import config
|
||||
|
||||
_FRONT_RE = re.compile(r"^---\n(.*?)\n---\n(.*)$", re.DOTALL)
|
||||
_PLATZHALTER_RE = re.compile(r"\{\{([a-z_]+)\}\}")
|
||||
_cache: dict[str, dict] = {}
|
||||
|
||||
|
||||
class SkillFehler(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _parse(pfad: Path) -> dict:
|
||||
text = pfad.read_text(encoding="utf-8")
|
||||
m = _FRONT_RE.match(text)
|
||||
if not m:
|
||||
raise SkillFehler(f"{pfad.name}: kein Frontmatter")
|
||||
meta: dict = {"platzhalter": []}
|
||||
for zeile in m.group(1).splitlines():
|
||||
if ":" not in zeile:
|
||||
continue
|
||||
k, v = zeile.split(":", 1)
|
||||
k, v = k.strip(), v.strip()
|
||||
if k == "platzhalter":
|
||||
meta[k] = [p.strip() for p in v.strip("[]").split(",") if p.strip()]
|
||||
else:
|
||||
meta[k] = v
|
||||
for pflicht in ("name", "art", "beschreibung"):
|
||||
if not meta.get(pflicht):
|
||||
raise SkillFehler(f"{pfad.name}: Frontmatter-Feld '{pflicht}' fehlt")
|
||||
if meta["art"] not in ("rolle", "aufgabe", "stil"):
|
||||
raise SkillFehler(f"{pfad.name}: unbekannte art '{meta['art']}'")
|
||||
body = m.group(2).strip()
|
||||
return {**meta, "body": body,
|
||||
"hash": hashlib.sha256(text.encode()).hexdigest()[:12]}
|
||||
|
||||
|
||||
def lade(name: str) -> dict:
|
||||
"""Skill per Name laden ('rolle/extraktor' oder nur 'extraktor' mit Suche)."""
|
||||
if name in _cache and not config.FAKE:
|
||||
return _cache[name]
|
||||
if "/" in name:
|
||||
pfad = config.SKILLS_DIR / f"{name}.md"
|
||||
else:
|
||||
treffer = list(config.SKILLS_DIR.glob(f"*/{name}.md"))
|
||||
if len(treffer) != 1:
|
||||
raise SkillFehler(f"Skill '{name}': {len(treffer)} Treffer")
|
||||
pfad = treffer[0]
|
||||
if not pfad.exists():
|
||||
raise SkillFehler(f"Skill-Datei fehlt: {pfad}")
|
||||
_cache[name] = _parse(pfad)
|
||||
return _cache[name]
|
||||
|
||||
|
||||
def komponieren(skill_namen: list[str], werte: dict) -> tuple[str, str, list]:
|
||||
"""→ (prompt, skill_hash, [(name, hash)]). Reihenfolge Rolle→Stil→Aufgabe;
|
||||
Prompt beginnt mit stabilem Präfix (MiniMax-Prompt-Caching ab 512 Tokens)."""
|
||||
geladen = [lade(n) for n in skill_namen]
|
||||
rollen = [s for s in geladen if s["art"] == "rolle"]
|
||||
aufgaben = [s for s in geladen if s["art"] == "aufgabe"]
|
||||
stile = [s for s in geladen if s["art"] == "stil"]
|
||||
if len(rollen) != 1 or len(aufgaben) != 1:
|
||||
raise SkillFehler(f"Komposition braucht genau 1 Rolle + 1 Aufgabe: {skill_namen}")
|
||||
aufgabe = aufgaben[0]
|
||||
|
||||
fehlt = [p for p in aufgabe["platzhalter"] if p not in werte]
|
||||
if fehlt:
|
||||
raise SkillFehler(f"Aufgabe '{aufgabe['name']}': Platzhalter fehlen: {fehlt}")
|
||||
body = aufgabe["body"]
|
||||
for p in aufgabe["platzhalter"]:
|
||||
body = body.replace("{{" + p + "}}", str(werte[p]))
|
||||
rest = _PLATZHALTER_RE.search(body)
|
||||
if rest:
|
||||
raise SkillFehler(f"Aufgabe '{aufgabe['name']}': unersetzt: {rest.group(0)}")
|
||||
|
||||
teile = [f"<!-- aufgabe:{aufgabe['name']} -->", # Fake-Registry-Schlüssel
|
||||
"## Rolle", rollen[0]["body"]]
|
||||
if stile:
|
||||
teile += ["## Arbeitsweise"] + [s["body"] for s in stile]
|
||||
teile += ["## Aufgabe", body]
|
||||
prompt = "\n\n".join(teile)
|
||||
|
||||
paare = [(s["name"], s["hash"]) for s in rollen + stile + [aufgabe]]
|
||||
skill_hash = hashlib.sha256(
|
||||
"\n".join(f"{n}:{h}" for n, h in paare).encode()).hexdigest()[:12]
|
||||
return prompt, skill_hash, paare
|
||||
|
||||
|
||||
def aufgaben_name(prompt: str) -> str:
|
||||
m = re.match(r"<!-- aufgabe:([a-z0-9-]+) -->", prompt)
|
||||
return m.group(1) if m else ""
|
||||
Reference in New Issue
Block a user