Skip to content

Commit 0f53ac0

Browse files
feat(openai-agents): Support span streaming
1 parent 6f67df0 commit 0f53ac0

10 files changed

Lines changed: 1807 additions & 258 deletions

File tree

sentry_sdk/integrations/openai_agents/patches/agent_run.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from sentry_sdk.consts import SPANDATA
55
from sentry_sdk.integrations import DidNotEnable
6+
from sentry_sdk.traces import StreamedSpan
67
from sentry_sdk.utils import capture_internal_exceptions, reraise
78

89
from ..spans import (
@@ -108,7 +109,13 @@ async def _run_single_turn(
108109
context_wrapper, agent, should_run_agent_start_hooks, kwargs
109110
)
110111

111-
if span is None or span.timestamp is not None:
112+
if (
113+
span is None
114+
or isinstance(span, StreamedSpan)
115+
and span.end_timestamp is not None
116+
or not isinstance(span, StreamedSpan)
117+
and span.timestamp is not None
118+
):
112119
return await original_run_single_turn(*args, **kwargs)
113120

114121
try:
@@ -188,7 +195,13 @@ async def _run_single_turn_streamed(
188195
is_streaming=True,
189196
)
190197

191-
if span is None or span.timestamp is not None:
198+
if (
199+
span is None
200+
or isinstance(span, StreamedSpan)
201+
and span.end_timestamp is not None
202+
or not isinstance(span, StreamedSpan)
203+
and span.timestamp is not None
204+
):
192205
return await original_run_single_turn_streamed(*args, **kwargs)
193206

194207
try:

sentry_sdk/integrations/openai_agents/patches/models.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import sentry_sdk
77
from sentry_sdk.consts import SPANDATA
88
from sentry_sdk.integrations import DidNotEnable
9+
from sentry_sdk.traces import StreamedSpan
910
from sentry_sdk.tracing import BAGGAGE_HEADER_NAME
1011
from sentry_sdk.tracing_utils import (
1112
add_sentry_baggage_to_headers,
@@ -34,7 +35,10 @@ def _set_response_model_on_agent_span(
3435
if response_model:
3536
agent_span = getattr(agent, "_sentry_agent_span", None)
3637
if agent_span:
37-
agent_span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model)
38+
if isinstance(agent_span, StreamedSpan):
39+
agent_span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model)
40+
else:
41+
agent_span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model)
3842

3943

4044
def _inject_trace_propagation_headers(
@@ -151,7 +155,12 @@ async def wrapped_stream_response(*args: "Any", **kwargs: "Any") -> "Any":
151155
for hosted_tool in hosted_tools:
152156
_inject_trace_propagation_headers(hosted_tool, span=span)
153157

154-
span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True)
158+
set_on_span = (
159+
span.set_attribute
160+
if isinstance(span, StreamedSpan)
161+
else span.set_data
162+
)
163+
set_on_span(SPANDATA.GEN_AI_RESPONSE_STREAMING, True)
155164

156165
streaming_response = None
157166
ttft_recorded = False
@@ -162,9 +171,7 @@ async def wrapped_stream_response(*args: "Any", **kwargs: "Any") -> "Any":
162171
# Detect first content token (text delta event)
163172
if not ttft_recorded and hasattr(event, "delta"):
164173
ttft = time.perf_counter() - start_time
165-
span.set_data(
166-
SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft
167-
)
174+
set_on_span(SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft)
168175
ttft_recorded = True
169176

170177
# Capture the full response from ResponseCompletedEvent

sentry_sdk/integrations/openai_agents/patches/runner.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import sentry_sdk
55
from sentry_sdk.consts import SPANDATA
66
from sentry_sdk.integrations import DidNotEnable
7+
from sentry_sdk.traces import StreamedSpan
78
from sentry_sdk.utils import capture_internal_exceptions, reraise
89

910
from ..spans import agent_workflow_span, update_invoke_agent_span
@@ -43,9 +44,15 @@ async def wrapper(*args: "Any", **kwargs: "Any") -> "Any":
4344
conversation_id = kwargs.get("conversation_id")
4445
if conversation_id:
4546
agent._sentry_conversation_id = conversation_id
46-
workflow_span.set_data(
47-
SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id
48-
)
47+
48+
if isinstance(workflow_span, StreamedSpan):
49+
workflow_span.set_attribute(
50+
SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id
51+
)
52+
else:
53+
workflow_span.set_data(
54+
SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id
55+
)
4956

5057
args = (agent, *args[1:])
5158
try:

