17
17
raise DidNotEnable ("pydantic-ai not installed" )
18
18
19
19
20
- def _create_run_wrapper (original_func ):
21
- # type: (Callable[..., Any]) -> Callable[..., Any]
20
+ class _StreamingContextManagerWrapper :
21
+ """Wrapper for streaming methods that return async context managers."""
22
+
23
+ def __init__ (self , agent , original_ctx_manager , is_streaming = True ):
24
+ # type: (Any, Any, bool) -> None
25
+ self .agent = agent
26
+ self .original_ctx_manager = original_ctx_manager
27
+ self .is_streaming = is_streaming
28
+ self ._isolation_scope = None
29
+ self ._workflow_span = None
30
+
31
+ async def __aenter__ (self ):
32
+ # type: () -> Any
33
+ # Set up isolation scope and workflow span
34
+ self ._isolation_scope = sentry_sdk .isolation_scope ()
35
+ self ._isolation_scope .__enter__ ()
36
+
37
+ # Store agent reference and streaming flag
38
+ sentry_sdk .get_current_scope ().set_context (
39
+ "pydantic_ai_agent" , {"_agent" : self .agent , "_streaming" : self .is_streaming }
40
+ )
41
+
42
+ # Create workflow span
43
+ self ._workflow_span = agent_workflow_span (self .agent )
44
+ self ._workflow_span .__enter__ ()
45
+
46
+ # Enter the original context manager
47
+ result = await self .original_ctx_manager .__aenter__ ()
48
+ return result
49
+
50
+ async def __aexit__ (self , exc_type , exc_val , exc_tb ):
51
+ # type: (Any, Any, Any) -> None
52
+ try :
53
+ # Exit the original context manager first
54
+ await self .original_ctx_manager .__aexit__ (exc_type , exc_val , exc_tb )
55
+ finally :
56
+ # Clean up workflow span
57
+ if self ._workflow_span :
58
+ self ._workflow_span .__exit__ (exc_type , exc_val , exc_tb )
59
+
60
+ # Clean up isolation scope
61
+ if self ._isolation_scope :
62
+ self ._isolation_scope .__exit__ (exc_type , exc_val , exc_tb )
63
+
64
+
65
+ def _create_run_wrapper (original_func , is_streaming = False ):
66
+ # type: (Callable[..., Any], bool) -> Callable[..., Any]
22
67
"""
23
68
Wraps the Agent.run method to create a root span for the agent workflow.
69
+
70
+ Args:
71
+ original_func: The original run method
72
+ is_streaming: Whether this is a streaming method (for future use)
24
73
"""
25
74
26
75
@wraps (original_func )
@@ -29,10 +78,10 @@ async def wrapper(self, *args, **kwargs):
29
78
# Isolate each workflow so that when agents are run in asyncio tasks they
30
79
# don't touch each other's scopes
31
80
with sentry_sdk .isolation_scope ():
32
- # Store agent reference in Sentry scope for access in nested spans
81
+ # Store agent reference and streaming flag in Sentry scope for access in nested spans
33
82
# We store the full agent to allow access to tools and system prompts
34
83
sentry_sdk .get_current_scope ().set_context (
35
- "pydantic_ai_agent" , {"_agent" : self }
84
+ "pydantic_ai_agent" , {"_agent" : self , "_streaming" : is_streaming }
36
85
)
37
86
38
87
with agent_workflow_span (self ):
@@ -57,6 +106,7 @@ def _create_run_sync_wrapper(original_func):
57
106
# type: (Callable[..., Any]) -> Callable[..., Any]
58
107
"""
59
108
Wraps the Agent.run_sync method to create a root span for the agent workflow.
109
+ Note: run_sync is always non-streaming.
60
110
"""
61
111
62
112
@wraps (original_func )
@@ -65,10 +115,10 @@ def wrapper(self, *args, **kwargs):
65
115
# Isolate each workflow so that when agents are run they
66
116
# don't touch each other's scopes
67
117
with sentry_sdk .isolation_scope ():
68
- # Store agent reference in Sentry scope for access in nested spans
118
+ # Store agent reference and streaming flag in Sentry scope for access in nested spans
69
119
# We store the full agent to allow access to tools and system prompts
70
120
sentry_sdk .get_current_scope ().set_context (
71
- "pydantic_ai_agent" , {"_agent" : self }
121
+ "pydantic_ai_agent" , {"_agent" : self , "_streaming" : False }
72
122
)
73
123
74
124
with agent_workflow_span (self ):
@@ -89,18 +139,84 @@ def wrapper(self, *args, **kwargs):
89
139
return wrapper
90
140
91
141
142
+ def _create_streaming_wrapper (original_func ):
143
+ # type: (Callable[..., Any]) -> Callable[..., Any]
144
+ """
145
+ Wraps run_stream method that returns an async context manager.
146
+ """
147
+
148
+ @wraps (original_func )
149
+ def wrapper (self , * args , ** kwargs ):
150
+ # type: (Any, *Any, **Any) -> Any
151
+ # Call original function to get the context manager
152
+ original_ctx_manager = original_func (self , * args , ** kwargs )
153
+
154
+ # Wrap it with our instrumentation
155
+ return _StreamingContextManagerWrapper (
156
+ agent = self , original_ctx_manager = original_ctx_manager , is_streaming = True
157
+ )
158
+
159
+ return wrapper
160
+
161
+
162
+ def _create_streaming_events_wrapper (original_func ):
163
+ # type: (Callable[..., Any]) -> Callable[..., Any]
164
+ """
165
+ Wraps run_stream_events method that returns an async generator/iterator.
166
+ """
167
+
168
+ @wraps (original_func )
169
+ async def wrapper (self , * args , ** kwargs ):
170
+ # type: (Any, *Any, **Any) -> Any
171
+ # Isolate each workflow so that when agents are run in asyncio tasks they
172
+ # don't touch each other's scopes
173
+ with sentry_sdk .isolation_scope ():
174
+ # Store agent reference and streaming flag in Sentry scope for access in nested spans
175
+ sentry_sdk .get_current_scope ().set_context (
176
+ "pydantic_ai_agent" , {"_agent" : self , "_streaming" : True }
177
+ )
178
+
179
+ with agent_workflow_span (self ):
180
+ try :
181
+ # Call the original generator and yield all events
182
+ async for event in original_func (self , * args , ** kwargs ):
183
+ yield event
184
+ except Exception as exc :
185
+ _capture_exception (exc )
186
+
187
+ # It could be that there is an "invoke agent" span still open
188
+ current_span = sentry_sdk .get_current_span ()
189
+ if current_span is not None and current_span .timestamp is None :
190
+ current_span .__exit__ (None , None , None )
191
+
192
+ raise exc from None
193
+
194
+ return wrapper
195
+
196
+
92
197
def _patch_agent_run ():
93
198
# type: () -> None
94
199
"""
95
200
Patches the Agent run methods to create spans for agent execution.
201
+
202
+ This patches both non-streaming (run, run_sync) and streaming
203
+ (run_stream, run_stream_events) methods.
96
204
"""
97
205
# Import here to avoid circular imports
98
206
from pydantic_ai .agent import Agent
99
207
100
208
# Store original methods
101
209
original_run = Agent .run
102
210
original_run_sync = Agent .run_sync
211
+ original_run_stream = Agent .run_stream
212
+ original_run_stream_events = Agent .run_stream_events
103
213
104
- # Wrap and apply patches
105
- Agent .run = _create_run_wrapper (original_run ) # type: ignore
214
+ # Wrap and apply patches for non-streaming methods
215
+ Agent .run = _create_run_wrapper (original_run , is_streaming = False ) # type: ignore
106
216
Agent .run_sync = _create_run_sync_wrapper (original_run_sync ) # type: ignore
217
+
218
+ # Wrap and apply patches for streaming methods
219
+ Agent .run_stream = _create_streaming_wrapper (original_run_stream ) # type: ignore
220
+ Agent .run_stream_events = _create_streaming_events_wrapper (
221
+ original_run_stream_events
222
+ ) # type: ignore
0 commit comments