diff --git a/py/src/braintrust/integrations/anthropic/test_anthropic.py b/py/src/braintrust/integrations/anthropic/test_anthropic.py index 414f3af4..3c03794c 100644 --- a/py/src/braintrust/integrations/anthropic/test_anthropic.py +++ b/py/src/braintrust/integrations/anthropic/test_anthropic.py @@ -17,6 +17,7 @@ from braintrust.integrations.anthropic.tracing import ( TracedMessageStream, _log_message_to_span, + _normalize_anthropic_input, ) from braintrust.integrations.test_utils import verify_autoinstrument_script from braintrust.span_types import SpanTypeAttribute @@ -940,6 +941,34 @@ def test_anthropic_messages_create_with_image_attachment_input(memory_logger): assert PNG_BASE64 not in str(span["input"]) +def test_managed_agents_input_materializes_attachments_inside_tuples(): + normalized = _normalize_anthropic_input( + { + "events": ( + { + "type": "user.message", + "content": ( + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": PNG_BASE64, + }, + }, + ), + }, + ), + } + ) + + event = normalized["events"][0] + image_block = event["content"][0] + assert isinstance(image_block["image_url"]["url"], Attachment) + assert "data" not in image_block["source"] + assert PNG_BASE64 not in str(normalized) + + @pytest.mark.vcr(match_on=["method", "scheme", "host", "port", "path"]) def test_anthropic_messages_create_with_document_attachment_input(memory_logger): assert not memory_logger.pop() diff --git a/py/src/braintrust/integrations/anthropic/tracing.py b/py/src/braintrust/integrations/anthropic/tracing.py index 8e304db4..27ea2220 100644 --- a/py/src/braintrust/integrations/anthropic/tracing.py +++ b/py/src/braintrust/integrations/anthropic/tracing.py @@ -930,19 +930,19 @@ async def _finish(self, exc_type=None, exc_value=None, traceback=None, error=Non def _normalize_anthropic_data(value: Any) -> Any: - converted = _try_to_dict(value) - if converted is not None: - value = converted - + if value is None or isinstance(value, (str, int, float, bool, bytes)): + return value if isinstance(value, tuple): value = list(value) - if isinstance(value, list): return [_normalize_anthropic_data(item) for item in value] - if isinstance(value, dict): return {key: _normalize_anthropic_data(item) for key, item in value.items()} + converted = _try_to_dict(value) + if converted is not None: + return _normalize_anthropic_data(converted) + return value @@ -951,11 +951,11 @@ def _normalize_anthropic_input(value: Any) -> Any: def _managed_agents_model_name(value: Any) -> str | None: - value = _normalize_anthropic_data(value) if isinstance(value, str): return value - if isinstance(value, dict) and isinstance(value.get("id"), str): - return value["id"] + model_id = value.get("id") if isinstance(value, dict) else getattr(value, "id", None) + if isinstance(model_id, str): + return model_id return None @@ -1014,7 +1014,7 @@ def _extract_managed_agents_result_metrics_and_metadata(result: Any) -> tuple[di def _log_managed_agents_result_to_span(result: Any, span, output_factory=None) -> None: - output = output_factory(result) if output_factory is not None else _normalize_anthropic_data(result) + output = output_factory(result) if output_factory is not None else result metrics, metadata = _extract_managed_agents_result_metrics_and_metadata(result) span_log_kwargs = {} @@ -1481,12 +1481,13 @@ def _log_server_tool_spans(content: Any, parent_span) -> None: pairs: list[tuple[dict[str, Any] | None, dict[str, Any] | None]] = [] for item in content: - item = _try_to_dict(item) - if not isinstance(item, dict): - continue - - item_type = item.get("type") + item_type = getattr(item, "type", None) + if item_type is None and isinstance(item, dict): + item_type = item.get("type") if item_type in _SERVER_TOOL_USE_TYPES: + item = _try_to_dict(item) + if not isinstance(item, dict): + continue call_id = item.get("id") if isinstance(call_id, str): calls_by_id[call_id] = item @@ -1500,6 +1501,10 @@ def _log_server_tool_spans(content: Any, parent_span) -> None: if not _is_server_tool_result_type(item_type): continue + item = _try_to_dict(item) + if not isinstance(item, dict): + continue + tool_use_id = item.get("tool_use_id") if isinstance(tool_use_id, str) and tool_use_id in calls_by_id: pairs.append((calls_by_id[tool_use_id], item))