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
22 changes: 18 additions & 4 deletions newrelic/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2948,12 +2948,26 @@ def _process_module_builtin_defaults():
"newrelic.hooks.mlmodel_autogen",
"instrument_autogen_agentchat_agents__assistant_agent",
)
_process_module_definition("strands.agent.agent", "newrelic.hooks.mlmodel_strands", "instrument_agent_agent")
_process_module_definition(
"strands.tools.executors._executor", "newrelic.hooks.mlmodel_strands", "instrument_tools_executors__executor"
"strands.agent.agent", "newrelic.hooks.mlmodel_strands", "instrument_strands_agent_agent"
)
_process_module_definition(
"strands.multiagent.graph", "newrelic.hooks.mlmodel_strands", "instrument_strands_multiagent_graph"
)
_process_module_definition(
"strands.multiagent.swarm", "newrelic.hooks.mlmodel_strands", "instrument_strands_multiagent_swarm"
)
_process_module_definition(
"strands.tools.executors._executor",
"newrelic.hooks.mlmodel_strands",
"instrument_strands_tools_executors__executor",
)
_process_module_definition(
"strands.tools.registry", "newrelic.hooks.mlmodel_strands", "instrument_strands_tools_registry"
)
_process_module_definition(
"strands.models.bedrock", "newrelic.hooks.mlmodel_strands", "instrument_strands_models_bedrock"
)
_process_module_definition("strands.tools.registry", "newrelic.hooks.mlmodel_strands", "instrument_tools_registry")
_process_module_definition("strands.models.bedrock", "newrelic.hooks.mlmodel_strands", "instrument_models_bedrock")

