feat(v9): Phase 8 — Cockpit zeigt Lernen & Aktivität

- hermes_control.py: learned_profile() liest USER.md (Curator-Nutzerprofil,
  § -getrennt) + MEMORY.md; insights() zieht Sessions/Tokens/Tool-Calls +
  Top-Tools aus `hermes insights` (Regex, best effort).
- routers/hermes.py: GET /api/hermes/{learned,insights}.
- HermesPanel-Cockpit: Kachel "Aktivität" (Kennzahlen + Top-Tool-Balken) und
  "Was Hermes über dich gelernt hat" (USER.md-Fakten) — beantwortet sichtbar,
  dass der Agent automatisch mitlernt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-24 17:06:51 +02:00
parent 7e8ed585a9
commit d4aa7eb8cf
4 changed files with 163 additions and 40 deletions
+55
View File
@@ -149,3 +149,58 @@ def skills() -> list[dict]:
return rows
return _cached("skills", _load) # type: ignore[return-value]
def learned_profile() -> dict:
"""Was Hermes ueber den Nutzer gelernt hat (Phase 8).
`~/.hermes/memories/USER.md` ist das vom Curator destillierte Nutzerprofil
(Eintraege mit `§` getrennt), `MEMORY.md` allgemeine gelernte Fakten. Direkter
Beleg, dass der Agent automatisch mitlernt.
"""
def _read(name: str) -> str:
try:
return (HERMES_HOME / "memories" / name).read_text(errors="replace").strip()
except Exception:
return ""
def _load() -> dict:
user_raw = _read("USER.md")
facts = [s.strip() for s in user_raw.split("§") if s.strip()]
return {"user_facts": facts, "memory": _read("MEMORY.md")}
return _cached("learned", _load) # type: ignore[return-value]
def insights() -> dict:
"""Aktivitaets-Kennzahlen via `hermes insights` (Phase 8).
Die CLI rendert ein Box-Art-Layout (kein --json) -> Headline-Zahlen + Top-Tools
per Regex herausziehen (best effort; faellt leer aus, wenn sich das Format aendert).
"""
def _num(pat: str, text: str):
m = re.search(pat, text)
return m.group(1).strip() if m else None
def _load() -> dict:
txt = _run_cli(["insights", "--days", "30"])
out: dict = {
"sessions": _num(r"Sessions:\s+([\d,]+)", txt),
"messages": _num(r"Messages:\s+([\d,]+)", txt),
"tool_calls": _num(r"Tool calls:\s+([\d,]+)", txt),
"total_tokens": _num(r"Total tokens:\s+([\d,]+)", txt),
"active_time": _num(r"Active time:\s+(~?[\dhm ]+?)\s{2,}", txt),
"top_tools": [],
}
# "Top Tools"-Sektion: Zeilen <name> <calls> <pct>%
sec = txt.split("Top Tools", 1)
if len(sec) == 2:
for ln in sec[1].splitlines():
m = re.match(r"\s*(\S.*?)\s+(\d+)\s+([\d.]+)%\s*$", ln)
if m:
out["top_tools"].append(
{"name": m.group(1), "calls": int(m.group(2)), "pct": float(m.group(3))}
)
return out
return _cached("insights", _load) # type: ignore[return-value]