Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions py/src/braintrust/integrations/anthropic/test_anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
35 changes: 20 additions & 15 deletions py/src/braintrust/integrations/anthropic/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Comment thread
AbhiPrasad marked this conversation as resolved.

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


Expand All @@ -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


Expand Down Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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
Expand All @@ -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))
Expand Down
Loading