Skip to content

Commit 35592bb

Browse files
fix(openai-agents): serialize complex output types to JSON for AttributeValue compatibility
Handle edge cases where FunctionCallOutput and ResponseCustomToolCallOutputParam contain complex output types (lists, dicts) that don't match AttributeValue type. Serialize these to JSON strings while preserving None and string values.
1 parent ea779da commit 35592bb

File tree

2 files changed

+89
-3
lines changed

2 files changed

+89
-3
lines changed

python/instrumentation/openinference-instrumentation-openai-agents/src/openinference/instrumentation/openai_agents/_processor.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -349,8 +349,14 @@ def _get_attributes_from_response_custom_tool_call_output_param(
349349
yield f"{prefix}{MessageAttributes.MESSAGE_ROLE}", "tool"
350350
if (call_id := obj.get("call_id")) is not None:
351351
yield f"{prefix}{MessageAttributes.MESSAGE_TOOL_CALL_ID}", call_id
352-
if (output := obj.get("output")) is not None:
353-
yield f"{prefix}{MessageAttributes.MESSAGE_CONTENT}", output
352+
if "output" in obj:
353+
output = obj["output"]
354+
# Serialize complex output types to JSON string, but preserve None
355+
if output is None or isinstance(output, str):
356+
output_value = output
357+
else:
358+
output_value = safe_json_dumps(output)
359+
yield f"{prefix}{MessageAttributes.MESSAGE_CONTENT}", output_value
354360

355361

356362
def _get_attributes_from_function_call_output(
@@ -359,7 +365,13 @@ def _get_attributes_from_function_call_output(
359365
) -> Iterator[tuple[str, AttributeValue]]:
360366
yield f"{prefix}{MESSAGE_ROLE}", "tool"
361367
yield f"{prefix}{MESSAGE_TOOL_CALL_ID}", obj["call_id"]
362-
yield f"{prefix}{MESSAGE_CONTENT}", obj["output"]
368+
# Serialize complex output types to JSON string, but preserve None
369+
output = obj["output"]
370+
if output is None or isinstance(output, str):
371+
output_value = output
372+
else:
373+
output_value = safe_json_dumps(output)
374+
yield f"{prefix}{MESSAGE_CONTENT}", output_value
363375

364376

365377
def _get_attributes_from_generation_span_data(

python/instrumentation/openinference-instrumentation-openai-agents/tests/test_span_attribute_helpers.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
ResponseUsage,
3939
Tool,
4040
)
41+
from openai.types.responses.response_custom_tool_call_output_param import (
42+
ResponseCustomToolCallOutputParam,
43+
)
4144
from openai.types.responses.response_function_web_search_param import ActionSearch
4245
from openai.types.responses.response_input_item_param import (
4346
ComputerCallOutput,
@@ -265,6 +268,53 @@
265268
},
266269
id="item_reference",
267270
),
271+
pytest.param(
272+
[
273+
ResponseCustomToolCallOutputParam(
274+
type="custom_tool_call_output",
275+
call_id="custom-123",
276+
output="simple result",
277+
)
278+
],
279+
{
280+
"llm.input_messages.1.message.content": "simple result",
281+
"llm.input_messages.1.message.role": "tool",
282+
"llm.input_messages.1.message.tool_call_id": "custom-123",
283+
},
284+
id="custom_tool_call_output_string",
285+
),
286+
pytest.param(
287+
[
288+
ResponseCustomToolCallOutputParam(
289+
type="custom_tool_call_output",
290+
call_id="custom-123",
291+
output=[{"type": "text", "text": "complex result"}], # type: ignore[typeddict-item]
292+
)
293+
],
294+
{
295+
"llm.input_messages.1.message.content": (
296+
'[{"type": "text", "text": "complex result"}]'
297+
),
298+
"llm.input_messages.1.message.role": "tool",
299+
"llm.input_messages.1.message.tool_call_id": "custom-123",
300+
},
301+
id="custom_tool_call_output_list",
302+
),
303+
pytest.param(
304+
[
305+
ResponseCustomToolCallOutputParam(
306+
type="custom_tool_call_output",
307+
call_id="custom-123",
308+
output={"status": "success", "data": 42}, # type: ignore[dict-item]
309+
)
310+
],
311+
{
312+
"llm.input_messages.1.message.content": '{"status": "success", "data": 42}',
313+
"llm.input_messages.1.message.role": "tool",
314+
"llm.input_messages.1.message.tool_call_id": "custom-123",
315+
},
316+
id="custom_tool_call_output_dict",
317+
),
268318
],
269319
)
270320
def test_get_attributes_from_input(
@@ -427,6 +477,30 @@ def test_get_attributes_from_response_function_tool_call_param(
427477
},
428478
id="none_output",
429479
),
480+
pytest.param(
481+
{
482+
"call_id": "123",
483+
"output": [{"type": "text", "text": "result"}],
484+
},
485+
{
486+
"message.content": '[{"type": "text", "text": "result"}]',
487+
"message.role": "tool",
488+
"message.tool_call_id": "123",
489+
},
490+
id="list_output",
491+
),
492+
pytest.param(
493+
{
494+
"call_id": "123",
495+
"output": {"result": "success", "value": 42},
496+
},
497+
{
498+
"message.content": '{"result": "success", "value": 42}',
499+
"message.role": "tool",
500+
"message.tool_call_id": "123",
501+
},
502+
id="dict_output",
503+
),
430504
],
431505
)
432506
def test_get_attributes_from_function_call_output(

0 commit comments

Comments
 (0)