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 -1
View File
@@ -10,6 +10,8 @@
let agent = $state<any>(null)
let cronJobs = $state<any[]>([])
let skills = $state<any[]>([])
let learned = $state<any>(null)
let insights = $state<any>(null)
let pubkey = $state('')
// ---- Status laden ----
@@ -29,14 +31,18 @@
async function loadCockpit() {
cockpitLoading = true
try {
const [a, c, s] = await Promise.all([
const [a, c, s, l, i] = await Promise.all([
api('/api/hermes/agent'),
api('/api/hermes/cron'),
api('/api/hermes/skills'),
api('/api/hermes/learned'),
api('/api/hermes/insights'),
])
agent = a
cronJobs = c.jobs || []
skills = s.skills || []
learned = l
insights = i
} catch { agent = null }
cockpitLoading = false
}
@@ -118,6 +124,54 @@
</div>
</div>
<!-- Aktivität (Phase 8) -->
{#if insights && (insights.sessions || insights.total_tokens)}
<div class="tile" style="margin-bottom:10px">
<div style="margin-bottom:8px"><b>Aktivität</b> <span class="card-sub" style="font-size:11px">(letzte 30 Tage)</span></div>
<div style="display:flex;gap:18px;flex-wrap:wrap;font-size:12.5px;margin-bottom:8px">
{#if insights.sessions}<span><b style="color:var(--accent)">{insights.sessions}</b> <span class="card-sub">Sessions</span></span>{/if}
{#if insights.messages}<span><b style="color:var(--accent)">{insights.messages}</b> <span class="card-sub">Nachrichten</span></span>{/if}
{#if insights.tool_calls}<span><b style="color:var(--accent)">{insights.tool_calls}</b> <span class="card-sub">Tool-Calls</span></span>{/if}
{#if insights.total_tokens}<span><b style="color:var(--accent)">{insights.total_tokens}</b> <span class="card-sub">Tokens</span></span>{/if}
{#if insights.active_time}<span><b style="color:var(--accent)">{insights.active_time}</b> <span class="card-sub">aktiv</span></span>{/if}
</div>
{#if insights.top_tools?.length}
<div class="card-sub" style="font-size:11px;margin-bottom:4px">Meistgenutzte Tools</div>
<div style="display:flex;flex-direction:column;gap:3px">
{#each insights.top_tools.slice(0, 6) as t}
<div style="display:flex;align-items:center;gap:8px;font-size:12px">
<span style="min-width:230px">{t.name}</span>
<span style="flex:1;height:6px;background:rgba(255,255,255,.05);border-radius:3px;overflow:hidden">
<span style="display:block;height:100%;width:{t.pct}%;background:var(--accent)"></span>
</span>
<span class="card-sub" style="font-size:11px;min-width:64px;text-align:right">{t.calls}× · {t.pct}%</span>
</div>
{/each}
</div>
{/if}
</div>
{/if}
<!-- Was Hermes gelernt hat (Phase 8) -->
{#if learned?.user_facts?.length || learned?.memory}
<div class="tile" style="margin-bottom:10px">
<div style="margin-bottom:6px"><b>Was Hermes über dich gelernt hat</b>
<span class="card-sub" style="font-size:11px">(automatisches Profil)</span></div>
{#if learned.user_facts?.length}
<ul style="margin:0 0 6px;padding-left:18px;display:flex;flex-direction:column;gap:3px">
{#each learned.user_facts as f}
<li style="font-size:12.5px;line-height:1.45">{f}</li>
{/each}
</ul>
{/if}
{#if learned.memory}
<div class="card-sub" style="font-size:11.5px;border-top:1px solid rgba(255,255,255,.06);padding-top:6px;margin-top:2px">
{learned.memory}
</div>
{/if}
</div>
{/if}
<!-- Cron-Scheduler -->
<div class="tile" style="margin-bottom:10px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
+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]
+14
View File
@@ -210,6 +210,20 @@ def hermes_skills():
return {"skills": skills()}
@router.get("/hermes/learned", dependencies=[Depends(auth)])
def hermes_learned():
"""Phase 8 — was Hermes ueber den Nutzer gelernt hat (USER.md/MEMORY.md)."""
from hermes_control import learned_profile
return learned_profile()
@router.get("/hermes/insights", dependencies=[Depends(auth)])
def hermes_insights():
"""Phase 8 — Aktivitaets-Kennzahlen (Sessions/Tokens/Top-Tools)."""
from hermes_control import insights
return insights()
@router.get("/hermes/pubkey", dependencies=[Depends(auth)])
def hermes_pubkey():
"""Gibt den SSH-Public-Key des Hermes Agent zurueck (fuer Windows authorized_keys)."""
+39 -39
View File
File diff suppressed because one or more lines are too long