Skills-Ansicht: Umbenennung + aktiv/inaktiv + Karten-Ergebnisse im Auftragsbuch
User-Feedback 15.07.:
1) 'Von allein' heisst jetzt 'Skills' (id #eigenleben bleibt, Links halten).
2) Jeder Skill traegt aktiv/inaktiv - Quelle ist Hermes' EIGENE Wahrheit
(hermes_cli.banner.get_available_skills via Hermes-venv, dieselbe Logik
wie das eingebaute WebUI: Plattform/Voraussetzungen/Config). Bewusst NUR
die Datenquelle wiederverwendet, nicht deren UI - Skill-VERWALTUNG bleibt
im Hermes-GUI-Tab, alles andere waere Bloat/Doppelbau.
3) Auftragsbuch: fertige Queue-Karten sind aufklappbar und zeigen das
ERGEBNIS des Workers (kanban_complete-Summary, lazy via
GET /api/ideen/{id}/ergebnis) - 'Dinge werden getestet, ich sehe aber
das Ergebnis nicht' ist damit zu.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,3 +55,8 @@ def archive(body: TaskIn) -> dict:
|
||||
@router.get("/ideen/{id}/log")
|
||||
def get_log(id: str) -> dict:
|
||||
return ideen.log_of(id)
|
||||
|
||||
|
||||
@router.get("/ideen/{id}/ergebnis")
|
||||
def get_ergebnis(id: str) -> dict:
|
||||
return ideen.ergebnis_of(id)
|
||||
|
||||
@@ -18,11 +18,15 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
SKILLS_DIR = Path.home() / ".hermes" / "skills"
|
||||
BUNDLED_DIR = Path.home() / ".hermes" / "hermes-agent" / "skills"
|
||||
REPO_DIR = Path.home() / "mission-control-v2" / "deploy" / "skills"
|
||||
HERMES_SRC = Path.home() / ".hermes" / "hermes-agent"
|
||||
HERMES_PY = HERMES_SRC / "venv" / "bin" / "python"
|
||||
USAGE_FILE = SKILLS_DIR / ".usage.json"
|
||||
AUFTRAGSBUCH_FILE = Path("/srv/models/mc2-auftragsbuch.json")
|
||||
ABLEHNUNGEN_FILE = Path("/srv/models/mc2-ablehnungen.jsonl")
|
||||
@@ -56,6 +60,35 @@ def _norm(s: str) -> str:
|
||||
return re.sub(r"[\s_-]+", "", (s or "").lower())
|
||||
|
||||
|
||||
# aktiv/inaktiv: dieselbe Wahrheit wie Hermes selbst (und sein eingebautes WebUI) —
|
||||
# hermes_cli.banner.get_available_skills() prüft Plattform/Voraussetzungen/Config und
|
||||
# liefert, was der Agent WIRKLICH laden kann. Wir rufen sie im Hermes-venv auf (MC2
|
||||
# läuft in eigenem Python) und cachen großzügig — der Skill-Satz ändert sich selten.
|
||||
_aktiv_cache: dict = {"ts": 0.0, "namen": None}
|
||||
_AKTIV_EVERY = 600.0 # s
|
||||
|
||||
|
||||
def _aktive_namen() -> set[str] | None:
|
||||
"""Normalisierte Namen aller ladbaren Skills; None = nicht ermittelbar (Dev-Modus)."""
|
||||
if not HERMES_PY.is_file():
|
||||
return None
|
||||
now = time.time()
|
||||
if _aktiv_cache["namen"] is not None and now - _aktiv_cache["ts"] < _AKTIV_EVERY:
|
||||
return _aktiv_cache["namen"]
|
||||
code = ("from hermes_cli.banner import get_available_skills;import json;"
|
||||
"print(json.dumps(get_available_skills()))")
|
||||
try:
|
||||
r = subprocess.run([str(HERMES_PY), "-c", code], capture_output=True,
|
||||
text=True, timeout=60, cwd=str(HERMES_SRC))
|
||||
idx = r.stdout.find("{")
|
||||
kategorien = json.loads(r.stdout[idx:]) if idx >= 0 else {}
|
||||
namen = {_norm(n) for liste in kategorien.values() for n in liste}
|
||||
except Exception:
|
||||
return _aktiv_cache["namen"] # alter Stand ist besser als flackerndes None
|
||||
_aktiv_cache.update(ts=now, namen=namen)
|
||||
return namen
|
||||
|
||||
|
||||
def list_skills() -> list[dict]:
|
||||
"""Alle Skills mit Beschreibung, Herkunft und Nutzungszahlen (leer im Dev-Modus)."""
|
||||
if not SKILLS_DIR.is_dir():
|
||||
@@ -65,6 +98,7 @@ def list_skills() -> list[dict]:
|
||||
except Exception:
|
||||
usage_raw = {}
|
||||
usage = {_norm(k): v for k, v in usage_raw.items()}
|
||||
aktive = _aktive_namen()
|
||||
|
||||
def eintrag(md: Path, rel: str, kategorie: str | None) -> dict:
|
||||
try:
|
||||
@@ -79,10 +113,14 @@ def list_skills() -> list[dict]:
|
||||
herkunft = "selbst"
|
||||
kurz = rel.split("/")[-1]
|
||||
u = usage.get(_norm(kurz)) or usage.get(_norm(fm.get("name", ""))) or {}
|
||||
aktiv = None
|
||||
if aktive is not None:
|
||||
aktiv = _norm(kurz) in aktive or _norm(fm.get("name", "")) in aktive
|
||||
return {
|
||||
"id": rel,
|
||||
"name": fm.get("name") or kurz,
|
||||
"kategorie": kategorie,
|
||||
"aktiv": aktiv,
|
||||
"beschreibung": fm.get("description") or "",
|
||||
"version": fm.get("version") or "",
|
||||
"autor": fm.get("author") or "",
|
||||
|
||||
@@ -35,6 +35,10 @@ _LIST_EVERY = 15.0 # s
|
||||
_log_cache: dict = {"ts": 0.0, "task_id": None, "data": None}
|
||||
_LOG_EVERY = 4.0 # s
|
||||
|
||||
# Cache für Ergebnis (Abschluss-Zusammenfassung) — ändert sich nach done nie mehr
|
||||
_erg_cache: dict = {}
|
||||
_ERG_MAX = 40 # simple Deckelung gegen unbegrenztes Wachsen
|
||||
|
||||
|
||||
def _available() -> bool:
|
||||
return os.name == "posix" and HERMES.is_file()
|
||||
@@ -213,6 +217,43 @@ def archive_task(task_id: str) -> dict:
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
def ergebnis_of(task_id: str) -> dict:
|
||||
"""Das ERGEBNIS einer fertigen Aufgabe — die Abschluss-Zusammenfassung des Workers.
|
||||
|
||||
User-Wunsch 15.07.: „im Auftragsbuch werden Dinge getestet, ich sehe aber das
|
||||
Ergebnis nicht." Die Wahrheit liegt beim kanban_complete-Aufruf (summary/result)
|
||||
im jüngsten `completed`-Event; `list --json` trägt sie nicht → lazy per `show`,
|
||||
nur wenn der User die Karte aufklappt. Fertige Ergebnisse ändern sich nie → Cache.
|
||||
"""
|
||||
if not _available():
|
||||
return {"available": False, "summary": "", "result": ""}
|
||||
if not _TASK_ID_RX.match(task_id or ""):
|
||||
return {"available": True, "summary": "", "result": ""}
|
||||
if task_id in _erg_cache:
|
||||
return _erg_cache[task_id]
|
||||
|
||||
try:
|
||||
r = _hermes(["show", task_id, "--json"], timeout=30)
|
||||
d = _parse_json(r.stdout)
|
||||
except Exception:
|
||||
log.warning("ideen: kanban show %s (ergebnis) fehlgeschlagen", task_id, exc_info=True)
|
||||
return {"available": True, "summary": "", "result": ""}
|
||||
summary, result = "", ""
|
||||
if isinstance(d, dict):
|
||||
if isinstance(d.get("result"), str):
|
||||
result = d["result"].strip()
|
||||
for ev in d.get("events") or []: # chronologisch → letztes completed gewinnt
|
||||
if isinstance(ev, dict) and ev.get("kind") == "completed":
|
||||
s = ((ev.get("payload") or {}).get("summary") or "").strip()
|
||||
if s:
|
||||
summary = s
|
||||
data = {"available": True, "summary": summary[:4000], "result": result[:4000]}
|
||||
if len(_erg_cache) >= _ERG_MAX:
|
||||
_erg_cache.pop(next(iter(_erg_cache)))
|
||||
_erg_cache[task_id] = data
|
||||
return data
|
||||
|
||||
|
||||
def log_of(task_id: str) -> dict:
|
||||
"""Worker-Log einer Idee (hermes kanban log <id>), ANSI entfernt, letzte ~120 Zeilen."""
|
||||
if not _available():
|
||||
|
||||
Reference in New Issue
Block a user