Skip to content

Commit 960e859

Browse files
authored
feat(ui): complete agent activity monitoring dashboard for issue #354 (#392)
1 parent 88adc39 commit 960e859

File tree

9 files changed

+261
-13
lines changed

9 files changed

+261
-13
lines changed

apps/ui/app/admin/agent-activity/[traceId]/page.tsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,36 @@ export default function AgentActivityTraceDetailPage() {
2727
(data && !data.tracing_enabled) || (isError && isTracingUnavailableError(error))
2828
);
2929

30+
const toolCalls = data?.tool_calls ?? [];
31+
const modelInvocations = data?.model_invocations ?? [];
32+
33+
const fallbackToolCalls =
34+
toolCalls.length > 0
35+
? toolCalls
36+
: (data?.spans ?? [])
37+
.filter((span) => span.tool_name)
38+
.map((span) => ({
39+
span_id: span.span_id,
40+
tool_name: span.tool_name ?? 'unknown-tool',
41+
input: span.tool_input,
42+
output: span.tool_output,
43+
status: span.status,
44+
}));
45+
46+
const fallbackModelInvocations =
47+
modelInvocations.length > 0
48+
? modelInvocations
49+
: (data?.spans ?? [])
50+
.filter((span) => Boolean(span.model_name) || Boolean(span.prompt_excerpt) || Boolean(span.completion_excerpt))
51+
.map((span) => ({
52+
span_id: span.span_id,
53+
model_name: span.model_name ?? span.service,
54+
model_tier: span.model_tier ?? 'unknown',
55+
prompt_excerpt: span.prompt_excerpt,
56+
completion_excerpt: span.completion_excerpt,
57+
latency_ms: span.duration_ms,
58+
}));
59+
3060
return (
3161
<MainLayout>
3262
<div className="space-y-6">
@@ -100,6 +130,79 @@ export default function AgentActivityTraceDetailPage() {
100130
<h2 className="mb-3 text-lg font-semibold text-gray-900 dark:text-white">Timing waterfall</h2>
101131
<TraceWaterfall spans={data.spans} />
102132
</Card>
133+
134+
<section className="grid grid-cols-1 gap-6 xl:grid-cols-2">
135+
<Card className="p-4">
136+
<h2 className="mb-3 text-lg font-semibold text-gray-900 dark:text-white">Tool calls</h2>
137+
{fallbackToolCalls.length === 0 ? (
138+
<p className="text-sm text-gray-500 dark:text-gray-400">No tool call details available.</p>
139+
) : (
140+
<ul className="space-y-3" aria-label="Tool call list">
141+
{fallbackToolCalls.map((toolCall) => (
142+
<li key={`${toolCall.span_id}-${toolCall.tool_name}`} className="rounded-md border border-gray-200 p-3 dark:border-gray-700">
143+
<p className="text-sm font-semibold text-gray-900 dark:text-white">{toolCall.tool_name}</p>
144+
{toolCall.input && (
145+
<p className="mt-1 text-xs text-gray-600 dark:text-gray-300">
146+
Input: {toolCall.input.slice(0, 180)}
147+
{toolCall.input.length > 180 ? '…' : ''}
148+
</p>
149+
)}
150+
{toolCall.output && (
151+
<p className="mt-1 text-xs text-gray-600 dark:text-gray-300">
152+
Output: {toolCall.output.slice(0, 180)}
153+
{toolCall.output.length > 180 ? '…' : ''}
154+
</p>
155+
)}
156+
</li>
157+
))}
158+
</ul>
159+
)}
160+
</Card>
161+
162+
<Card className="p-4">
163+
<h2 className="mb-3 text-lg font-semibold text-gray-900 dark:text-white">Model invocations</h2>
164+
{fallbackModelInvocations.length === 0 ? (
165+
<p className="text-sm text-gray-500 dark:text-gray-400">No model invocation details available.</p>
166+
) : (
167+
<ul className="space-y-3" aria-label="Model invocation list">
168+
{fallbackModelInvocations.map((invocation) => (
169+
<li key={`${invocation.span_id}-${invocation.model_name}`} className="rounded-md border border-gray-200 p-3 dark:border-gray-700">
170+
<p className="text-sm font-semibold text-gray-900 dark:text-white">
171+
{invocation.model_name} ({invocation.model_tier})
172+
</p>
173+
{invocation.prompt_excerpt && (
174+
<p className="mt-1 text-xs text-gray-600 dark:text-gray-300">
175+
Prompt: {invocation.prompt_excerpt.slice(0, 220)}
176+
{invocation.prompt_excerpt.length > 220 ? '…' : ''}
177+
</p>
178+
)}
179+
{invocation.completion_excerpt && (
180+
<p className="mt-1 text-xs text-gray-600 dark:text-gray-300">
181+
Completion: {invocation.completion_excerpt.slice(0, 220)}
182+
{invocation.completion_excerpt.length > 220 ? '…' : ''}
183+
</p>
184+
)}
185+
</li>
186+
))}
187+
</ul>
188+
)}
189+
</Card>
190+
</section>
191+
192+
<Card className="p-4">
193+
<h2 className="mb-3 text-lg font-semibold text-gray-900 dark:text-white">Decision outcome</h2>
194+
<p className="text-sm text-gray-700 dark:text-gray-300">
195+
{data.decision_outcome ?? data.spans.find((span) => span.decision_outcome)?.decision_outcome ?? 'Not captured'}
196+
</p>
197+
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
198+
Confidence:{' '}
199+
{typeof data.decision_confidence === 'number'
200+
? `${Math.round(data.decision_confidence * 100)}%`
201+
: typeof data.spans.find((span) => typeof span.confidence_score === 'number')?.confidence_score === 'number'
202+
? `${Math.round((data.spans.find((span) => typeof span.confidence_score === 'number')?.confidence_score ?? 0) * 100)}%`
203+
: 'N/A'}
204+
</p>
205+
</Card>
103206
</>
104207
)}
105208
</div>

