Skip to content

Commit 049f7e3

Browse files
authored
fix(openai-agents): span attribute handling for tool calls and results (#3422)
1 parent b01af56 commit 049f7e3

File tree

3 files changed

+732
-12
lines changed

3 files changed

+732
-12
lines changed

packages/opentelemetry-instrumentation-openai-agents/opentelemetry/instrumentation/openai_agents/_hooks.py

Lines changed: 103 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -239,19 +239,111 @@ def on_span_end(self, span):
239239
input_data = getattr(span_data, 'input', [])
240240
if input_data:
241241
for i, message in enumerate(input_data):
242-
if hasattr(message, 'role') and hasattr(message, 'content'):
243-
otel_span.set_attribute(f"{SpanAttributes.LLM_PROMPTS}.{i}.role", message.role)
244-
content = message.content
242+
prefix = f"{SpanAttributes.LLM_PROMPTS}.{i}"
243+
244+
# Convert message to dict for unified handling
245+
if isinstance(message, dict):
246+
msg = message
247+
else:
248+
# Convert object to dict
249+
msg = {}
250+
for attr in [
251+
"role",
252+
"content",
253+
"tool_call_id",
254+
"tool_calls",
255+
"type",
256+
"name",
257+
"arguments",
258+
"call_id",
259+
"output",
260+
]:
261+
if hasattr(message, attr):
262+
msg[attr] = getattr(message, attr)
263+
264+
# Determine message format and extract data
265+
role = None
266+
content = None
267+
tool_call_id = None
268+
tool_calls = None
269+
270+
if 'role' in msg:
271+
# Standard OpenAI chat format
272+
role = msg['role']
273+
content = msg.get('content')
274+
tool_call_id = msg.get('tool_call_id')
275+
tool_calls = msg.get('tool_calls')
276+
elif 'type' in msg:
277+
# OpenAI Agents SDK format
278+
msg_type = msg['type']
279+
if msg_type == 'function_call':
280+
# Tool calls are assistant messages
281+
role = 'assistant'
282+
# Create tool_calls structure matching OpenAI SDK format
283+
tool_calls = [{
284+
'id': msg.get('id', ''),
285+
'name': msg.get('name', ''),
286+
'arguments': msg.get('arguments', '')
287+
}]
288+
elif msg_type == 'function_call_output':
289+
# Tool outputs are tool messages
290+
role = 'tool'
291+
content = msg.get('output')
292+
tool_call_id = msg.get('call_id')
293+
294+
# Set role attribute
295+
if role:
296+
otel_span.set_attribute(f"{prefix}.role", role)
297+
298+
# Set content attribute
299+
if content is not None:
245300
if not isinstance(content, str):
246301
content = json.dumps(content)
247-
otel_span.set_attribute(f"{SpanAttributes.LLM_PROMPTS}.{i}.content", content)
248-
elif isinstance(message, dict):
249-
if 'role' in message and 'content' in message:
250-
otel_span.set_attribute(f"{SpanAttributes.LLM_PROMPTS}.{i}.role", message['role'])
251-
content = message['content']
252-
if isinstance(content, dict):
253-
content = json.dumps(content)
254-
otel_span.set_attribute(f"{SpanAttributes.LLM_PROMPTS}.{i}.content", content)
302+
otel_span.set_attribute(f"{prefix}.content", content)
303+
304+
# Set tool_call_id for tool result messages
305+
if tool_call_id:
306+
otel_span.set_attribute(f"{prefix}.tool_call_id", tool_call_id)
307+
308+
# Set tool_calls for assistant messages with tool calls
309+
if tool_calls:
310+
for j, tool_call in enumerate(tool_calls):
311+
# Convert to dict if needed
312+
if not isinstance(tool_call, dict):
313+
tc_dict = {}
314+
if hasattr(tool_call, 'id'):
315+
tc_dict['id'] = tool_call.id
316+
if hasattr(tool_call, 'function'):
317+
func = tool_call.function
318+
if hasattr(func, 'name'):
319+
tc_dict['name'] = func.name
320+
if hasattr(func, 'arguments'):
321+
tc_dict['arguments'] = func.arguments
322+
elif hasattr(tool_call, 'name'):
323+
tc_dict['name'] = tool_call.name
324+
if hasattr(tool_call, 'arguments'):
325+
tc_dict['arguments'] = tool_call.arguments
326+
tool_call = tc_dict
327+
328+
# Extract function details if nested (standard OpenAI format)
329+
if 'function' in tool_call:
330+
function = tool_call['function']
331+
tool_call = {
332+
'id': tool_call.get('id'),
333+
'name': function.get('name'),
334+
'arguments': function.get('arguments')
335+
}
336+
337+
# Set tool call attributes
338+
if tool_call.get('id'):
339+
otel_span.set_attribute(f"{prefix}.tool_calls.{j}.id", tool_call['id'])
340+
if tool_call.get('name'):
341+
otel_span.set_attribute(f"{prefix}.tool_calls.{j}.name", tool_call['name'])
342+
if tool_call.get('arguments'):
343+
args = tool_call['arguments']
344+
if not isinstance(args, str):
345+
args = json.dumps(args)
346+
otel_span.set_attribute(f"{prefix}.tool_calls.{j}.arguments", args)
255347

256348
# Add function/tool specifications to the request using OpenAI semantic conventions
257349
response = getattr(span_data, 'response', None)

0 commit comments

Comments
 (0)