Skip to content

Commit 1e70994

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

File tree

4 files changed

+97
-16
lines changed

4 files changed

+97
-16
lines changed

src/strands/agent/agent.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -658,7 +658,10 @@ async def stream_async(
658658
# Process input and get message to add (if any)
659659
messages = self._convert_prompt_to_messages(prompt)
660660

661-
self.trace_span = self._start_agent_trace_span(messages)
661+
self.trace_span = self._start_agent_trace_span(
662+
messages,
663+
all_tools_config=self.tool_registry.get_all_tools_config() or {},
664+
)
662665

663666
with trace_api.use_span(self.trace_span):
664667
try:
@@ -922,22 +925,41 @@ def _record_tool_execution(
922925
self._append_message(tool_result_msg)
923926
self._append_message(assistant_msg)
924927

925-
def _start_agent_trace_span(self, messages: Messages) -> trace_api.Span:
928+
def _start_agent_trace_span(self, messages: Messages, all_tools_config: Optional[dict] = None) -> trace_api.Span:
926929
"""Starts a trace span for the agent.
927930
928931
Args:
929932
messages: The input messages.
933+
all_tools_config: Optional dictionary of tool configurations.
930934
"""
931935
model_id = self.model.config.get("model_id") if hasattr(self.model, "config") else None
932-
return self.tracer.start_agent_span(
936+
span = self.tracer.start_agent_span(
933937
messages=messages,
934938
agent_name=self.name,
935939
model_id=model_id,
936-
tools=self.tool_names,
937940
system_prompt=self.system_prompt,
938941
custom_trace_attributes=self.trace_attributes,
939942
)
940943

944+
if self.tracer.include_tool_definitions and all_tools_config:
945+
try:
946+
tool_details = [
947+
{
948+
"name": name,
949+
"description": spec.get("description"),
950+
"inputSchema": spec.get("inputSchema"),
951+
"outputSchema": spec.get("outputSchema"),
952+
}
953+
for name, spec in all_tools_config.items()
954+
]
955+
serialized_tools = serialize(tool_details)
956+
span.set_attribute("gen_ai.tool.definitions", serialized_tools)
957+
except Exception:
958+
# A failure in telemetry should not crash the agent
959+
logger.exception("failed to attach tool metadata to agent span")
960+
961+
return span
962+
941963
def _end_agent_trace_span(
942964
self,
943965
response: Optional[AgentResult] = None,

src/strands/telemetry/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ def __init__(
8989
Args:
9090
tracer_provider: Optional pre-configured tracer provider.
9191
If None, a new one will be created and set as global.
92+
include_tool_definitions: Whether to include tool definitions in traces.
93+
Defaults to False.
9294
9395
The instance is ready to use immediately after initialization, though
9496
trace exporters must be configured separately using the setup methods.

src/strands/telemetry/tracer.py

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

84-
def __init__(
85-
self,
86-
) -> None:
84+
def __init__(self) -> None:
8785
"""Initialize the tracer."""
8886
self.service_name = __name__
8987
self.tracer_provider: Optional[trace_api.TracerProvider] = None
@@ -92,17 +90,18 @@ def __init__(
9290
ThreadingInstrumentor().instrument()
9391

9492
# Read OTEL_SEMCONV_STABILITY_OPT_IN environment variable
95-
self.use_latest_genai_conventions = self._parse_semconv_opt_in()
93+
opt_in_values = self._parse_semconv_opt_in()
94+
self.use_latest_genai_conventions = "gen_ai_latest_experimental" in opt_in_values
95+
self.include_tool_definitions = "gen_ai_tool_definitions" in opt_in_values
9696

97-
def _parse_semconv_opt_in(self) -> bool:
97+
def _parse_semconv_opt_in(self) -> set[str]:
9898
"""Parse the OTEL_SEMCONV_STABILITY_OPT_IN environment variable.
9999
100100
Returns:
101-
Set of opt-in values from the environment variable
101+
A set of opt-in values from the environment variable.
102102
"""
103103
opt_in_env = os.getenv("OTEL_SEMCONV_STABILITY_OPT_IN", "")
104-
105-
return "gen_ai_latest_experimental" in opt_in_env
104+
return {value.strip() for value in opt_in_env.split(",")}
106105

107106
def _start_span(
108107
self,

tests/strands/agent/test_agent.py

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,7 +1353,6 @@ def test_agent_call_creates_and_ends_span_on_success(mock_get_tracer, mock_model
13531353
messages=[{"content": [{"text": "test prompt"}], "role": "user"}],
13541354
agent_name="Strands Agents",
13551355
model_id=unittest.mock.ANY,
1356-
tools=agent.tool_names,
13571356
system_prompt=agent.system_prompt,
13581357
custom_trace_attributes=agent.trace_attributes,
13591358
)
@@ -1387,7 +1386,6 @@ async def test_event_loop(*args, **kwargs):
13871386
messages=[{"content": [{"text": "test prompt"}], "role": "user"}],
13881387
agent_name="Strands Agents",
13891388
model_id=unittest.mock.ANY,
1390-
tools=agent.tool_names,
13911389
system_prompt=agent.system_prompt,
13921390
custom_trace_attributes=agent.trace_attributes,
13931391
)
@@ -1425,7 +1423,6 @@ def test_agent_call_creates_and_ends_span_on_exception(mock_get_tracer, mock_mod
14251423
messages=[{"content": [{"text": "test prompt"}], "role": "user"}],
14261424
agent_name="Strands Agents",
14271425
model_id=unittest.mock.ANY,
1428-
tools=agent.tool_names,
14291426
system_prompt=agent.system_prompt,
14301427
custom_trace_attributes=agent.trace_attributes,
14311428
)
@@ -1461,7 +1458,6 @@ async def test_agent_stream_async_creates_and_ends_span_on_exception(mock_get_tr
14611458
messages=[{"content": [{"text": "test prompt"}], "role": "user"}],
14621459
agent_name="Strands Agents",
14631460
model_id=unittest.mock.ANY,
1464-
tools=agent.tool_names,
14651461
system_prompt=agent.system_prompt,
14661462
custom_trace_attributes=agent.trace_attributes,
14671463
)
@@ -2162,6 +2158,68 @@ def shell(command: str):
21622158
assert agent.messages[-1] == {"content": [{"text": "I invoked a tool!"}], "role": "assistant"}
21632159

21642160

2161+
def test_agent_does_not_include_tools_in_trace_by_default(tool_decorated, monkeypatch):
2162+
"""Verify that by default, the agent does not add tool specs to the trace."""
2163+
monkeypatch.setenv("OTEL_SEMCONV_STABILITY_OPT_IN", "")
2164+
with unittest.mock.patch("strands.agent.agent.get_tracer") as mock_get_tracer:
2165+
# We need to re-import the tracer to pick up the new env var
2166+
import importlib
2167+
2168+
from strands.telemetry import tracer
2169+
2170+
importlib.reload(tracer)
2171+
2172+
mock_tracer_instance = tracer.Tracer()
2173+
mock_span = unittest.mock.MagicMock()
2174+
mock_tracer_instance.start_agent_span = unittest.mock.MagicMock(return_value=mock_span)
2175+
mock_get_tracer.return_value = mock_tracer_instance
2176+
2177+
mock_model = MockedModelProvider([{"role": "assistant", "content": [{"text": "hello!"}]}])
2178+
2179+
agent = Agent(tools=[tool_decorated], model=mock_model)
2180+
agent("test prompt")
2181+
2182+
# Check that set_attribute was not called for our specific key
2183+
called_attributes = [call.args[0] for call in mock_span.set_attribute.call_args_list]
2184+
assert "gen_ai.tool.definitions" not in called_attributes
2185+
2186+
2187+
def test_agent_includes_tools_in_trace_when_enabled(tool_decorated, monkeypatch):
2188+
"""Verify that the agent adds tool specs to the trace when the flag is enabled."""
2189+
monkeypatch.setenv("OTEL_SEMCONV_STABILITY_OPT_IN", "gen_ai_tool_definitions")
2190+
with unittest.mock.patch("strands.agent.agent.get_tracer") as mock_get_tracer:
2191+
# We need to re-import the tracer to pick up the new env var
2192+
import importlib
2193+
2194+
from strands.telemetry import tracer
2195+
2196+
importlib.reload(tracer)
2197+
2198+
mock_tracer_instance = tracer.Tracer()
2199+
mock_span = unittest.mock.MagicMock()
2200+
mock_tracer_instance.start_agent_span = unittest.mock.MagicMock(return_value=mock_span)
2201+
mock_get_tracer.return_value = mock_tracer_instance
2202+
2203+
mock_model = MockedModelProvider([{"role": "assistant", "content": [{"text": "hello!"}]}])
2204+
2205+
agent = Agent(tools=[tool_decorated], model=mock_model)
2206+
agent("test prompt")
2207+
2208+
# Verify the correct data is serialized and set as an attribute
2209+
tool_spec = tool_decorated.tool_spec
2210+
expected_tool_details = [
2211+
{
2212+
"name": tool_spec.get("name"),
2213+
"description": tool_spec.get("description"),
2214+
"inputSchema": tool_spec.get("inputSchema"),
2215+
"outputSchema": tool_spec.get("outputSchema"),
2216+
}
2217+
]
2218+
expected_json = serialize(expected_tool_details)
2219+
2220+
mock_span.set_attribute.assert_any_call("gen_ai.tool.definitions", expected_json)
2221+
2222+
21652223
@pytest.mark.parametrize(
21662224
"content, expected",
21672225
[

0 commit comments

Comments
 (0)