diff --git a/libs/core/langchain_core/tools/structured.py b/libs/core/langchain_core/tools/structured.py index bb01a8d7c5..724db1e7e1 100644 --- a/libs/core/langchain_core/tools/structured.py +++ b/libs/core/langchain_core/tools/structured.py @@ -46,25 +46,10 @@ def _serialize_as_str(value: Any) -> str: return str(value) -def _serialize_args_schema(args_schema: ArgsSchema) -> Any: - """Represent a schema class as a string when dumping to JSON. - - A Pydantic model class has no JSON form, so leaving it to the default - serializer raises `PydanticSerializationError`. A dict schema is already - JSON-compatible and is returned unchanged. - """ - if isinstance(args_schema, dict): - return args_schema - return str(args_schema) - - # Attached to the fields via `Annotated` rather than declared with # `@field_serializer`, which would take the field's one serializer slot and make # any subclass that declares its own serializer for the same field fail at class # creation with `PydanticUserError: Multiple field serializer functions ...`. -_JsonSchemaFallback = PlainSerializer( - _serialize_args_schema, when_used="json-unless-none" -) _JsonCallableFallback = PlainSerializer(_serialize_as_str, when_used="json-unless-none") @@ -73,8 +58,13 @@ class StructuredTool(BaseTool): description: str = "" - args_schema: Annotated[ArgsSchema, SkipValidation(), _JsonSchemaFallback] = Field( - ..., description="The tool schema." + # Of the accepted schema forms -- a v1 or v2 model class, or a JSON schema + # dict -- only the dict has a JSON form, so dumping the field raises + # `PydanticSerializationError` unless it is left out. `exclude` applies to + # every mode at once (pydantic has no per-mode variant), so the dict form is + # dropped alongside the classes. + args_schema: Annotated[ArgsSchema, SkipValidation()] = Field( + ..., description="The tool schema.", exclude=True ) """The input arguments' schema.""" diff --git a/libs/core/tests/unit_tests/test_tools.py b/libs/core/tests/unit_tests/test_tools.py index 07869ccb3b..bd38bd0263 100644 --- a/libs/core/tests/unit_tests/test_tools.py +++ b/libs/core/tests/unit_tests/test_tools.py @@ -29,6 +29,7 @@ from pydantic import ( ConfigDict, Field, RootModel, + SkipValidation, ValidationError, field_serializer, ) @@ -4381,14 +4382,16 @@ def test_structured_tool_json_dump() -> None: dumped = write_file.model_dump(mode="json") # Round-trips through the stdlib encoder, i.e. it really is JSON-native. json.dumps(dumped) - assert dumped["args_schema"].startswith(" None: @@ -4410,8 +4413,8 @@ def test_structured_tool_json_dump_respects_options() -> None: assert "coroutine" not in write_file.model_dump(mode="json", exclude_none=True) -def test_structured_tool_json_dump_keeps_dict_args_schema() -> None: - """A dict schema is already JSON-native and must be passed through as-is.""" +def test_structured_tool_json_dump_drops_dict_args_schema() -> None: + """A dict schema is JSON-native, but `exclude` has no per-mode variant.""" schema = {"type": "object", "properties": {"a": {"type": "string"}}} dict_tool = StructuredTool( name="d", @@ -4419,7 +4422,8 @@ def test_structured_tool_json_dump_keeps_dict_args_schema() -> None: func=lambda **kwargs: "x", args_schema=schema, ) - assert dict_tool.model_dump(mode="json")["args_schema"] == schema + assert "args_schema" not in dict_tool.model_dump(mode="json") + assert dict_tool.args_schema == schema def test_structured_tool_subclass_can_override_json_serializers() -> None: @@ -4427,10 +4431,13 @@ def test_structured_tool_subclass_can_override_json_serializers() -> None: A subclass declaring its own `@field_serializer` for the same fields used to fail at class creation with `PydanticUserError: Multiple field serializer - functions were defined`. + functions were defined`. `args_schema` has to be re-declared to undo its + exclusion before a serializer for it can have any effect. """ class MyTool(StructuredTool): + args_schema: Annotated[ArgsSchema, SkipValidation()] = Field(...) + @field_serializer("func", when_used="json-unless-none") def _my_func_repr(self, func: Any) -> str: return "custom-func"