1313 has_span_streaming_enabled ,
1414 should_truncate_gen_ai_input ,
1515)
16- from sentry_sdk .utils import safe_serialize
1716
17+ from .._extract import (
18+ extract_model_info ,
19+ extract_request_messages ,
20+ extract_response_parts ,
21+ extract_system_instructions ,
22+ )
1823from ..consts import SPAN_ORIGIN
1924from ..utils import (
20- _get_model_name ,
2125 _set_agent_data ,
2226 _set_available_tools ,
2327 _set_model_data ,
2428 _should_send_prompts ,
2529 get_current_agent ,
2630 get_is_streaming ,
2731)
28- from .utils import (
29- _serialize_binary_content_item ,
30- _serialize_image_url_item ,
31- _set_usage_data ,
32- )
32+ from .utils import _set_usage_data
3333
3434if TYPE_CHECKING :
35- from typing import Any , Dict , List , Optional , Union
35+ from typing import Any , Optional , Union
3636
37- from pydantic_ai .messages import ModelMessage , ModelResponse , SystemPromptPart
37+ from pydantic_ai .messages import ModelResponse
3838
39- from sentry_sdk import _types
4039 from sentry_sdk .traces import StreamedSpan
4140
42- try :
43- from pydantic_ai .messages import (
44- BaseToolCallPart ,
45- BaseToolReturnPart ,
46- BinaryContent ,
47- ImageUrl ,
48- SystemPromptPart ,
49- TextPart ,
50- ThinkingPart ,
51- UserPromptPart ,
52- )
53- except ImportError :
54- # Fallback if these classes are not available
55- BaseToolCallPart = None # type: ignore[misc,assignment]
56- BaseToolReturnPart = None # type: ignore[misc,assignment]
57- SystemPromptPart = None # type: ignore[misc,assignment]
58- UserPromptPart = None # type: ignore[misc,assignment]
59- TextPart = None # type: ignore[misc,assignment]
60- ThinkingPart = None # type: ignore[misc,assignment]
61- BinaryContent = None # type: ignore[misc,assignment]
62- ImageUrl = None # type: ignore[misc,assignment]
63- ThinkingPart = None # type: ignore[misc,assignment]
64-
65-
66- def _transform_system_instructions (
67- permanent_instructions : "list[SystemPromptPart]" ,
68- current_instructions : "list[str]" ,
69- ) -> "list[_types.TextPart]" :
70- text_parts : "list[_types.TextPart]" = [
71- {
72- "type" : "text" ,
73- "content" : instruction .content ,
74- }
75- for instruction in permanent_instructions
76- ]
77-
78- text_parts .extend (
79- {
80- "type" : "text" ,
81- "content" : instruction ,
82- }
83- for instruction in current_instructions
84- )
85-
86- return text_parts
87-
88-
89- def _get_system_instructions (
90- messages : "list[ModelMessage]" ,
91- ) -> "tuple[list[SystemPromptPart], list[str]]" :
92- permanent_instructions = []
93- current_instructions = []
94-
95- for msg in messages :
96- if hasattr (msg , "parts" ):
97- for part in msg .parts :
98- if SystemPromptPart is not None and isinstance (part , SystemPromptPart ):
99- permanent_instructions .append (part )
100-
101- if hasattr (msg , "instructions" ) and msg .instructions is not None :
102- current_instructions .append (msg .instructions )
103-
104- return permanent_instructions , current_instructions
105-
10641
10742def _set_input_messages (
10843 span : "Union[sentry_sdk.tracing.Span, StreamedSpan]" , messages : "Any"
@@ -114,97 +49,16 @@ def _set_input_messages(
11449 if not messages :
11550 return
11651
117- permanent_instructions , current_instructions = _get_system_instructions (messages )
118- if len ( permanent_instructions ) > 0 or len ( current_instructions ) > 0 :
52+ system_instructions = extract_system_instructions (messages )
53+ if system_instructions :
11954 _set_span_data_attribute (
12055 span ,
12156 SPANDATA .GEN_AI_SYSTEM_INSTRUCTIONS ,
122- json .dumps (
123- _transform_system_instructions (
124- permanent_instructions , current_instructions
125- )
126- ),
57+ json .dumps (system_instructions ),
12758 )
12859
12960 try :
130- formatted_messages = []
131-
132- for msg in messages :
133- if hasattr (msg , "parts" ):
134- for part in msg .parts :
135- role = "user"
136- # Use isinstance checks with proper base classes
137- if SystemPromptPart is not None and isinstance (
138- part , SystemPromptPart
139- ):
140- continue
141- elif (
142- (TextPart is not None and isinstance (part , TextPart ))
143- or (ThinkingPart is not None and isinstance (part , ThinkingPart ))
144- or (
145- BaseToolCallPart is not None
146- and isinstance (part , BaseToolCallPart )
147- )
148- ):
149- role = "assistant"
150- elif BaseToolReturnPart is not None and isinstance (
151- part , BaseToolReturnPart
152- ):
153- role = "tool"
154-
155- content : "List[Dict[str, Any] | str]" = []
156- tool_calls = None
157- tool_call_id = None
158-
159- # Handle ToolCallPart (assistant requesting tool use)
160- if BaseToolCallPart is not None and isinstance (
161- part , BaseToolCallPart
162- ):
163- tool_call_data = {}
164- if hasattr (part , "tool_name" ):
165- tool_call_data ["name" ] = part .tool_name
166- if hasattr (part , "args" ):
167- tool_call_data ["arguments" ] = safe_serialize (part .args )
168- if tool_call_data :
169- tool_calls = [tool_call_data ]
170- # Handle ToolReturnPart (tool result)
171- elif BaseToolReturnPart is not None and isinstance (
172- part , BaseToolReturnPart
173- ):
174- if hasattr (part , "tool_name" ):
175- tool_call_id = part .tool_name
176- if hasattr (part , "content" ):
177- content .append ({"type" : "text" , "text" : str (part .content )})
178- # Handle regular content
179- elif hasattr (part , "content" ):
180- if isinstance (part .content , str ):
181- content .append ({"type" : "text" , "text" : part .content })
182- elif isinstance (part .content , list ):
183- for item in part .content :
184- if isinstance (item , str ):
185- content .append ({"type" : "text" , "text" : item })
186- elif ImageUrl is not None and isinstance (
187- item , ImageUrl
188- ):
189- content .append (_serialize_image_url_item (item ))
190- elif BinaryContent is not None and isinstance (
191- item , BinaryContent
192- ):
193- content .append (_serialize_binary_content_item (item ))
194- else :
195- content .append (safe_serialize (item ))
196- else :
197- content .append ({"type" : "text" , "text" : str (part .content )})
198- # Add message if we have content or tool calls
199- if content or tool_calls :
200- message : "Dict[str, Any]" = {"role" : role }
201- if content :
202- message ["content" ] = content
203- if tool_calls :
204- message ["tool_calls" ] = tool_calls
205- if tool_call_id :
206- message ["tool_call_id" ] = tool_call_id
207- formatted_messages .append (message )
61+ formatted_messages = extract_request_messages (messages )
20862
20963 if formatted_messages :
21064 normalized_messages = normalize_message_roles (formatted_messages )
@@ -240,42 +94,13 @@ def _set_output_data(
24094 )
24195
24296 try :
243- if hasattr (response , "parts" ):
244- parts : "list[Union[_types.TextPart, _types.ReasoningPart, _types.ToolCallPart]]" = []
245-
246- for part in response .parts :
247- if (
248- TextPart is not None
249- and isinstance (part , TextPart )
250- and hasattr (part , "content" )
251- ):
252- parts .append ({"type" : "text" , "content" : part .content })
253-
254- elif ThinkingPart is not None and isinstance (part , ThinkingPart ):
255- parts .append (
256- {
257- "type" : "reasoning" ,
258- "content" : part .content ,
259- }
260- )
261-
262- elif BaseToolCallPart is not None and isinstance (
263- part , BaseToolCallPart
264- ):
265- tool_part : "_types.ToolCallPart" = {"type" : "tool_call" }
266- if hasattr (part , "tool_name" ):
267- tool_part ["name" ] = part .tool_name
268- if hasattr (part , "args" ):
269- tool_part ["arguments" ] = safe_serialize (part .args )
270- parts .append (tool_part )
271-
272- if parts :
273- _set_span_data_attribute (
274- span ,
275- SPANDATA .GEN_AI_OUTPUT_MESSAGES ,
276- json .dumps ([{"role" : "assistant" , "parts" : parts }]),
277- )
278-
97+ parts = extract_response_parts (response )
98+ if parts :
99+ _set_span_data_attribute (
100+ span ,
101+ SPANDATA .GEN_AI_OUTPUT_MESSAGES ,
102+ json .dumps ([{"role" : "assistant" , "parts" : parts }]),
103+ )
279104 except Exception :
280105 # If we fail to format output, just skip it
281106 pass
@@ -292,12 +117,11 @@ def ai_client_span(
292117 model: Model object
293118 model_settings: Model settings
294119 """
295- # Determine model name for span name
296- model_obj = model
297- if agent and hasattr (agent , "model" ):
298- model_obj = agent .model
299-
300- model_name = _get_model_name (model_obj ) or "unknown"
120+ # Determine model name for span name, resolving the same way as
121+ # _set_model_data so the span name and gen_ai.request.model agree
122+ model_name = (
123+ extract_model_info (model , None , agent or get_current_agent ()).name or "unknown"
124+ )
301125
302126 span_streaming = has_span_streaming_enabled (sentry_sdk .get_client ().options )
303127 if span_streaming :
0 commit comments