sentry_sdk/integrations/openai_agents/spans/agent_workflow.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import sentry_sdk
44
from sentry_sdk.ai.utils import get_start_span_function
5+
from sentry_sdk.tracing_utils import has_span_streaming_enabled
56

67
from ..consts import SPAN_ORIGIN
78

@@ -11,6 +12,14 @@
1112

1213
def agent_workflow_span(agent: "agents.Agent") -> "sentry_sdk.tracing.Span":
1314
# Create a transaction or a span if an transaction is already active
15+
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
16+
if span_streaming:
17+
span = sentry_sdk.traces.start_span(
18+
name=f"{agent.name} workflow", attributes={"sentry.origin": SPAN_ORIGIN}
19+
)
20+
21+
return span
22+
1423
span = get_start_span_function()(
1524
name=f"{agent.name} workflow",
1625
origin=SPAN_ORIGIN,

sentry_sdk/integrations/openai_agents/spans/ai_client.py

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import sentry_sdk
44
from sentry_sdk.consts import OP, SPANDATA
5+
from sentry_sdk.traces import StreamedSpan
6+
from sentry_sdk.tracing_utils import has_span_streaming_enabled
57

68
from ..consts import SPAN_ORIGIN
79
from ..utils import (
@@ -12,7 +14,7 @@
1214
)
1315

1416
if TYPE_CHECKING:
15-
from typing import Any, Optional
17+
from typing import Any, Optional, Union
1618

1719
from agents import Agent
1820

@@ -28,13 +30,24 @@ def ai_client_span(
2830
elif hasattr(agent, "_sentry_request_model"):
2931
model_name = agent._sentry_request_model
3032

31-
span = sentry_sdk.start_span(
32-
op=OP.GEN_AI_CHAT,
33-
name=f"chat {model_name}",
34-
origin=SPAN_ORIGIN,
35-
)
36-
# TODO-anton: remove hardcoded stuff and replace something that also works for embedding and so on
37-
span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat")
33+
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
34+
if span_streaming:
35+
span = sentry_sdk.traces.start_span(
36+
name=f"chat {model_name}",
37+
attributes={
38+
"sentry.op": OP.GEN_AI_CHAT,
39+
"sentry.origin": SPAN_ORIGIN,
40+
SPANDATA.GEN_AI_OPERATION_NAME: "chat",
41+
},
42+
)
43+
else:
44+
span = sentry_sdk.start_span(
45+
op=OP.GEN_AI_CHAT,
46+
name=f"chat {model_name}",
47+
origin=SPAN_ORIGIN,
48+
)
49+
# TODO-anton: remove hardcoded stuff and replace something that also works for embedding and so on
50+
span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat")
3851

3952
_set_agent_data(span, agent)
4053
_set_input_data(span, get_response_kwargs)
@@ -43,7 +56,7 @@ def ai_client_span(
4356

4457

4558
def update_ai_client_span(
46-
span: "sentry_sdk.tracing.Span",
59+
span: "Union[sentry_sdk.tracing.Span, StreamedSpan]",
4760
response: "Any",
4861
response_model: "Optional[str]" = None,
4962
agent: "Optional[Agent]" = None,
@@ -55,13 +68,17 @@ def update_ai_client_span(
5568
if hasattr(response, "output") and response.output:
5669
_set_output_data(span, response)
5770

71+
set_on_span = (
72+
span.set_attribute if isinstance(span, StreamedSpan) else span.set_data
73+
)
74+
5875
if response_model is not None:
59-
span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model)
76+
set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model)
6077
elif hasattr(response, "model") and response.model:
61-
span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, str(response.model))
78+
set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, str(response.model))
6279

6380
# Set conversation ID from agent if available
6481
if agent:
6582
conv_id = getattr(agent, "_sentry_conversation_id", None)
6683
if conv_id:
67-
span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id)
84+
set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id)

sentry_sdk/integrations/openai_agents/spans/execute_tool.py

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,58 @@
33
import sentry_sdk
44
from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS
55
from sentry_sdk.scope import should_send_default_pii
6+
from sentry_sdk.traces import SpanStatus, StreamedSpan
7+
from sentry_sdk.tracing_utils import has_span_streaming_enabled
68

79
from ..consts import SPAN_ORIGIN
810
from ..utils import _set_agent_data
911

1012
if TYPE_CHECKING:
11-
from typing import Any
13+
from typing import Any, Union
1214

1315
import agents
1416

1517