apps/ui/app/admin/agent-activity/page.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export default function AgentActivityPage() {
2323
(data && !data.tracing_enabled) || (isError && isTracingUnavailableError(error))
2424
);
2525

26+
const errorLog = (data?.trace_feed ?? [])
27+
.filter((trace) => trace.status === 'error' || trace.error_count > 0)
28+
.slice(0, 8);
29+
2630
return (
2731
<MainLayout>
2832
<div className="space-y-6">
@@ -135,6 +139,51 @@ export default function AgentActivityPage() {
135139
</div>
136140
</Card>
137141
</section>
142+
143+
<Card className="p-0 overflow-hidden">
144+
<div className="border-b border-gray-200 p-4 dark:border-gray-700">
145+
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Error / retry log</h2>
146+
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
147+
Recent failed or retried traces with current recovery status.
148+
</p>
149+
</div>
150+
151+
{errorLog.length === 0 ? (
152+
<p className="p-4 text-sm text-gray-500 dark:text-gray-400">No failed traces for this range.</p>
153+
) : (
154+
<div className="overflow-x-auto">
155+
<table className="min-w-full text-sm">
156+
<thead className="bg-gray-50 dark:bg-gray-900/40">
157+
<tr>
158+
<th className="px-4 py-2 text-left font-semibold text-gray-700 dark:text-gray-300">Trace</th>
159+
<th className="px-4 py-2 text-left font-semibold text-gray-700 dark:text-gray-300">Agent</th>
160+
<th className="px-4 py-2 text-left font-semibold text-gray-700 dark:text-gray-300">Errors</th>
161+
<th className="px-4 py-2 text-left font-semibold text-gray-700 dark:text-gray-300">Retry status</th>
162+
</tr>
163+
</thead>
164+
<tbody>
165+
{errorLog.map((trace) => (
166+
<tr key={`error-${trace.trace_id}`} className="border-t border-gray-200 dark:border-gray-700">
167+
<td className="px-4 py-2">
168+
<Link
169+
href={`/admin/agent-activity/${trace.trace_id}`}
170+
className="text-blue-600 hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:text-blue-400"
171+
>
172+
{trace.trace_id}
173+
</Link>
174+
</td>
175+
<td className="px-4 py-2 text-gray-700 dark:text-gray-300">{trace.agent_name}</td>
176+
<td className="px-4 py-2 text-gray-700 dark:text-gray-300">{trace.error_count}</td>
177+
<td className="px-4 py-2 text-gray-700 dark:text-gray-300">
178+
{trace.status === 'error' ? 'Needs retry' : 'Recovered'}
179+
</td>
180+
</tr>
181+
))}
182+
</tbody>
183+
</table>
184+
</div>
185+
)}
186+
</Card>
138187
</>
139188
)}
140189
</div>

