Skip to content
Closed
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
12 changes: 12 additions & 0 deletions py/noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,18 @@ def test_cohere(session, version):
_run_tests(session, f"{INTEGRATION_DIR}/cohere/test_cohere.py", version=version)


BOTO3_VERSIONS = _get_matrix_versions("boto3")


@nox.session()
@nox.parametrize("version", BOTO3_VERSIONS, ids=BOTO3_VERSIONS)
def test_boto3(session, version):
"""Test the native boto3 SDK integration."""
_install_test_deps(session)
_install_matrix_dep(session, "boto3", version)
_run_tests(session, f"{INTEGRATION_DIR}/boto3/test_boto3.py", version=version)


INSTRUCTOR_VERSIONS = _get_matrix_versions("instructor")


Expand Down
5 changes: 5 additions & 0 deletions py/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,9 @@ latest = "pytest==9.1.0"
[tool.braintrust.matrix.braintrust-core]
latest = "braintrust-core==0.0.59"

[tool.braintrust.matrix.boto3]
latest = "boto3==1.43.26"

# ---------------------------------------------------------------------------
# Vendor packages — optional third-party packages the SDK can work without.
# Keys are matrix keys; values are Python import names. The noxfile uses this
Expand All @@ -461,6 +464,7 @@ agentscope = ["agentscope"]
agno = ["agno"]
autogen = ["autogen-agentchat"]
anthropic = ["anthropic"]
boto3 = ["boto3"]
cohere = ["cohere"]
claude_agent_sdk = ["claude-agent-sdk"]
crewai = ["crewai"]
Expand All @@ -485,6 +489,7 @@ agentscope = "agentscope"
autogen-agentchat = "autogen_agentchat"
autogen-ext = "autogen_ext"
anthropic = "anthropic"
boto3 = "boto3"
cohere = "cohere"
autoevals = "autoevals"
braintrust-core = "braintrust_core"
Expand Down
5 changes: 4 additions & 1 deletion py/src/braintrust/auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
AgnoIntegration,
AnthropicIntegration,
AutoGenIntegration,
Boto3Integration,
ClaudeAgentSDKIntegration,
CohereIntegration,
CrewAIIntegration,
Expand Down Expand Up @@ -76,6 +77,7 @@ def auto_instrument(
strands: bool = True,
temporal: bool = True,
livekit_agents: bool = True,
boto3: bool = True,
) -> dict[str, bool]:
"""
Auto-instrument supported AI/ML libraries for Braintrust tracing.
Expand Down Expand Up @@ -203,7 +205,8 @@ def auto_instrument(
results["temporal"] = _instrument_integration(TemporalIntegration)
if livekit_agents:
results["livekit_agents"] = _instrument_integration(LiveKitAgentsIntegration)

if boto3:
results["boto3"] = _instrument_integration(Boto3Integration)
return results


Expand Down
2 changes: 2 additions & 0 deletions py/src/braintrust/integrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .agno import AgnoIntegration
from .anthropic import AnthropicIntegration
from .autogen import AutoGenIntegration
from .boto3 import Boto3Integration
from .claude_agent_sdk import ClaudeAgentSDKIntegration
from .cohere import CohereIntegration
from .crewai import CrewAIIntegration
Expand All @@ -29,6 +30,7 @@
"AgnoIntegration",
"AnthropicIntegration",
"AutoGenIntegration",
"Boto3Integration",
"ClaudeAgentSDKIntegration",
"CohereIntegration",
"CrewAIIntegration",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import os

import boto3
from braintrust.auto import auto_instrument
from braintrust.integrations.boto3.patchers import Boto3ConversePatcher
from braintrust.integrations.test_utils import autoinstrument_test_context


def is_patched(target, patcher):
return bool(getattr(target, patcher.nested_patch_marker_attr(), False))


# verify not patched initially
original_client = boto3.client(
"bedrock-runtime",
aws_access_key_id="test-key",
aws_secret_access_key="test-access-key",
aws_session_token="test-session-key",
region_name="us-east-1",
)

assert is_patched(original_client, Boto3ConversePatcher) is False

# instrument
results = auto_instrument()
assert results.get("boto3") is True

# patch
patched_client = boto3.client(
"bedrock-runtime",
aws_access_key_id="",
aws_secret_access_key="",
aws_session_token="",
region_name="us-east-1",
)

# idempotent
results = auto_instrument()
assert results.get("boto3") is True


assert is_patched(patched_client, Boto3ConversePatcher) is True

# make api call
with autoinstrument_test_context("test_auto_boto3", integration="boto3") as memory_logger:
client = boto3.client(
"bedrock-runtime",
region_name="us-east-1",
aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"),
aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"),
aws_session_token=os.getenv("AWS_SESSION_TOKEN"),
)

response = response = client.converse(
modelId="anthropic.claude-3-haiku-20240307-v1:0",
messages=[{"role": "user", "content": [{"text": "what's 2+2"}]}],
system=[{"text": "answer only in single integer"}],
)

spans = memory_logger.pop()
assert len(spans) == 1, f"Expected 1 span, got {len(spans)}"

span = spans[0]
assert span["metadata"]["model_provider"] == "anthropic"
assert "claude" in span["metadata"]["model"]
24 changes: 24 additions & 0 deletions py/src/braintrust/integrations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,30 @@ def _wrapper(cls, wrapped: Any, instance: Any, args: Any, kwargs: Any) -> Any:
return cls.wrapper(wrapped, instance, args, kwargs)


class NestedFunctionWrapperPatcher(FunctionWrapperPatcher):

@Mahhheshh Mahhheshh (Mahhheshh) Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could have followed the similar approach to Anthropics patching, I found this simple, convenient, it's ugly but this works.

"""Add Wrapping for target attribute when the object is formed; useful for dynamic sdk builders such as boto3"""

target_attribute: ClassVar[str]
nested_wrapper: ClassVar[Any]
wrapper: ClassVar[Any] = staticmethod(_call_wrapped)

@classmethod
def nested_patch_marker_attr(cls) -> str:
"""Return the sentinel attribute used to mark an instance as set up."""
suffix = re.sub(r"\W+", "_", cls.name).strip("_")
return f"__braintrust_nested_patched_{suffix}__"

@classmethod
def _wrapper(cls, wrapped: Any, instance: Any, args: Any, kwargs: Any) -> Any:
obj = cls.wrapper(wrapped, instance, args, kwargs)
if not getattr(obj, cls.target_attribute, False):
return
marker = cls.nested_patch_marker_attr()
wrap_function_wrapper(obj, cls.target_attribute, cls.nested_wrapper)
setattr(obj, marker, True)
return obj


class ClassReplacementPatcher(BasePatcher):
"""Base patcher for replacing an exported class with a tracing wrapper class.

