Skip to content

Commit a1fac79

Browse files
ArthurKnauschromy
authored andcommitted
feat(agent-spans): Display used tools (#101506)
Format available tools nicely and highlight used ones. https://github.com/user-attachments/assets/fd8784a3-e24c-4d67-b2d7-e54f4d9655e9
1 parent 85966ca commit a1fac79

File tree

7 files changed

+101
-16
lines changed

7 files changed

+101
-16
lines changed

static/app/views/insights/agents/utils/query.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// AI Runs - equivalent to OTEL Invoke Agent span
22
// https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-agent-spans.md#invoke-agent-span
3-
const AI_RUN_OPS = [
3+
export const AI_RUN_OPS = [
44
'ai.run.generateText',
55
'ai.run.generateObject',
66
'gen_ai.invoke_agent',
@@ -81,6 +81,10 @@ export const getAIGenerationsFilter = () => {
8181
return `span.op:gen_ai.* !span.op:[${joinValues(NON_GENERATION_OPS)}]`;
8282
};
8383

84+
export const getToolSpansFilter = () => {
85+
return `span.op:"gen_ai.execute_tool"`;
86+
};
87+
8488
export const getAITracesFilter = () => {
8589
return `span.op:gen_ai.*`;
8690
};

static/app/views/insights/agents/utils/referrers.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export enum Referrer {
22
MODELS_TABLE = 'api.insights.agent-monitoring.models-table',
33
TOOLS_TABLE = 'api.insights.agent-monitoring.tools-table',
44
TRACE_DRAWER = 'api.insights.agent-monitoring.trace-drawer',
5+
TRACE_DRAWER_TOOL_USAGE = 'api.insights.agent-monitoring.trace-drawer-tool-usage',
56
TRACES_TABLE = 'api.insights.agent-monitoring.traces-table',
67
TOKEN_USAGE_WIDGET = 'api.insights.agent-monitoring.token-usage-widget',
78
MODEL_COST_WIDGET = 'api.insights.agent-monitoring.token-cost-widget',

static/app/views/performance/newTraceDetails/traceDrawer/details/highlightedAttributes.spec.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ describe('getHighlightedSpanAttributes', () => {
2525

2626
getHighlightedSpanAttributes({
2727
op: 'gen_ai.chat',
28+
spanId: '123',
2829
attributes,
2930
});
3031

@@ -57,6 +58,7 @@ describe('getHighlightedSpanAttributes', () => {
5758

5859
getHighlightedSpanAttributes({
5960
op: 'gen_ai.chat',
61+
spanId: '123',
6062
attributes,
6163
});
6264

@@ -70,6 +72,7 @@ describe('getHighlightedSpanAttributes', () => {
7072

7173
getHighlightedSpanAttributes({
7274
op: 'gen_ai.chat',
75+
spanId: '123',
7376
attributes,
7477
});
7578

@@ -84,6 +87,7 @@ describe('getHighlightedSpanAttributes', () => {
8487

8588
getHighlightedSpanAttributes({
8689
op: 'http.request',
90+
spanId: '123',
8791
attributes,
8892
});
8993

static/app/views/performance/newTraceDetails/traceDrawer/details/highlightedAttributes.tsx

Lines changed: 88 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
import styled from '@emotion/styled';
22
import * as Sentry from '@sentry/react';
33

4+
import {Tag} from '@sentry/scraps/badge';
5+
import {Flex} from '@sentry/scraps/layout';
6+
47
import {Tooltip} from 'sentry/components/core/tooltip';
58
import Count from 'sentry/components/count';
69
import {StructuredData} from 'sentry/components/structuredEventData';
7-
import {t} from 'sentry/locale';
10+
import {t, tn} from 'sentry/locale';
811
import {prettifyAttributeName} from 'sentry/views/explore/components/traceItemAttributes/utils';
912
import type {TraceItemResponseAttribute} from 'sentry/views/explore/hooks/useTraceItemDetails';
1013
import {LLMCosts} from 'sentry/views/insights/agents/components/llmCosts';
1114
import {ModelName} from 'sentry/views/insights/agents/components/modelName';
12-
import {AI_CREATE_AGENT_OPS, getIsAiSpan} from 'sentry/views/insights/agents/utils/query';
15+
import {
16+
AI_CREATE_AGENT_OPS,
17+
AI_RUN_OPS,
18+
getIsAiSpan,
19+
getToolSpansFilter,
20+
} from 'sentry/views/insights/agents/utils/query';
21+
import {Referrer} from 'sentry/views/insights/agents/utils/referrers';
22+
import {useSpans} from 'sentry/views/insights/common/queries/useDiscover';
23+
import {SpanFields} from 'sentry/views/insights/types';
1324

1425
type HighlightedAttribute = {
1526
name: string;
@@ -26,15 +37,17 @@ function tryParseJson(value: string) {
2637

2738
export function getHighlightedSpanAttributes({
2839
op,
40+
spanId,
2941
attributes = {},
3042
}: {
3143
attributes: Record<string, string> | undefined | TraceItemResponseAttribute[];
3244
op: string | undefined;
45+
spanId: string;
3346
}): HighlightedAttribute[] {
3447
const attributeObject = ensureAttributeObject(attributes);
3548

3649
if (getIsAiSpan({op})) {
37-
return getAISpanAttributes(attributeObject, op);
50+
return getAISpanAttributes({attributes: attributeObject, op, spanId});
3851
}
3952

4053
if (op?.startsWith('mcp.')) {
@@ -62,10 +75,15 @@ function ensureAttributeObject(
6275
return attributes;
6376
}
6477

65-
function getAISpanAttributes(
66-
attributes: Record<string, string | number | boolean>,
67-
op?: string
68-
) {
78+
function getAISpanAttributes({
79+
op,
80+
spanId,
81+
attributes = {},
82+
}: {
83+
attributes: Record<string, string | number | boolean>;
84+
op: string | undefined;
85+
spanId: string;
86+
}) {
6987
const highlightedAttributes = [];
7088

7189
const agentName = attributes['gen_ai.agent.name'] || attributes['gen_ai.function_id'];
@@ -142,16 +160,16 @@ function getAISpanAttributes(
142160
}
143161

144162
const availableTools = attributes['gen_ai.request.available_tools'];
145-
if (availableTools && AI_CREATE_AGENT_OPS.includes(op!)) {
163+
const toolsArray = tryParseJson(availableTools?.toString() || '');
164+
if (
165+
toolsArray &&
166+
Array.isArray(toolsArray) &&
167+
toolsArray.length > 0 &&
168+
[...AI_RUN_OPS, ...AI_CREATE_AGENT_OPS].includes(op!)
169+
) {
146170
highlightedAttributes.push({
147171
name: t('Available Tools'),
148-
value: (
149-
<StructuredData
150-
value={tryParseJson(availableTools.toString())}
151-
withAnnotatedText
152-
maxDefaultDepth={0}
153-
/>
154-
),
172+
value: <HighlightedTools availableTools={toolsArray} spanId={spanId} />,
155173
});
156174
}
157175

@@ -196,6 +214,61 @@ function getMCPAttributes(attributes: Record<string, string | number | boolean>)
196214
return highlightedAttributes;
197215
}
198216

217+
function HighlightedTools({
218+
availableTools,
219+
spanId,
220+
}: {
221+
availableTools: any[];
222+
spanId: string;
223+
}) {
224+
const toolNames = availableTools.map(tool => tool.name).filter(Boolean);
225+
const hasToolNames = toolNames.length > 0;
226+
const toolSpansQuery = useSpans(
227+
{
228+
search: `parent_span:${spanId} has:${SpanFields.GEN_AI_TOOL_NAME} ${getToolSpansFilter()}`,
229+
fields: [SpanFields.GEN_AI_TOOL_NAME],
230+
enabled: hasToolNames,
231+
},
232+
Referrer.TRACE_DRAWER_TOOL_USAGE
233+
);
234+
235+
const usedTools: Map<string, number> = new Map();
236+
toolSpansQuery.data?.forEach(span => {
237+
const toolName = span[SpanFields.GEN_AI_TOOL_NAME];
238+
usedTools.set(toolName, (usedTools.get(toolName) ?? 0) + 1);
239+
});
240+
241+
// Fall back to showing formatted JSON if tool names cannot be parsed
242+
if (!hasToolNames) {
243+
return (
244+
<StructuredData value={availableTools} withAnnotatedText maxDefaultDepth={0} />
245+
);
246+
}
247+
248+
return (
249+
<Flex direction="row" gap="xs" wrap="wrap">
250+
{toolNames.sort().map(tool => {
251+
const usageCount = usedTools.get(tool) ?? 0;
252+
return (
253+
<Tooltip
254+
key={tool}
255+
disabled={toolSpansQuery.isPending}
256+
title={
257+
usageCount === 0
258+
? t('Not used by agent')
259+
: tn('Used %s time', 'Used %s times', usageCount)
260+
}
261+
>
262+
<Tag key={tool} type={usedTools.has(tool) ? 'info' : 'default'}>
263+
{tool}
264+
</Tag>
265+
</Tooltip>
266+
);
267+
})}
268+
</Flex>
269+
);
270+
}
271+
199272
function HighlightedTokenAttributes({
200273
inputTokens,
201274
cachedTokens,

static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/description.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ export function SpanDescription({
274274
hideNodeActions={hideNodeActions}
275275
highlightedAttributes={getHighlightedSpanAttributes({
276276
attributes,
277+
spanId: span.event_id,
277278
op: span.op,
278279
})}
279280
/>

static/app/views/performance/newTraceDetails/traceDrawer/details/span/sections/description.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ export function SpanDescription({
199199
hideNodeActions={hideNodeActions}
200200
highlightedAttributes={getHighlightedSpanAttributes({
201201
attributes: span.data,
202+
spanId: span.span_id,
202203
op: span.op,
203204
})}
204205
/>

static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/highlights.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export function TransactionHighlights(props: HighlightProps) {
8989
hideNodeActions={props.hideNodeActions}
9090
highlightedAttributes={getHighlightedSpanAttributes({
9191
attributes: props.event.contexts.trace?.data,
92+
spanId: props.node.value.span_id,
9293
op: props.node.value['transaction.op'],
9394
})}
9495
/>

0 commit comments

Comments
 (0)