mirror of
https://github.com/langchain-ai/langchain.git
synced 2026-10-05 09:25:14 +03:00
fix(core): exclude args_schema from tool dumps
Of the accepted schema forms -- a `pydantic.BaseModel` subclass, a `pydantic.v1.BaseModel` subclass, or a JSON schema dict -- only the dict has a JSON form, so dumping the field is what raises `PydanticSerializationError`. Mark it `exclude=True`, matching how `BaseTool.callbacks` and the chat model `cache`/`rate_limiter` fields already handle values that only mean something in this process. `exclude` applies to every dump mode at once; pydantic has no per-mode variant, and `exclude_if` (2.12+) is both above this package's floor and blind to the mode. So a dict `args_schema`, which serialized fine before, is dropped too, and `model_dump()` no longer round-trips into `StructuredTool(**dumped)`. A subclass that wants the field in its dumps can re-declare it.
This commit is contained in:
1 parent
1d993ca683
commit
69b4da868f
2 files changed
+21
-24
No files matched your search
@@ -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."""
|
||||
|
||||
|
||||
@@ -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("<class ")
|
||||
assert "args_schema" not in dumped
|
||||
assert "write_file" in dumped["func"]
|
||||
json.loads(write_file.model_dump_json())
|
||||
|
||||
# Python-mode dumps still hand out the live objects.
|
||||
# Python-mode dumps still hand out the live callable, and excluding
|
||||
# `args_schema` from dumps must not affect the tool itself.
|
||||
native = write_file.model_dump()
|
||||
assert callable(native["func"])
|
||||
assert isinstance(native["args_schema"], type)
|
||||
assert "args_schema" not in native
|
||||
assert set(write_file.args) == {"file_path", "content"}
|
||||
|
||||
|
||||
def test_structured_tool_json_dump_respects_options() -> 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"
|
||||
|
||||
Reference in new issue
Block a user