New /analytics route: token usage dashboard with aggregate summary, per-session breakdown, context window stats, and per-category token distribution. Data served from existing agent_sessions + tool_cost_stats. New /results route: browsable archive of orchestrator flow runs and arena battles. Two-tab layout (Analysis Runs / Arena Battles) using existing API endpoints (no new backend). Sidebar gains Results (ScrollText icon) and Token Analytics (BarChart3 icon) nav buttons above Settings.
79 lines
2.7 KiB
TypeScript
79 lines
2.7 KiB
TypeScript
import type { FastifyInstance } from 'fastify';
|
|
import type { Sql } from '../db.js';
|
|
|
|
// token-analyzer-ui: aggregate token/cost analytics across all agent_sessions.
|
|
// v1 — global view only (no per-project or per-user filtering).
|
|
|
|
export interface AnalyticsSummary {
|
|
total_input_tokens: number;
|
|
total_output_tokens: number;
|
|
total_cost: number;
|
|
session_count: number;
|
|
}
|
|
|
|
export interface SessionAnalyticsRow {
|
|
session_id: string;
|
|
session_name: string;
|
|
total_input_tokens: number;
|
|
total_output_tokens: number;
|
|
total_cost: number;
|
|
last_active_at: string | null;
|
|
}
|
|
|
|
export interface TokenBreakdownAgg {
|
|
category: string;
|
|
total_tokens: number;
|
|
}
|
|
|
|
export function registerAnalyticsRoutes(app: FastifyInstance, sql: Sql): void {
|
|
// GET /api/analytics/summary — aggregate totals across all agent_sessions.
|
|
app.get('/api/analytics/summary', async () => {
|
|
const [row] = await sql<AnalyticsSummary[]>`
|
|
SELECT
|
|
COALESCE(SUM(a.input_tokens), 0)::BIGINT AS total_input_tokens,
|
|
COALESCE(SUM(a.output_tokens), 0)::BIGINT AS total_output_tokens,
|
|
COALESCE(SUM(a.cost), 0)::DOUBLE PRECISION AS total_cost,
|
|
COUNT(DISTINCT c.session_id)::INT AS session_count
|
|
FROM agent_sessions a
|
|
JOIN chats c ON c.id = a.chat_id
|
|
`;
|
|
return row ?? { total_input_tokens: 0, total_output_tokens: 0, total_cost: 0, session_count: 0 };
|
|
});
|
|
|
|
// GET /api/analytics/sessions — per-session token/cost breakdown.
|
|
app.get('/api/analytics/sessions', async () => {
|
|
const rows = await sql<SessionAnalyticsRow[]>`
|
|
SELECT
|
|
c.session_id AS session_id,
|
|
s.name AS session_name,
|
|
COALESCE(SUM(a.input_tokens), 0)::BIGINT AS total_input_tokens,
|
|
COALESCE(SUM(a.output_tokens), 0)::BIGINT AS total_output_tokens,
|
|
COALESCE(SUM(a.cost), 0)::DOUBLE PRECISION AS total_cost,
|
|
MAX(a.last_active_at) AS last_active_at
|
|
FROM agent_sessions a
|
|
JOIN chats c ON c.id = a.chat_id
|
|
JOIN sessions s ON s.id = c.session_id
|
|
GROUP BY c.session_id, s.name
|
|
ORDER BY MAX(a.last_active_at) DESC NULLS LAST
|
|
`;
|
|
return { sessions: rows };
|
|
});
|
|
|
|
// GET /api/analytics/token-breakdown — aggregate token_breakdown categories
|
|
// across all tasks that carry the JSONB field.
|
|
app.get('/api/analytics/token-breakdown', async () => {
|
|
const rows = await sql<{ category: string; total_tokens: number }[]>`
|
|
SELECT
|
|
key AS category,
|
|
SUM((value->>0)::BIGINT)::BIGINT AS total_tokens
|
|
FROM tasks,
|
|
LATERAL jsonb_each(token_breakdown)
|
|
WHERE token_breakdown IS NOT NULL
|
|
AND jsonb_typeof(token_breakdown) = 'object'
|
|
GROUP BY key
|
|
ORDER BY total_tokens DESC
|
|
`;
|
|
return { categories: rows };
|
|
});
|
|
}
|