Skip to content

Commit 7d97d96

Browse files
codefromthecryptaxiomofjoy
authored andcommitted
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 22b5eb5 commit 7d97d96

File tree

3 files changed

+93
-4
lines changed

3 files changed

+93
-4
lines changed

python/instrumentation/openinference-instrumentation-openai-agents/pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ module = [
8484
"agents.*",
8585
]
8686

87+
# openai library types differ between v1.99.9 (py39) and v2.7.2 (py313-latest),
88+
# causing type ignores to be unused in one version but required in the other.
89+
[[tool.mypy.overrides]]
90+
module = "tests.test_span_attribute_helpers"
91+
warn_unused_ignores = false
92+
8793
[tool.ruff]
8894
line-length = 100
8995
target-version = "py38"

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
@@ -361,8 +361,14 @@ def _get_attributes_from_response_custom_tool_call_output_param(
361361
yield f"{prefix}{MessageAttributes.MESSAGE_ROLE}", "tool"
362362
if (call_id := obj.get("call_id")) is not None:
363363
yield f"{prefix}{MessageAttributes.MESSAGE_TOOL_CALL_ID}", call_id
364-
if (output := obj.get("output")) is not None:
365-
yield f"{prefix}{MessageAttributes.MESSAGE_CONTENT}", output
364+
if "output" in obj:
365+
output = obj["output"]
366+
if output is not None:
367+
if isinstance(output, str):
368+
output_value = output
369+
else:
370+
output_value = safe_json_dumps(output)
371+
yield f"{prefix}{MessageAttributes.MESSAGE_CONTENT}", output_value
366372

367373

368374
def _get_attributes_from_function_call_output(
@@ -371,7 +377,13 @@ def _get_attributes_from_function_call_output(
371377
) -> Iterator[tuple[str, AttributeValue]]:
372378
yield f"{prefix}{MESSAGE_ROLE}", "tool"
373379
yield f"{prefix}{MESSAGE_TOOL_CALL_ID}", obj["call_id"]
374-
yield f"{prefix}{MESSAGE_CONTENT}", obj["output"]
380+
output = obj["output"]
381+
if output is not None:
382+
if isinstance(output, str):
383+
output_value = output
384+
else:
385+
output_value = safe_json_dumps(output)
386+
yield f"{prefix}{MESSAGE_CONTENT}", output_value
375387

376388

377389
def _get_attributes_from_generation_span_data(

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

Lines changed: 72 additions & 1 deletion
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,51 @@
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=["item1", "item2"], # type: ignore[typeddict-item,list-item]
292+
)
293+
],
294+
{
295+
"llm.input_messages.1.message.content": '["item1", "item2"]',
296+
"llm.input_messages.1.message.role": "tool",
297+
"llm.input_messages.1.message.tool_call_id": "custom-123",
298+
},
299+
id="custom_tool_call_output_list",
300+
),
301+
pytest.param(
302+
[
303+
ResponseCustomToolCallOutputParam(
304+
type="custom_tool_call_output",
305+
call_id="custom-123",
306+
output={"status": "success", "data": 42}, # type: ignore
307+
)
308+
],
309+
{
310+
"llm.input_messages.1.message.content": '{"status": "success", "data": 42}',
311+
"llm.input_messages.1.message.role": "tool",
312+
"llm.input_messages.1.message.tool_call_id": "custom-123",
313+
},
314+
id="custom_tool_call_output_dict",
315+
),
268316
],
269317
)
270318
def test_get_attributes_from_input(
@@ -421,12 +469,35 @@ def test_get_attributes_from_response_function_tool_call_param(
421469
"output": None,
422470
},
423471
{
424-
"message.content": None,
425472
"message.role": "tool",
426473
"message.tool_call_id": "123",
427474
},
428475
id="none_output",
429476
),
477+
pytest.param(
478+
{
479+
"call_id": "123",
480+
"output": [{"type": "text", "text": "result"}],
481+
},
482+
{
483+
"message.content": '[{"type": "text", "text": "result"}]',
484+
"message.role": "tool",
485+
"message.tool_call_id": "123",
486+
},
487+
id="list_output",
488+
),
489+
pytest.param(
490+
{
491+
"call_id": "123",
492+
"output": {"result": "success", "value": 42},
493+
},
494+
{
495+
"message.content": '{"result": "success", "value": 42}',
496+
"message.role": "tool",
497+
"message.tool_call_id": "123",
498+
},
499+
id="dict_output",
500+
),
430501
],
431502
)
432503
def test_get_attributes_from_function_call_output(

0 commit comments

Comments
 (0)