mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-25 08:45:09 +00:00
fix: preserve strict tool schema property names (#6628)
This commit is contained in:
@@ -9,7 +9,7 @@ authors = [
|
||||
requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"Pillow~=12.3.0",
|
||||
"pypdf~=6.13.3",
|
||||
"pypdf~=6.14.2",
|
||||
"python-magic>=0.4.27",
|
||||
"aiocache~=0.12.3",
|
||||
"aiofiles~=24.1.0",
|
||||
@@ -19,8 +19,8 @@ dependencies = [
|
||||
|
||||
[tool.uv]
|
||||
exclude-newer = "3 days"
|
||||
# pypdf 6.13.3 is a security fix newer than the global supply-chain cutoff.
|
||||
exclude-newer-package = { pypdf = "2026-06-18T00:00:00Z" }
|
||||
# pypdf 6.14.2 is a security fix newer than the global supply-chain cutoff.
|
||||
exclude-newer-package = { pypdf = "2026-06-24T00:00:00Z" }
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
|
||||
@@ -549,7 +549,13 @@ _CLAUDE_STRICT_UNSUPPORTED: Final[tuple[str, ...]] = (
|
||||
def _strip_keys_recursive(
|
||||
d: Any, keys: tuple[str, ...], _seen: set[int] | None = None
|
||||
) -> Any:
|
||||
"""Recursively delete a fixed set of keys from a schema."""
|
||||
"""Recursively delete schema metadata keys without deleting property names.
|
||||
|
||||
JSON Schema stores fields under a ``properties`` map. A user/tool argument can
|
||||
legitimately be named ``title``, ``default``, or another metadata key. Those
|
||||
entries must stay in the map; only metadata inside the schema nodes should be
|
||||
stripped.
|
||||
"""
|
||||
if _seen is None:
|
||||
_seen = set()
|
||||
if isinstance(d, dict):
|
||||
@@ -558,8 +564,12 @@ def _strip_keys_recursive(
|
||||
_seen.add(id(d))
|
||||
for key in keys:
|
||||
d.pop(key, None)
|
||||
for v in d.values():
|
||||
_strip_keys_recursive(v, keys, _seen)
|
||||
for key, value in d.items():
|
||||
if key == "properties" and isinstance(value, dict):
|
||||
for property_schema in value.values():
|
||||
_strip_keys_recursive(property_schema, keys, _seen)
|
||||
else:
|
||||
_strip_keys_recursive(value, keys, _seen)
|
||||
elif isinstance(d, list):
|
||||
if id(d) in _seen:
|
||||
return d
|
||||
|
||||
@@ -933,7 +933,49 @@ class TestResolveRefsRecursive:
|
||||
assert resolved["properties"]["x"]["type"] == "integer"
|
||||
|
||||
|
||||
class TestSanitizeRecursiveSchemas:
|
||||
class TestSanitizeStrictSchemas:
|
||||
def test_openai_strict_preserves_property_named_title(self) -> None:
|
||||
from crewai.utilities.pydantic_schema_utils import sanitize_tool_params_for_openai_strict
|
||||
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"title": "Title", "type": "string"},
|
||||
"url": {"title": "Url", "type": "string"},
|
||||
},
|
||||
"required": ["title", "url"],
|
||||
}
|
||||
|
||||
san = sanitize_tool_params_for_openai_strict(deepcopy(schema))
|
||||
|
||||
assert "title" in san["properties"]
|
||||
assert set(san["required"]) == set(san["properties"].keys())
|
||||
assert "title" not in san["properties"]["title"]
|
||||
|
||||
def test_openai_strict_preserves_nested_property_named_title(self) -> None:
|
||||
from crewai.utilities.pydantic_schema_utils import sanitize_tool_params_for_openai_strict
|
||||
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"payload": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"title": "Nested Title", "type": "string"},
|
||||
},
|
||||
"required": ["title"],
|
||||
},
|
||||
},
|
||||
"required": ["payload"],
|
||||
}
|
||||
|
||||
san = sanitize_tool_params_for_openai_strict(deepcopy(schema))
|
||||
payload = san["properties"]["payload"]
|
||||
|
||||
assert "title" in payload["properties"]
|
||||
assert payload["required"] == ["title"]
|
||||
assert "title" not in payload["properties"]["title"]
|
||||
|
||||
def test_anthropic_strict_preserves_recursive_type(self) -> None:
|
||||
from crewai.utilities.pydantic_schema_utils import sanitize_tool_params_for_anthropic_strict
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ info = "Commits must follow Conventional Commits 1.0.0."
|
||||
[tool.uv]
|
||||
exclude-newer = "3 days"
|
||||
# These security fixes are newer than the global supply-chain cutoff.
|
||||
exclude-newer-package = { pypdf = "2026-06-18T00:00:00Z", msgpack = "2026-06-20T00:00:00Z", pydantic-settings = "2026-06-20T00:00:00Z", langsmith = "2026-06-20T00:00:00Z" }
|
||||
exclude-newer-package = { pypdf = "2026-06-24T00:00:00Z", msgpack = "2026-06-20T00:00:00Z", pydantic-settings = "2026-06-20T00:00:00Z", langsmith = "2026-06-20T00:00:00Z" }
|
||||
|
||||
# composio-core pins rich<14 but textual requires rich>=14.
|
||||
# onnxruntime 1.24+ dropped Python 3.10 wheels; cap it so qdrant[fastembed] resolves on 3.10.
|
||||
@@ -183,7 +183,7 @@ exclude-newer-package = { pypdf = "2026-06-18T00:00:00Z", msgpack = "2026-06-20T
|
||||
# transformers 4.57.6 has CVE-2026-1839; force 5.4+ (docling 2.84 allows huggingface-hub>=1).
|
||||
# cryptography 46.0.6 has CVE-2026-39892; force 46.0.7+.
|
||||
# pypdf <6.10.2 has GHSA-4pxv-j86v-mhcw, GHSA-7gw9-cf7v-778f, GHSA-x284-j5p8-9c5p.
|
||||
# pypdf <6.13.3 has GHSA-jm82-fx9c-mx94; force 6.13.3+.
|
||||
# pypdf <6.14.2 has GHSA-jm82-fx9c-mx94 and GHSA-5qjq-93h5-hrgp/GHSA-55h5-xmcq-c37v/GHSA-g867-7843-wf8q/GHSA-5xf7-4p34-54qr; force 6.14.2+.
|
||||
# uv <0.11.15 has GHSA-4gg8-gxpx-9rph (and earlier GHSA-pjjw-68hj-v9mw); force 0.11.15+.
|
||||
# python-multipart <0.0.27 has GHSA-pp6c-gr5w-3c5g (DoS via unbounded multipart headers).
|
||||
# gitpython <3.1.50 has GHSA-mv93-w799-cj2w (config_writer newline injection bypassing the 3.1.49 patch -> RCE via core.hooksPath).
|
||||
@@ -213,7 +213,7 @@ override-dependencies = [
|
||||
"urllib3>=2.7.0",
|
||||
"transformers>=5.4.0; python_version >= '3.10'",
|
||||
"cryptography>=46.0.7",
|
||||
"pypdf>=6.13.3,<7",
|
||||
"pypdf>=6.14.2,<7",
|
||||
"uv>=0.11.15,<1",
|
||||
"python-multipart>=0.0.27,<1",
|
||||
"gitpython>=3.1.51,<4",
|
||||
|
||||
14
uv.lock
generated
14
uv.lock
generated
@@ -13,13 +13,13 @@ resolution-markers = [
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
|
||||
exclude-newer = "2026-07-21T05:40:28.001333135Z"
|
||||
exclude-newer-span = "P3D"
|
||||
|
||||
[options.exclude-newer-package]
|
||||
msgpack = "2026-06-20T00:00:00Z"
|
||||
langsmith = "2026-06-20T00:00:00Z"
|
||||
pypdf = "2026-06-18T00:00:00Z"
|
||||
pypdf = "2026-06-24T00:00:00Z"
|
||||
pydantic-settings = "2026-06-20T00:00:00Z"
|
||||
|
||||
[manifest]
|
||||
@@ -48,7 +48,7 @@ overrides = [
|
||||
{ name = "pip", specifier = ">=26.1.2" },
|
||||
{ name = "pyasn1", specifier = ">=0.6.4" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.14.2" },
|
||||
{ name = "pypdf", specifier = ">=6.13.3,<7" },
|
||||
{ name = "pypdf", specifier = ">=6.14.2,<7" },
|
||||
{ name = "python-multipart", specifier = ">=0.0.27,<1" },
|
||||
{ name = "rich", specifier = ">=13.7.1" },
|
||||
{ name = "setuptools", specifier = ">=83.0.0" },
|
||||
@@ -1595,7 +1595,7 @@ requires-dist = [
|
||||
{ name = "aiofiles", specifier = "~=24.1.0" },
|
||||
{ name = "av", specifier = "~=13.0.0" },
|
||||
{ name = "pillow", specifier = "~=12.3.0" },
|
||||
{ name = "pypdf", specifier = "~=6.13.3" },
|
||||
{ name = "pypdf", specifier = "~=6.14.2" },
|
||||
{ name = "python-magic", specifier = ">=0.4.27" },
|
||||
{ name = "tinytag", specifier = "~=2.2.1" },
|
||||
]
|
||||
@@ -7181,14 +7181,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pypdf"
|
||||
version = "6.13.3"
|
||||
version = "6.14.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/17/18/9947cc201af9ccf76720fd3347bf4f70eb882ce3fcf4cb05f7443e4cf871/pypdf-6.13.3.tar.gz", hash = "sha256:f3cb822769725f1bac658c406cfc9460399043f3750c2d3e4650e0a85eacabd7", size = 6484063, upload-time = "2026-06-17T15:22:00.898Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/03/72/7dfd5ff1c9c37de97a731701f51af091325f123d9d4270361c9c69e4431f/pypdf-6.14.2.tar.gz", hash = "sha256:7873f502fe4385e79539b21d872392dc0c4e3714327c15881cbc7fbfd1f95b25", size = 6491182, upload-time = "2026-06-23T14:18:30.859Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/56/2967e621598987905fb8cdfadd8f8de6b5c68c9351f0523c4df8409f28f1/pypdf-6.13.3-py3-none-any.whl", hash = "sha256:c6e3f86afb625791510b02ad5480e94b63970bb957df75d44657c282ecc52224", size = 347288, upload-time = "2026-06-17T15:21:59.512Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/e6/136aa8993a2ae7214e0b0ef2edaa0d2e08d1d4e4982635b08a835ff31ec8/pypdf-6.14.2-py3-none-any.whl", hash = "sha256:3f07891af76dc002657e04993ab9b4de81de29f9013b9761d0b7968bff12e946", size = 349514, upload-time = "2026-06-23T14:18:28.867Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user