Skip to content

Commit f6eabe2

Browse files
committed
chore(trace): updated semantic conventions with tool mappings
1 parent ad87f9e commit f6eabe2

File tree

2 files changed

+95
-23
lines changed

2 files changed

+95
-23
lines changed

src/strands/telemetry/tracer.py

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ def end_model_invoke_span(
316316
[
317317
{
318318
"role": message["role"],
319-
"parts": [{"type": "text", "content": message["content"]}],
319+
"parts": self._map_content_blocks_to_otel_parts(message["content"]),
320320
"finish_reason": str(stop_reason),
321321
}
322322
]
@@ -371,7 +371,7 @@ def start_tool_call_span(self, tool: ToolUse, parent_span: Optional[Span] = None
371371
"type": "tool_call",
372372
"name": tool["name"],
373373
"id": tool["toolUseId"],
374-
"arguments": [{"content": tool["input"]}],
374+
"arguments": tool["input"],
375375
}
376376
],
377377
}
@@ -426,7 +426,7 @@ def end_tool_call_span(
426426
{
427427
"type": "tool_call_response",
428428
"id": tool_result.get("toolUseId", ""),
429-
"result": tool_result.get("content"),
429+
"response": tool_result.get("content"),
430430
}
431431
],
432432
}
@@ -513,7 +513,7 @@ def end_event_loop_cycle_span(
513513
[
514514
{
515515
"role": tool_result_message["role"],
516-
"parts": [{"type": "text", "content": tool_result_message["content"]}],
516+
"parts": self._map_content_blocks_to_otel_parts(tool_result_message["content"]),
517517
}
518518
]
519519
)
@@ -643,19 +643,23 @@ def start_multiagent_span(
643643
)
644644

645645
span = self._start_span(operation, attributes=attributes, span_kind=trace_api.SpanKind.CLIENT)
646-
content = serialize(task) if isinstance(task, list) else task
647646

648647
if self.use_latest_genai_conventions:
648+
parts: list[dict[str, Any]] = []
649+
if isinstance(task, list):
650+
parts = self._map_content_blocks_to_otel_parts(task)
651+
else:
652+
parts = [{"type": "text", "content": task}]
649653
self._add_event(
650654
span,
651655
"gen_ai.client.inference.operation.details",
652-
{"gen_ai.input.messages": serialize([{"role": "user", "parts": [{"type": "text", "content": task}]}])},
656+
{"gen_ai.input.messages": serialize([{"role": "user", "parts": parts}])},
653657
)
654658
else:
655659
self._add_event(
656660
span,
657661
"gen_ai.user.message",
658-
event_attributes={"content": content},
662+
event_attributes={"content": serialize(task) if isinstance(task, list) else task},
659663
)
660664

661665
return span
@@ -727,7 +731,7 @@ def _add_event_messages(self, span: Span, messages: Messages) -> None:
727731
input_messages: list = []
728732
for message in messages:
729733
input_messages.append(
730-
{"role": message["role"], "parts": [{"type": "text", "content": message["content"]}]}
734+
{"role": message["role"], "parts": self._map_content_blocks_to_otel_parts(message["content"])}
731735
)
732736
self._add_event(
733737
span, "gen_ai.client.inference.operation.details", {"gen_ai.input.messages": serialize(input_messages)}
@@ -740,6 +744,41 @@ def _add_event_messages(self, span: Span, messages: Messages) -> None:
740744
{"content": serialize(message["content"])},
741745
)
742746

747+
def _map_content_blocks_to_otel_parts(self, content_blocks: list[ContentBlock]) -> list[dict[str, Any]]:
748+
"""Map ContentBlock objects to OpenTelemetry parts format."""
749+
parts: list[dict[str, Any]] = []
750+
751+
for block in content_blocks:
752+
if "text" in block:
753+
# Standard TextPart
754+
parts.append({"type": "text", "content": block["text"]})
755+
elif "toolUse" in block:
756+
# Standard ToolCallRequestPart
757+
tool_use = block["toolUse"]
758+
parts.append(
759+
{
760+
"type": "tool_call",
761+
"name": tool_use["name"],
762+
"id": tool_use["toolUseId"],
763+
"arguments": tool_use["input"],
764+
}
765+
)
766+
elif "toolResult" in block:
767+
# Standard ToolCallResponsePart
768+
tool_result = block["toolResult"]
769+
parts.append(
770+
{
771+
"type": "tool_call_response",
772+
"id": tool_result["toolUseId"],
773+
"response": tool_result["content"],
774+
}
775+
)
776+
else:
777+
# For all other ContentBlock types, use the key as type and value as content
778+
for key, value in block.items():
779+
parts.append({"type": key, "content": value})
780+
return parts
781+
743782

744783
# Singleton instance for global access
745784
_tracer_instance = None

tests/strands/telemetry/test_tracer.py

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,15 @@ def test_start_model_invoke_span_latest_conventions(mock_tracer):
173173
mock_span = mock.MagicMock()
174174
mock_tracer.start_span.return_value = mock_span
175175

176-
messages = [{"role": "user", "content": [{"text": "Hello"}]}]
176+
messages = [
177+
{"role": "user", "content": [{"text": "Hello 2025-1993"}]},
178+
{
179+
"role": "assistant",
180+
"content": [
181+
{"toolUse": {"input": '"expression": "2025-1993"', "name": "calculator", "toolUseId": "123"}}
182+
],
183+
},
184+
]
177185
model_id = "test-model"
178186

