Skip to content
Open
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
1 change: 1 addition & 0 deletions src/strands/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,7 @@ def _start_agent_trace_span(self, messages: Messages) -> trace_api.Span:
tools=self.tool_names,
system_prompt=self.system_prompt,
custom_trace_attributes=self.trace_attributes,
tools_config=self.tool_registry.get_all_tools_config(),
)

def _end_agent_trace_span(
Expand Down
40 changes: 30 additions & 10 deletions src/strands/telemetry/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,7 @@ class Tracer:
are sent to the OTLP endpoint.
"""

def __init__(
self,
) -> None:
def __init__(self) -> None:
"""Initialize the tracer."""
self.service_name = __name__
self.tracer_provider: Optional[trace_api.TracerProvider] = None
Expand All @@ -92,17 +90,18 @@ def __init__(
ThreadingInstrumentor().instrument()

# Read OTEL_SEMCONV_STABILITY_OPT_IN environment variable
self.use_latest_genai_conventions = self._parse_semconv_opt_in()
opt_in_values = self._parse_semconv_opt_in()
self.use_latest_genai_conventions = "gen_ai_latest_experimental" in opt_in_values
self.include_tool_definitions = "gen_ai_tool_definitions" in opt_in_values

def _parse_semconv_opt_in(self) -> bool:
def _parse_semconv_opt_in(self) -> set[str]:
"""Parse the OTEL_SEMCONV_STABILITY_OPT_IN environment variable.

Returns:
Set of opt-in values from the environment variable
A set of opt-in values from the environment variable.
"""
opt_in_env = os.getenv("OTEL_SEMCONV_STABILITY_OPT_IN", "")

return "gen_ai_latest_experimental" in opt_in_env
return {value.strip() for value in opt_in_env.split(",")}

def _start_span(
self,
Expand Down Expand Up @@ -551,6 +550,7 @@ def start_agent_span(
model_id: Optional[str] = None,
tools: Optional[list] = None,
custom_trace_attributes: Optional[Mapping[str, AttributeValue]] = None,
tools_config: Optional[dict] = None,
**kwargs: Any,
) -> Span:
"""Start a new span for an agent invocation.
Expand All @@ -561,6 +561,7 @@ def start_agent_span(
model_id: Optional model identifier.
tools: Optional list of tools being used.
custom_trace_attributes: Optional mapping of custom trace attributes to include in the span.
tools_config: Optional dictionary of tool configurations.
**kwargs: Additional attributes to add to the span.

Returns:
Expand All @@ -577,8 +578,15 @@ def start_agent_span(
attributes["gen_ai.request.model"] = model_id

if tools:
tools_json = serialize(tools)
attributes["gen_ai.agent.tools"] = tools_json
attributes["gen_ai.agent.tools"] = serialize(tools)

if self.include_tool_definitions and tools_config:
try:
tool_definitions = self._construct_tool_definitions(tools_config)
attributes["gen_ai.tool.definitions"] = serialize(tool_definitions)
except Exception:
# A failure in telemetry should not crash the agent
logger.warning("failed to attach tool metadata to agent span", exc_info=True)

# Add custom trace attributes if provided
if custom_trace_attributes:
Expand Down Expand Up @@ -649,6 +657,18 @@ def end_agent_span(

self._end_span(span, attributes, error)

def _construct_tool_definitions(self, tools_config: dict) -> list[dict[str, Any]]:
"""Constructs a list of tool definitions from the provided tools_config."""
return [
{
"name": name,
"description": spec.get("description"),
"inputSchema": spec.get("inputSchema"),
"outputSchema": spec.get("outputSchema"),
}
for name, spec in tools_config.items()
]

def start_multiagent_span(
self,
task: str | list[ContentBlock],
Expand Down
8 changes: 6 additions & 2 deletions tests/strands/agent/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1360,6 +1360,7 @@ def test_agent_call_creates_and_ends_span_on_success(mock_get_tracer, mock_model
tools=agent.tool_names,
system_prompt=agent.system_prompt,
custom_trace_attributes=agent.trace_attributes,
tools_config=unittest.mock.ANY,
)

# Verify span was ended with the result
Expand Down Expand Up @@ -1394,6 +1395,7 @@ async def test_event_loop(*args, **kwargs):
tools=agent.tool_names,
system_prompt=agent.system_prompt,
custom_trace_attributes=agent.trace_attributes,
tools_config=unittest.mock.ANY,
)

expected_response = AgentResult(
Expand Down Expand Up @@ -1432,6 +1434,7 @@ def test_agent_call_creates_and_ends_span_on_exception(mock_get_tracer, mock_mod
tools=agent.tool_names,
system_prompt=agent.system_prompt,
custom_trace_attributes=agent.trace_attributes,
tools_config=unittest.mock.ANY,
)

# Verify span was ended with the exception
Expand Down Expand Up @@ -1468,6 +1471,7 @@ async def test_agent_stream_async_creates_and_ends_span_on_exception(mock_get_tr
tools=agent.tool_names,
system_prompt=agent.system_prompt,
custom_trace_attributes=agent.trace_attributes,
tools_config=unittest.mock.ANY,
)

# Verify span was ended with the exception
Expand Down Expand Up @@ -2240,8 +2244,8 @@ def test_agent_backwards_compatibility_single_text_block():

# Should extract text for backwards compatibility
assert agent.system_prompt == text


@pytest.mark.parametrize(
"content, expected",
[
Expand Down
57 changes: 57 additions & 0 deletions tests/strands/telemetry/test_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1324,3 +1324,60 @@ def test_start_event_loop_cycle_span_with_tool_result_message(mock_tracer):
"gen_ai.tool.message", attributes={"content": json.dumps(messages[0]["content"])}
)
assert span is not None


def test_start_agent_span_does_not_include_tool_definitions_by_default():
"""Verify that start_agent_span does not include tool definitions by default."""
tracer = Tracer()
tracer.include_tool_definitions = False
tracer._start_span = mock.MagicMock()

tools_config = {
"my_tool": {
"name": "my_tool",
"description": "A test tool",
"inputSchema": {"json": {}},
"outputSchema": {"json": {}},
}
}

tracer.start_agent_span(messages=[], agent_name="TestAgent", tools_config=tools_config)

tracer._start_span.assert_called_once()
_, call_kwargs = tracer._start_span.call_args
attributes = call_kwargs.get("attributes", {})
assert "gen_ai.tool.definitions" not in attributes


def test_start_agent_span_includes_tool_definitions_when_enabled():
"""Verify that start_agent_span includes tool definitions when enabled."""
tracer = Tracer()
tracer.include_tool_definitions = True
tracer._start_span = mock.MagicMock()

tools_config = {
"my_tool": {
"name": "my_tool",
"description": "A test tool",
"inputSchema": {"json": {"type": "object", "properties": {}}},
"outputSchema": {"json": {"type": "object", "properties": {}}},
}
}

tracer.start_agent_span(messages=[], agent_name="TestAgent", tools_config=tools_config)

tracer._start_span.assert_called_once()
_, call_kwargs = tracer._start_span.call_args
attributes = call_kwargs.get("attributes", {})

assert "gen_ai.tool.definitions" in attributes
expected_tool_details = [
{
"name": "my_tool",
"description": "A test tool",
"inputSchema": {"json": {"type": "object", "properties": {}}},
"outputSchema": {"json": {"type": "object", "properties": {}}},
}
]
expected_json = serialize(expected_tool_details)
assert attributes["gen_ai.tool.definitions"] == expected_json
Loading