diff --git a/dashboard/api.js b/dashboard/api.js index 599ac22..092a5a7 100644 --- a/dashboard/api.js +++ b/dashboard/api.js @@ -91,6 +91,8 @@ const api = { // Agent Time Monitor getAgentTime: (recompute = false) => api.get(`/api/agent-time${recompute ? '?recompute=true' : ''}`), recomputeAgentTime: () => api.post('/api/agent-time/recompute', {}), + // Agent Insights (AI usage analytics) + getAgentInsights: (recompute = false) => api.get(`/api/agent-insights${recompute ? '?recompute=true' : ''}`), // Agent Registry getAgents: () => api.get('/api/agents'), registerAgent: (data) => api.post('/api/agents/register', data), diff --git a/dashboard/index.html b/dashboard/index.html index 2985fd0..da7abd9 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -49,6 +49,7 @@ ๐ŸฅAgent Health0 ๐ŸงญSmart Router ๐Ÿ“ŠLearning Analytics + ๐Ÿง Agent Insights ๐Ÿ”„Session Replay โฑAgent Time diff --git a/dashboard/pages/agent-insights.js b/dashboard/pages/agent-insights.js new file mode 100644 index 0000000..a3d5199 --- /dev/null +++ b/dashboard/pages/agent-insights.js @@ -0,0 +1,193 @@ +// Agent Insights โ€” live AI-agent usage analytics. +// Answers: who I chat with most, who spends the most time, who's best +// suited for what, and which models I use per agent. All derived from +// real data via /api/agent-insights. + +function fmtDuration(sec) { + sec = Math.max(0, Math.round(sec || 0)); + const h = Math.floor(sec / 3600); + const m = Math.floor((sec % 3600) / 60); + const s = sec % 60; + if (h > 0) return `${h}h ${m}m`; + if (m > 0) return `${m}m ${s}s`; + return `${s}s`; +} + +function agentColor(agent) { + const palette = { + opencode: '#6c5ce7', hermes: '#00b894', gemini: '#0984e3', + jarvis: '#fd79a8', kilocode: '#e17055', codex: '#fdcb6e', + system: '#636e72', test: '#b2bec3', + }; + return palette[agent] || '#74b9ff'; +} + +let _charts = {}; +function destroyCharts() { + Object.values(_charts).forEach(c => { try { c.destroy(); } catch (e) {} }); + _charts = {}; +} + +async function renderAgentInsights() { + const content = document.getElementById('pageContent'); + content.innerHTML = ` + +
Analyzing agent usageโ€ฆ
+ `; + + let data; + try { + data = await api.getAgentInsights(); + } catch (err) { + document.getElementById('insightsBody').innerHTML = ` +
โš 
+
Could not load insights
+
${escapeHtml(err.message)}
`; + return; + } + + const t = data.totals || {}; + const pa = data.per_agent || {}; + const body = document.getElementById('insightsBody'); + + body.innerHTML = ` +
+
๐Ÿ’ฌ
${(t.total_user_turns||0).toLocaleString()}
Your chat turns
+
โฑ
${fmtDuration(t.total_time_seconds)}
Total agent time
+
๐Ÿค–
${t.agents_observed||0}
Agents observed
+
๐Ÿ’ฐ
$${(t.total_cost||0).toFixed(4)}
Tracked cost
+
+ +
+
+
๐Ÿ’ฌ Most chatted-with agents
+
+
+
+
โฑ Agents spending the most time
+
+
+
+ +
+
๐ŸŽฏ Best suited for (derived from registry + router keywords + roles)
+
+
+ +
+
๐Ÿง  Models used per agent
+
+
+ +
+
๐Ÿ“Š Per-agent breakdown
+
+
+ +

Generated ${formatDate(data.generated_at)} ยท data: chat-history, cost-history, agent-time, agent-registry, router-keywords

+ `; + + // Charts + const chats = (data.most_chatted || []).slice(0, 8); + const times = (data.most_time || []).slice(0, 8); + + _charts.chats = new Chart(document.getElementById('chatsChart'), { + type: 'bar', + data: { + labels: chats.map(c => c.display_name || c.agent), + datasets: [{ + label: 'Your chat turns', + data: chats.map(c => c.user_turns), + backgroundColor: chats.map(c => agentColor(c.agent)), + borderRadius: 6, + }], + }, + options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { precision: 0 } } } }, + }); + + _charts.time = new Chart(document.getElementById('timeChart'), { + type: 'bar', + data: { + labels: times.map(c => c.display_name || c.agent), + datasets: [{ + label: 'Time spent', + data: times.map(c => Math.round(c.time_seconds / 60)), + backgroundColor: times.map(c => agentColor(c.agent)), + borderRadius: 6, + }], + }, + options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { callbacks: { label: (ctx) => `${ctx.parsed.y} min` } } }, scales: { y: { beginAtZero: true, title: { display: true, text: 'minutes' } } } }, + }); + + // Suite cards + const suiteGrid = document.getElementById('suiteGrid'); + const suiteAgents = Object.values(pa) + .filter(a => (a.suite && (a.suite.description || a.suite.keywords?.length))) + .sort((a, b) => (b.user_turns + b.time_seconds / 60) - (a.user_turns + a.time_seconds / 60)); + if (suiteAgents.length === 0) { + suiteGrid.innerHTML = '
No role data available yet.
'; + } else { + suiteGrid.innerHTML = suiteAgents.map(a => { + const s = a.suite || {}; + const kws = (s.keywords || []).slice(0, 8).map(k => `${escapeHtml(k)}`).join(''); + return ` +
+
+ ${escapeHtml(a.display_name || a.agent)} + ${a.user_turns} chats ยท ${fmtDuration(a.time_seconds)} +
+
${escapeHtml(s.description || s.role_hint || 'โ€”')}
+
${kws || 'no keywords'}
+
`; + }).join(''); + } + + // Models per agent + const modelsRows = Object.values(pa) + .filter(a => a.top_models && a.top_models.length) + .sort((a, b) => b.tokens - a.tokens); + document.getElementById('modelsTable').innerHTML = modelsRows.length + ? `
+ + ${modelsRows.map(a => ` + + + + + + `).join('')} +
AgentTop models (by uses)TokensCost
${escapeHtml(a.display_name || a.agent)}${a.top_models.slice(0, 4).map(([m, c]) => `${escapeHtml(m)} ร—${c}`).join(' ')}${(a.tokens || 0).toLocaleString()}$${(a.cost || 0).toFixed(4)}
` + : '
No cost/model data recorded yet.
'; + + // Full breakdown + const rows = Object.values(pa).sort((a, b) => b.user_turns - a.user_turns); + document.getElementById('breakdownTable').innerHTML = ` +
+ + ${rows.map(a => ` + + + + + + + + + + `).join('')} +
AgentChat turnsMsgsTimeSessionsTop modelFirst seenLast seen
${escapeHtml(a.display_name || a.agent)}${a.user_turns}${a.chat_messages}${fmtDuration(a.time_seconds)}${a.sessions}${a.top_models && a.top_models.length ? escapeHtml(a.top_models[0][0]) : 'โ€”'}${a.first_seen ? formatDate(a.first_seen) : 'โ€”'}${a.last_seen ? formatDate(a.last_seen) : 'โ€”'}
`; +} + +async function recomputeAndRenderInsights() { + try { await api.recomputeAgentTime(); } catch (e) {} + await renderAgentInsights(); +} diff --git a/dashboard/utils.js b/dashboard/utils.js index b3afcc5..da28eec 100644 --- a/dashboard/utils.js +++ b/dashboard/utils.js @@ -117,6 +117,7 @@ const PAGE_TITLES = { scheduler: { title: 'Scheduler', breadcrumb: 'Automated workflows' }, audit: { title: 'Audit Log', breadcrumb: 'System activity trail' }, cost: { title: 'Cost Analytics', breadcrumb: 'Usage & spending' }, + 'agent-insights': { title: 'Agent Insights', breadcrumb: 'AI usage analytics' }, plugins: { title: 'Plugin Registry', breadcrumb: 'Manage plugins' }, backups: { title: 'Backups', breadcrumb: 'Disaster recovery' }, prompts: { title: 'Prompt Library', breadcrumb: 'Reusable templates' }, diff --git a/server.py b/server.py index 494085d..ea84839 100644 --- a/server.py +++ b/server.py @@ -2341,6 +2341,174 @@ def agent_time_skin(): body = f"{report.get('total_human', '0h 0m 0s')}\n{total} {per}" return Response(content=body, media_type="text/plain; charset=utf-8") + +# โ”€โ”€โ”€ Routes: Agent Insights (AI usage analytics) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Aggregates real usage data across chat history, cost history, derived +# agent-time, the agent registry, and router keywords to answer: +# - which agent do I chat with most +# - which agent spends the most time completing tasks +# - which agent seems best suited for what +# - which models I use most with each agent +# All source data is read-only; no writes happen on this endpoint. + +ROUTER_KEYWORDS_FILE = BASE_DIR / "data" / "router-keywords.json" + +# Human-readable role hints keyed by agent name (falls back to registry +# description / router keywords when unknown). +AGENT_ROLE_HINTS = { + "opencode": "Code generation, file ops, DevOps/infra, git, software engineering", + "hermes": "Persistent memory, cron scheduling, messaging channels, skill hub, coordination", + "gemini": "Web research, multi-modal/image/PDF analysis, reasoning, data analysis", + "jarvis": "Local-first personal AI: deep research, knowledge, memory, general reasoning", + "kilocode": "AI coding assistant: implement, refactor, programming tasks", + "codex": "OpenAI Codex: coding, debugging, tests, builds", +} + + +@app.get("/api/agent-insights") +def get_agent_insights(recompute: bool = False): + # Pull the four required datasets from disk. + chat = load_chat_history().get("messages", []) + cost = read_json( + BASE_DIR / "data" / "cost-history.json", + {"entries": []}, + ).get("entries", []) + registry = load_agent_registry() + router_kw = read_json(ROUTER_KEYWORDS_FILE, {}) + + if recompute or not AGENT_TIME_REPORT.exists(): + time_report = _recompute_agent_time() + else: + try: + time_report = json.loads(AGENT_TIME_REPORT.read_text(encoding="utf-8")) + except Exception: + time_report = _recompute_agent_time() + time_agents = time_report.get("agents", {}) + + # 1) Chat volume per agent (count user+assistant messages; pair count + # = number of user turns, which is the most meaningful "chat" metric). + chat_counts = {} + user_turns = {} + first_chat = {} + last_chat = {} + for m in chat: + a = m.get("agent") or "unknown" + chat_counts[a] = chat_counts.get(a, 0) + 1 + if m.get("role") == "user": + user_turns[a] = user_turns.get(a, 0) + 1 + ts = m.get("timestamp") + if ts: + if a not in first_chat or ts < first_chat[a]: + first_chat[a] = ts + if a not in last_chat or ts > last_chat[a]: + last_chat[a] = ts + + # 2) Time per agent (from derived agent-time report). + time_per_agent = { + a: { + "total_seconds": d.get("total_seconds", 0), + "sessions": d.get("sessions", 0), + "touches": d.get("touches", 0), + } + for a, d in time_agents.items() + } + + # 3) Models used per agent (from cost history). + models_per_agent = {} + tokens_per_agent = {} + cost_per_agent = {} + for e in cost: + a = e.get("agent") or "unknown" + model = e.get("model") or "unknown" + models_per_agent.setdefault(a, {}) + models_per_agent[a][model] = models_per_agent[a].get(model, 0) + 1 + tokens_per_agent[a] = tokens_per_agent.get(a, 0) + (e.get("tokens", 0) or 0) + cost_per_agent[a] = cost_per_agent.get(a, 0) + (e.get("cost", 0) or 0) + + # 4) Best-suited-for: combine registry description, router keywords, + # and AGENTS.md role hints into a per-agent "suite" summary. + suite = {} + for name in set(list(registry.keys()) + list(router_kw.keys()) + + list(chat_counts.keys()) + list(time_per_agent.keys())): + desc = (registry.get(name, {}) or {}).get("description", "") + role = AGENT_ROLE_HINTS.get(name, "") + kws = router_kw.get(name, []) + suite[name] = { + "description": desc or role, + "role_hint": role, + "keywords": kws, + } + + # Build a per-agent consolidated view. + all_agents = set( + list(chat_counts.keys()) + list(time_per_agent.keys()) + + list(models_per_agent.keys()) + list(suite.keys()) + ) + per_agent = {} + for a in all_agents: + per_agent[a] = { + "agent": a, + "display_name": (registry.get(a, {}) or {}).get("display_name", a), + "chat_messages": chat_counts.get(a, 0), + "user_turns": user_turns.get(a, 0), + "time_seconds": time_per_agent.get(a, {}).get("total_seconds", 0), + "sessions": time_per_agent.get(a, {}).get("sessions", 0), + "touches": time_per_agent.get(a, {}).get("touches", 0), + "tokens": tokens_per_agent.get(a, 0), + "cost": round(cost_per_agent.get(a, 0), 6), + "top_models": sorted( + models_per_agent.get(a, {}).items(), + key=lambda kv: kv[1], reverse=True, + ), + "suite": suite.get(a, {}), + "first_seen": first_chat.get(a), + "last_seen": last_chat.get(a), + } + + # Rankings + by_chats = sorted( + per_agent.values(), key=lambda x: x["user_turns"], reverse=True + ) + by_time = sorted( + per_agent.values(), key=lambda x: x["time_seconds"], reverse=True + ) + + # Most-used model overall per agent already in top_models; also a global + # model popularity map. + global_models = {} + for a, models in models_per_agent.items(): + for m, c in models.items(): + global_models[m] = global_models.get(m, 0) + c + + return { + "generated_at": get_timestamp(), + "totals": { + "total_chat_messages": sum(chat_counts.values()), + "total_user_turns": sum(user_turns.values()), + "total_time_seconds": time_report.get("total_seconds", 0), + "total_time_human": time_report.get("total_human", "0h 0m 0s"), + "total_tokens": sum(tokens_per_agent.values()), + "total_cost": round(sum(cost_per_agent.values()), 6), + "agents_observed": len(per_agent), + }, + "most_chatted": [ + {"agent": x["agent"], "display_name": x["display_name"], + "user_turns": x["user_turns"], "chat_messages": x["chat_messages"]} + for x in by_chats if x["user_turns"] > 0 + ], + "most_time": [ + {"agent": x["agent"], "display_name": x["display_name"], + "time_seconds": x["time_seconds"], "sessions": x["sessions"], + "touches": x["touches"]} + for x in by_time if x["time_seconds"] > 0 + ], + "global_models": sorted( + global_models.items(), key=lambda kv: kv[1], reverse=True + ), + "per_agent": per_agent, + } + + # โ”€โ”€โ”€ Favicon โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ FAVICON_SVG = ''