mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-07 10:12:38 +00:00
Compare commits
3 Commits
1.14.5a2
...
devin/1776
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3a3f658fe | ||
|
|
73e3fe7c86 | ||
|
|
c7aaf23a6f |
@@ -120,7 +120,12 @@ def resolve_refs(schema: dict[str, Any]) -> dict[str, Any]:
|
||||
if def_name not in defs:
|
||||
raise KeyError(f"Definition '{def_name}' not found in $defs.")
|
||||
if def_name in expanding:
|
||||
return {}
|
||||
# Preserve the type information instead of degrading to {}
|
||||
def_schema = defs[def_name]
|
||||
stub: dict[str, Any] = {"type": def_schema.get("type", "object")}
|
||||
if "description" in def_schema:
|
||||
stub["description"] = def_schema["description"]
|
||||
return stub
|
||||
expanding.add(def_name)
|
||||
try:
|
||||
return _resolve(deepcopy(defs[def_name]))
|
||||
@@ -759,6 +764,32 @@ def build_rich_field_description(prop_schema: dict[str, Any]) -> str:
|
||||
return ". ".join(parts) if parts else ""
|
||||
|
||||
|
||||
def _inline_top_level_ref(schema: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Resolve a top-level ``$ref`` while preserving ``$defs`` for nested resolution.
|
||||
|
||||
When ``jsonref.replace_refs`` fails on circular schemas, this helper
|
||||
manually inlines the top-level ``$ref`` so that
|
||||
:func:`_build_model_from_schema` receives a concrete object schema.
|
||||
Inner ``$ref`` pointers are left intact — they are resolved lazily by
|
||||
:func:`_resolve_ref` during model construction, and cycles are caught
|
||||
by the ``in_progress`` map.
|
||||
|
||||
The returned dict deliberately shares sub-objects with the original
|
||||
``$defs`` entries so that ``id()``-based cycle detection works.
|
||||
"""
|
||||
schema = deepcopy(schema)
|
||||
ref = schema.get("$ref")
|
||||
if isinstance(ref, str) and ref.startswith("#/$defs/"):
|
||||
def_name = ref[len("#/$defs/") :]
|
||||
defs = schema.get("$defs", {})
|
||||
if def_name in defs:
|
||||
resolved: dict[str, Any] = defs[def_name]
|
||||
# Attach $defs so effective_root can resolve inner $refs.
|
||||
resolved.setdefault("$defs", defs)
|
||||
return resolved
|
||||
return schema
|
||||
|
||||
|
||||
def create_model_from_schema( # type: ignore[no-any-unimported]
|
||||
json_schema: dict[str, Any],
|
||||
*,
|
||||
@@ -813,7 +844,13 @@ def create_model_from_schema( # type: ignore[no-any-unimported]
|
||||
>>> person.name
|
||||
'John'
|
||||
"""
|
||||
json_schema = dict(jsonref.replace_refs(json_schema, proxies=False))
|
||||
try:
|
||||
json_schema = dict(jsonref.replace_refs(json_schema, proxies=False))
|
||||
except jsonref.JsonRefError:
|
||||
# Circular $ref schemas cannot be fully inlined by jsonref.
|
||||
# Manually resolve the top-level $ref (if present) and let
|
||||
# _build_model_from_schema handle cycles via its in_progress map.
|
||||
json_schema = _inline_top_level_ref(json_schema)
|
||||
|
||||
effective_root = root_schema or json_schema
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ from crewai.utilities.pydantic_schema_utils import (
|
||||
ensure_type_in_schemas,
|
||||
force_additional_properties_false,
|
||||
resolve_refs,
|
||||
sanitize_tool_params_for_anthropic_strict,
|
||||
sanitize_tool_params_for_openai_strict,
|
||||
strip_null_from_types,
|
||||
strip_unsupported_formats,
|
||||
)
|
||||
@@ -882,3 +884,145 @@ class TestEndToEndMCPSchema:
|
||||
)
|
||||
assert obj.filters.date_from == datetime.date(2025, 1, 1)
|
||||
assert obj.filters.categories == ["news", "tech"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Recursive / circular $ref schemas (issue #5490)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
RECURSIVE_NODE_SCHEMA: dict[str, Any] = {
|
||||
"$defs": {
|
||||
"Node": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/$defs/Node"},
|
||||
},
|
||||
},
|
||||
"required": ["name"],
|
||||
}
|
||||
},
|
||||
"$ref": "#/$defs/Node",
|
||||
}
|
||||
|
||||
|
||||
class TestResolveRefsRecursive:
|
||||
"""resolve_refs must not silently degrade recursive $refs to {}."""
|
||||
|
||||
def test_circular_ref_preserves_type(self) -> None:
|
||||
result = resolve_refs(deepcopy(RECURSIVE_NODE_SCHEMA))
|
||||
items = result["properties"]["children"]["items"]
|
||||
# Before the fix this was {} — now it should carry type info.
|
||||
assert items != {}, "Circular $ref must not degrade to {}"
|
||||
assert items.get("type") == "object"
|
||||
|
||||
def test_non_recursive_schema_still_resolves(self) -> None:
|
||||
schema: dict[str, Any] = {
|
||||
"$defs": {
|
||||
"Address": {
|
||||
"type": "object",
|
||||
"properties": {"city": {"type": "string"}},
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"home": {"$ref": "#/$defs/Address"},
|
||||
},
|
||||
}
|
||||
result = resolve_refs(deepcopy(schema))
|
||||
assert result["properties"]["home"]["type"] == "object"
|
||||
assert "city" in result["properties"]["home"]["properties"]
|
||||
|
||||
|
||||
class TestSanitizeRecursiveSchemas:
|
||||
"""Sanitization pipelines must not silently degrade recursive schemas."""
|
||||
|
||||
def test_anthropic_strict_preserves_recursive_type(self) -> None:
|
||||
san = sanitize_tool_params_for_anthropic_strict(deepcopy(RECURSIVE_NODE_SCHEMA))
|
||||
items = san["properties"]["children"]["items"]
|
||||
assert items != {}, "Circular $ref must not degrade to {}"
|
||||
assert items.get("type") == "object"
|
||||
|
||||
def test_openai_strict_preserves_recursive_type(self) -> None:
|
||||
san = sanitize_tool_params_for_openai_strict(deepcopy(RECURSIVE_NODE_SCHEMA))
|
||||
items = san["properties"]["children"]["items"]
|
||||
assert items != {}, "Circular $ref must not degrade to {}"
|
||||
assert items.get("type") == "object"
|
||||
|
||||
|
||||
class TestCreateModelFromSchemaRecursive:
|
||||
"""create_model_from_schema must handle recursive $ref schemas."""
|
||||
|
||||
def test_model_creation_succeeds(self) -> None:
|
||||
Model = create_model_from_schema(
|
||||
deepcopy(RECURSIVE_NODE_SCHEMA), model_name="NodeModel"
|
||||
)
|
||||
assert Model is not None
|
||||
assert issubclass(Model, BaseModel)
|
||||
assert "name" in Model.model_fields
|
||||
assert "children" in Model.model_fields
|
||||
|
||||
def test_model_accepts_valid_recursive_data(self) -> None:
|
||||
Model = create_model_from_schema(
|
||||
deepcopy(RECURSIVE_NODE_SCHEMA), model_name="NodeModel"
|
||||
)
|
||||
obj = Model(
|
||||
name="root",
|
||||
children=[
|
||||
{"name": "child1", "children": []},
|
||||
{
|
||||
"name": "child2",
|
||||
"children": [{"name": "grandchild", "children": []}],
|
||||
},
|
||||
],
|
||||
)
|
||||
assert obj.name == "root"
|
||||
assert len(obj.children) == 2
|
||||
|
||||
def test_model_rejects_missing_required_field(self) -> None:
|
||||
Model = create_model_from_schema(
|
||||
deepcopy(RECURSIVE_NODE_SCHEMA), model_name="NodeModel"
|
||||
)
|
||||
with pytest.raises(Exception):
|
||||
Model(children=[]) # "name" is required
|
||||
|
||||
def test_model_with_enrich_descriptions(self) -> None:
|
||||
Model = create_model_from_schema(
|
||||
deepcopy(RECURSIVE_NODE_SCHEMA),
|
||||
model_name="NodeModel",
|
||||
enrich_descriptions=True,
|
||||
)
|
||||
assert Model is not None
|
||||
assert "name" in Model.model_fields
|
||||
|
||||
def test_mutual_recursion_schema(self) -> None:
|
||||
"""Two definitions that reference each other."""
|
||||
schema: dict[str, Any] = {
|
||||
"$defs": {
|
||||
"Person": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"pets": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/$defs/Pet"},
|
||||
},
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
"Pet": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"species": {"type": "string"},
|
||||
"owner": {"$ref": "#/$defs/Person"},
|
||||
},
|
||||
"required": ["species"],
|
||||
},
|
||||
},
|
||||
"$ref": "#/$defs/Person",
|
||||
}
|
||||
Model = create_model_from_schema(deepcopy(schema), model_name="PersonModel")
|
||||
assert "name" in Model.model_fields
|
||||
assert "pets" in Model.model_fields
|
||||
|
||||
Reference in New Issue
Block a user