1618
def execute_tool_span(
1719
tool: "agents.Tool", *args: "Any", **kwargs: "Any"
1820
) -> "sentry_sdk.tracing.Span":
19-
span = sentry_sdk.start_span(
20-
op=OP.GEN_AI_EXECUTE_TOOL,
21-
name=f"execute_tool {tool.name}",
22-
origin=SPAN_ORIGIN,
23-
)
21+
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
22+
if span_streaming:
23+
span = sentry_sdk.traces.start_span(
24+
name=f"execute_tool {tool.name}",
25+
attributes={
26+
"sentry.op": OP.GEN_AI_EXECUTE_TOOL,
27+
"sentry.origin": SPAN_ORIGIN,
28+
SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool",
29+
SPANDATA.GEN_AI_TOOL_NAME: tool.name,
30+
SPANDATA.GEN_AI_TOOL_DESCRIPTION: tool.description,
31+
},
32+
)
33+
34+
set_on_span = span.set_attribute
35+
else:
36+
span = sentry_sdk.start_span(
37+
op=OP.GEN_AI_EXECUTE_TOOL,
38+
name=f"execute_tool {tool.name}",
39+
origin=SPAN_ORIGIN,
40+
)
41+
42+
span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool")
2443

25-
span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool")
44+
span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool.name)
45+
span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool.description)
2646

27-
span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool.name)
28-
span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool.description)
47+
set_on_span = span.set_data
2948

3049
if should_send_default_pii():
3150
input = args[1]
32-
span.set_data(SPANDATA.GEN_AI_TOOL_INPUT, input)
51+
set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, input)
3352

3453
return span
3554

3655

3756
def update_execute_tool_span(
38-
span: "sentry_sdk.tracing.Span",
57+
span: "Union[sentry_sdk.tracing.Span, StreamedSpan]",
3958
agent: "agents.Agent",
4059
tool: "agents.Tool",
4160
result: "Any",
@@ -45,12 +64,19 @@ def update_execute_tool_span(
4564
if isinstance(result, str) and result.startswith(
4665
"An error occurred while running the tool"
4766
):
48-
span.set_status(SPANSTATUS.INTERNAL_ERROR)
67+
if isinstance(span, StreamedSpan):
68+
span.status = SpanStatus.ERROR
69+
else:
70+
span.set_status(SPANSTATUS.INTERNAL_ERROR)
71+
72+
set_on_span = (
73+
span.set_attribute if isinstance(span, StreamedSpan) else span.set_data
74+
)
4975

5076
if should_send_default_pii():
51-
span.set_data(SPANDATA.GEN_AI_TOOL_OUTPUT, result)
77+
set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, result)
5278

5379
# Add conversation ID from agent
5480
conv_id = getattr(agent, "_sentry_conversation_id", None)
5581
if conv_id:
56-
span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id)
82+
set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id)

sentry_sdk/integrations/openai_agents/spans/handoff.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import sentry_sdk
44
from sentry_sdk.consts import OP, SPANDATA
5+
from sentry_sdk.tracing_utils import has_span_streaming_enabled
56

67
from ..consts import SPAN_ORIGIN
78

@@ -12,14 +13,29 @@
1213
def handoff_span(
1314
context: "agents.RunContextWrapper", from_agent: "agents.Agent", to_agent_name: str
1415
) -> None:
15-
with sentry_sdk.start_span(
16-
op=OP.GEN_AI_HANDOFF,
17-
name=f"handoff from {from_agent.name} to {to_agent_name}",
18-
origin=SPAN_ORIGIN,
19-
) as span:
20-
span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "handoff")
16+
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
17+
if span_streaming:
18+
with sentry_sdk.traces.start_span(
19+
name=f"handoff from {from_agent.name} to {to_agent_name}",
20+
attributes={
21+
"sentry.op": OP.GEN_AI_HANDOFF,
22+
"sentry.origin": SPAN_ORIGIN,
23+
SPANDATA.GEN_AI_OPERATION_NAME: "handoff",
24+
},
25+
) as span:
26+
# Add conversation ID from agent
27+
conv_id = getattr(from_agent, "_sentry_conversation_id", None)
28+
if conv_id:
29+
span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id)
30+
else:
31+
with sentry_sdk.start_span(
32+
op=OP.GEN_AI_HANDOFF,
33+
name=f"handoff from {from_agent.name} to {to_agent_name}",
34+
origin=SPAN_ORIGIN,
35+
) as span:
36+
span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "handoff")
2137

22-
# Add conversation ID from agent
23-
conv_id = getattr(from_agent, "_sentry_conversation_id", None)
24-
if conv_id:
25-
span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id)
38+
# Add conversation ID from agent
39+
conv_id = getattr(from_agent, "_sentry_conversation_id", None)
40+
if conv_id:
41+
span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id)

0 commit comments

Comments
 (0)