2 The @tool Decorator
The first piece we need: a way to turn Python functions into tools the LLM can call.
Code Reference: code/v0.1/src/agentsilex/
tool.pyfunction_tool.pyextract_function_schema.py
2.1 The Problem
LLMs need tools described in JSON Schema format:
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"}
},
"required": ["city"]
}
}
}Writing this by hand is tedious and error-prone. We want to write normal Python and have the schema generated automatically.
2.2 FunctionTool: The Data Structure
First, we define what a tool looks like (function_tool.py):
from dataclasses import dataclass
from typing import Callable, Any, Dict
@dataclass
class FunctionTool:
name: str
description: str
function: Callable
parameters_specification: Dict[str, Any]
def __call__(self, *args: Any, **kwargs: Any) -> Any:
return self.function(*args, **kwargs)Simple dataclass with four fields:
name: Tool name for the LLMdescription: What the tool doesfunction: The actual Python functionparameters_specification: JSON Schema for parameters
The __call__ method makes FunctionTool callable — you can use it like a regular function.
2.3 Schema Extraction: The Core Logic
The extract_function_schema() function extracts metadata from a Python function (extract_function_schema.py):
def extract_function_schema(
func: Any,
name_override: str | None = None,
description_override: str | None = None,
) -> tuple[str, str | None, dict[str, Any]]:
# 1. Basic information
func_name = name_override or func.__name__
sig = inspect.signature(func)
# 2. Parse docstring using docstring_parser
docstring = parse(func.__doc__ or "")
description = description_override or docstring.short_description
# Create parameter description mapping
param_docs = {param.arg_name: param.description for param in docstring.params}
# 3. Get type hints
type_hints = get_type_hints(func)
# 4. Build Pydantic fields
fields = {}
for param_name, param in sig.parameters.items():
if param_name in ('self', 'cls'):
continue
param_type = type_hints.get(param_name, Any)
param_desc = param_docs.get(param_name)
is_optional, base_type = _unwrap_optional(param_type)
if param.default == inspect.Parameter.empty and not is_optional:
# Required parameter
fields[param_name] = (base_type, Field(..., description=param_desc))
else:
# Optional parameter
default_value = param.default if param.default != inspect.Parameter.empty else None
fields[param_name] = (base_type, Field(default=default_value, description=param_desc))
# 5. Create dynamic Pydantic model
dynamic_model = create_model(f"{func_name}_params", **fields)
# 6. Generate JSON schema
json_schema = dynamic_model.model_json_schema()
return func_name, description, json_schemaThe process:
- Get function name from
__name__ - Parse docstring with
docstring_parser(supports Google/NumPy/ReST styles) - Extract type hints with
get_type_hints() - Build Pydantic fields from parameters
- Create a dynamic Pydantic model
- Generate JSON Schema from the model
2.4 The @tool Decorator
Now the decorator itself is trivial (tool.py):
from typing import Callable
from agentsilex.function_tool import FunctionTool
from agentsilex.extract_function_schema import extract_function_schema
def tool(func: Callable) -> FunctionTool:
name, description, params_json_schema = extract_function_schema(func)
return FunctionTool(
name=name,
description=description or "",
function=func,
parameters_specification=params_json_schema,
)Just 10 lines! Extract schema, wrap in FunctionTool, done.
2.5 Usage Example
from agentsilex import tool
@tool
def get_weather(city: str, units: str = "celsius") -> str:
"""
Get current weather for a city.
Args:
city: The city name to get weather for
units: Temperature units (celsius or fahrenheit)
"""
# In real code, call a weather API
return f"Weather in {city}: 22°{units[0].upper()}"
# The decorator returns a FunctionTool
print(get_weather.name) # "get_weather"
print(get_weather.description) # "Get current weather for a city."
print(get_weather.parameters_specification)
# {
# "properties": {
# "city": {"type": "string", "description": "The city name..."},
# "units": {"type": "string", "default": "celsius", ...}
# },
# "required": ["city"],
# "type": "object"
# }
# Still callable
result = get_weather(city="Tokyo") # Works!2.6 Key Design Decisions
| Decision | Why |
|---|---|
| Docstring for description | No extra annotations needed |
| Type hints for schema | Leverages Python’s type system |
| Pydantic for JSON Schema | Battle-tested, handles edge cases |
Decorator returns FunctionTool |
Not a wrapper, the actual tool object |
__call__ on FunctionTool |
Tool is still callable like original function |
2.7 Dependencies
from docstring_parser import parse # Parse any docstring format
from pydantic import BaseModel, Field, create_model # Dynamic model + JSON SchemaThese two libraries do the heavy lifting:
docstring_parser: Automatically detects Google/NumPy/ReST stylepydantic: Creates JSON Schema from Python types
cd code/v0.1You now have the complete tool system. Next: let’s build the Agent and Runner.