From ecf7448fe7c2396051dc8398e0feaf215323a704 Mon Sep 17 00:00:00 2001 From: snoopyIsADog <35551877+yefuyou@users.noreply.github.com> Date: Sat, 15 Aug 2026 20:55:13 +0800 Subject: [PATCH 1/5] feat(sight): show LLM latency metrics in agent sessions --- src/agentsight/dashboard/src/i18n.tsx | 28 ++++ .../dashboard/src/pages/AgentSessionsPage.tsx | 128 +++++++++++++++++- .../dashboard/src/utils/apiClient.ts | 32 +++++ .../tests/apiClient-regression.test.cjs | 44 ++++++ 4 files changed, 227 insertions(+), 5 deletions(-) diff --git a/src/agentsight/dashboard/src/i18n.tsx b/src/agentsight/dashboard/src/i18n.tsx index 777d7f689a..cefbd3dcd9 100644 --- a/src/agentsight/dashboard/src/i18n.tsx +++ b/src/agentsight/dashboard/src/i18n.tsx @@ -29,6 +29,20 @@ const enUSMessages = { 'nav.riskEnforcement': 'Risk Enforcement', 'nav.trajectoryViewer': 'Trajectory Viewer', 'nav.settings': 'Settings', + 'latency.title': 'Latency Metrics', + 'latency.agent': 'Agent', + 'latency.calls': 'Calls', + 'latency.streaming': 'Streaming', + 'latency.ttft': 'TTFT', + 'latency.tps': 'TPS', + 'latency.tpot': 'TPOT', + 'latency.e2e': 'E2E', + 'latency.p50': 'P50', + 'latency.p95': 'P95', + 'latency.p99': 'P99', + 'latency.loading': 'Loading latency metrics...', + 'latency.empty': 'No latency data in this range', + 'latency.error': 'Failed to load latency metrics', 'login.subtitle': 'Enter your dashboard token to continue', 'login.tokenLabel': 'Dashboard Token', 'login.tokenPlaceholder': 'Paste your token here', @@ -48,6 +62,20 @@ export type MessageKey = keyof typeof enUSMessages; const messages: Record> = { 'en-US': enUSMessages, 'zh-CN': { + 'latency.title': '延迟指标', + 'latency.agent': 'Agent', + 'latency.calls': '调用数', + 'latency.streaming': '流式调用', + 'latency.ttft': 'TTFT', + 'latency.tps': 'TPS', + 'latency.tpot': 'TPOT', + 'latency.e2e': 'E2E', + 'latency.p50': 'P50', + 'latency.p95': 'P95', + 'latency.p99': 'P99', + 'latency.loading': '正在加载延迟指标...', + 'latency.empty': '当前范围内暂无延迟数据', + 'latency.error': '延迟指标加载失败', 'app.title': 'Agent可观测', 'app.loading': '加载中...', 'language.label': '语言', diff --git a/src/agentsight/dashboard/src/pages/AgentSessionsPage.tsx b/src/agentsight/dashboard/src/pages/AgentSessionsPage.tsx index 927f303a2f..94fdbd89d5 100644 --- a/src/agentsight/dashboard/src/pages/AgentSessionsPage.tsx +++ b/src/agentsight/dashboard/src/pages/AgentSessionsPage.tsx @@ -1,7 +1,8 @@ -import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; -import { fetchSessions, fetchTrajectories } from '../utils/apiClient'; -import type { SessionSummary, TrajectorySummary } from '../utils/apiClient'; +import { useI18n } from '../i18n'; +import { fetchLatencyMetrics, fetchSessions, fetchTrajectories } from '../utils/apiClient'; +import type { LatencyMetricsSummary, MetricPercentiles, SessionSummary, TrajectorySummary } from '../utils/apiClient'; import { CopyButton } from '../components/CopyButton'; // ─── Merged session model ───────────────────────────────────────────────────── @@ -184,10 +185,37 @@ const SourceBadge: React.FC<{ sources?: SessionSource[] }> = ({ sources }) => { ); }; +function formatMetricValue(value: number): string { + return value.toLocaleString(undefined, { maximumFractionDigits: 2 }); +} + +interface PercentileLabels { + p50: string; + p95: string; + p99: string; +} + +const MetricPercentileCell: React.FC<{ + metric: MetricPercentiles | null; + unit: string; + labels: PercentileLabels; +}> = ({ metric, unit, labels }) => { + if (!metric) return ; + + return ( +
+
{labels.p50} {formatMetricValue(metric.p50)}{unit}
+
{labels.p95} {formatMetricValue(metric.p95)}{unit}
+
{labels.p99} {formatMetricValue(metric.p99)}{unit}
+
+ ); +}; + // ─── Main Page ──────────────────────────────────────────────────────────────── export const AgentSessionsPage: React.FC = () => { const navigate = useNavigate(); + const { t } = useI18n(); const [merged, setMerged] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -197,6 +225,10 @@ export const AgentSessionsPage: React.FC = () => { const [search, setSearch] = useState(''); const [page, setPage] = useState(1); const [autoRefresh, setAutoRefresh] = useState(false); + const [latencyMetrics, setLatencyMetrics] = useState([]); + const [latencyLoading, setLatencyLoading] = useState(true); + const [latencyError, setLatencyError] = useState(null); + const latencyRequestIdRef = useRef(0); const loadData = useCallback(async () => { setLoading(true); @@ -217,16 +249,45 @@ export const AgentSessionsPage: React.FC = () => { } }, [rangeMs]); + const loadLatency = useCallback(async () => { + const requestId = ++latencyRequestIdRef.current; + setLatencyLoading(true); + setLatencyError(null); + try { + const endNs = Date.now() * 1_000_000; + const startNs = endNs - rangeMs * 1_000_000; + const agentName = agentFilter === 'all' ? undefined : agentFilter; + const data = await fetchLatencyMetrics(startNs, endNs, agentName); + if (requestId === latencyRequestIdRef.current) { + setLatencyMetrics(data); + setLatencyError(null); + } + } catch (e: any) { + if (requestId === latencyRequestIdRef.current) { + setLatencyError(e.message || t('latency.error')); + } + } finally { + if (requestId === latencyRequestIdRef.current) { + setLatencyLoading(false); + } + } + }, [agentFilter, rangeMs, t]); useEffect(() => { loadData(); }, [loadData]); + useEffect(() => { + void loadLatency(); + }, [loadLatency]); // Auto-refresh every 10s when enabled useEffect(() => { if (!autoRefresh) return; - const interval = setInterval(loadData, 10_000); + const interval = setInterval(() => { + void loadData(); + void loadLatency(); + }, 10_000); return () => clearInterval(interval); - }, [autoRefresh, loadData]); + }, [autoRefresh, loadData, loadLatency]); // Reset to page 1 when filters change useEffect(() => { @@ -280,6 +341,11 @@ export const AgentSessionsPage: React.FC = () => { const safePage = Math.min(page, totalPages); const paged = filtered.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE); + const percentileLabels: PercentileLabels = { + p50: t('latency.p50'), + p95: t('latency.p95'), + p99: t('latency.p99'), + }; return (
{/* ── Toolbar: total + time range + refresh ── */} @@ -368,6 +434,58 @@ export const AgentSessionsPage: React.FC = () => { )} {/* ── Session table ── */} +
+
+

