Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/agents/function_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,14 @@ def function_schema(
args_of_tuple = get_args(ann)
if len(args_of_tuple) == 2 and args_of_tuple[1] is Ellipsis:
ann = list[ann] # type: ignore
# tuple[()] parameterizes an empty tuple and reports no args, while a bare
# typing.Tuple is unparameterized and carries no element type to reject.
elif hasattr(ann, "__args__"):
raise UserError(
f"Variadic parameter `*{name}` in function {func.__name__} is annotated"
f" with the fixed-length tuple `{ann}`. A variadic annotation describes"
" each positional argument, so use tuple[T, ...] or list[T] instead."
)
else:
ann = list[Any]
else:
Expand Down
47 changes: 47 additions & 0 deletions tests/test_function_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,53 @@ def func(*args: tuple[int, ...]) -> int:
fs.params_pydantic_model.model_validate({"args": [1, 2, 3]})


def _var_positional_fixed_pair(*args: tuple[int, str]) -> int:
return len(args)


def _var_positional_fixed_single(*args: tuple[int]) -> int:
return len(args)


def _var_positional_empty_tuple(*args: tuple[()]) -> int:
return len(args)


@pytest.mark.parametrize(
"func",
[_var_positional_fixed_pair, _var_positional_fixed_single, _var_positional_empty_tuple],
)
def test_var_positional_fixed_length_tuple_annotation_is_rejected(func: Any):
# A fixed-length tuple cannot describe every positional argument, so reject it at
# construction instead of silently widening each argument to Any. tuple[()] reports no
# args yet is still parameterized, so it belongs in this group.
with pytest.raises(UserError, match=r"use tuple\[T, \.\.\.\] or list\[T\] instead"):
function_schema(func, use_docstring_info=False)


def _var_positional_homogeneous_tuple(*args: tuple[int, ...]) -> int:
return len(args)


def _var_positional_list(*args: list[int]) -> int:
return len(args)


def _var_positional_scalar(*args: int) -> int:
return len(args)


@pytest.mark.parametrize(
"func",
[_var_positional_homogeneous_tuple, _var_positional_list, _var_positional_scalar],
)
def test_var_positional_supported_annotations_still_build(func: Any):
# The alternatives named by the rejection message must keep working.
fs = function_schema(func, use_docstring_info=False)

assert fs.params_json_schema["properties"]["args"]["type"] == "array"


def test_var_keyword_dict_annotation():
# Case 3:
# A ``**kwargs: X`` annotation applies to each keyword *value* (PEP 484), so a
Expand Down
Loading