Skip to content

Commit e080c31

Browse files
committed
Add support for OTel GenAI Semantic Convention patterns in LLO handler
1 parent 4ea7469 commit e080c31

File tree

3 files changed

+391
-3
lines changed

3 files changed

+391
-3
lines changed

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/llo_handler.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
ROLE_SYSTEM = "system"
1717
ROLE_USER = "user"
1818
ROLE_ASSISTANT = "assistant"
19+
ROLE_TOOL = "tool"
1920

2021
_logger = logging.getLogger(__name__)
2122

@@ -137,6 +138,35 @@ class PatternConfig(TypedDict, total=False):
137138
"role": ROLE_USER,
138139
"source": "prompt",
139140
},
141+
# OTel GenAI Semantic Convention used by the latest Strands SDK
142+
# References:
143+
# - OTel GenAI SemConv: https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-events/
144+
# - Strands SDK PR: https://github.com/strands-agents/sdk-python/pull/319
145+
"gen_ai.user.message": {
146+
"type": PatternType.DIRECT,
147+
"role": ROLE_USER,
148+
"source": "prompt",
149+
},
150+
"gen_ai.assistant.message": {
151+
"type": PatternType.DIRECT,
152+
"role": ROLE_ASSISTANT,
153+
"source": "output",
154+
},
155+
"gen_ai.system.message": {
156+
"type": PatternType.DIRECT,
157+
"role": ROLE_SYSTEM,
158+
"source": "prompt",
159+
},
160+
"gen_ai.tool.message": {
161+
"type": PatternType.DIRECT,
162+
"role": ROLE_TOOL,
163+
"source": "prompt",
164+
},
165+
"gen_ai.choice": {
166+
"type": PatternType.DIRECT,
167+
"role": ROLE_ASSISTANT,
168+
"source": "output",
169+
},
140170
}
141171

142172

@@ -214,6 +244,7 @@ def _collect_all_llo_messages(self, span: ReadableSpan, attributes: types.Attrib
214244
for attr_key, value in attributes.items():
215245
if attr_key in self._exact_match_patterns:
216246
config = self._pattern_configs[attr_key]
247+
217248
messages.append(
218249
{"content": value, "role": config.get("role", "unknown"), "source": config.get("source", "unknown")}
219250
)
@@ -279,6 +310,12 @@ def _collect_llo_attributes_from_span(self, span: ReadableSpan) -> Dict[str, Any
279310
# Collect from span events
280311
if span.events:
281312
for event in span.events:
313+
# Check if event name itself is an LLO pattern (e.g., "gen_ai.user.message")
314+
if self._is_llo_attribute(event.name):
315+
# Put all event attributes as the content as LLO in log event
316+
all_llo_attributes[event.name] = dict(event.attributes) if event.attributes else {}
317+
318+
# Also check traditional pattern - LLO attributes within event attributes
282319
if event.attributes:
283320
for key, value in event.attributes.items():
284321
if self._is_llo_attribute(key):
@@ -372,6 +409,10 @@ def _filter_span_events(self, span: ReadableSpan) -> None:
372409
updated_events = []
373410

374411
for event in span.events:
412+
# Skip entire event if event name is an LLO pattern
413+
if self._is_llo_attribute(event.name):
414+
continue
415+
375416
if not event.attributes:
376417
updated_events.append(event)
377418
continue
@@ -417,7 +458,7 @@ def _group_messages_by_type(self, messages: List[Dict[str, Any]]) -> Dict[str, L
417458
elif role == ROLE_ASSISTANT:
418459
output_messages.append(formatted_message)
419460
else:
420-
# Route based on source for non-standard roles
461+
# Route based on source for non-standard roles including tool
421462
if any(key in message.get("source", "") for key in ["completion", "output", "result"]):
422463
output_messages.append(formatted_message)
423464
else:

0 commit comments

Comments
 (0)