mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 01:21:43 +00:00
feat: add skill analytics to the dashboard
Expose skill usage in analytics so the dashboard and insights output can
show which skills the agent loads and manages over time.
This adds skill aggregation to the InsightsEngine by extracting
`skill_view` and `skill_manage` calls from assistant tool_calls,
computing per-skill totals, and including the results in both terminal
and gateway insights formatting. It also extends the dashboard analytics
API and Analytics page to render a Top Skills table.
Terminology is aligned with the skills docs:
- Agent Loaded = `skill_view` events
- Agent Managed = `skill_manage` actions
Architecture:
- agent/insights.py collects and aggregates per-skill usage
- hermes_cli/web_server.py exposes `skills` on `/api/analytics/usage`
- web/src/lib/api.ts adds analytics skill response types
- web/src/pages/AnalyticsPage.tsx renders the Top Skills table
- web/src/i18n/{en,zh}.ts updates user-facing labels
Tests:
- tests/agent/test_insights.py covers skill aggregation and formatting
- tests/hermes_cli/test_web_server.py covers analytics API contract
including the `skills` payload
- verified with `cd web && npm run build`
Files changed:
- agent/insights.py
- hermes_cli/web_server.py
- tests/agent/test_insights.py
- tests/hermes_cli/test_web_server.py
- web/src/i18n/en.ts
- web/src/i18n/types.ts
- web/src/i18n/zh.ts
- web/src/lib/api.ts
- web/src/pages/AnalyticsPage.tsx
This commit is contained in:
parent
1525624904
commit
857b543543
9 changed files with 399 additions and 6 deletions
|
|
@ -101,14 +101,19 @@ class TestWebServerEndpoints:
|
|||
"""Test the FastAPI REST endpoints using Starlette TestClient."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup_test_client(self):
|
||||
"""Create a TestClient — import is deferred to avoid requiring fastapi."""
|
||||
def _setup_test_client(self, monkeypatch, _isolate_hermes_home):
|
||||
"""Create a TestClient and isolate the state DB under the test HERMES_HOME."""
|
||||
try:
|
||||
from starlette.testclient import TestClient
|
||||
except ImportError:
|
||||
pytest.skip("fastapi/starlette not installed")
|
||||
|
||||
import hermes_state
|
||||
from hermes_constants import get_hermes_home
|
||||
from hermes_cli.web_server import app, _SESSION_TOKEN
|
||||
|
||||
monkeypatch.setattr(hermes_state, "DEFAULT_DB_PATH", get_hermes_home() / "state.db")
|
||||
|
||||
self.client = TestClient(app)
|
||||
self.client.headers["Authorization"] = f"Bearer {_SESSION_TOKEN}"
|
||||
|
||||
|
|
@ -511,12 +516,18 @@ class TestNewEndpoints:
|
|||
"""Tests for session detail, logs, cron, skills, tools, raw config, analytics."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup(self):
|
||||
def _setup(self, monkeypatch, _isolate_hermes_home):
|
||||
try:
|
||||
from starlette.testclient import TestClient
|
||||
except ImportError:
|
||||
pytest.skip("fastapi/starlette not installed")
|
||||
|
||||
import hermes_state
|
||||
from hermes_constants import get_hermes_home
|
||||
from hermes_cli.web_server import app, _SESSION_TOKEN
|
||||
|
||||
monkeypatch.setattr(hermes_state, "DEFAULT_DB_PATH", get_hermes_home() / "state.db")
|
||||
|
||||
self.client = TestClient(app)
|
||||
self.client.headers["Authorization"] = f"Bearer {_SESSION_TOKEN}"
|
||||
|
||||
|
|
@ -692,8 +703,74 @@ class TestNewEndpoints:
|
|||
assert "daily" in data
|
||||
assert "by_model" in data
|
||||
assert "totals" in data
|
||||
assert "skills" in data
|
||||
assert isinstance(data["daily"], list)
|
||||
assert "total_sessions" in data["totals"]
|
||||
assert data["skills"] == {
|
||||
"summary": {
|
||||
"total_skill_loads": 0,
|
||||
"total_skill_edits": 0,
|
||||
"total_skill_actions": 0,
|
||||
"distinct_skills_used": 0,
|
||||
},
|
||||
"top_skills": [],
|
||||
}
|
||||
|
||||
def test_analytics_usage_includes_skill_breakdown(self):
|
||||
from hermes_state import SessionDB
|
||||
|
||||
db = SessionDB()
|
||||
try:
|
||||
db.create_session(
|
||||
session_id="skills-analytics-test",
|
||||
source="cli",
|
||||
model="anthropic/claude-sonnet-4",
|
||||
)
|
||||
db.update_token_counts(
|
||||
"skills-analytics-test",
|
||||
input_tokens=120,
|
||||
output_tokens=45,
|
||||
)
|
||||
db.append_message(
|
||||
"skills-analytics-test",
|
||||
role="assistant",
|
||||
content="Loading and updating skills.",
|
||||
tool_calls=[
|
||||
{
|
||||
"function": {
|
||||
"name": "skill_view",
|
||||
"arguments": '{"name":"github-pr-workflow"}',
|
||||
}
|
||||
},
|
||||
{
|
||||
"function": {
|
||||
"name": "skill_manage",
|
||||
"arguments": '{"name":"github-code-review"}',
|
||||
}
|
||||
},
|
||||
],
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
resp = self.client.get("/api/analytics/usage?days=7")
|
||||
assert resp.status_code == 200
|
||||
|
||||
data = resp.json()
|
||||
assert data["skills"]["summary"] == {
|
||||
"total_skill_loads": 1,
|
||||
"total_skill_edits": 1,
|
||||
"total_skill_actions": 2,
|
||||
"distinct_skills_used": 2,
|
||||
}
|
||||
assert len(data["skills"]["top_skills"]) == 2
|
||||
|
||||
top_skill = data["skills"]["top_skills"][0]
|
||||
assert top_skill["skill"] == "github-pr-workflow"
|
||||
assert top_skill["view_count"] == 1
|
||||
assert top_skill["manage_count"] == 0
|
||||
assert top_skill["total_count"] == 1
|
||||
assert top_skill["last_used_at"] is not None
|
||||
|
||||
def test_session_token_endpoint_removed(self):
|
||||
"""GET /api/auth/session-token no longer exists."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue