Skip to content
Closed
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
28 changes: 26 additions & 2 deletions src/agents/function_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

# griffelib exposes the `griffe` package at runtime but currently does not ship typing markers.
from griffe import Docstring, DocstringSectionKind # type: ignore[import-untyped]
from pydantic import BaseModel, Field, create_model
from pydantic import BaseModel, ConfigDict, Field, create_model
from pydantic.fields import FieldInfo

from .exceptions import UserError
Expand All @@ -19,6 +19,19 @@
from .tool_context import ToolContext


class _ToolArgsBaseModel(BaseModel):
"""Base for dynamically generated tool-argument models.

``protected_namespaces=()`` disables Pydantic's ``model_`` guard so a tool may declare
parameters whose names collide with ``BaseModel`` members (for example ``model_dump`` or
``model_validate``) without raising at tool-definition time. The runtime call layer reads
argument values from instance ``__dict__`` rather than by attribute access, so a field that
shadows a model member is still resolved correctly.
"""

model_config = ConfigDict(protected_namespaces=())


@dataclass
class FuncSchema:
"""
Expand Down Expand Up @@ -470,7 +483,18 @@ def function_schema(
)

# 3. Dynamically build a Pydantic model
dynamic_model = create_model(f"{func_name}_args", __base__=BaseModel, **fields)
#
# ``model_config`` cannot be a field name: ``create_model`` interprets a ``model_config``
# keyword as the model's configuration, not a field, which otherwise fails with an opaque
# ``TypeError`` deep inside Pydantic. Reject it with an actionable error. Other names that
# collide with ``BaseModel`` members (``model_dump``, ``model_validate``, ...) are handled by
# ``_ToolArgsBaseModel`` disabling the protected-namespace guard.
if "model_config" in fields:
raise UserError(
"Tool parameter 'model_config' is not supported because Pydantic reserves that name "
"for model configuration. Rename the parameter (for example to 'config')."
)
dynamic_model = create_model(f"{func_name}_args", __base__=_ToolArgsBaseModel, **fields)

# 4. Build JSON schema from that model
json_schema = dynamic_model.model_json_schema()
Expand Down
31 changes: 31 additions & 0 deletions tests/test_function_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,37 @@ def test_to_call_args_does_not_shadow_pydantic_model_fields_set():
assert result == "hello:42"


@pytest.mark.parametrize(
"param_name",
["model_dump", "model_dump_json", "model_validate", "model_validate_json", "model_json_schema"],
)
def test_parameter_named_like_a_basemodel_method_is_supported(param_name: str) -> None:
"""A tool parameter that shadows a ``BaseModel`` method must not crash at definition time."""
namespace: dict[str, Any] = {}
exec(
f"def tool({param_name}: int, query: str) -> str:\n"
f" return f'{{{param_name}}}:{{query}}'",
namespace,
)
tool = namespace["tool"]

func_schema = function_schema(tool, use_docstring_info=False, strict_json_schema=False)
parsed = func_schema.params_pydantic_model.model_validate({param_name: 7, "query": "q"})

args, kwargs_dict = func_schema.to_call_args(parsed)
assert tool(*args, **kwargs_dict) == "7:q"


def test_parameter_named_model_config_raises_actionable_error() -> None:
"""``model_config`` is reserved by ``create_model``; surface a clear error, not a TypeError."""

def tool(model_config: str) -> str:
return model_config

with pytest.raises(UserError, match="model_config"):
function_schema(tool, use_docstring_info=False, strict_json_schema=False)


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

Expand Down