Skip to content

Commit 5dc1892

Browse files
authored
feat(agents): Improve AI span presentation with tool input preview and response model (#112579)
1 parent 955d0c0 commit 5dc1892

File tree

6 files changed

+97
-3
lines changed

6 files changed

+97
-3
lines changed

src/sentry/api/endpoints/organization_ai_conversation_details.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
"gen_ai.response.object",
4747
"gen_ai.response.text",
4848
"gen_ai.tool.name",
49+
"gen_ai.tool.call.arguments",
50+
"gen_ai.tool.input",
4951
"gen_ai.usage.total_tokens",
5052
"gen_ai.request.model",
5153
"gen_ai.response.model",

static/app/views/insights/pages/agents/components/aiSpanList.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {t} from 'sentry/locale';
1313
import {getDuration} from 'sentry/utils/duration/getDuration';
1414
import {LLMCosts} from 'sentry/views/insights/pages/agents/components/llmCosts';
1515
import {
16+
getFirstToolInputValue,
1617
getGenAiOpType,
1718
getIsAiAgentNode,
1819
getNumberAttr,
@@ -475,6 +476,7 @@ function getSpanPresentation(
475476
case GenAiOperationType.AI_CLIENT: {
476477
const tokens = getNumberAttr(node, SpanFields.GEN_AI_USAGE_TOTAL_TOKENS);
477478
const cost = getNumberAttr(node, SpanFields.GEN_AI_COST_TOTAL_TOKENS);
479+
const responseModel = getStringAttr(node, SpanFields.GEN_AI_RESPONSE_MODEL);
478480
const tokenLabel = tokens ? (
479481
<Fragment>
480482
<Count value={tokens} />
@@ -484,7 +486,7 @@ function getSpanPresentation(
484486
return {
485487
icon: <IconChat size="md" />,
486488
color,
487-
title: description || op,
489+
title: responseModel || description || op,
488490
subtitle:
489491
tokenLabel && cost ? (
490492
<Fragment>
@@ -497,11 +499,12 @@ function getSpanPresentation(
497499
}
498500
case GenAiOperationType.TOOL: {
499501
const toolName = getStringAttr(node, SpanFields.GEN_AI_TOOL_NAME);
502+
const firstInputValue = getFirstToolInputValue(node);
500503
return {
501504
icon: <IconFix size="md" />,
502505
color,
503506
title: toolName || op,
504-
subtitle: toolName ? op : '',
507+
subtitle: firstInputValue || (toolName ? op : ''),
505508
};
506509
}
507510
case GenAiOperationType.HANDOFF:

static/app/views/insights/pages/agents/hooks/useAITrace.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ const AI_TRACE_BASE_ATTRIBUTES = [
3535
SpanFields.GEN_AI_OPERATION_NAME,
3636
SpanFields.SPAN_STATUS,
3737
'status',
38+
'gen_ai.tool.call.arguments',
39+
'gen_ai.tool.input',
3840
];
3941

4042
export function useAITrace(traceSlug: string, timestamp?: number): UseAITraceResult {

static/app/views/insights/pages/agents/utils/aiTraceNodes.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,40 @@ export function getNumberAttr(node: AITraceSpanNode, field: string): number | un
139139
return undefined;
140140
}
141141

142+
const MAX_TOOL_INPUT_PREVIEW_LENGTH = 80;
143+
144+
/**
145+
* Parses tool input JSON and returns the value of the first key.
146+
* Used to show a preview of the tool input next to the tool name.
147+
*/
148+
export function getFirstToolInputValue(node: AITraceSpanNode): string | undefined {
149+
const toolInput =
150+
getStringAttr(node, 'gen_ai.tool.call.arguments') ||
151+
getStringAttr(node, 'gen_ai.tool.input');
152+
if (!toolInput) {
153+
return undefined;
154+
}
155+
156+
try {
157+
const parsed = JSON.parse(toolInput);
158+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
159+
const firstKey = Object.keys(parsed)[0];
160+
if (firstKey !== undefined) {
161+
const value = parsed[firstKey];
162+
const str = typeof value === 'string' ? value : JSON.stringify(value);
163+
if (str.length > MAX_TOOL_INPUT_PREVIEW_LENGTH) {
164+
return str.slice(0, MAX_TOOL_INPUT_PREVIEW_LENGTH) + '\u2026';
165+
}
166+
return str;
167+
}
168+
}
169+
} catch {
170+
// Invalid JSON, return undefined
171+
}
172+
173+
return undefined;
174+
}
175+
142176
export function hasError(node: AITraceSpanNode): boolean {
143177
if (node.errors.size > 0) {
144178
return true;

static/app/views/insights/pages/conversations/hooks/useConversation.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ interface ConversationApiSpan {
4545
'gen_ai.response.model'?: string;
4646
'gen_ai.response.object'?: string;
4747
'gen_ai.response.text'?: string;
48+
'gen_ai.tool.call.arguments'?: string;
49+
'gen_ai.tool.input'?: string;
4850
'gen_ai.tool.name'?: string;
4951
'gen_ai.usage.total_tokens'?: number;
5052
'span.name'?: string;
@@ -114,6 +116,8 @@ function createNodeFromApiSpan(
114116
[SpanFields.GEN_AI_RESPONSE_MODEL]: apiSpan['gen_ai.response.model'] ?? '',
115117
[SpanFields.GEN_AI_AGENT_NAME]: apiSpan['gen_ai.agent.name'] ?? '',
116118
[SpanFields.GEN_AI_TOOL_NAME]: apiSpan['gen_ai.tool.name'] ?? '',
119+
'gen_ai.tool.call.arguments': apiSpan['gen_ai.tool.call.arguments'] ?? '',
120+
'gen_ai.tool.input': apiSpan['gen_ai.tool.input'] ?? '',
117121
[SpanFields.GEN_AI_USAGE_TOTAL_TOKENS]: apiSpan['gen_ai.usage.total_tokens'] ?? 0,
118122
[SpanFields.GEN_AI_COST_TOTAL_TOKENS]: apiSpan['gen_ai.cost.total_tokens'] ?? 0,
119123
[SpanFields.SPAN_STATUS]: apiSpan['span.status'],

static/app/views/performance/newTraceDetails/traceRow/traceEAPSpanRow.tsx

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@ import React from 'react';
22
import {PlatformIcon} from 'platformicons';
33

44
import {ellipsize} from 'sentry/utils/string/ellipsize';
5+
import {
6+
getFirstToolInputValue,
7+
getStringAttr,
8+
} from 'sentry/views/insights/pages/agents/utils/aiTraceNodes';
9+
import {
10+
GenAiOperationType,
11+
getGenAiOperationTypeFromSpanOp,
12+
} from 'sentry/views/insights/pages/agents/utils/query';
13+
import {SpanFields} from 'sentry/views/insights/types';
514
import {TraceIcons} from 'sentry/views/performance/newTraceDetails/traceIcons';
615
import type {EapSpanNode} from 'sentry/views/performance/newTraceDetails/traceModels/traceTreeNode/eapSpanNode';
716
import {TraceBar} from 'sentry/views/performance/newTraceDetails/traceRow/traceBar';
@@ -13,6 +22,43 @@ import {
1322
type TraceRowProps,
1423
} from 'sentry/views/performance/newTraceDetails/traceRow/traceRow';
1524

25+
/**
26+
* Returns an enriched description for AI spans when attributes are available.
27+
* - Tool spans: "toolName: firstInputValue"
28+
* - AI client spans: responseModel (e.g., "gpt-4o")
29+
* Falls back to undefined so the caller can use the default description.
30+
*/
31+
function getAIEnhancedDescription(node: EapSpanNode): string | undefined {
32+
const attrs = node.attributes;
33+
if (!attrs) {
34+
return undefined;
35+
}
36+
37+
const opType =
38+
(attrs[SpanFields.GEN_AI_OPERATION_TYPE] as string | undefined) ??
39+
getGenAiOperationTypeFromSpanOp(node.op);
40+
41+
if (!opType) {
42+
return undefined;
43+
}
44+
45+
if (opType === GenAiOperationType.TOOL) {
46+
const toolName = getStringAttr(node as any, SpanFields.GEN_AI_TOOL_NAME);
47+
const firstValue = getFirstToolInputValue(node as any);
48+
if (toolName && firstValue) {
49+
return `${toolName}: ${firstValue}`;
50+
}
51+
return toolName ?? undefined;
52+
}
53+
54+
if (opType === GenAiOperationType.AI_CLIENT) {
55+
const responseModel = getStringAttr(node as any, SpanFields.GEN_AI_RESPONSE_MODEL);
56+
return responseModel ?? undefined;
57+
}
58+
59+
return undefined;
60+
}
61+
1662
export function TraceEAPSpanRow(props: TraceRowProps<EapSpanNode>) {
1763
const spanId = props.node.id;
1864

@@ -22,7 +68,10 @@ export function TraceEAPSpanRow(props: TraceRowProps<EapSpanNode>) {
2268
<PlatformIcon platform={props.projects[props.node.projectSlug ?? ''] ?? 'default'} />
2369
);
2470

25-
const description = props.node.description || props.node.value.name;
71+
const description =
72+
getAIEnhancedDescription(props.node) ||
73+
props.node.description ||
74+
props.node.value.name;
2675

2776
return (
2877
<div

0 commit comments

Comments
 (0)