Source code for toolregistry.tool
import inspect
from typing import Any, Callable, Dict, Literal, Optional
from pydantic import BaseModel, Field
from .parameter_models import _generate_parameters_model
from .utils import normalize_tool_name
[docs]
class Tool(BaseModel):
"""Base class representing an executable tool/function.
Provides core functionality for:
- Function wrapping and metadata management
- Parameter validation using Pydantic
- Synchronous/asynchronous execution
- JSON schema generation
"""
name: str = Field(description="Name of the tool")
"""The name of the tool.
Used as the primary identifier when calling the tool.
Must be unique within a tool registry.
"""
description: str = Field(description="Description of what the tool does")
"""Detailed description of the tool's functionality.
Should clearly explain what the tool does, its purpose,
and any important usage considerations.
"""
parameters: Dict[str, Any] = Field(description="JSON schema for tool parameters")
"""Parameter schema defining the tool's expected inputs.
Follows JSON Schema format. Automatically generated from
the wrapped function's type hints when using from_function().
"""
callable: Callable[..., Any] = Field(exclude=True)
"""The underlying function/method that implements the tool's logic.
This is excluded from serialization to prevent accidental exposure
of sensitive implementation details.
"""
is_async: bool = Field(default=False, description="Whether the tool is async")
"""Flag indicating if the tool requires async execution.
Automatically detected from the wrapped function when using
from_function(). Defaults to False for synchronous tools.
"""
parameters_model: Optional[Any] = Field(
default=None, description="Pydantic Model for tool parameters"
)
"""Pydantic model used for parameter validation.
Automatically generated from the wrapped function's type hints
when using from_function(). Can be None for tools without
parameter validation.
"""
[docs]
@classmethod
def from_function(
cls,
func: Callable[..., Any],
name: Optional[str] = None,
description: Optional[str] = None,
namespace: Optional[str] = None,
) -> "Tool":
"""Factory method to create Tool from callable.
Automatically:
- Extracts function metadata
- Generates parameter schema
- Handles async/sync detection
Args:
func (Callable[..., Any]): Function to convert to tool.
name (Optional[str]): Override tool name (defaults to function name).
description (Optional[str]): Override description (defaults to docstring).
Returns:
Tool: Configured Tool instance.
Raises:
ValueError: For unnamed lambda functions.
"""
func_name = name or func.__name__
if func_name == "<lambda>":
raise ValueError("You must provide a name for lambda functions")
func_name = normalize_tool_name(func_name)
func_doc = description or func.__doc__ or ""
is_async = inspect.iscoroutinefunction(func)
parameters_model = None
try:
parameters_model = _generate_parameters_model(func)
except Exception:
parameters_model = None
parameters_schema = (
parameters_model.model_json_schema() if parameters_model else {}
)
tool = cls(
name=func_name,
description=func_doc,
parameters=parameters_schema,
callable=func,
is_async=is_async,
parameters_model=parameters_model if parameters_model is not None else None,
)
if namespace:
tool.update_namespace(namespace)
return tool
[docs]
def get_json_schema(self) -> Dict[str, Any]:
"""Generate JSON Schema representation of tool.
Schema includes:
- Name and description
- Parameter definitions
- Async flag
Returns:
Dict[str, Any]: JSON Schema compliant tool definition.
"""
description_json = {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": self.parameters,
"is_async": self.is_async,
},
}
return description_json
describe = get_json_schema
"""Alias for get_json_schema.
:return: JSON schema representation of the tool
:rtype: Dict[str, Any]
"""
def _validate_parameters(self, parameters: Dict[str, Any]) -> Dict[str, Any]:
"""Validate parameters against tool schema.
Uses Pydantic model if available, otherwise performs basic validation.
Args:
parameters (Dict[str, Any]): Raw input parameters.
Returns:
Dict[str, Any]: Validated and normalized parameters.
"""
if self.parameters_model is None:
validated_params = parameters
else:
model = self.parameters_model(**parameters)
validated_params = model.model_dump_one_level()
return validated_params
[docs]
def run(self, parameters: Dict[str, Any]) -> Any:
"""Execute tool synchronously.
Args:
parameters (Dict[str, Any]): Validated input parameters.
Returns:
Any: Tool execution result.
Raises:
Exception: On execution failure.
"""
try:
validated_params = self._validate_parameters(parameters)
return self.callable(**validated_params)
except Exception as e:
return f"Error executing {self.name}: {str(e)}"
[docs]
async def arun(self, parameters: Dict[str, Any]) -> Any:
"""Execute tool asynchronously.
Args:
parameters (Dict[str, Any]): Validated input parameters.
Returns:
Any: Tool execution result.
Raises:
NotImplementedError: If async execution unsupported.
Exception: On execution failure.
"""
try:
validated_params = self._validate_parameters(parameters)
if inspect.iscoroutinefunction(self.callable):
return await self.callable(**validated_params)
elif hasattr(self.callable, "__call__"):
return await self.callable(**validated_params)
raise NotImplementedError(
"Async execution requires either an async function (coroutine) "
"or a callable whose __call__ method is async or returns an awaitable object."
)
except Exception as e:
return f"Error executing {self.name}: {str(e)}"
[docs]
def update_namespace(
self,
namespace: Optional[str],
force: bool = False,
sep: Literal["-", "."] = "-",
) -> None:
"""Updates the namespace of a tool.
This method checks if the tool's name already contains a namespace (indicated by the presence of a separator character).
OpenAI requires that function names match the pattern ``^[a-zA-Z0-9_-]+$``. Some other providers allow dot (`.`) as separator.
If it does and `force` is `True`, the existing namespace is replaced with the provided `namespace`.
If `force` is `False` and an existing namespace is present, no changes are made.
If the tool's name does not contain a namespace, the `namespace` is prepended as a prefix to the tool's name.
Args:
namespace (str): The new namespace to apply to the tool's name.
force (bool, optional): If `True`, forces the replacement of an existing namespace. Defaults to `False`.
Returns:
None: This method modifies the `tool.name` attribute in place and does not return a value.
Example:
>>> tool = Tool(name="example_tool")
>>> tool.update_namespace("new_namespace")
>>> tool.name
'new_namespace-example_tool'
>>> tool = Tool(name="old_namespace.example_tool")
>>> tool.update_namespace("new_namespace", force=False)
>>> tool.name
'old_namespace-example_tool'
>>> tool = Tool(name="old_namespace.example_tool")
>>> tool.update_namespace("new_namespace", force=True, sep=".")
>>> tool.name
'new_namespace.example_tool'
"""
if not namespace:
return
namespace = normalize_tool_name(namespace)
if sep in self.name:
if force:
# Replace existing namespace with the new one if force is True
self.name = f"{namespace}{sep}{self.name.split(sep, 1)[1]}"
else:
# Do not change the name if force is False and an existing namespace is present
pass
else:
# Add the new namespace as a prefix if there is no existing namespace
self.name = f"{namespace}{sep}{self.name}"