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:
@@ -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">
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)."""
|
||||
|
||||
Vendored
+39
-39
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user