2  The @tool Decorator

The first piece we need: a way to turn Python functions into tools the LLM can call.

Note

Code Reference: code/v0.1/src/agentsilex/

  • tool.py
  • function_tool.py
  • extract_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 LLM
  • description: What the tool does
  • function: The actual Python function
  • parameters_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_schema

The process:

  1. Get function name from __name__
  2. Parse docstring with docstring_parser (supports Google/NumPy/ReST styles)
  3. Extract type hints with get_type_hints()
  4. Build Pydantic fields from parameters
  5. Create a dynamic Pydantic model
  6. 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 Schema

These two libraries do the heavy lifting:

  • docstring_parser: Automatically detects Google/NumPy/ReST style
  • pydantic: Creates JSON Schema from Python types
TipCheckpoint
cd code/v0.1

You now have the complete tool system. Next: let’s build the Agent and Runner.