diff --git a/src/agents/agent_output.py b/src/agents/agent_output.py index 069087bcfa..ba9c6fc174 100644 --- a/src/agents/agent_output.py +++ b/src/agents/agent_output.py @@ -180,15 +180,25 @@ def _is_subclass_of_base_model_or_dict(t: Any) -> bool: return issubclass(t, BaseModel | dict) -def _type_to_str(t: type[Any]) -> str: +def _type_to_str(t: Any) -> str: origin = get_origin(t) args = get_args(t) if origin is None: - # It's a simple type like `str`, `int`, etc. - return t.__name__ + # Plain type (str, int, MyModel, ...) or a non-type value supplied as a + # type argument — e.g. the "ok" inside `Literal["ok"]` is a str instance, + # not a class, so `t.__name__` would raise. Fall back to repr() in that + # case so nested forms like `list[Literal["ok"]]` still format cleanly. + if isinstance(t, type): + return t.__name__ + return repr(t) elif args: args_str = ", ".join(_type_to_str(arg) for arg in args) - return f"{origin.__name__}[{args_str}]" + # `typing.Literal`/`typing.Union`/etc. expose `_name` rather than + # `__name__` on some Python versions. + origin_name = ( + getattr(origin, "__name__", None) or getattr(origin, "_name", None) or str(origin) + ) + return f"{origin_name}[{args_str}]" else: return str(t) diff --git a/tests/test_output_tool.py b/tests/test_output_tool.py index 38d0f1d3e8..d8a4447be6 100644 --- a/tests/test_output_tool.py +++ b/tests/test_output_tool.py @@ -1,5 +1,5 @@ import json -from typing import Any +from typing import Any, Literal, cast import pytest from pydantic import BaseModel @@ -94,6 +94,33 @@ def test_structured_output_generic_dict_rejects_wrapper_shape(): output_schema.validate_json(json.dumps({"response": {"foo": 1}})) +def test_structured_output_literal_name_does_not_crash(): + # `AgentOutputSchema.name()` used to raise `AttributeError` on `Literal["ok"]` + # because the Literal value "ok" is a `str` instance rather than a class, and + # the name formatter unconditionally read `__name__`. See issue #3357. + schema = AgentOutputSchema(cast(type[Any], Literal["ok"])) + assert schema.name() == "Literal['ok']" + + # Multiple Literal members format cleanly. + schema_multi = AgentOutputSchema(cast(type[Any], Literal["ok", "done"])) + assert schema_multi.name() == "Literal['ok', 'done']" + + # Literal nested inside a generic still works. + schema_nested = AgentOutputSchema( + cast(type[Any], list[Literal["ok", "done"]]), + strict_json_schema=False, + ) + assert schema_nested.name() == "list[Literal['ok', 'done']]" + + # Non-string Literal values use repr() so they keep their original literal form. + schema_int = AgentOutputSchema(cast(type[Any], Literal[1, 2])) + assert schema_int.name() == "Literal[1, 2]" + + # Plain and other generic types are unchanged by the fix. + assert AgentOutputSchema(str).name() == "str" + assert AgentOutputSchema(list[int]).name() == "list[int]" + + def test_bad_json_raises_error(mocker): agent = Agent(name="test", output_type=Foo) output_schema = get_output_schema(agent)