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
6 changes: 5 additions & 1 deletion src/agents/function_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,18 @@ def to_call_args(self, data: BaseModel) -> tuple[list[Any], dict[str, Any]]:
positional_args: list[Any] = []
keyword_args: dict[str, Any] = {}
seen_var_positional = False
# Read instance storage first so Pydantic properties such as ``model_extra``
# and ``model_fields_set`` do not shadow tool parameters of the same name.
# ``model_dump()`` is unsuitable here because it converts nested models to dicts.
instance_values = object.__getattribute__(data, "__dict__")

# Use enumerate() so we can skip the first parameter if it's context.
for idx, (name, param) in enumerate(self.signature.parameters.items()):
# If the function takes a RunContextWrapper and this is the first parameter, skip it.
if self.takes_context and idx == 0:
continue

value = getattr(data, name, None)
value = instance_values[name] if name in instance_values else getattr(data, name, None)
if param.kind == param.VAR_POSITIONAL:
# e.g. *args: extend positional args and mark that *args is now seen
positional_args.extend(value or [])
Expand Down
38 changes: 38 additions & 0 deletions tests/test_function_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,44 @@ def test_simple_function():
func_schema.params_pydantic_model(**{"a": "not an integer"})


def function_with_model_extra_param(query: str, model_extra: str) -> str:
return f"{query}:{model_extra}"


def function_with_model_fields_set_param(query: str, model_fields_set: int) -> str:
return f"{query}:{model_fields_set}"


def test_to_call_args_does_not_shadow_pydantic_model_extra():
"""A parameter named ``model_extra`` must not be replaced by BaseModel.model_extra."""

with pytest.warns(UserWarning, match="model_extra"):
func_schema = function_schema(function_with_model_extra_param, use_docstring_info=False)
parsed = func_schema.params_pydantic_model.model_validate(
{"query": "hello", "model_extra": "gpt-4.1"}
)

args, kwargs_dict = func_schema.to_call_args(parsed)
result = function_with_model_extra_param(*args, **kwargs_dict)
assert result == "hello:gpt-4.1"


def test_to_call_args_does_not_shadow_pydantic_model_fields_set():
"""A parameter named ``model_fields_set`` must not be replaced by BaseModel.model_fields_set."""

with pytest.warns(UserWarning, match="model_fields_set"):
func_schema = function_schema(
function_with_model_fields_set_param, use_docstring_info=False
)
parsed = func_schema.params_pydantic_model.model_validate(
{"query": "hello", "model_fields_set": 42}
)

args, kwargs_dict = func_schema.to_call_args(parsed)
result = function_with_model_fields_set_param(*args, **kwargs_dict)
assert result == "hello:42"


def varargs_function(x: int, *numbers: float, flag: bool = False, **kwargs: Any):
return x, numbers, flag, kwargs

Expand Down
Loading