From c1639322c2daf1a1c0df10ecb2d5539a8ab0ec0f Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Thu, 4 Jun 2026 06:53:33 -0400 Subject: [PATCH] perf(dashboard): skip full InsightsEngine on /api/analytics/usage (#18511) --- agent/insights.py | 16 +++++++++++ hermes_cli/web_server.py | 11 +------- tests/agent/test_insights.py | 43 +++++++++++++++++++++++++++++ tests/hermes_cli/test_web_server.py | 9 ++++++ 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/agent/insights.py b/agent/insights.py index fd4083b1b724e..16edddad4db01 100644 --- a/agent/insights.py +++ b/agent/insights.py @@ -196,6 +196,18 @@ class InsightsEngine: "top_sessions": top_sessions, } + def get_skill_breakdown(self, days: int = 30, source: str = None) -> Dict[str, Any]: + """Return only the skills section without running a full generate(). + + Uses the instr()-prefiltered _get_skill_usage query so only messages + that reference skill_view or skill_manage are loaded from SQLite, + avoiding the cost of _get_sessions, _get_tool_usage, and the other + compute passes that generate() performs. + """ + cutoff = time.time() - (days * 86400) + skill_usage = self._get_skill_usage(cutoff, source) + return self._compute_skill_breakdown(skill_usage) + # ========================================================================= # Data gathering (SQL queries) # ========================================================================= @@ -254,6 +266,8 @@ class InsightsEngine: " JOIN sessions s ON s.id = m.session_id" " WHERE s.started_at >= ? AND s.source = ?" " AND m.role = 'assistant' AND m.tool_calls IS NOT NULL" + " AND (instr(m.tool_calls, 'skill_view') > 0" + " OR instr(m.tool_calls, 'skill_manage') > 0)" ) _GET_SKILL_CALLS_ALL = ( "SELECT m.tool_calls, m.timestamp" @@ -261,6 +275,8 @@ class InsightsEngine: " JOIN sessions s ON s.id = m.session_id" " WHERE s.started_at >= ?" " AND m.role = 'assistant' AND m.tool_calls IS NOT NULL" + " AND (instr(m.tool_calls, 'skill_view') > 0" + " OR instr(m.tool_calls, 'skill_manage') > 0)" ) def _get_sessions(self, cutoff: float, source: str = None) -> List[Dict]: diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 3168d44209c3e..2d20fff3cb3da 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -14073,16 +14073,7 @@ def _get_usage_analytics(days: int = 30, profile: Optional[str] = None): FROM sessions WHERE started_at > ? """, (cutoff,)) totals = dict(cur3.fetchone()) - insights_report = InsightsEngine(db).generate(days=days) - skills = insights_report.get("skills", { - "summary": { - "total_skill_loads": 0, - "total_skill_edits": 0, - "total_skill_actions": 0, - "distinct_skills_used": 0, - }, - "top_skills": [], - }) + skills = InsightsEngine(db).get_skill_breakdown(days=days) return { "daily": daily, diff --git a/tests/agent/test_insights.py b/tests/agent/test_insights.py index 73bce06f4fb41..7215d555d1c06 100644 --- a/tests/agent/test_insights.py +++ b/tests/agent/test_insights.py @@ -473,6 +473,49 @@ class TestInsightsPopulated: # And with the index present, the pin stays. assert "INDEXED BY" in InsightsEngine._GET_TOOL_CALLS_ALL + def test_get_skill_breakdown_matches_full_generate(self, populated_db): + engine = InsightsEngine(populated_db) + full = engine.generate(days=30) + focused = engine.get_skill_breakdown(days=30) + assert focused == full["skills"] + + def test_get_skill_breakdown_respects_source_filter(self, populated_db): + engine = InsightsEngine(populated_db) + # Only s1 (cli) has skill_view "github-pr-workflow" + focused = engine.get_skill_breakdown(days=30, source="cli") + skill_names = [s["skill"] for s in focused["top_skills"]] + assert "github-pr-workflow" in skill_names + # github-code-review was in discord (s4), not cli + assert "github-code-review" not in skill_names + + def test_get_skill_breakdown_empty_db(self, db): + focused = InsightsEngine(db).get_skill_breakdown(days=30) + assert focused == { + "summary": { + "total_skill_loads": 0, + "total_skill_edits": 0, + "total_skill_actions": 0, + "distinct_skills_used": 0, + }, + "top_skills": [], + } + + def test_get_skill_usage_prefilter_ignores_non_skill_substring(self, db): + # "my_skill_view_helper" contains "skill_view" as a substring; instr() + # will match but the Python-side name check keeps the set clean. + # More importantly, messages with no skill_* tools must be excluded. + db.create_session(session_id="sx", source="cli", model="gpt-4o") + db.append_message( + "sx", + role="assistant", + content="Just using read_file.", + tool_calls=[{"function": {"name": "read_file", "arguments": '{"path":"/tmp/x"}'}}], + ) + db._conn.commit() + focused = InsightsEngine(db).get_skill_breakdown(days=30) + assert focused["summary"]["total_skill_actions"] == 0 + assert focused["top_skills"] == [] + # ========================================================================= # Formatting diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index f29284c0ae9c5..16dd0f58a768c 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -2222,6 +2222,15 @@ class TestNewEndpoints: assert top_skill["total_count"] == 1 assert top_skill["last_used_at"] is not None + def test_analytics_usage_skips_full_insights_generate(self): + """get_usage_analytics must call get_skill_breakdown, not generate().""" + from unittest.mock import patch + from agent.insights import InsightsEngine + + with patch.object(InsightsEngine, "generate") as mock_generate: + resp = self.client.get("/api/analytics/usage?days=7") + assert resp.status_code == 200 + mock_generate.assert_not_called() # --------------------------------------------------------------------------- # Model context length: normalize/denormalize + /api/model/info