{t('latency.title')}

+
+
+ {latencyLoading ? ( +
{t('latency.loading')}
+ ) : latencyError ? ( +
{latencyError}
+ ) : latencyMetrics.length === 0 ? ( +
{t('latency.empty')}
+ ) : ( + + + + + + + + + + + + + + {latencyMetrics.map((metric, index) => ( + + + + + + + + + + ))} + +
{t('latency.agent')}{t('latency.calls')}{t('latency.streaming')}{t('latency.ttft')} (ms){t('latency.tps')} (tokens/s){t('latency.tpot')} (ms/token){t('latency.e2e')} (ms)
+ {metric.agent_name ?? } + {metric.call_count.toLocaleString()}{metric.streaming_call_count.toLocaleString()} + + + + + + + +
+ )} +
+
+
{loading && merged.length === 0 ? (
正在加载会话列表...
diff --git a/src/agentsight/dashboard/src/utils/apiClient.ts b/src/agentsight/dashboard/src/utils/apiClient.ts index 9c24881744..3f7ba6604b 100644 --- a/src/agentsight/dashboard/src/utils/apiClient.ts +++ b/src/agentsight/dashboard/src/utils/apiClient.ts @@ -557,6 +557,38 @@ export interface TimeseriesResponse { model_series: ModelTimeseriesBucket[]; } +export interface MetricPercentiles { + p50: number; + p95: number; + p99: number; +} + +export interface LatencyMetricsSummary { + agent_name: string | null; + call_count: number; + streaming_call_count: number; + ttft_ms: MetricPercentiles | null; + tps_tokens_per_second: MetricPercentiles | null; + tpot_ms_per_token: MetricPercentiles | null; + e2e_latency_ms: MetricPercentiles | null; +} + +/** + * Fetch percentile latency metrics grouped by Agent. + */ +export async function fetchLatencyMetrics( + startNs: number, + endNs: number, + agentName?: string +): Promise { + const params = new URLSearchParams({ + start_ns: String(startNs), + end_ns: String(endNs), + }); + if (agentName) params.set('agent_name', agentName); + return apiFetch(`${API_BASE}/api/metrics/latency?${params.toString()}`); +} + /** * Fetch time-bucketed token stats and per-model breakdowns. */ diff --git a/src/agentsight/dashboard/tests/apiClient-regression.test.cjs b/src/agentsight/dashboard/tests/apiClient-regression.test.cjs index c2928ca937..9cda73e765 100644 --- a/src/agentsight/dashboard/tests/apiClient-regression.test.cjs +++ b/src/agentsight/dashboard/tests/apiClient-regression.test.cjs @@ -8,6 +8,7 @@ const { enforcementSupportsMode, enforcementViolationTotal, fetchContainmentPlan, + fetchLatencyMetrics, fetchSecurityCase, fetchSecurityStatus, reviewSecurityCase, @@ -99,6 +100,49 @@ test('fetchSecurityStatus preserves a non-2xx availability state envelope', asyn assert.deepEqual(response.data, { error: 'socket unavailable' }); }); +test('fetchLatencyMetrics forwards ranges and preserves nullable percentile data', async () => { + let requestedUrl = null; + global.fetch = async (url) => { + requestedUrl = String(url); + return new Response(JSON.stringify([{ + agent_name: 'claude', + call_count: 3, + streaming_call_count: 2, + ttft_ms: { p50: 10, p95: 20, p99: 30 }, + tps_tokens_per_second: { p50: 40, p95: 50, p99: 60 }, + tpot_ms_per_token: null, + e2e_latency_ms: { p50: 100, p95: 200, p99: 300 }, + }]), { status: 200 }); + }; + + const response = await fetchLatencyMetrics(1_000_000_000, 2_000_000_000, 'claude'); + const url = new URL(requestedUrl); + + assert.equal(url.pathname, '/api/metrics/latency'); + assert.equal(url.searchParams.get('start_ns'), '1000000000'); + assert.equal(url.searchParams.get('end_ns'), '2000000000'); + assert.equal(url.searchParams.get('agent_name'), 'claude'); + assert.deepEqual(response[0].ttft_ms, { p50: 10, p95: 20, p99: 30 }); + assert.deepEqual(response[0].tps_tokens_per_second, { p50: 40, p95: 50, p99: 60 }); + assert.deepEqual(response[0].e2e_latency_ms, { p50: 100, p95: 200, p99: 300 }); + assert.equal(response[0].tpot_ms_per_token, null); +}); + +test('fetchLatencyMetrics omits agent_name when no filter is provided', async () => { + let requestedUrl = null; + global.fetch = async (url) => { + requestedUrl = String(url); + return new Response('[]', { status: 200 }); + }; + + await fetchLatencyMetrics(3_000_000_000, 4_000_000_000); + const url = new URL(requestedUrl); + + assert.equal(url.searchParams.get('start_ns'), '3000000000'); + assert.equal(url.searchParams.get('end_ns'), '4000000000'); + assert.equal(url.searchParams.has('agent_name'), false); +}); + test('fetchSecurityCase accepts a valid system-audit detail response', async () => { global.fetch = async () => new Response(JSON.stringify({ state: 'ok', From a0059cc0f21a7cbc42047c358f2125fd81cc7153 Mon Sep 17 00:00:00 2001 From: snoopyIsADog <35551877+yefuyou@users.noreply.github.com> Date: Sun, 16 Aug 2026 00:41:02 +0800 Subject: [PATCH 2/5] fix(sight): refresh latency metrics manually --- src/agentsight/dashboard/src/pages/AgentSessionsPage.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/agentsight/dashboard/src/pages/AgentSessionsPage.tsx b/src/agentsight/dashboard/src/pages/AgentSessionsPage.tsx index 94fdbd89d5..664effb8c9 100644 --- a/src/agentsight/dashboard/src/pages/AgentSessionsPage.tsx +++ b/src/agentsight/dashboard/src/pages/AgentSessionsPage.tsx @@ -272,6 +272,10 @@ export const AgentSessionsPage: React.FC = () => { } } }, [agentFilter, rangeMs, t]); + const refreshAll = () => { + void loadData(); + void loadLatency(); + }; useEffect(() => { loadData(); }, [loadData]); @@ -378,7 +382,7 @@ export const AgentSessionsPage: React.FC = () => { 自动刷新 + {open && ( + + )} +
); }; diff --git a/src/agentsight/dashboard/src/pages/AgentHealthPage.tsx b/src/agentsight/dashboard/src/pages/AgentHealthPage.tsx index c4ec00d0fe..1c94096dff 100644 --- a/src/agentsight/dashboard/src/pages/AgentHealthPage.tsx +++ b/src/agentsight/dashboard/src/pages/AgentHealthPage.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { useI18n } from '../i18n'; import { fetchAgentHealth, deleteAgentHealth, @@ -6,8 +7,14 @@ import { fetchInterruptions, resolveInterruption, INTERRUPTION_TYPE_CN, + fetchLatencyMetrics, +} from '../utils/apiClient'; +import type { + InterruptionRecord, + InterruptionSeverity, + LatencyMetricsSummary, + MetricPercentiles, } from '../utils/apiClient'; -import type { InterruptionRecord, InterruptionSeverity } from '../utils/apiClient'; import type { AgentHealthStatus } from '../types'; // ─── Agent status section ───────────────────────────────────────────────────── @@ -391,6 +398,159 @@ const AgentStatusSection: React.FC<{ addToast: (msg: string) => void }> = ({ add ); }; +// ─── Latency metrics section ───────────────────────────────────────────────── + +function formatMetricValue(value: number): string { + return value.toLocaleString(undefined, { maximumFractionDigits: 2 }); +} + +interface PercentileLabels { + p50: string; + p95: string; + p99: string; +} + +const MetricPercentileCell: React.FC<{ + metric: MetricPercentiles | null; + unit: string; + labels: PercentileLabels; +}> = ({ metric, unit, labels }) => { + if (!metric) return ; + + return ( +
+
{labels.p50} {formatMetricValue(metric.p50)} {unit}
+
{labels.p95} {formatMetricValue(metric.p95)} {unit}
+
{labels.p99} {formatMetricValue(metric.p99)} {unit}
+
+ ); +}; + +const LATENCY_TIME_PRESETS = [ + { key: 'latency.range24h', ms: 24 * 3600 * 1000 }, + { key: 'latency.range7d', ms: 7 * 24 * 3600 * 1000 }, + { key: 'latency.range30d', ms: 30 * 24 * 3600 * 1000 }, +] as const; + +const LatencyMetricsSection: React.FC = () => { + const { t } = useI18n(); + const [rangeMs, setRangeMs] = useState(7 * 24 * 3600 * 1000); + const [latencyMetrics, setLatencyMetrics] = useState([]); + const [latencyLoading, setLatencyLoading] = useState(true); + const [latencyError, setLatencyError] = useState(null); + const latencyRequestIdRef = useRef(0); + + const loadLatency = useCallback(async () => { + const requestId = ++latencyRequestIdRef.current; + setLatencyLoading(true); + setLatencyError(null); + try { + const endNs = Date.now() * 1_000_000; + const startNs = endNs - rangeMs * 1_000_000; + const data = await fetchLatencyMetrics(startNs, endNs); + if (requestId === latencyRequestIdRef.current) { + setLatencyMetrics(data); + setLatencyError(null); + } + } catch (e: any) { + if (requestId === latencyRequestIdRef.current) { + setLatencyError(e.message || ''); + } + } finally { + if (requestId === latencyRequestIdRef.current) { + setLatencyLoading(false); + } + } + }, [rangeMs]); + + useEffect(() => { + void loadLatency(); + }, [loadLatency]); + + const percentileLabels: PercentileLabels = { + p50: t('latency.p50'), + p95: t('latency.p95'), + p99: t('latency.p99'), + }; + + return ( +
+
+

{t('latency.title')}

+
+ {LATENCY_TIME_PRESETS.map(({ key, ms }) => ( + + ))} +
+
+ +
+ {latencyLoading && latencyMetrics.length > 0 && ( +
{t('latency.loading')}
+ )} + {latencyError !== null && latencyMetrics.length > 0 && ( +
{latencyError || t('latency.error')}
+ )} + {latencyError !== null && latencyMetrics.length === 0 ? ( +
{latencyError || t('latency.error')}
+ ) : latencyMetrics.length === 0 ? ( +
+ {latencyLoading ? t('latency.loading') : t('latency.empty')} +
+ ) : ( + + + + + + + + + + + + + + {latencyMetrics.map((metric, index) => ( + + + + + + + + + + ))} + +
{t('latency.agent')}{t('latency.calls')}{t('latency.streaming')}{t('latency.ttft')} (ms){t('latency.tps')} (tokens/s){t('latency.tpot')} (ms/token){t('latency.e2e')} (ms)
+ {metric.agent_name ?? } + {metric.call_count.toLocaleString()}{metric.streaming_call_count.toLocaleString()} + + + + + + + +
+ )} +
+
+ ); +}; + // ─── Interruption events section ────────────────────────────────────────────── const SEVERITY_DOT: Record = { @@ -830,6 +990,7 @@ export const AgentHealthPage: React.FC = () => {
+ ); diff --git a/src/agentsight/dashboard/src/pages/AgentSessionsPage.tsx b/src/agentsight/dashboard/src/pages/AgentSessionsPage.tsx index 664effb8c9..f7708b429b 100644 --- a/src/agentsight/dashboard/src/pages/AgentSessionsPage.tsx +++ b/src/agentsight/dashboard/src/pages/AgentSessionsPage.tsx @@ -1,8 +1,7 @@ -import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useI18n } from '../i18n'; -import { fetchLatencyMetrics, fetchSessions, fetchTrajectories } from '../utils/apiClient'; -import type { LatencyMetricsSummary, MetricPercentiles, SessionSummary, TrajectorySummary } from '../utils/apiClient'; +import { fetchSessions, fetchTrajectories } from '../utils/apiClient'; +import type { SessionSummary, TrajectorySummary } from '../utils/apiClient'; import { CopyButton } from '../components/CopyButton'; // ─── Merged session model ───────────────────────────────────────────────────── @@ -185,37 +184,10 @@ const SourceBadge: React.FC<{ sources?: SessionSource[] }> = ({ sources }) => { ); }; -function formatMetricValue(value: number): string { - return value.toLocaleString(undefined, { maximumFractionDigits: 2 }); -} - -interface PercentileLabels { - p50: string; - p95: string; - p99: string; -} - -const MetricPercentileCell: React.FC<{ - metric: MetricPercentiles | null; - unit: string; - labels: PercentileLabels; -}> = ({ metric, unit, labels }) => { - if (!metric) return ; - - return ( -
-
{labels.p50} {formatMetricValue(metric.p50)}{unit}
-
{labels.p95} {formatMetricValue(metric.p95)}{unit}
-
{labels.p99} {formatMetricValue(metric.p99)}{unit}
-
- ); -}; - // ─── Main Page ──────────────────────────────────────────────────────────────── export const AgentSessionsPage: React.FC = () => { const navigate = useNavigate(); - const { t } = useI18n(); const [merged, setMerged] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -225,10 +197,6 @@ export const AgentSessionsPage: React.FC = () => { const [search, setSearch] = useState(''); const [page, setPage] = useState(1); const [autoRefresh, setAutoRefresh] = useState(false); - const [latencyMetrics, setLatencyMetrics] = useState([]); - const [latencyLoading, setLatencyLoading] = useState(true); - const [latencyError, setLatencyError] = useState(null); - const latencyRequestIdRef = useRef(0); const loadData = useCallback(async () => { setLoading(true); @@ -249,49 +217,18 @@ export const AgentSessionsPage: React.FC = () => { } }, [rangeMs]); - const loadLatency = useCallback(async () => { - const requestId = ++latencyRequestIdRef.current; - setLatencyLoading(true); - setLatencyError(null); - try { - const endNs = Date.now() * 1_000_000; - const startNs = endNs - rangeMs * 1_000_000; - const agentName = agentFilter === 'all' ? undefined : agentFilter; - const data = await fetchLatencyMetrics(startNs, endNs, agentName); - if (requestId === latencyRequestIdRef.current) { - setLatencyMetrics(data); - setLatencyError(null); - } - } catch (e: any) { - if (requestId === latencyRequestIdRef.current) { - setLatencyError(e.message || t('latency.error')); - } - } finally { - if (requestId === latencyRequestIdRef.current) { - setLatencyLoading(false); - } - } - }, [agentFilter, rangeMs, t]); - const refreshAll = () => { - void loadData(); - void loadLatency(); - }; useEffect(() => { loadData(); }, [loadData]); - useEffect(() => { - void loadLatency(); - }, [loadLatency]); // Auto-refresh every 10s when enabled useEffect(() => { if (!autoRefresh) return; const interval = setInterval(() => { void loadData(); - void loadLatency(); }, 10_000); return () => clearInterval(interval); - }, [autoRefresh, loadData, loadLatency]); + }, [autoRefresh, loadData]); // Reset to page 1 when filters change useEffect(() => { @@ -345,11 +282,6 @@ export const AgentSessionsPage: React.FC = () => { const safePage = Math.min(page, totalPages); const paged = filtered.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE); - const percentileLabels: PercentileLabels = { - p50: t('latency.p50'), - p95: t('latency.p95'), - p99: t('latency.p99'), - }; return (
{/* ── Toolbar: total + time range + refresh ── */} @@ -382,7 +314,7 @@ export const AgentSessionsPage: React.FC = () => { 自动刷新
)} - {/* ── Session table ── */} -
-
-

