|
14 | 14 | get_start_span_function, |
15 | 15 | normalize_message_roles, |
16 | 16 | set_data_normalized, |
| 17 | + truncate_and_annotate_messages, |
| 18 | + transform_content_part, |
17 | 19 | ) |
18 | 20 | from sentry_sdk.consts import OP, SPANDATA |
19 | 21 | from sentry_sdk.integrations import DidNotEnable, Integration |
@@ -127,6 +129,39 @@ def _get_ai_system(all_params: "Dict[str, Any]") -> "Optional[str]": |
127 | 129 | } |
128 | 130 |
|
129 | 131 |
|
| 132 | +def _transform_langchain_content_block( |
| 133 | + content_block: "Dict[str, Any]", |
| 134 | +) -> "Dict[str, Any]": |
| 135 | + """ |
| 136 | + Transform a LangChain content block using the shared transform_content_part function. |
| 137 | +
|
| 138 | + Returns the original content block if transformation is not applicable |
| 139 | + (e.g., for text blocks or unrecognized formats). |
| 140 | + """ |
| 141 | + result = transform_content_part(content_block) |
| 142 | + return result if result is not None else content_block |
| 143 | + |
| 144 | + |
| 145 | +def _transform_langchain_message_content(content: "Any") -> "Any": |
| 146 | + """ |
| 147 | + Transform LangChain message content, handling both string content and |
| 148 | + list of content blocks. |
| 149 | + """ |
| 150 | + if isinstance(content, str): |
| 151 | + return content |
| 152 | + |
| 153 | + if isinstance(content, (list, tuple)): |
| 154 | + transformed = [] |
| 155 | + for block in content: |
| 156 | + if isinstance(block, dict): |
| 157 | + transformed.append(_transform_langchain_content_block(block)) |
| 158 | + else: |
| 159 | + transformed.append(block) |
| 160 | + return transformed |
| 161 | + |
| 162 | + return content |
| 163 | + |
| 164 | + |
130 | 165 | # Contextvar to track agent names in a stack for re-entrant agent support |
131 | 166 | _agent_stack: "contextvars.ContextVar[Optional[List[Optional[str]]]]" = ( |
132 | 167 | contextvars.ContextVar("langchain_agent_stack", default=None) |
@@ -278,7 +313,9 @@ def _handle_error(self, run_id: "UUID", error: "Any") -> None: |
278 | 313 | del self.span_map[run_id] |
279 | 314 |
|
280 | 315 | def _normalize_langchain_message(self, message: "BaseMessage") -> "Any": |
281 | | - parsed = {"role": message.type, "content": message.content} |
| 316 | + # Transform content to handle multimodal data (images, audio, video, files) |
| 317 | + transformed_content = _transform_langchain_message_content(message.content) |
| 318 | + parsed = {"role": message.type, "content": transformed_content} |
282 | 319 | parsed.update(message.additional_kwargs) |
283 | 320 | return parsed |
284 | 321 |
|
@@ -376,12 +413,17 @@ def on_llm_start( |
376 | 413 | } |
377 | 414 | for prompt in prompts |
378 | 415 | ] |
379 | | - set_data_normalized( |
380 | | - span, |
381 | | - SPANDATA.GEN_AI_REQUEST_MESSAGES, |
382 | | - normalized_messages, |
383 | | - unpack=False, |
| 416 | + scope = sentry_sdk.get_current_scope() |
| 417 | + messages_data = truncate_and_annotate_messages( |
| 418 | + normalized_messages, span, scope |
384 | 419 | ) |
| 420 | + if messages_data is not None: |
| 421 | + set_data_normalized( |
| 422 | + span, |
| 423 | + SPANDATA.GEN_AI_REQUEST_MESSAGES, |
| 424 | + messages_data, |
| 425 | + unpack=False, |
| 426 | + ) |
385 | 427 |
|
386 | 428 | def on_chat_model_start( |
387 | 429 | self: "SentryLangchainCallback", |
@@ -451,12 +493,17 @@ def on_chat_model_start( |
451 | 493 | self._normalize_langchain_message(message) |
452 | 494 | ) |
453 | 495 | normalized_messages = normalize_message_roles(normalized_messages) |
454 | | - set_data_normalized( |
455 | | - span, |
456 | | - SPANDATA.GEN_AI_REQUEST_MESSAGES, |
457 | | - normalized_messages, |
458 | | - unpack=False, |
| 496 | + scope = sentry_sdk.get_current_scope() |
| 497 | + messages_data = truncate_and_annotate_messages( |
| 498 | + normalized_messages, span, scope |
459 | 499 | ) |
| 500 | + if messages_data is not None: |
| 501 | + set_data_normalized( |
| 502 | + span, |
| 503 | + SPANDATA.GEN_AI_REQUEST_MESSAGES, |
| 504 | + messages_data, |
| 505 | + unpack=False, |
| 506 | + ) |
460 | 507 |
|
461 | 508 | def on_chat_model_end( |
462 | 509 | self: "SentryLangchainCallback", |
@@ -968,12 +1015,17 @@ def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": |
968 | 1015 | and integration.include_prompts |
969 | 1016 | ): |
970 | 1017 | normalized_messages = normalize_message_roles([input]) |
971 | | - set_data_normalized( |
972 | | - span, |
973 | | - SPANDATA.GEN_AI_REQUEST_MESSAGES, |
974 | | - normalized_messages, |
975 | | - unpack=False, |
| 1018 | + scope = sentry_sdk.get_current_scope() |
| 1019 | + messages_data = truncate_and_annotate_messages( |
| 1020 | + normalized_messages, span, scope |
976 | 1021 | ) |
| 1022 | + if messages_data is not None: |
| 1023 | + set_data_normalized( |
| 1024 | + span, |
| 1025 | + SPANDATA.GEN_AI_REQUEST_MESSAGES, |
| 1026 | + messages_data, |
| 1027 | + unpack=False, |
| 1028 | + ) |
977 | 1029 |
|
978 | 1030 | output = result.get("output") |
979 | 1031 | if ( |
@@ -1025,12 +1077,17 @@ def new_stream(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": |
1025 | 1077 | and integration.include_prompts |
1026 | 1078 | ): |
1027 | 1079 | normalized_messages = normalize_message_roles([input]) |
1028 | | - set_data_normalized( |
1029 | | - span, |
1030 | | - SPANDATA.GEN_AI_REQUEST_MESSAGES, |
1031 | | - normalized_messages, |
1032 | | - unpack=False, |
| 1080 | + scope = sentry_sdk.get_current_scope() |
| 1081 | + messages_data = truncate_and_annotate_messages( |
| 1082 | + normalized_messages, span, scope |
1033 | 1083 | ) |
| 1084 | + if messages_data is not None: |
| 1085 | + set_data_normalized( |
| 1086 | + span, |
| 1087 | + SPANDATA.GEN_AI_REQUEST_MESSAGES, |
| 1088 | + messages_data, |
| 1089 | + unpack=False, |
| 1090 | + ) |
1034 | 1091 |
|
1035 | 1092 | # Run the agent |
1036 | 1093 | result = f(self, *args, **kwargs) |
|
0 commit comments