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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user