{t('latency.title')}

-
-
- {latencyLoading ? ( -
{t('latency.loading')}
- ) : latencyError ? ( -
{latencyError}
- ) : latencyMetrics.length === 0 ? ( -
{t('latency.empty')}
- ) : ( - - - - - - - - - - - - - - {latencyMetrics.map((metric, index) => ( - - - - - - - - - - ))} - -
{t('latency.agent')}{t('latency.calls')}{t('latency.streaming')}{t('latency.ttft')} (ms){t('latency.tps')} (tokens/s){t('latency.tpot')} (ms/token){t('latency.e2e')} (ms)
- {metric.agent_name ?? } - {metric.call_count.toLocaleString()}{metric.streaming_call_count.toLocaleString()} - - - - - - - -
- )} -
-
-
{loading && merged.length === 0 ? (
正在加载会话列表...
From ef7dccc279a74e5494b285063302bbed1722c876 Mon Sep 17 00:00:00 2001 From: snoopyIsADog <35551877+yefuyou@users.noreply.github.com> Date: Tue, 18 Aug 2026 21:25:46 +0800 Subject: [PATCH 4/5] fix(sight): embed latency metrics in agent cards --- .../dashboard/src/pages/AgentHealthPage.tsx | 312 +++++++++--------- 1 file changed, 154 insertions(+), 158 deletions(-) diff --git a/src/agentsight/dashboard/src/pages/AgentHealthPage.tsx b/src/agentsight/dashboard/src/pages/AgentHealthPage.tsx index 1c94096dff..a40c694ae1 100644 --- a/src/agentsight/dashboard/src/pages/AgentHealthPage.tsx +++ b/src/agentsight/dashboard/src/pages/AgentHealthPage.tsx @@ -64,13 +64,81 @@ interface Toast { message: string; } +function formatMetricValue(value: number): string { + return value.toLocaleString(undefined, { maximumFractionDigits: 2 }); +} + +function formatMetricP50(metric: MetricPercentiles | null, unit: string): string { + return metric ? formatMetricValue(metric.p50) + ' ' + unit : '—'; +} + +const LatencyMetricsRow: React.FC<{ metrics: LatencyMetricsSummary }> = ({ metrics }) => { + const { t } = useI18n(); + const items = [ + { label: t('latency.ttft'), metric: metrics.ttft_ms, unit: 'ms' }, + { label: t('latency.tps'), metric: metrics.tps_tokens_per_second, unit: 'tokens/s' }, + { label: t('latency.tpot'), metric: metrics.tpot_ms_per_token, unit: 'ms/token' }, + { label: t('latency.e2e'), metric: metrics.e2e_latency_ms, unit: 'ms' }, + ]; + + if (!items.some(item => item.metric !== null)) return null; + + const tooltip = items + .map(({ label, metric, unit }) => { + if (!metric) return label + ' —'; + return ( + label + + ' ' + + t('latency.p50') + + ' ' + + formatMetricValue(metric.p50) + + ' ' + + unit + + ' · ' + + t('latency.p95') + + ' ' + + formatMetricValue(metric.p95) + + ' ' + + unit + + ' · ' + + t('latency.p99') + + ' ' + + formatMetricValue(metric.p99) + + ' ' + + unit + ); + }) + .join(' · '); + + return ( +
+ {items.map(({ label, metric, unit }) => ( + + {label} {formatMetricP50(metric, unit)} + + ))} +
+ ); +}; + +const LATENCY_TIME_PRESETS = [ + { key: 'latency.range24h', ms: 24 * 3600 * 1000 }, + { key: 'latency.range7d', ms: 7 * 24 * 3600 * 1000 }, + { key: 'latency.range30d', ms: 30 * 24 * 3600 * 1000 }, +] as const; + const AgentCard: React.FC<{ agent: AgentHealthStatus; related: AgentHealthStatus[]; onDelete: (pid: number) => void; onRestart: (pid: number) => void; restarting: boolean; -}> = ({ agent, related, onDelete, onRestart, restarting }) => { + latency?: LatencyMetricsSummary; +}> = ({ agent, related, onDelete, onRestart, restarting, latency }) => { const [showRelated, setShowRelated] = useState(false); // 区分:真 Gateway = 本身在监听端口的服务进程(如 OpenClaw Gateway) @@ -175,6 +243,7 @@ const AgentCard: React.FC<{
)} + {latency && } {(isOffline || canRestart) && (
{isOffline && ( @@ -225,6 +294,7 @@ const AgentCard: React.FC<{ }; const AgentStatusSection: React.FC<{ addToast: (msg: string) => void }> = ({ addToast }) => { + const { t } = useI18n(); const [agents, setAgents] = useState([]); const [clientAgents, setClientAgents] = useState([]); const [showOrphans, setShowOrphans] = useState(false); @@ -233,6 +303,34 @@ const AgentStatusSection: React.FC<{ addToast: (msg: string) => void }> = ({ add const [error, setError] = useState(null); const [restartingPids, setRestartingPids] = useState>(new Set()); const hasDataRef = useRef(false); + const [rangeMs, setRangeMs] = useState(7 * 24 * 3600 * 1000); + const [latencyMetrics, setLatencyMetrics] = useState([]); + const [latencyLoading, setLatencyLoading] = useState(true); + const [latencyError, setLatencyError] = useState(null); + const latencyRequestIdRef = useRef(0); + + const loadLatency = useCallback(async () => { + const requestId = ++latencyRequestIdRef.current; + setLatencyLoading(true); + setLatencyError(null); + try { + const endNs = Date.now() * 1_000_000; + const startNs = endNs - rangeMs * 1_000_000; + const data = await fetchLatencyMetrics(startNs, endNs); + if (requestId === latencyRequestIdRef.current) { + setLatencyMetrics(data); + setLatencyError(null); + } + } catch (e: any) { + if (requestId === latencyRequestIdRef.current) { + setLatencyError(e.message || ''); + } + } finally { + if (requestId === latencyRequestIdRef.current) { + setLatencyLoading(false); + } + } + }, [rangeMs]); const refresh = useCallback(async () => { try { @@ -286,6 +384,10 @@ const AgentStatusSection: React.FC<{ addToast: (msg: string) => void }> = ({ add return () => clearInterval(timer); }, [refresh]); + useEffect(() => { + void loadLatency(); + }, [loadLatency]); + // 排序:hung/unhealthy 首位(真有问题),正常中间,offline 最后(不抢眼) const sorted = [...agents].sort((a, b) => { const order: Record = { @@ -303,6 +405,24 @@ const AgentStatusSection: React.FC<{ addToast: (msg: string) => void }> = ({ add const offlineCount = agents.filter(a => a.status === 'offline').length; const hungCount = agents.filter(a => a.status === 'hung').length; const totalCount = agents.length; + const canonicalAgentKey = (agentName: string): string => agentName.toLowerCase(); + const latencyByAgent = new Map(); + for (const metric of latencyMetrics) { + if (metric.agent_name !== null) { + const key = canonicalAgentKey(metric.agent_name); + const summaries = latencyByAgent.get(key); + if (summaries) { + summaries.push(metric); + } else { + latencyByAgent.set(key, [metric]); + } + } + } + const latencyForAgent = (agentName: string): LatencyMetricsSummary | undefined => { + const summaries = latencyByAgent.get(canonicalAgentKey(agentName)); + // Do not silently choose one when casing variants produce separate summaries. + return summaries?.length === 1 ? summaries[0] : undefined; + }; const gatewayPids = new Set(sorted.map(a => a.pid)); // 孤儿关联进程:Worker 但父进程不是任何主卡(不应出现,兜底)。 @@ -339,9 +459,38 @@ const AgentStatusSection: React.FC<{ addToast: (msg: string) => void }> = ({ add )}
- {lastScan > 0 && ( - 上次扫描: {relativeTime(lastScan)} - )} +
+
+ {t('latency.title')} + {LATENCY_TIME_PRESETS.map(({ key, ms }) => ( + + ))} +
+ {latencyLoading && ( + {t('latency.loading')} + )} + {latencyError !== null && ( + + {t('latency.error')} + + )} + {lastScan > 0 && ( + 上次扫描: {relativeTime(lastScan)} + )} +
{loading ? ( @@ -364,6 +513,7 @@ const AgentStatusSection: React.FC<{ addToast: (msg: string) => void }> = ({ add onDelete={handleDelete} onRestart={handleRestart} restarting={restartingPids.has(agent.pid)} + latency={latencyForAgent(agent.agent_name)} /> ))} @@ -398,159 +548,6 @@ const AgentStatusSection: React.FC<{ addToast: (msg: string) => void }> = ({ add ); }; -// ─── Latency metrics section ───────────────────────────────────────────────── - -function formatMetricValue(value: number): string { - return value.toLocaleString(undefined, { maximumFractionDigits: 2 }); -} - -interface PercentileLabels { - p50: string; - p95: string; - p99: string; -} - -const MetricPercentileCell: React.FC<{ - metric: MetricPercentiles | null; - unit: string; - labels: PercentileLabels; -}> = ({ metric, unit, labels }) => { - if (!metric) return ; - - return ( -
-
{labels.p50} {formatMetricValue(metric.p50)} {unit}
-
{labels.p95} {formatMetricValue(metric.p95)} {unit}
-
{labels.p99} {formatMetricValue(metric.p99)} {unit}
-
- ); -}; - -const LATENCY_TIME_PRESETS = [ - { key: 'latency.range24h', ms: 24 * 3600 * 1000 }, - { key: 'latency.range7d', ms: 7 * 24 * 3600 * 1000 }, - { key: 'latency.range30d', ms: 30 * 24 * 3600 * 1000 }, -] as const; - -const LatencyMetricsSection: React.FC = () => { - const { t } = useI18n(); - const [rangeMs, setRangeMs] = useState(7 * 24 * 3600 * 1000); - const [latencyMetrics, setLatencyMetrics] = useState([]); - const [latencyLoading, setLatencyLoading] = useState(true); - const [latencyError, setLatencyError] = useState(null); - const latencyRequestIdRef = useRef(0); - - const loadLatency = useCallback(async () => { - const requestId = ++latencyRequestIdRef.current; - setLatencyLoading(true); - setLatencyError(null); - try { - const endNs = Date.now() * 1_000_000; - const startNs = endNs - rangeMs * 1_000_000; - const data = await fetchLatencyMetrics(startNs, endNs); - if (requestId === latencyRequestIdRef.current) { - setLatencyMetrics(data); - setLatencyError(null); - } - } catch (e: any) { - if (requestId === latencyRequestIdRef.current) { - setLatencyError(e.message || ''); - } - } finally { - if (requestId === latencyRequestIdRef.current) { - setLatencyLoading(false); - } - } - }, [rangeMs]); - - useEffect(() => { - void loadLatency(); - }, [loadLatency]); - - const percentileLabels: PercentileLabels = { - p50: t('latency.p50'), - p95: t('latency.p95'), - p99: t('latency.p99'), - }; - - return ( -
-
-

{t('latency.title')}

-
- {LATENCY_TIME_PRESETS.map(({ key, ms }) => ( - - ))} -
-
- -
- {latencyLoading && latencyMetrics.length > 0 && ( -
{t('latency.loading')}
- )} - {latencyError !== null && latencyMetrics.length > 0 && ( -
{latencyError || t('latency.error')}
- )} - {latencyError !== null && latencyMetrics.length === 0 ? ( -
{latencyError || t('latency.error')}
- ) : latencyMetrics.length === 0 ? ( -
- {latencyLoading ? t('latency.loading') : t('latency.empty')} -
- ) : ( - - - - - - - - - - - - - - {latencyMetrics.map((metric, index) => ( - - - - - - - - - - ))} - -
{t('latency.agent')}{t('latency.calls')}{t('latency.streaming')}{t('latency.ttft')} (ms){t('latency.tps')} (tokens/s){t('latency.tpot')} (ms/token){t('latency.e2e')} (ms)
- {metric.agent_name ?? } - {metric.call_count.toLocaleString()}{metric.streaming_call_count.toLocaleString()} - - - - - - - -
- )} -
-
- ); -}; - // ─── Interruption events section ────────────────────────────────────────────── const SEVERITY_DOT: Record = { @@ -990,7 +987,6 @@ export const AgentHealthPage: React.FC = () => { - ); From 3dbc8d448eed032cf8e19b8a996d355dc7e54f37 Mon Sep 17 00:00:00 2001 From: snoopyIsADog <35551877+yefuyou@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:51:55 +0800 Subject: [PATCH 5/5] chore(sight): trim latency UI diff --- src/agentsight/dashboard/src/i18n.tsx | 8 -------- src/agentsight/dashboard/src/pages/AgentSessionsPage.tsx | 5 ++--- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/agentsight/dashboard/src/i18n.tsx b/src/agentsight/dashboard/src/i18n.tsx index 7b3d3e44a3..139d4445ac 100644 --- a/src/agentsight/dashboard/src/i18n.tsx +++ b/src/agentsight/dashboard/src/i18n.tsx @@ -38,9 +38,6 @@ const enUSMessages = { 'nav.trajectoryViewer': 'Trajectory Viewer', 'nav.settings': 'Settings', 'latency.title': 'Latency Metrics', - 'latency.agent': 'Agent', - 'latency.calls': 'Calls', - 'latency.streaming': 'Streaming', 'latency.ttft': 'TTFT', 'latency.tps': 'TPS', 'latency.tpot': 'TPOT', @@ -49,7 +46,6 @@ const enUSMessages = { 'latency.p95': 'P95', 'latency.p99': 'P99', 'latency.loading': 'Loading latency metrics...', - 'latency.empty': 'No latency data in this range', 'latency.error': 'Failed to load latency metrics', 'latency.range24h': 'Last 24h', 'latency.range7d': 'Last 7d', @@ -1036,9 +1032,6 @@ export const messages: Record> = { 'nav.trajectoryViewer': '轨迹查看', 'nav.settings': '设置', 'latency.title': '延迟指标', - 'latency.agent': 'Agent', - 'latency.calls': '调用数', - 'latency.streaming': '流式调用', 'latency.ttft': 'TTFT', 'latency.tps': 'TPS', 'latency.tpot': 'TPOT', @@ -1047,7 +1040,6 @@ export const messages: Record> = { 'latency.p95': 'P95', 'latency.p99': 'P99', 'latency.loading': '正在加载延迟指标...', - 'latency.empty': '当前范围内暂无延迟数据', 'latency.error': '延迟指标加载失败', 'latency.range24h': '最近 24h', 'latency.range7d': '最近 7d', diff --git a/src/agentsight/dashboard/src/pages/AgentSessionsPage.tsx b/src/agentsight/dashboard/src/pages/AgentSessionsPage.tsx index 8c30af4bd2..27f090570c 100644 --- a/src/agentsight/dashboard/src/pages/AgentSessionsPage.tsx +++ b/src/agentsight/dashboard/src/pages/AgentSessionsPage.tsx @@ -233,9 +233,7 @@ export const AgentSessionsPage: React.FC = () => { // Auto-refresh every 10s when enabled useEffect(() => { if (!autoRefresh) return; - const interval = setInterval(() => { - void loadData(); - }, 10_000); + const interval = setInterval(loadData, 10_000); return () => clearInterval(interval); }, [autoRefresh, loadData]); @@ -378,6 +376,7 @@ export const AgentSessionsPage: React.FC = () => { )} + {/* ── Session table ── */}
{loading && merged.length === 0 ? (
{t('as.loadingSessions')}