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 agent = $state<any>(null)
|
||||||
let cronJobs = $state<any[]>([])
|
let cronJobs = $state<any[]>([])
|
||||||
let skills = $state<any[]>([])
|
let skills = $state<any[]>([])
|
||||||
|
let learned = $state<any>(null)
|
||||||
|
let insights = $state<any>(null)
|
||||||
let pubkey = $state('')
|
let pubkey = $state('')
|
||||||
|
|
||||||
// ---- Status laden ----
|
// ---- Status laden ----
|
||||||
@@ -29,14 +31,18 @@
|
|||||||
async function loadCockpit() {
|
async function loadCockpit() {
|
||||||
cockpitLoading = true
|
cockpitLoading = true
|
||||||
try {
|
try {
|
||||||
const [a, c, s] = await Promise.all([
|
const [a, c, s, l, i] = await Promise.all([
|
||||||
api('/api/hermes/agent'),
|
api('/api/hermes/agent'),
|
||||||
api('/api/hermes/cron'),
|
api('/api/hermes/cron'),
|
||||||
api('/api/hermes/skills'),
|
api('/api/hermes/skills'),
|
||||||
|
api('/api/hermes/learned'),
|
||||||
|
api('/api/hermes/insights'),
|
||||||
])
|
])
|
||||||
agent = a
|
agent = a
|
||||||
cronJobs = c.jobs || []
|
cronJobs = c.jobs || []
|
||||||
skills = s.skills || []
|
skills = s.skills || []
|
||||||
|
learned = l
|
||||||
|
insights = i
|
||||||
} catch { agent = null }
|
} catch { agent = null }
|
||||||
cockpitLoading = false
|
cockpitLoading = false
|
||||||
}
|
}
|
||||||
@@ -118,6 +124,54 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Cron-Scheduler -->
|
||||||
<div class="tile" style="margin-bottom:10px">
|
<div class="tile" style="margin-bottom:10px">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
|
<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 rows
|
||||||
|
|
||||||
return _cached("skills", _load) # type: ignore[return-value]
|
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()}
|
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)])
|
@router.get("/hermes/pubkey", dependencies=[Depends(auth)])
|
||||||
def hermes_pubkey():
|
def hermes_pubkey():
|
||||||
"""Gibt den SSH-Public-Key des Hermes Agent zurueck (fuer Windows authorized_keys)."""
|
"""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