perf(dashboard): keep tools in focused analytics usage (#18511)

This commit is contained in:
Rod Boev 2026-07-13 21:25:15 -04:00 committed by kshitij
parent c1639322c2
commit e3ce092f8d
4 changed files with 45 additions and 9 deletions

View File

@ -196,17 +196,23 @@ 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().
def get_usage_breakdown(self, days: int = 30, source: str = None) -> Dict[str, Any]:
"""Return the analytics-usage payload 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.
that reference skill_view or skill_manage are loaded from SQLite, while
still preserving the per-tool breakdown used by the dashboard route.
"""
cutoff = time.time() - (days * 86400)
tool_usage = self._get_tool_usage(cutoff, source)
skill_usage = self._get_skill_usage(cutoff, source)
return self._compute_skill_breakdown(skill_usage)
return {
"tools": self._compute_tool_breakdown(tool_usage),
"skills": self._compute_skill_breakdown(skill_usage),
}
def get_skill_breakdown(self, days: int = 30, source: str = None) -> Dict[str, Any]:
return self.get_usage_breakdown(days=days, source=source)["skills"]
# =========================================================================
# Data gathering (SQL queries)

View File

@ -14073,7 +14073,7 @@ def _get_usage_analytics(days: int = 30, profile: Optional[str] = None):
FROM sessions WHERE started_at > ?
""", (cutoff,))
totals = dict(cur3.fetchone())
skills = InsightsEngine(db).get_skill_breakdown(days=days)
usage = InsightsEngine(db).get_usage_breakdown(days=days)
return {
"daily": daily,
@ -14083,10 +14083,10 @@ def _get_usage_analytics(days: int = 30, profile: Optional[str] = None):
"by_task": _aux_task_summary(aux_rows),
"totals": totals,
"period_days": days,
"skills": skills,
"skills": usage["skills"],
# Per-tool-name call counts (already computed by InsightsEngine);
# the desktop Capabilities page aggregates these per toolset.
"tools": insights_report.get("tools", []),
"tools": usage["tools"],
}
finally:
db.close()

View File

@ -479,6 +479,13 @@ class TestInsightsPopulated:
focused = engine.get_skill_breakdown(days=30)
assert focused == full["skills"]
def test_get_usage_breakdown_matches_full_generate(self, populated_db):
engine = InsightsEngine(populated_db)
full = engine.generate(days=30)
focused = engine.get_usage_breakdown(days=30)
assert focused["skills"] == full["skills"]
assert focused["tools"] == full["tools"]
def test_get_skill_breakdown_respects_source_filter(self, populated_db):
engine = InsightsEngine(populated_db)
# Only s1 (cli) has skill_view "github-pr-workflow"

View File

@ -2226,11 +2226,34 @@ class TestNewEndpoints:
"""get_usage_analytics must call get_skill_breakdown, not generate()."""
from unittest.mock import patch
from agent.insights import InsightsEngine
from hermes_state import SessionDB
db = SessionDB()
try:
db.create_session(
session_id="usage-tools-test",
source="cli",
model="anthropic/claude-sonnet-4",
)
db.update_token_counts(
"usage-tools-test",
input_tokens=10,
output_tokens=5,
)
db.append_message(
"usage-tools-test",
role="tool",
content="read output",
tool_name="read_file",
)
finally:
db.close()
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()
assert any(tool["tool"] == "read_file" for tool in resp.json()["tools"])
# ---------------------------------------------------------------------------
# Model context length: normalize/denormalize + /api/model/info