apps/ui/app/admin/enrichment-monitor/[entityId]/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ export default function EnrichmentMonitorDetailPage() {
3434
<span className="text-gray-900 dark:text-white">{entityId}</span>
3535
<span className="mx-2" aria-hidden="true">·</span>
3636
<Link
37-
href="/admin/agent-activity"
37+
href={data?.trace_id ? `/admin/agent-activity/${data.trace_id}` : '/admin/agent-activity'}
3838
className="text-blue-600 dark:text-blue-400 hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 rounded"
3939
>
40-
Agent Activity
40+
{data?.trace_id ? 'Related Trace' : 'Agent Activity'}
4141
</Link>
4242
</nav>
4343

apps/ui/app/search/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,10 @@ export default function SearchPage() {
107107
<div className="mb-8 space-y-4">
108108
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Search</h1>
109109
<Link
110-
href="/admin/agent-activity"
110+
href={data?.trace_id ? `/admin/agent-activity/${data.trace_id}` : '/admin/agent-activity'}
111111
className="inline-flex text-sm text-blue-600 hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:text-blue-400"
112112
>
113-
View Agent Activity
113+
{data?.trace_id ? 'View search trace' : 'View Agent Activity'}
114114
</Link>
115115
<SearchInput
116116
placeholder="Search products..."

apps/ui/lib/hooks/useAgentMonitor.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,23 @@ export function useAgentMonitorDashboard(timeRange: AgentMonitorTimeRange) {
3333
return useQuery({
3434
queryKey: ['admin', 'agent-activity', 'dashboard', timeRange],
3535
queryFn: () => agentMonitorService.getDashboard(timeRange),
36-
refetchInterval: 15_000,
36+
refetchInterval: 10_000,
37+
});
38+
}
39+
40+
export function useAgentHealth(timeRange: AgentMonitorTimeRange) {
41+
return useQuery({
42+
queryKey: ['admin', 'agent-activity', 'health-cards', timeRange],
43+
queryFn: () => agentMonitorService.getAgentHealth(timeRange),
44+
refetchInterval: 30_000,
45+
});
46+
}
47+
48+
export function useRecentTraces(agentName: string | undefined, timeRange: AgentMonitorTimeRange, limit = 25) {
49+
return useQuery({
50+
queryKey: ['admin', 'agent-activity', 'recent-traces', agentName, limit, timeRange],
51+
queryFn: () => agentMonitorService.getRecentTraces(agentName, limit, timeRange),
52+
refetchInterval: 10_000,
3753
});
3854
}
3955

@@ -42,14 +58,30 @@ export function useAgentTraceDetail(traceId: string, timeRange: AgentMonitorTime
4258
queryKey: ['admin', 'agent-activity', 'trace-detail', traceId, timeRange],
4359
queryFn: () => agentMonitorService.getTraceDetail(traceId, timeRange),
4460
enabled: Boolean(traceId),
45-
refetchInterval: 30_000,
4661
});
4762
}
4863

4964
export function useAgentEvaluations(timeRange: AgentMonitorTimeRange) {
5065
return useQuery({
5166
queryKey: ['admin', 'agent-activity', 'evaluations', timeRange],
52-
queryFn: () => agentMonitorService.getEvaluations(timeRange),
67+
queryFn: () => agentMonitorService.getLatestEvaluations(timeRange),
68+
refetchInterval: 30_000,
69+
});
70+
}
71+
72+
export function useModelUsageStats(timeRange: AgentMonitorTimeRange) {
73+
return useQuery({
74+
queryKey: ['admin', 'agent-activity', 'model-usage', timeRange],
75+
queryFn: () => agentMonitorService.getModelUsageStats(timeRange),
76+
refetchInterval: 30_000,
77+
});
78+
}
79+
80+
export function useEvaluationTrends(timeRange: AgentMonitorTimeRange) {
81+
return useQuery({
82+
queryKey: ['admin', 'agent-activity', 'evaluation-trends', timeRange],
83+
queryFn: () => agentMonitorService.getLatestEvaluations(timeRange),
84+
select: (payload) => payload.trends,
5385
refetchInterval: 30_000,
5486
});
5587
}

apps/ui/lib/services/agentMonitorService.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import apiClient, { handleApiError } from '../api/client';
22
import API_ENDPOINTS from '../api/endpoints';
33
import type {
44
AgentEvaluationsPayload,
5+
AgentHealthCardMetric,
56
AgentMonitorDashboard,
7+
AgentModelUsageRow,
68
AgentMonitorTimeRange,
9+
AgentTraceSummary,
710
AgentTraceDetail,
811
} from '../types/api';
912

@@ -13,6 +16,25 @@ function withTimeRange(path: string, timeRange: AgentMonitorTimeRange): string {
1316
}
1417

