Skip to content
Open
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
26 changes: 23 additions & 3 deletions src/strands/tools/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def my_tool(param1: str, param2: int = 42) -> dict:
import docstring_parser
from pydantic import BaseModel, Field, create_model
from pydantic.fields import FieldInfo
from pydantic_core import PydanticSerializationError
from pydantic_core import PydanticSerializationError, PydanticUndefined
from typing_extensions import override

from ..interrupt import InterruptException
Expand Down Expand Up @@ -163,8 +163,28 @@ def _extract_annotated_metadata(
final_description = description
if final_description is None:
final_description = self.param_descriptions.get(param_name) or f"Parameter {param_name}"
# Create FieldInfo object from scratch
final_field = Field(default=param_default, description=final_description)
# Create FieldInfo object from scratch.
# When the caller uses ``Field(default_factory=list)`` (or similar) as a
# parameter default, ``param_default`` is already a ``FieldInfo`` instance.
# Passing it directly to ``Field(default=...)`` would wrap it in *another*
# FieldInfo, producing a non-serialisable default and triggering a
# ``PydanticJsonSchemaWarning``. Detect this case and forward the
# original default / default_factory instead.
field_kwargs: dict[str, Any] = {"description": final_description}
if isinstance(param_default, FieldInfo):
# Prefer the description already extracted from Annotated / docstring,
# but fall back to the one embedded in the FieldInfo when no other
# source provides one.
if final_description == f"Parameter {param_name}" and param_default.description:
field_kwargs["description"] = param_default.description
if param_default.default_factory is not None:
field_kwargs["default_factory"] = param_default.default_factory
elif param_default.default is not PydanticUndefined:
field_kwargs["default"] = param_default.default
else:
field_kwargs["default"] = param_default

final_field = Field(**field_kwargs)

return actual_type, final_field

Expand Down
42 changes: 42 additions & 0 deletions tests/strands/tools/test_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2101,3 +2101,45 @@ def my_tool(name: str, tag: str | None = None) -> str:
# Since tag is not required, anyOf should be simplified away
assert "anyOf" not in schema["properties"]["tag"]
assert schema["properties"]["tag"]["type"] == "string"


def test_tool_field_default_factory_no_warning():
"""Field(default_factory=...) must not trigger PydanticJsonSchemaWarning.

Regression test for https://github.com/strands-agents/sdk-python/issues/1914.
"""
import warnings

from pydantic import Field

with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")

@strands.tool
def factory_tool(items: list = Field(default_factory=list), tags: list = Field(default_factory=list)): # noqa: B008
"""Tool with default_factory params."""
return items + tags

pydantic_warnings = [w for w in caught if "PydanticJsonSchema" in w.category.__name__]
assert pydantic_warnings == [], f"Unexpected warnings: {pydantic_warnings}"

schema = factory_tool.tool_spec["inputSchema"]["json"]
# items and tags should be optional (not in required)
assert "items" not in schema.get("required", [])
assert "tags" not in schema.get("required", [])
assert schema["properties"]["items"]["type"] == "array"
assert schema["properties"]["tags"]["type"] == "array"


def test_tool_field_default_value():
"""Field(default=...) as a parameter default is handled correctly."""
from pydantic import Field

@strands.tool
def default_val_tool(name: str = Field(default="world", description="A name")):
"""Tool with Field default value."""
return name

schema = default_val_tool.tool_spec["inputSchema"]["json"]
assert "name" not in schema.get("required", [])
assert schema["properties"]["name"]["description"] == "A name"