Skip to content

Commit 6c00982

Browse files
committed
feat(agent): Add opt-in flag to include tool specs in traces for evaluation
1 parent 417ebea commit 6c00982

File tree

4 files changed

+79
-3
lines changed

4 files changed

+79
-3
lines changed

src/strands/agent/agent.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,27 @@ async def stream_async(
660660

661661
self.trace_span = self._start_agent_trace_span(messages)
662662

663+
if self.tracer.include_tool_definitions:
664+
try:
665+
all_tools_config = self.tool_registry.get_all_tools_config() or {}
666+
667+
tool_details = [
668+
{
669+
"name": name,
670+
"description": spec.get("description"),
671+
"inputSchema": spec.get("inputSchema"),
672+
"outputSchema": spec.get("outputSchema"),
673+
}
674+
for name, spec in all_tools_config.items()
675+
]
676+
677+
serialized_tools = serialize(tool_details)
678+
self.trace_span.set_attribute("gen_ai.tool.definitions", serialized_tools)
679+
680+
except Exception:
681+
# A failure in telemetry should not crash the agent.
682+
logger.exception("failed to attach tool metadata to agent span")
683+
663684
with trace_api.use_span(self.trace_span):
664685
try:
665686
events = self._run_loop(messages, merged_state, structured_output_model)

src/strands/telemetry/config.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter, SimpleSpanProcessor
2121
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
2222

23+
from . import tracer
24+
2325
logger = logging.getLogger(__name__)
2426

2527

@@ -83,12 +85,15 @@ class StrandsTelemetry:
8385
def __init__(
8486
self,
8587
tracer_provider: SDKTracerProvider | None = None,
88+
include_tool_definitions: bool = False,
8689
) -> None:
8790
"""Initialize the StrandsTelemetry instance.
8891
8992
Args:
9093
tracer_provider: Optional pre-configured tracer provider.
9194
If None, a new one will be created and set as global.
95+
include_tool_definitions: Whether to include tool definitions in traces.
96+
Defaults to False.
9297
9398
The instance is ready to use immediately after initialization, though
9499
trace exporters must be configured separately using the setup methods.
@@ -99,6 +104,9 @@ def __init__(
99104
else:
100105
self._initialize_tracer()
101106

107+
# Overwrite the singleton tracer instance to apply the new configuration.
108+
tracer._tracer_instance = tracer.Tracer(include_tool_definitions=include_tool_definitions)
109+
102110
def _initialize_tracer(self) -> None:
103111
"""Initialize the OpenTelemetry tracer."""
104112
logger.info("Initializing tracer")

src/strands/telemetry/tracer.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,10 @@ class Tracer:
8181
are sent to the OTLP endpoint.
8282
"""
8383

84-
def __init__(
85-
self,
86-
) -> None:
84+
def __init__(self, include_tool_definitions: bool = False) -> None:
8785
"""Initialize the tracer."""
8886
self.service_name = __name__
87+
self.include_tool_definitions = include_tool_definitions
8988
self.tracer_provider: Optional[trace_api.TracerProvider] = None
9089
self.tracer_provider = trace_api.get_tracer_provider()
9190
self.tracer = self.tracer_provider.get_tracer(self.service_name)

tests/strands/agent/test_agent.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2162,6 +2162,54 @@ def shell(command: str):
21622162
assert agent.messages[-1] == {"content": [{"text": "I invoked a tool!"}], "role": "assistant"}
21632163

21642164

2165+
def test_agent_does_not_include_tools_in_trace_by_default(tool_decorated):
2166+
"""Verify that by default, the agent does not add tool specs to the trace."""
2167+
with unittest.mock.patch("strands.agent.agent.get_tracer") as mock_get_tracer:
2168+
mock_tracer_instance = unittest.mock.MagicMock()
2169+
mock_span = unittest.mock.MagicMock()
2170+
mock_tracer_instance.start_agent_span.return_value = mock_span
2171+
mock_tracer_instance.include_tool_definitions = False # Default behavior
2172+
mock_get_tracer.return_value = mock_tracer_instance
2173+
2174+
mock_model = MockedModelProvider([{"role": "assistant", "content": [{"text": "hello!"}]}])
2175+
2176+
agent = Agent(tools=[tool_decorated], model=mock_model)
2177+
agent("test prompt")
2178+
2179+
# Check that set_attribute was not called for our specific key
2180+
called_attributes = [call.args[0] for call in mock_span.set_attribute.call_args_list]
2181+
assert "gen_ai.tool.definitions" not in called_attributes
2182+
2183+
2184+
def test_agent_includes_tools_in_trace_when_enabled(tool_decorated):
2185+
"""Verify that the agent adds tool specs to the trace when the flag is enabled."""
2186+
with unittest.mock.patch("strands.agent.agent.get_tracer") as mock_get_tracer:
2187+
mock_tracer_instance = unittest.mock.MagicMock()
2188+
mock_span = unittest.mock.MagicMock()
2189+
mock_tracer_instance.start_agent_span.return_value = mock_span
2190+
mock_tracer_instance.include_tool_definitions = True # Enable the feature
2191+
mock_get_tracer.return_value = mock_tracer_instance
2192+
2193+
mock_model = MockedModelProvider([{"role": "assistant", "content": [{"text": "hello!"}]}])
2194+
2195+
agent = Agent(tools=[tool_decorated], model=mock_model)
2196+
agent("test prompt")
2197+
2198+
# Verify the correct data is serialized and set as an attribute
2199+
tool_spec = tool_decorated.tool_spec
2200+
expected_tool_details = [
2201+
{
2202+
"name": tool_spec.get("name"),
2203+
"description": tool_spec.get("description"),
2204+
"inputSchema": tool_spec.get("inputSchema"),
2205+
"outputSchema": tool_spec.get("outputSchema"),
2206+
}
2207+
]
2208+
expected_json = serialize(expected_tool_details)
2209+
2210+
mock_span.set_attribute.assert_called_with("gen_ai.tool.definitions", expected_json)
2211+
2212+
21652213
@pytest.mark.parametrize(
21662214
"content, expected",
21672215
[

0 commit comments

Comments
 (0)