Expand Down
23 changes: 23 additions & 0 deletions py/src/braintrust/integrations/boto3/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import logging

from braintrust.logger import NOOP_SPAN, current_span, init_logger

from .integration import Boto3Integration


logger = logging.getLogger(__name__)

__all__ = ["Boto3Integration", "setup_boto3"]


def setup_boto3(
api_key: str | None = None,
project_id: str | None = None,
project_name: str | None = None,
) -> bool:
span = current_span()

if span == NOOP_SPAN:
init_logger(project=project_name, api_key=api_key, project_id=project_id)

return Boto3Integration.setup()
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
interactions:
- request:
body: '{"messages": [{"role": "user", "content": [{"text": "what''s 2+2"}]}],
"system": [{"text": "answer only in single integer"}]}'
headers:
Content-Length:
- '124'
Content-Type:
- !!binary |
YXBwbGljYXRpb24vanNvbg==
User-Agent:
- !!binary |
Qm90bzMvMS40My4yNiBtZC9Cb3RvY29yZSMxLjQzLjI4IHVhLzIuMSBvcy9saW51eCM2LjguMC0x
MDUyLWF6dXJlIG1kL2FyY2gjeDg2XzY0IGxhbmcvcHl0aG9uIzMuMTIuMSBtZC9weWltcGwjQ1B5
dGhvbiBtL1osYixELGUgY2ZnL3JldHJ5LW1vZGUjbGVnYWN5IEJvdG9jb3JlLzEuNDMuMjg=
X-Amz-Date:
- !!binary |
MjAyNjA2MTJUMDkwMjE5Wg==
X-Amz-Security-Token:
- !!binary |
SVFvSmIzSnBaMmx1WDJWakVFWWFDWFZ6TFhkbGMzUXRNaUpITUVVQ0lFSlVDZDFxQlZncWhMd3BM
cFluTW92aWxVbnRZOVFTNm1HWitkT2lGdWxBQWlFQXFzYzJ0bXloWjk0N2J2ZXJsVW9yV3BkU0xK
UURmMEdlK0pIUHo4U0dhNWdxZ3dNSUR4QUFHZ3czTWpjMk5EWTBOelE0TkRVaURGWWh1dm52blpJ
bjJtMjYvaXJnQXZiNVIvV2FzOFFSeHBEQXozQ0ppM1hNcUFjR2JlU1FCS0psZklMUDBJWjQ2cW90
d1VNeGRRR0hkbnVvQk9XcnJGTzNGdVlrVkxqYXZHd0tDeWRhSkFlcC9iODR6QzRlbGhWZVNQNUNx
L1hNeGl5SHZaSHROZFdvMEVsN0Fma2dQNVhSUnNYM3ZRM0VER1Y4VW9JL0tSa3JHcGhwbWxtWWQv
dG9JdzJzeTRTYWJoNXpBL0NDRUJWRC9ETlJUaDkxR0MrQ1kvR0YrTk9JUk9lVFY1VmtzWndyOVpz
Tmh2NExqcURmeVdHVXoxbERwZmRzRUhPNHpRbjVBVy9oRGNuc04rYlFuR1NtSThaTjk1VTJsdVlr
elBPOGJUUzJobFJNeFN0KzZvbE1NZ09CUkFkbW1OamlYVGNaK3FkNnBCM2paWDcwVWpOZURIdCtJ
SDRoNUozVGxmVXR5ZG01TlBXdzZsb1l2WitPV1M2dUwyTVg1ZlhtWTlJMVBlZlV1M21KQTRFNlhN
aHNncmlYaWQ5THY1UFJoMU1ybTUvN2pOZTVkLzhuSTdJYTZlLytwTVJXYXEreHVvYzIxQWdob2JF
Q3lWUU1XcWZKSWZrclcxMGpMU3FPYlMwd2tMbXUwUVk2bVFHdmZCaitadTN6TnNpL0hRRUIzRjB6
QWtmRENHYmhYbXJrTzVjRkJPNG0zdFdjNmJoWGlST2o0QVRmVVRzSGVvOFlzOExhTEdsSGtSbmdt
WWc4Y3dPVG5FSTBUNldUQjVEYm41L2IybUN6dHdJNHdCTFBUUUhkcVRxVDJ2M05JbC8zTitpd0dQ
UytGRGRnYldPZXVjZEV6TWluWnNNRXJ4RUExK1dZMVNtMkp6alN6VmFOc0NZTDZXQ3A5dlNneUE3
U001aDFQVjlLRCtnPQ==
amz-sdk-invocation-id:
- !!binary |
MzkwYzg5YjktMjM4Ni00NWI1LTkzZTYtYjgyMjc1YWM2OWY2
amz-sdk-request:
- !!binary |
YXR0ZW1wdD0x
method: POST
uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-3-haiku-20240307-v1%3A0/converse
response:
body:
string: '{"metrics":{"latencyMs":269},"output":{"message":{"content":[{"text":"4"}],"role":"assistant"}},"stopReason":"end_turn","usage":{"inputTokens":18,"outputTokens":5,"serverToolUsage":{},"totalTokens":23}}'
headers:
Connection:
- keep-alive
Content-Length:
- '202'
Content-Type:
- application/json
Date:
- Fri, 12 Jun 2026 09:02:20 GMT
x-amzn-RequestId:
- 20c1e6b5-f545-444a-8de6-a47e4c4c295a
status:
code: 200
message: OK
version: 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
interactions:
- request:
body: '{"messages": [{"role": "user", "content": [{"text": "what''s 2+2"}]}],
"system": [{"text": "answer only in single integer"}]}'
headers:
Content-Length:
- '124'
Content-Type:
- !!binary |
YXBwbGljYXRpb24vanNvbg==
User-Agent:
- !!binary |
Qm90bzMvMS40My4yNiBtZC9Cb3RvY29yZSMxLjQzLjI4IHVhLzIuMSBvcy9saW51eCM2LjguMC0x
MDUyLWF6dXJlIG1kL2FyY2gjeDg2XzY0IGxhbmcvcHl0aG9uIzMuMTIuMSBtZC9weWltcGwjQ1B5
dGhvbiBtL2UsYixaLEQgY2ZnL3JldHJ5LW1vZGUjbGVnYWN5IEJvdG9jb3JlLzEuNDMuMjg=
X-Amz-Date:
- !!binary |
MjAyNjA2MTJUMDYxMzAxWg==
X-Amz-Security-Token:
- !!binary |
SVFvSmIzSnBaMmx1WDJWakVFWWFDWFZ6TFhkbGMzUXRNaUpITUVVQ0lFSlVDZDFxQlZncWhMd3BM
cFluTW92aWxVbnRZOVFTNm1HWitkT2lGdWxBQWlFQXFzYzJ0bXloWjk0N2J2ZXJsVW9yV3BkU0xK
UURmMEdlK0pIUHo4U0dhNWdxZ3dNSUR4QUFHZ3czTWpjMk5EWTBOelE0TkRVaURGWWh1dm52blpJ
bjJtMjYvaXJnQXZiNVIvV2FzOFFSeHBEQXozQ0ppM1hNcUFjR2JlU1FCS0psZklMUDBJWjQ2cW90
d1VNeGRRR0hkbnVvQk9XcnJGTzNGdVlrVkxqYXZHd0tDeWRhSkFlcC9iODR6QzRlbGhWZVNQNUNx
L1hNeGl5SHZaSHROZFdvMEVsN0Fma2dQNVhSUnNYM3ZRM0VER1Y4VW9JL0tSa3JHcGhwbWxtWWQv
dG9JdzJzeTRTYWJoNXpBL0NDRUJWRC9ETlJUaDkxR0MrQ1kvR0YrTk9JUk9lVFY1VmtzWndyOVpz
Tmh2NExqcURmeVdHVXoxbERwZmRzRUhPNHpRbjVBVy9oRGNuc04rYlFuR1NtSThaTjk1VTJsdVlr
elBPOGJUUzJobFJNeFN0KzZvbE1NZ09CUkFkbW1OamlYVGNaK3FkNnBCM2paWDcwVWpOZURIdCtJ
SDRoNUozVGxmVXR5ZG01TlBXdzZsb1l2WitPV1M2dUwyTVg1ZlhtWTlJMVBlZlV1M21KQTRFNlhN
aHNncmlYaWQ5THY1UFJoMU1ybTUvN2pOZTVkLzhuSTdJYTZlLytwTVJXYXEreHVvYzIxQWdob2JF
Q3lWUU1XcWZKSWZrclcxMGpMU3FPYlMwd2tMbXUwUVk2bVFHdmZCaitadTN6TnNpL0hRRUIzRjB6
QWtmRENHYmhYbXJrTzVjRkJPNG0zdFdjNmJoWGlST2o0QVRmVVRzSGVvOFlzOExhTEdsSGtSbmdt
WWc4Y3dPVG5FSTBUNldUQjVEYm41L2IybUN6dHdJNHdCTFBUUUhkcVRxVDJ2M05JbC8zTitpd0dQ
UytGRGRnYldPZXVjZEV6TWluWnNNRXJ4RUExK1dZMVNtMkp6alN6VmFOc0NZTDZXQ3A5dlNneUE3
U001aDFQVjlLRCtnPQ==
amz-sdk-invocation-id:
- !!binary |
MzBlZjQ1MzktMmFlNS00M2IwLTkwMjUtMWQwZDU3MDkzMjQ5
amz-sdk-request:
- !!binary |
YXR0ZW1wdD0x
method: POST
uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-3-haiku-20240307-v1%3A0/converse
response:
body:
string: '{"metrics":{"latencyMs":257},"output":{"message":{"content":[{"text":"4"}],"role":"assistant"}},"stopReason":"end_turn","usage":{"inputTokens":18,"outputTokens":5,"serverToolUsage":{},"totalTokens":23}}'
headers:
Connection:
- keep-alive
Content-Length:
- '202'
Content-Type:
- application/json
Date:
- Fri, 12 Jun 2026 06:13:02 GMT
x-amzn-RequestId:
- f9de30ea-4863-4786-9de1-a01f0232e54c
status:
code: 200
message: OK
version: 1
9 changes: 9 additions & 0 deletions py/src/braintrust/integrations/boto3/integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from braintrust.integrations.base import BaseIntegration

from .patchers import Boto3ConversePatcher


class Boto3Integration(BaseIntegration):
name = "boto3_integration"
patchers = (Boto3ConversePatcher,)
import_names = ("botocore",)
13 changes: 13 additions & 0 deletions py/src/braintrust/integrations/boto3/patchers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from braintrust.integrations.base import NestedFunctionWrapperPatcher

from .tracing import converse_tracer


class Boto3ConversePatcher(NestedFunctionWrapperPatcher):
"""Patcher class for Boto3.BedrockRuntime.Client.Converse API"""

name = "boto3"
target_module = "botocore.client"
target_path = "ClientCreator._create_client_class"
target_attribute = "converse"
nested_wrapper = converse_tracer
Loading