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
11 changes: 9 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ dependencies = [
"requests>=2.32.3",
"rich>=13.9.4",
"jinja2>=3.1.4",
"pillow>=10.0.1", # Security fix for CVE-2023-4863: https://pillow.readthedocs.io/en/stable/releasenotes/10.0.1.html
"python-dotenv"
"pillow>=10.0.1",
# Security fix for CVE-2023-4863: https://pillow.readthedocs.io/en/stable/releasenotes/10.0.1.html
"python-dotenv",
"pydantic>=2.11.3",
]

[project.optional-dependencies]
Expand Down Expand Up @@ -127,6 +129,11 @@ lines-after-imports = 2
[tool.setuptools.package-data]
"smolagents.prompts" = ["*.yaml"]

[tool.uv]
dev-dependencies = [
"pytest>=8.3.4",
]

[project.scripts]
smolagent = "smolagents.cli:main"
webagent = "smolagents.vision_web_browser:main"
120 changes: 120 additions & 0 deletions src/smolagents/_function_type_hints_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,125 @@
get_type_hints,
)

import pydantic


IMPORT_TO_PACKAGE_MAPPING = {
"wikipediaapi": "wikipedia-api",
}


def _is_pydantic_model(type_hint: type) -> bool:
"""
Check if a type hint represents a Pydantic BaseModel.

Args:
type_hint: The type to check

Returns:
bool: True if the type is a Pydantic BaseModel, False otherwise
"""
return inspect.isclass(type_hint) and issubclass(type_hint, pydantic.BaseModel)


def _process_pydantic_schema(schema: dict) -> dict:
"""
Process a Pydantic JSON schema to make it compatible with smolagents.

This function handles:
- Resolving $refs to inline definitions
- Converting enum constraints to proper format
- Ensuring required fields are properly marked
- Converting anyOf patterns with null to nullable: True

Args:
schema: Raw Pydantic JSON schema

Returns:
dict: Processed schema compatible with smolagents
"""
# Make a copy to avoid modifying the original
processed_schema = copy(schema)

# Get definitions if they exist
definitions = schema.get("$defs", {})

def resolve_refs(obj):
"""Recursively resolve $ref references in the schema."""
if isinstance(obj, dict):
if "$ref" in obj:
# Extract the reference path
ref_path = obj["$ref"]
if ref_path.startswith("#/$defs/"):
def_name = ref_path.split("/")[-1]
if def_name in definitions:
# Replace the $ref with the actual definition
resolved = resolve_refs(definitions[def_name])
return resolved
# If we cannot resolve the ref, return the object as is
return obj
else:
# Recursively process all values in the dictionary
return {k: resolve_refs(v) for k, v in obj.items()}
elif isinstance(obj, list):
# Recursively process all items in the list
return [resolve_refs(item) for item in obj]
else:
# Return primitive values as-is
return obj

def convert_anyof_to_nullable(obj):
Copy link
Collaborator Author

@aymeric-roucher aymeric-roucher Aug 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@albertvillanova a big part of the logic in there is to handle our "nullable" parameter with Pydantic models that follow a different json schema spec.

Maybe it would be wise in a follow-up PR to handle json schemas directly rather than converting their logic to our nullable fomat
Handling json shcemas directly could use either of the solutions below:

Solution 1:
Just add a completely new validation logic, with a tool.input_schema parameter that follows the pydantic schema : each tool init could either (exclusive OR) use tool.inputs or tool.input_schema, depending on what was use for initialization.
Then after some time, tool.inputs could be deprecated to only follow the new version.

Solution 2:
Let tool.inputs be either our legacy way of describing inputs, or a json schema. This is detected dynamically, then either logic is used for validation.

"""Convert anyOf patterns with null to nullable: True for smolagents compatibility."""
if isinstance(obj, dict):
# Check if this is an anyOf pattern with null
if "anyOf" in obj and isinstance(obj["anyOf"], list):
# Look for a pattern like [{'type': 'string'}, {'type': 'null'}]
non_null_types = []
has_null = False

for item in obj["anyOf"]:
if isinstance(item, dict) and item.get("type") == "null":
has_null = True
elif isinstance(item, dict) and "type" in item:
non_null_types.append(item)

# If we have exactly one non-null type and a null type, convert to nullable
if has_null and len(non_null_types) == 1:
# Create new schema based on the non-null type
new_obj = non_null_types[0].copy()

# Copy over any other properties from the original object
for key, value in obj.items():
if key not in ["anyOf"] and key not in new_obj:
new_obj[key] = value

# Mark as nullable
new_obj["nullable"] = True

return convert_anyof_to_nullable(new_obj)

# Recursively process all values in the dictionary
return {k: convert_anyof_to_nullable(v) for k, v in obj.items()}
elif isinstance(obj, list):
# Recursively process all items in the list
return [convert_anyof_to_nullable(item) for item in obj]
else:
# Return primitive values as-is
return obj

# Resolve all $refs in the schema
processed_schema = resolve_refs(processed_schema)

# Convert anyOf patterns to nullable
processed_schema = convert_anyof_to_nullable(processed_schema)

# Remove $defs since we have inlined everything
if "$defs" in processed_schema:
del processed_schema["$defs"]

return processed_schema


def get_package_name(import_name: str) -> str:
"""
Return the package name for a given import name.
Expand Down Expand Up @@ -328,6 +441,13 @@ def _parse_type_hint(hint: type) -> dict:
args = get_args(hint)

if origin is None:
# Check if this is a Pydantic model before falling back to regular type parsing
if _is_pydantic_model(hint):
# Get the Pydantic schema and process it
pydantic_schema = hint.model_json_schema()
processed_schema = _process_pydantic_schema(pydantic_schema)
return processed_schema

try:
return _get_json_schema_type(hint)
except KeyError:
Expand Down
1 change: 1 addition & 0 deletions src/smolagents/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ def _setup_managed_agents(self, managed_agents: list | None = None) -> None:
"additional_args": {
"type": "object",
"description": "Dictionary of extra inputs to pass to the managed agent, e.g. images, dataframes, or any other contextual data it may need.",
"nullable": True,
},
}
agent.output_type = "string"
Expand Down
Loading