_process_module_definition("mcp.client.session", "newrelic.hooks.adapter_mcp", "instrument_mcp_client_session")
_process_module_definition(
Expand Down
24 changes: 20 additions & 4 deletions newrelic/hooks/mlmodel_strands.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ def wrap_bedrock_model__stream(wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)


def instrument_agent_agent(module):
def instrument_strands_agent_agent(module):
if hasattr(module, "Agent"):
if hasattr(module.Agent, "__call__"): # noqa: B004
wrap_function_wrapper(module, "Agent.__call__", wrap_agent__call__)
Expand All @@ -471,19 +471,35 @@ def instrument_agent_agent(module):
wrap_function_wrapper(module, "Agent.stream_async", wrap_stream_async)


def instrument_tools_executors__executor(module):
def instrument_strands_multiagent_graph(module):
if hasattr(module, "Graph"):
if hasattr(module.Graph, "__call__"): # noqa: B004
wrap_function_wrapper(module, "Graph.__call__", wrap_agent__call__)
if hasattr(module.Graph, "invoke_async"):
wrap_function_wrapper(module, "Graph.invoke_async", wrap_agent_invoke_async)


def instrument_strands_multiagent_swarm(module):
if hasattr(module, "Swarm"):
if hasattr(module.Swarm, "__call__"): # noqa: B004
wrap_function_wrapper(module, "Swarm.__call__", wrap_agent__call__)
if hasattr(module.Swarm, "invoke_async"):
wrap_function_wrapper(module, "Swarm.invoke_async", wrap_agent_invoke_async)


def instrument_strands_tools_executors__executor(module):
if hasattr(module, "ToolExecutor"):
if hasattr(module.ToolExecutor, "_stream"):
wrap_function_wrapper(module, "ToolExecutor._stream", wrap_tool_executor__stream)


def instrument_tools_registry(module):
def instrument_strands_tools_registry(module):
if hasattr(module, "ToolRegistry"):
if hasattr(module.ToolRegistry, "register_tool"):
wrap_function_wrapper(module, "ToolRegistry.register_tool", wrap_ToolRegister_register_tool)


def instrument_models_bedrock(module):
def instrument_strands_models_bedrock(module):
# This instrumentation only exists to pass trace context due to bedrock models using a separate thread.
if hasattr(module, "BedrockModel"):
if hasattr(module.BedrockModel, "stream"):
Expand Down
13 changes: 13 additions & 0 deletions tests/mlmodel_strands/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
165 changes: 165 additions & 0 deletions tests/mlmodel_strands/_test_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pytest
from strands import tool

from ._mock_model_provider import MockedModelProvider


# Example tool for testing purposes
@tool
async def add_exclamation(message: str) -> str:
return f"{message}!"


@tool
async def throw_exception_coro(message: str) -> str:
raise RuntimeError("Oops")


@tool
async def throw_exception_agen(message: str) -> str:
raise RuntimeError("Oops")
yield


@pytest.fixture
def single_tool_model():
model = MockedModelProvider(
[
{
"role": "assistant",
"content": [
{"text": "Calling add_exclamation tool"},
{"toolUse": {"name": "add_exclamation", "toolUseId": "123", "input": {"message": "Hello"}}},
],
},
{"role": "assistant", "content": [{"text": "Success!"}]},
]
)
return model


@pytest.fixture
def single_tool_model_runtime_error_coro():
model = MockedModelProvider(
[
{
"role": "assistant",
"content": [
{"text": "Calling throw_exception_coro tool"},
# Set arguments to an invalid type to trigger error in tool
{"toolUse": {"name": "throw_exception_coro", "toolUseId": "123", "input": {"message": "Hello"}}},
],
},
{"role": "assistant", "content": [{"text": "Success!"}]},
]
)
return model


@pytest.fixture
def single_tool_model_runtime_error_agen():
model = MockedModelProvider(
[
{
"role": "assistant",
"content": [
{"text": "Calling throw_exception_agen tool"},
# Set arguments to an invalid type to trigger error in tool
{"toolUse": {"name": "throw_exception_agen", "toolUseId": "123", "input": {"message": "Hello"}}},
],
},
{"role": "assistant", "content": [{"text": "Success!"}]},
]
)
return model


@pytest.fixture
def multi_tool_model():
model = MockedModelProvider(
[
{
"role": "assistant",
"content": [
{"text": "Calling add_exclamation tool"},
{"toolUse": {"name": "add_exclamation", "toolUseId": "123", "input": {"message": "Hello"}}},
],
},
{
"role": "assistant",
"content": [
{"text": "Calling compute_sum tool"},
{"toolUse": {"name": "compute_sum", "toolUseId": "123", "input": {"a": 5, "b": 3}}},
],
},
{
"role": "assistant",
"content": [
{"text": "Calling add_exclamation tool"},
{"toolUse": {"name": "add_exclamation", "toolUseId": "123", "input": {"message": "Goodbye"}}},
],
},
{
"role": "assistant",
"content": [
{"text": "Calling compute_sum tool"},
{"toolUse": {"name": "compute_sum", "toolUseId": "123", "input": {"a": 123, "b": 2}}},
],
},
{"role": "assistant", "content": [{"text": "Success!"}]},
]
)
return model


@pytest.fixture
def multi_tool_model_error():
model = MockedModelProvider(
[
{
"role": "assistant",
"content": [
{"text": "Calling add_exclamation tool"},
{"toolUse": {"name": "add_exclamation", "toolUseId": "123", "input": {"message": "Hello"}}},
],
},
{
"role": "assistant",
"content": [
{"text": "Calling compute_sum tool"},
{"toolUse": {"name": "compute_sum", "toolUseId": "123", "input": {"a": 5, "b": 3}}},
],
},
{
"role": "assistant",
"content": [
{"text": "Calling add_exclamation tool"},
{"toolUse": {"name": "add_exclamation", "toolUseId": "123", "input": {"message": "Goodbye"}}},
],
},
{
"role": "assistant",
"content": [
{"text": "Calling compute_sum tool"},
# Set insufficient arguments to trigger error in tool
{"toolUse": {"name": "compute_sum", "toolUseId": "123", "input": {"a": 123}}},
],
},
{"role": "assistant", "content": [{"text": "Success!"}]},
]
)
return model
91 changes: 91 additions & 0 deletions tests/mlmodel_strands/_test_multiagent_graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pytest
from strands import Agent, tool
from strands.multiagent.graph import GraphBuilder

from ._mock_model_provider import MockedModelProvider


@pytest.fixture
def math_model():
model = MockedModelProvider(
[
{
"role": "assistant",
"content": [
{"text": "I'll calculate the sum of 15 and 27 for you."},
{"toolUse": {"name": "calculate_sum", "toolUseId": "123", "input": {"a": 15, "b": 27}}},
],
},
{"role": "assistant", "content": [{"text": "The sum of 15 and 27 is 42."}]},
]
)
return model


@pytest.fixture
def analysis_model():
model = MockedModelProvider(
[
{
"role": "assistant",
"content": [
{"text": "I'll validate the calculation result of 42 from the calculator."},
{"toolUse": {"name": "analyze_result", "toolUseId": "456", "input": {"value": 42}}},
],
},
{
"role": "assistant",
"content": [{"text": "The calculation is correct, and 42 is a positive integer result."}],
},
]
)
return model


# Example tool for testing purposes
@tool
async def calculate_sum(a: int, b: int) -> int:
"""Calculate the sum of two numbers."""
return a + b


@tool
async def analyze_result(value: int) -> str:
"""Analyze a numeric result."""
return f"The result {value} is {'positive' if value > 0 else 'zero or negative'}"


@pytest.fixture
def math_agent(math_model):
return Agent(name="math_agent", model=math_model, tools=[calculate_sum])


@pytest.fixture
def analysis_agent(analysis_model):
return Agent(name="analysis_agent", model=analysis_model, tools=[analyze_result])


@pytest.fixture
def agent_graph(math_agent, analysis_agent):
# Build graph
builder = GraphBuilder()
builder.add_node(math_agent, "math")
builder.add_node(analysis_agent, "analysis")
builder.add_edge("math", "analysis")
builder.set_entry_point("math")

return builder.build()
Loading
Loading