Skip to content

Commit c3e5f6b

Browse files
authored
chore(telemetry): added gen_ai.tool.description and gen_ai.tool.json_schema (#1027)
1 parent 355b3bb commit c3e5f6b

File tree

2 files changed

+96
-1
lines changed

2 files changed

+96
-1
lines changed

src/strands/tools/executors/_executor.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from ...hooks import AfterToolCallEvent, BeforeToolCallEvent
1515
from ...telemetry.metrics import Trace
16-
from ...telemetry.tracer import get_tracer
16+
from ...telemetry.tracer import get_tracer, serialize
1717
from ...types._events import ToolCancelEvent, ToolResultEvent, ToolStreamEvent, TypedEvent
1818
from ...types.content import Message
1919
from ...types.tools import ToolChoice, ToolChoiceAuto, ToolConfig, ToolResult, ToolUse
@@ -59,6 +59,14 @@ async def _stream(
5959

6060
tool_info = agent.tool_registry.dynamic_tools.get(tool_name)
6161
tool_func = tool_info if tool_info is not None else agent.tool_registry.registry.get(tool_name)
62+
tool_spec = tool_func.tool_spec if tool_func is not None else None
63+
64+
current_span = trace_api.get_current_span()
65+
if current_span and tool_spec is not None:
66+
current_span.set_attribute("gen_ai.tool.description", tool_spec["description"])
67+
input_schema = tool_spec["inputSchema"]
68+
if "json" in input_schema:
69+
current_span.set_attribute("gen_ai.tool.json_schema", serialize(input_schema["json"]))
6270

6371
invocation_state.update(
6472
{

tests/strands/tools/executors/test_executor.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,90 @@ def cancel_callback(event):
250250
tru_results = tool_results
251251
exp_results = [exp_events[-1].tool_result]
252252
assert tru_results == exp_results
253+
254+
255+
@pytest.mark.asyncio
256+
async def test_executor_stream_sets_span_attributes(
257+
executor, agent, tool_results, invocation_state, weather_tool, alist
258+
):
259+
"""Test that span attributes are set correctly when tool_spec is available."""
260+
with unittest.mock.patch("strands.tools.executors._executor.trace_api") as mock_trace_api:
261+
mock_span = unittest.mock.MagicMock()
262+
mock_trace_api.get_current_span.return_value = mock_span
263+
264+
# Mock tool_spec with inputSchema containing json field
265+
with unittest.mock.patch.object(
266+
type(weather_tool), "tool_spec", new_callable=unittest.mock.PropertyMock
267+
) as mock_tool_spec:
268+
mock_tool_spec.return_value = {
269+
"name": "weather_tool",
270+
"description": "Get weather information",
271+
"inputSchema": {"json": {"type": "object", "properties": {}}, "type": "object"},
272+
}
273+
274+
tool_use: ToolUse = {"name": "weather_tool", "toolUseId": "1", "input": {}}
275+
stream = executor._stream(agent, tool_use, tool_results, invocation_state)
276+
277+
await alist(stream)
278+
279+
# Verify set_attribute was called with correct values
280+
calls = mock_span.set_attribute.call_args_list
281+
assert len(calls) == 2
282+
283+
# Check description attribute
284+
assert calls[0][0][0] == "gen_ai.tool.description"
285+
assert calls[0][0][1] == "Get weather information"
286+
287+
# Check json_schema attribute
288+
assert calls[1][0][0] == "gen_ai.tool.json_schema"
289+
# The serialize function should have been called on the json field
290+
291+
292+
@pytest.mark.asyncio
293+
async def test_executor_stream_handles_missing_json_in_input_schema(
294+
executor, agent, tool_results, invocation_state, weather_tool, alist
295+
):
296+
"""Test that span attributes handle inputSchema without json field gracefully."""
297+
with unittest.mock.patch("strands.tools.executors._executor.trace_api") as mock_trace_api:
298+
mock_span = unittest.mock.MagicMock()
299+
mock_trace_api.get_current_span.return_value = mock_span
300+
301+
# Mock tool_spec with inputSchema but no json field
302+
with unittest.mock.patch.object(
303+
type(weather_tool), "tool_spec", new_callable=unittest.mock.PropertyMock
304+
) as mock_tool_spec:
305+
mock_tool_spec.return_value = {
306+
"name": "weather_tool",
307+
"description": "Get weather information",
308+
"inputSchema": {"type": "object", "properties": {}},
309+
}
310+
311+
tool_use: ToolUse = {"name": "weather_tool", "toolUseId": "1", "input": {}}
312+
stream = executor._stream(agent, tool_use, tool_results, invocation_state)
313+
314+
# Should not raise an error - json_schema attribute just won't be set
315+
await alist(stream)
316+
317+
# Verify only description attribute was set (not json_schema)
318+
calls = mock_span.set_attribute.call_args_list
319+
assert len(calls) == 1
320+
assert calls[0][0][0] == "gen_ai.tool.description"
321+
322+
323+
@pytest.mark.asyncio
324+
async def test_executor_stream_no_span_attributes_when_no_tool_spec(
325+
executor, agent, tool_results, invocation_state, alist
326+
):
327+
"""Test that no span attributes are set when tool_spec is None."""
328+
with unittest.mock.patch("strands.tools.executors._executor.trace_api") as mock_trace_api:
329+
mock_span = unittest.mock.MagicMock()
330+
mock_trace_api.get_current_span.return_value = mock_span
331+
332+
# Use unknown tool which will have no tool_spec
333+
tool_use: ToolUse = {"name": "unknown_tool", "toolUseId": "1", "input": {}}
334+
stream = executor._stream(agent, tool_use, tool_results, invocation_state)
335+
336+
await alist(stream)
337+
338+
# Verify set_attribute was not called since tool_spec is None
339+
mock_span.set_attribute.assert_not_called()

0 commit comments

Comments
 (0)