1518
export const agentMonitorService = {
19+
async getAgentHealth(timeRange: AgentMonitorTimeRange): Promise<AgentHealthCardMetric[]> {
20+
const dashboard = await this.getDashboard(timeRange);
21+
return dashboard.health_cards;
22+
},
23+
24+
async getRecentTraces(
25+
agentName: string | undefined,
26+
limit: number,
27+
timeRange: AgentMonitorTimeRange
28+
): Promise<AgentTraceSummary[]> {
29+
const dashboard = await this.getDashboard(timeRange);
30+
const normalizedAgentName = agentName?.trim().toLowerCase();
31+
const filtered = normalizedAgentName
32+
? dashboard.trace_feed.filter((trace) => trace.agent_name.toLowerCase() === normalizedAgentName)
33+
: dashboard.trace_feed;
34+
35+
return filtered.slice(0, Math.max(limit, 0));
36+
},
37+
1638
async getDashboard(timeRange: AgentMonitorTimeRange): Promise<AgentMonitorDashboard> {
1739
try {
1840
const response = await apiClient.get<AgentMonitorDashboard>(
@@ -56,6 +78,15 @@ export const agentMonitorService = {
5678
throw handleApiError(error);
5779
}
5880
},
81+
82+
async getModelUsageStats(timeRange: AgentMonitorTimeRange): Promise<AgentModelUsageRow[]> {
83+
const dashboard = await this.getDashboard(timeRange);
84+
return dashboard.model_usage;
85+
},
86+
87+
async getLatestEvaluations(timeRange: AgentMonitorTimeRange): Promise<AgentEvaluationsPayload> {
88+
return this.getEvaluations(timeRange);
89+
},
5990
};
6091

6192
export default agentMonitorService;

apps/ui/lib/services/semanticSearchService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface SemanticSearchResponse {
3737
items: UiProduct[];
3838
source: 'agent' | 'crud';
3939
mode: 'keyword' | 'intelligent';
40+
trace_id?: string;
4041
intent?: SemanticSearchIntent | null;
4142
subqueries?: string[];
4243
}
@@ -69,6 +70,7 @@ export const semanticSearchService = {
6970
items: mapAcpProductsToUi(results),
7071
source: 'agent',
7172
mode,
73+
trace_id: typeof payload.trace_id === 'string' ? payload.trace_id : undefined,
7274
intent: (payload.intent as SemanticSearchIntent | undefined) || null,
7375
subqueries: Array.isArray(payload.subqueries)
7476
? payload.subqueries.filter((value: unknown): value is string => typeof value === 'string')

apps/ui/lib/types/api.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,7 @@ export interface EnrichmentEntityDetail {
655655
title: string;
656656
status: EnrichmentJobStatus;
657657
confidence: number;
658+
trace_id?: string;
658659
source_assets: string[];
659660
image_evidence: string[];
660661
reasoning: string;
@@ -702,6 +703,34 @@ export interface AgentTraceSpan {
702703
duration_ms: number;
703704
model_tier?: AgentModelTier;
704705
error_message?: string;
706+
tool_name?: string;
707+
tool_input?: string;
708+
tool_output?: string;
709+
model_name?: string;
710+
prompt_excerpt?: string;
711+
completion_excerpt?: string;
712+
decision_outcome?: string;
713+
confidence_score?: number;
714+
}
715+
716+
export interface AgentTraceToolCall {
717+
span_id: string;
718+
tool_name: string;
719+
input?: string;
720+
output?: string;
721+
status?: AgentTraceStatus;
722+
}
723+
724+
export interface AgentTraceModelInvocation {
725+
span_id: string;
726+
model_name: string;
727+
model_tier: AgentModelTier;
728+
prompt_excerpt?: string;
729+
completion_excerpt?: string;
730+
input_tokens?: number;
731+
output_tokens?: number;
732+
latency_ms?: number;
733+
cost_usd?: number;
705734
}
706735

707736
export interface AgentModelUsageRow {
@@ -731,6 +760,10 @@ export interface AgentTraceDetail {
731760
started_at: string;
732761
duration_ms: number;
733762
spans: AgentTraceSpan[];
763+
tool_calls?: AgentTraceToolCall[];
764+
model_invocations?: AgentTraceModelInvocation[];
765+
decision_outcome?: string;
766+
decision_confidence?: number;
734767
}
735768

736769
export interface AgentEvaluationTrend {

0 commit comments

Comments
 (0)