179187
span = tracer.start_model_invoke_span(messages=messages, agent_name="TestAgent", model_id=model_id)
@@ -191,8 +199,19 @@ def test_start_model_invoke_span_latest_conventions(mock_tracer):
191199
[
192200
{
193201
"role": messages[0]["role"],
194-
"parts": [{"type": "text", "content": messages[0]["content"]}],
195-
}
202+
"parts": [{"type": "text", "content": "Hello 2025-1993"}],
203+
},
204+
{
205+
"role": messages[1]["role"],
206+
"parts": [
207+
{
208+
"type": "tool_call",
209+
"name": "calculator",
210+
"id": "123",
211+
"arguments": '"expression": "2025-1993"',
212+
}
213+
],
214+
},
196215
]
197216
)
198217
},
@@ -255,7 +274,7 @@ def test_end_model_invoke_span_latest_conventions(mock_span):
255274
[
256275
{
257276
"role": "assistant",
258-
"parts": [{"type": "text", "content": message["content"]}],
277+
"parts": [{"type": "text", "content": "Response"}],
259278
"finish_reason": "end_turn",
260279
}
261280
]
@@ -324,7 +343,7 @@ def test_start_tool_call_span_latest_conventions(mock_tracer):
324343
"type": "tool_call",
325344
"name": tool["name"],
326345
"id": tool["toolUseId"],
327-
"arguments": [{"content": tool["input"]}],
346+
"arguments": tool["input"],
328347
}
329348
],
330349
}
@@ -404,7 +423,7 @@ def test_start_swarm_span_with_contentblock_task_latest_conventions(mock_tracer)
404423
"gen_ai.client.inference.operation.details",
405424
attributes={
406425
"gen_ai.input.messages": serialize(
407-
[{"role": "user", "parts": [{"type": "text", "content": [{"text": "Original Task: foo bar"}]}]}]
426+
[{"role": "user", "parts": [{"type": "text", "content": "Original Task: foo bar"}]}]
408427
)
409428
},
410429
)
@@ -492,7 +511,7 @@ def test_end_tool_call_span_latest_conventions(mock_span):
492511
"""Test ending a tool call span with the latest semantic conventions."""
493512
tracer = Tracer()
494513
tracer.use_latest_genai_conventions = True
495-
tool_result = {"status": "success", "content": [{"text": "Tool result"}]}
514+
tool_result = {"status": "success", "content": [{"text": "Tool result"}, {"json": {"foo": "bar"}}]}
496515

497516
tracer.end_tool_call_span(mock_span, tool_result)
498517

@@ -508,7 +527,7 @@ def test_end_tool_call_span_latest_conventions(mock_span):
508527
{
509528
"type": "tool_call_response",
510529
"id": tool_result.get("toolUseId", ""),
511-
"result": tool_result.get("content"),
530+
"response": tool_result.get("content"),
512531
}
513532
],
514533
}
@@ -564,9 +583,7 @@ def test_start_event_loop_cycle_span_latest_conventions(mock_tracer):
564583
mock_span.add_event.assert_any_call(
565584
"gen_ai.client.inference.operation.details",
566585
attributes={
567-
"gen_ai.input.messages": serialize(
568-
[{"role": "user", "parts": [{"type": "text", "content": messages[0]["content"]}]}]
569-
)
586+
"gen_ai.input.messages": serialize([{"role": "user", "parts": [{"type": "text", "content": "Hello"}]}])
570587
},
571588
)
572589
assert span is not None
@@ -576,7 +593,12 @@ def test_end_event_loop_cycle_span(mock_span):
576593
"""Test ending an event loop cycle span."""
577594
tracer = Tracer()
578595
message = {"role": "assistant", "content": [{"text": "Response"}]}
579-
tool_result_message = {"role": "assistant", "content": [{"toolResult": {"response": "Success"}}]}
596+
tool_result_message = {
597+
"role": "assistant",
598+
"content": [
599+
{"toolResult": {"toolUseId": "123", "status": "success", "content": [{"text": "Weather is sunny"}]}}
600+
],
601+
}
580602

581603
tracer.end_event_loop_cycle_span(mock_span, message, tool_result_message)
582604

@@ -596,7 +618,12 @@ def test_end_event_loop_cycle_span_latest_conventions(mock_span):
596618
tracer = Tracer()
597619
tracer.use_latest_genai_conventions = True
598620
message = {"role": "assistant", "content": [{"text": "Response"}]}
599-
tool_result_message = {"role": "assistant", "content": [{"toolResult": {"response": "Success"}}]}
621+
tool_result_message = {
622+
"role": "assistant",
623+
"content": [
624+
{"toolResult": {"toolUseId": "123", "status": "success", "content": [{"text": "Weather is sunny"}]}}
625+
],
626+
}
600627

601628
tracer.end_event_loop_cycle_span(mock_span, message, tool_result_message)
602629

@@ -607,7 +634,13 @@ def test_end_event_loop_cycle_span_latest_conventions(mock_span):
607634
[
608635
{
609636
"role": "assistant",
610-
"parts": [{"type": "text", "content": tool_result_message["content"]}],
637+
"parts": [
638+
{
639+
"type": "tool_call_response",
640+
"id": "123",
641+
"response": [{"text": "Weather is sunny"}],
642+
}
643+
],
611644
}
612645
]
613646
)
@@ -682,7 +715,7 @@ def test_start_agent_span_latest_conventions(mock_tracer):
682715
"gen_ai.client.inference.operation.details",
683716
attributes={
684717
"gen_ai.input.messages": serialize(
685-
[{"role": "user", "parts": [{"type": "text", "content": [{"text": "test prompt"}]}]}]
718+
[{"role": "user", "parts": [{"type": "text", "content": "test prompt"}]}]
686719
)
687720
},
688721
)

0 commit comments

Comments
 (0)