Ship Herman chat, Hermes KPI explorer, and intelligence analytics.

Adds floating Herman assistant with voice, Hermes technical KPI dashboard with file explorer, and a wins/kansen analytics pulse with Hermes findings hooks. Includes optional local HTTPS via Caddy for mic permissions (public certs only).
This commit is contained in:
Aissa
2026-08-01 17:50:45 +00:00
parent 62239f0a4e
commit 5e43b6b246
28 changed files with 2686 additions and 1149 deletions
+126 -1
View File
@@ -2369,18 +2369,128 @@ class HermesSearchBody(BaseModel):
limit: int = Field(default=15, ge=1, le=30)
def _hermes_feed_base() -> str:
return (getattr(settings, "HERMES_FEED_URL", None) or "http://10.4.7.10:8755").rstrip("/")
@admin_router.get("/hermes/stats")
async def hermes_stats_proxy() -> dict[str, Any]:
import httpx
try:
async with httpx.AsyncClient(timeout=10.0) as client:
r = await client.get(f"{_hermes_feed_base()}/stats")
if r.status_code == 200:
return r.json()
except Exception:
pass
async with httpx.AsyncClient(timeout=30.0) as client:
r = await client.get(f"{settings.TOOLS_API_URL.rstrip('/')}/brain/stats")
r.raise_for_status()
return r.json()
@admin_router.get("/hermes/kpis")
async def hermes_kpis_proxy() -> dict[str, Any]:
"""Technical KPIs from Hermes Agent + Herman cockpit event counts."""
import httpx
from app.db import fetch_all
payload: dict[str, Any] = {
"online": False,
"synced_at": None,
"source": "hermes-agent",
"overview": {},
"buckets": [],
"tools_top": [],
"sources": [],
"herman_cockpit": [],
}
try:
async with httpx.AsyncClient(timeout=12.0) as client:
r = await client.get(f"{_hermes_feed_base()}/kpis")
if r.status_code == 200:
payload.update(r.json())
except Exception as exc:
payload["error"] = str(exc)[:160]
try:
rows = fetch_all(
"""
SELECT event_type, COUNT(*)::int AS count
FROM agent_events
GROUP BY event_type
ORDER BY COUNT(*) DESC
LIMIT 12
"""
)
payload["herman_cockpit"] = [
{"event_type": r.get("event_type") or "unknown", "count": int(r.get("count") or 0)}
for r in (rows or [])
]
except Exception:
payload["herman_cockpit"] = []
return payload
@admin_router.get("/hermes/bucket/{key}")
async def hermes_bucket_detail_proxy(key: str, limit: int = 60, tool: str | None = None) -> dict[str, Any]:
"""Bucket detail: recent tool events + touched files for explorer popup."""
import httpx
params: dict[str, Any] = {"limit": limit}
if tool:
params["tool"] = tool
async with httpx.AsyncClient(timeout=20.0) as client:
r = await client.get(f"{_hermes_feed_base()}/bucket/{key}", params=params)
if r.status_code == 404:
return {"error": "unknown bucket", "key": key, "events": [], "files": []}
r.raise_for_status()
return r.json()
@admin_router.get("/hermes/explorer")
async def hermes_explorer_proxy(path: str | None = None) -> dict[str, Any]:
"""Browse Hermes Agent filesystem (read-only, restricted roots)."""
import httpx
params = {}
if path:
params["path"] = path
async with httpx.AsyncClient(timeout=15.0) as client:
r = await client.get(f"{_hermes_feed_base()}/explorer", params=params)
r.raise_for_status()
return r.json()
@admin_router.get("/hermes/explorer/file")
async def hermes_explorer_file_proxy(path: str) -> dict[str, Any]:
"""Preview a file from Hermes Agent filesystem."""
import httpx
async with httpx.AsyncClient(timeout=20.0) as client:
r = await client.get(f"{_hermes_feed_base()}/explorer/file", params={"path": path})
if r.status_code in (403, 404, 400):
try:
err = (r.json() or {}).get("error") or r.text
except Exception:
err = r.text
return {"error": err, "path": path, "content": None}
r.raise_for_status()
return r.json()
@admin_router.get("/hermes/conversations")
async def hermes_conversations_proxy(limit: int = 50) -> dict[str, Any]:
"""Live conversations from Hermes Agent (not June Postgres archive)."""
import httpx
try:
async with httpx.AsyncClient(timeout=15.0) as client:
r = await client.get(f"{_hermes_feed_base()}/conversations", params={"limit": limit})
if r.status_code == 200:
return r.json()
except Exception:
pass
async with httpx.AsyncClient(timeout=30.0) as client:
r = await client.get(f"{settings.TOOLS_API_URL.rstrip('/')}/brain/conversations", params={"limit": limit})
r.raise_for_status()
@@ -2393,10 +2503,18 @@ async def hermes_feed_proxy(
limit: int = 80,
offset: int = 0,
) -> dict[str, Any]:
"""Live Telegram feed from Hermes Agent state.db."""
import httpx
params = {"limit": limit, "offset": offset}
params: dict[str, Any] = {"limit": limit, "offset": offset}
if chat_id is not None:
params["chat_id"] = chat_id
try:
async with httpx.AsyncClient(timeout=20.0) as client:
r = await client.get(f"{_hermes_feed_base()}/feed", params=params)
if r.status_code == 200:
return r.json()
except Exception:
pass
async with httpx.AsyncClient(timeout=30.0) as client:
r = await client.get(f"{settings.TOOLS_API_URL.rstrip('/')}/brain/feed", params=params)
r.raise_for_status()
@@ -2469,6 +2587,13 @@ async def hermes_pa_slot_screenshot(label: str):
@admin_router.get("/hermes/users")
async def hermes_users_proxy() -> dict[str, Any]:
import httpx
try:
async with httpx.AsyncClient(timeout=10.0) as client:
r = await client.get(f"{_hermes_feed_base()}/users")
if r.status_code == 200 and (r.json().get("users") is not None):
return r.json()
except Exception:
pass
users: list[dict[str, Any]] = []
try:
async with httpx.AsyncClient(timeout=8.0) as client: