Skip to content

chore: updated semantic conventions on Generative AI spans #319

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
6 changes: 3 additions & 3 deletions src/strands/event_loop/event_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def event_loop_cycle(
# Create tracer span for this event loop cycle
tracer = get_tracer()
cycle_span = tracer.start_event_loop_cycle_span(
event_loop_kwargs=kwargs, parent_span=event_loop_parent_span, messages=messages
event_loop_kwargs=kwargs, messages=messages, parent_span=event_loop_parent_span
)
kwargs["event_loop_cycle_span"] = cycle_span

Expand All @@ -126,8 +126,8 @@ def event_loop_cycle(
for attempt in range(MAX_ATTEMPTS):
model_id = model.config.get("model_id") if hasattr(model, "config") else None
model_invoke_span = tracer.start_model_invoke_span(
parent_span=cycle_span,
messages=messages,
parent_span=cycle_span,
model_id=model_id,
)

Expand All @@ -142,7 +142,7 @@ def event_loop_cycle(
kwargs.setdefault("request_state", {})

if model_invoke_span:
tracer.end_model_invoke_span(model_invoke_span, message, usage)
tracer.end_model_invoke_span(model_invoke_span, message, usage, stop_reason)
break # Success! Break out of retry loop

except ContextWindowOverflowException as e:
Expand Down
124 changes: 89 additions & 35 deletions src/strands/telemetry/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from ..agent.agent_result import AgentResult
from ..types.content import Message, Messages
from ..types.streaming import Usage
from ..types.streaming import StopReason, Usage
from ..types.tools import ToolResult, ToolUse
from ..types.traces import AttributeValue

Expand Down Expand Up @@ -196,20 +196,31 @@ def end_span_with_error(self, span: Span, error_message: str, exception: Optiona
error = exception or Exception(error_message)
self._end_span(span, error=error)

def _add_event(self, span: Optional[Span], event_name: str, event_attributes: Dict[str, AttributeValue]) -> None:
"""Add an event with attributes to a span.

Args:
span: The span to add the event to
event_name: Name of the event
event_attributes: Dictionary of attributes to set on the event
"""
if not span:
return

span.add_event(event_name, attributes=event_attributes)

def start_model_invoke_span(
self,
messages: Messages,
parent_span: Optional[Span] = None,
agent_name: str = "Strands Agent",
messages: Optional[Messages] = None,
model_id: Optional[str] = None,
**kwargs: Any,
) -> Optional[Span]:
"""Start a new span for a model invocation.

Args:
messages: Messages being sent to the model.
parent_span: Optional parent span to link this span to.
agent_name: Name of the agent making the model call.
messages: Optional messages being sent to the model.
model_id: Optional identifier for the model being invoked.
**kwargs: Additional attributes to add to the span.

Expand All @@ -219,8 +230,6 @@ def start_model_invoke_span(
attributes: Dict[str, AttributeValue] = {
"gen_ai.system": "strands-agents",
"gen_ai.operation.name": "chat",
"gen_ai.agent.name": agent_name,
"gen_ai.prompt": serialize(messages),
}

if model_id:
Expand All @@ -229,28 +238,41 @@ def start_model_invoke_span(
# Add additional kwargs as attributes
attributes.update({k: v for k, v in kwargs.items() if isinstance(v, (str, int, float, bool))})

return self._start_span("Model invoke", parent_span, attributes, span_kind=trace_api.SpanKind.CLIENT)
span = self._start_span("Model invoke", parent_span, attributes, span_kind=trace_api.SpanKind.CLIENT)
for message in messages:
self._add_event(
span,
f"gen_ai.{message['role']}.message",
{"content": serialize(message["content"])},
)
return span

def end_model_invoke_span(
self, span: Span, message: Message, usage: Usage, error: Optional[Exception] = None
self, span: Span, message: Message, usage: Usage, stop_reason: StopReason, error: Optional[Exception] = None
) -> None:
"""End a model invocation span with results and metrics.

Args:
span: The span to end.
message: The message response from the model.
usage: Token usage information from the model call.
stop_reason (StopReason): The reason the model stopped generating.
error: Optional exception if the model call failed.
"""
attributes: Dict[str, AttributeValue] = {
"gen_ai.completion": serialize(message["content"]),
"gen_ai.usage.prompt_tokens": usage["inputTokens"],
"gen_ai.usage.input_tokens": usage["inputTokens"],
"gen_ai.usage.completion_tokens": usage["outputTokens"],
"gen_ai.usage.output_tokens": usage["outputTokens"],
"gen_ai.usage.total_tokens": usage["totalTokens"],
}

self._add_event(
span,
"gen_ai.choice",
event_attributes={"finish_reason": str(stop_reason), "message": serialize(message["content"])},
)

self._end_span(span, attributes, error)

def start_tool_call_span(self, tool: ToolUse, parent_span: Optional[Span] = None, **kwargs: Any) -> Optional[Span]:
Expand All @@ -265,18 +287,29 @@ def start_tool_call_span(self, tool: ToolUse, parent_span: Optional[Span] = None
The created span, or None if tracing is not enabled.
"""
attributes: Dict[str, AttributeValue] = {
"gen_ai.prompt": serialize(tool),
"gen_ai.operation.name": "execute_tool",
"gen_ai.system": "strands-agents",
"tool.name": tool["name"],
"tool.id": tool["toolUseId"],
"tool.parameters": serialize(tool["input"]),
"gen_ai.tool.name": tool["name"],
"gen_ai.tool.call.id": tool["toolUseId"],
}

# Add additional kwargs as attributes
attributes.update(kwargs)

span_name = f"Tool: {tool['name']}"
return self._start_span(span_name, parent_span, attributes, span_kind=trace_api.SpanKind.INTERNAL)
span = self._start_span(span_name, parent_span, attributes, span_kind=trace_api.SpanKind.INTERNAL)

self._add_event(
span,
"gen_ai.tool.message",
event_attributes={
"role": "tool",
"content": serialize(tool["input"]),
"id": tool["toolUseId"],
},
)

return span

def end_tool_call_span(
self, span: Span, tool_result: Optional[ToolResult], error: Optional[Exception] = None
Expand All @@ -293,30 +326,36 @@ def end_tool_call_span(
status = tool_result.get("status")
status_str = str(status) if status is not None else ""

tool_result_content_json = serialize(tool_result.get("content"))
attributes.update(
{
"tool.result": tool_result_content_json,
"gen_ai.completion": tool_result_content_json,
"tool.status": status_str,
}
)

self._add_event(
span,
"gen_ai.choice",
event_attributes={
"message": serialize(tool_result.get("content")),
"id": tool_result.get("toolUseId", ""),
},
)

self._end_span(span, attributes, error)

def start_event_loop_cycle_span(
self,
event_loop_kwargs: Any,
messages: Messages,
parent_span: Optional[Span] = None,
messages: Optional[Messages] = None,
**kwargs: Any,
) -> Optional[Span]:
"""Start a new span for an event loop cycle.

Args:
event_loop_kwargs: Arguments for the event loop cycle.
parent_span: Optional parent span to link this span to.
messages: Optional messages being processed in this cycle.
messages: Messages being processed in this cycle.
**kwargs: Additional attributes to add to the span.

Returns:
Expand All @@ -326,7 +365,6 @@ def start_event_loop_cycle_span(
parent_span = parent_span if parent_span else event_loop_kwargs.get("event_loop_parent_span")

attributes: Dict[str, AttributeValue] = {
"gen_ai.prompt": serialize(messages),
"event_loop.cycle_id": event_loop_cycle_id,
}

Expand All @@ -337,7 +375,15 @@ def start_event_loop_cycle_span(
attributes.update({k: v for k, v in kwargs.items() if isinstance(v, (str, int, float, bool))})

span_name = f"Cycle {event_loop_cycle_id}"
return self._start_span(span_name, parent_span, attributes, span_kind=trace_api.SpanKind.INTERNAL)
span = self._start_span(span_name, parent_span, attributes)
for message in messages or []:
self._add_event(
span,
f"gen_ai.{message['role']}.message",
{"content": serialize(message["content"])},
)

return span

def end_event_loop_cycle_span(
self,
Expand All @@ -354,13 +400,12 @@ def end_event_loop_cycle_span(
tool_result_message: Optional tool result message if a tool was called.
error: Optional exception if the cycle failed.
"""
attributes: Dict[str, AttributeValue] = {
"gen_ai.completion": serialize(message["content"]),
}
attributes: Dict[str, AttributeValue] = {}
event_attributes: Dict[str, AttributeValue] = {"message": serialize(message["content"])}

if tool_result_message:
attributes["tool.result"] = serialize(tool_result_message["content"])

event_attributes["tool.result"] = serialize(tool_result_message["content"])
self._add_event(span, "gen_ai.choice", event_attributes=event_attributes)
self._end_span(span, attributes, error)

def start_agent_span(
Expand All @@ -387,17 +432,15 @@ def start_agent_span(
"""
attributes: Dict[str, AttributeValue] = {
"gen_ai.system": "strands-agents",
"agent.name": agent_name,
"gen_ai.agent.name": agent_name,
"gen_ai.prompt": prompt,
"gen_ai.operation.name": "invoke_agent",
}

if model_id:
attributes["gen_ai.request.model"] = model_id

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

# Add custom trace attributes if provided
Expand All @@ -407,7 +450,18 @@ def start_agent_span(
# Add additional kwargs as attributes
attributes.update({k: v for k, v in kwargs.items() if isinstance(v, (str, int, float, bool))})

return self._start_span(agent_name, attributes=attributes, span_kind=trace_api.SpanKind.CLIENT)
span = self._start_span(
f"invoke_agent {agent_name}", attributes=attributes, span_kind=trace_api.SpanKind.CLIENT
)
self._add_event(
span,
"gen_ai.user.message",
event_attributes={
"content": prompt,
},
)

return span

def end_agent_span(
self,
Expand All @@ -426,10 +480,10 @@ def end_agent_span(
attributes: Dict[str, AttributeValue] = {}

if response:
attributes.update(
{
"gen_ai.completion": str(response),
}
self._add_event(
span,
"gen_ai.choice",
event_attributes={"message": str(response), "finish_reason": str(response.stop_reason)},
)

if hasattr(response, "metrics") and hasattr(response.metrics, "accumulated_usage"):
Expand Down
Loading
Loading