|
11 | 11 | normalize_message_roles, |
12 | 12 | truncate_and_annotate_messages, |
13 | 13 | get_start_span_function, |
| 14 | + transform_anthropic_content_part, |
14 | 15 | ) |
15 | 16 | from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS |
16 | 17 | from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration |
@@ -122,6 +123,27 @@ def _collect_ai_data( |
122 | 123 | return model, input_tokens, output_tokens, content_blocks |
123 | 124 |
|
124 | 125 |
|
| 126 | +def _transform_anthropic_content_block( |
| 127 | + content_block: "dict[str, Any]", |
| 128 | +) -> "dict[str, Any]": |
| 129 | + """ |
| 130 | + Transform an Anthropic content block using the Anthropic-specific transformer, |
| 131 | + with special handling for Anthropic's text-type documents. |
| 132 | + """ |
| 133 | + # Handle Anthropic's text-type documents specially (not covered by shared function) |
| 134 | + if content_block.get("type") == "document": |
| 135 | + source = content_block.get("source") |
| 136 | + if isinstance(source, dict) and source.get("type") == "text": |
| 137 | + return { |
| 138 | + "type": "text", |
| 139 | + "text": source.get("data", ""), |
| 140 | + } |
| 141 | + |
| 142 | + # Use Anthropic-specific transformation |
| 143 | + result = transform_anthropic_content_part(content_block) |
| 144 | + return result if result is not None else content_block |
| 145 | + |
| 146 | + |
125 | 147 | def _set_input_data( |
126 | 148 | span: "Span", kwargs: "dict[str, Any]", integration: "AnthropicIntegration" |
127 | 149 | ) -> None: |
@@ -166,19 +188,41 @@ def _set_input_data( |
166 | 188 | and "content" in message |
167 | 189 | and isinstance(message["content"], (list, tuple)) |
168 | 190 | ): |
| 191 | + transformed_content = [] |
169 | 192 | for item in message["content"]: |
170 | | - if item.get("type") == "tool_result": |
171 | | - normalized_messages.append( |
172 | | - { |
173 | | - "role": GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL, |
174 | | - "content": { # type: ignore[dict-item] |
175 | | - "tool_use_id": item.get("tool_use_id"), |
176 | | - "output": item.get("content"), |
177 | | - }, |
178 | | - } |
179 | | - ) |
| 193 | + # Skip tool_result items - they can contain images/documents |
| 194 | + # with nested structures that are difficult to redact properly |
| 195 | + if isinstance(item, dict) and item.get("type") == "tool_result": |
| 196 | + continue |
| 197 | + |
| 198 | + # Transform content blocks (images, documents, etc.) |
| 199 | + transformed_content.append( |
| 200 | + _transform_anthropic_content_block(item) |
| 201 | + if isinstance(item, dict) |
| 202 | + else item |
| 203 | + ) |
| 204 | + |
| 205 | + # If there are non-tool-result items, add them as a message |
| 206 | + if transformed_content: |
| 207 | + normalized_messages.append( |
| 208 | + { |
| 209 | + "role": message.get("role"), |
| 210 | + "content": transformed_content, |
| 211 | + } |
| 212 | + ) |
180 | 213 | else: |
181 | | - normalized_messages.append(message) |
| 214 | + # Transform content for non-list messages or assistant messages |
| 215 | + transformed_message = message.copy() |
| 216 | + if "content" in transformed_message: |
| 217 | + content = transformed_message["content"] |
| 218 | + if isinstance(content, (list, tuple)): |
| 219 | + transformed_message["content"] = [ |
| 220 | + _transform_anthropic_content_block(item) |
| 221 | + if isinstance(item, dict) |
| 222 | + else item |
| 223 | + for item in content |
| 224 | + ] |
| 225 | + normalized_messages.append(transformed_message) |
182 | 226 |
|
183 | 227 | role_normalized_messages = normalize_message_roles(normalized_messages) |
184 | 228 | scope = sentry_sdk.get_current_scope() |
|
0 commit comments