How to Define a Tool Using the Decorator Pattern in Needle

Needle's @tool decorator automatically converts any Python function into an LLM-callable tool by attaching an OpenAI-compatible JSON schema to the function via a private _needle_tool attribute.

The Needle framework provides a clean, decorator-based API for exposing Python functions to AI agents. By applying a single decorator, developers can transform ordinary callables into well-documented tools that language models can discover and invoke. This article walks through the implementation details in needle/agent/tools.py and demonstrates practical usage patterns.

The Core Mechanism: @tool Decorator

The tool decorator defined in needle/agent/tools.py (lines 73-76) is the entry point for tool definition. When applied to a function, it:

  1. Inspects the function signature and type hints
  2. Extracts parameter metadata from docstrings
  3. Generates an OpenAI-style JSON schema
  4. Attaches the schema to the function as _needle_tool
from needle.agent.tools import tool, Field

@tool
def translate(text: str, target_lang: str = "en") -> str:
    """Translate the given text to the target language.

    Args:
        text: The source text to translate.
        target_lang: ISO code of the language to translate into (default "en").
    """
    return f"[{target_lang}] {text}"

After decoration, the function carries its complete schema:

>>> translate._needle_tool
{
    "name": "translate",
    "description": "Translate the given text to the target language.",
    "parameters": {
        "type": "object",
        "properties": {
            "text": {"type": "string", "description": "The source text to translate."},
            "target_lang": {"type": "string", "default": "en", "description": "ISO code of the language to translate into (default \"en\")."}
        },
        "required": ["text"]
    }
}

Schema Generation Internals

The build_schema function (lines 20-51 in needle/agent/tools.py) powers the decorator's automatic schema creation. It uses Python's inspect module to analyze function signatures and produces a complete OpenAI-compatible description.

Key Components

  • inspect.signature — extracts parameter names, types, and defaults
  • _json_type (lines 57-86) — maps Python types to JSON Schema types
  • _field_of — extracts Field metadata including constraints and descriptions

The builder distinguishes between required parameters (no default) and optional parameters (has default), populating the required array accordingly. Type hints are translated through _json_type, which handles str, int, float, bool, list, dict, and Pydantic models.

Adding Parameter Constraints with Field

The Field class allows you to specify validation constraints that flow into the generated schema. Annotate parameters with Field(default=..., ge=..., le=..., enum=...) to enforce bounds or restrict values.

from needle.agent.tools import tool, Field

@tool
def resize(
    width: int = Field(default=256, ge=1, le=4096),
    height: int = Field(default=256, ge=1, le=4096)
):
    """Resize an image to the given dimensions in pixels."""
    return f"Resized to {width}x{height}"

This produces a schema with minimum, maximum, and default values:

{
    "name": "resize",
    "parameters": {
        "properties": {
            "width": {"type": "integer", "default": 256, "minimum": 1, "maximum": 4096},
            "height": {"type": "integer", "default": 256, "minimum": 1, "maximum": 4096}
        },
        "required": []
    }
}

Runtime Tool Invocation

At runtime, Needle agents locate tools by checking for the _needle_tool attribute. The original function remains callable as standard Python while also serving the LLM's structured interface.

from needle.agent import fetch, tools

# Tools decorated with @tool are automatically discoverable

registered_tools = {translate.__name__: translate}

def call_tool(name: str, **kwargs):
    """Execute a tool by name with parsed arguments."""
    fn = registered_tools[name]
    return fn(**kwargs)

# Simulated agent call

result = call_tool("translate", text="Hola", target_lang="en")
print(result)  # → [en] Hola

The separation between schema generation and function execution lets you test tools independently of the agent framework.

File Structure Reference

Understanding where the decorator pattern is implemented helps when extending or debugging tools:

File Purpose Key Functions
needle/agent/tools.py Core implementation tool, build_schema, _json_type, Field
needle/agent/__init__.py Runtime integration Tool registration for agent loop
tests/test_tools.py Validation suite Unit tests for schema generation

Summary

  • Apply @tool from needle/agent.tools to any function to expose it to LLM agents
  • Type hints and docstrings automatically populate the OpenAI-compatible JSON schema
  • Field annotations add validation constraints like ge, le, enum
  • _needle_tool stores the complete schema as a function attribute for runtime discovery
  • Schema generation runs once at import time with no overhead during tool execution

Frequently Asked Questions

What Python types does the @tool decorator support?

The decorator supports all JSON-serializable primitives (str, int, float, bool), collections (list, dict), and Pydantic models. The _json_type helper in needle/agent/tools.py maps these to JSON Schema types automatically. Custom classes require either Pydantic integration or manual schema construction.

Can I use the @tool decorator on class methods?

Yes, though the current implementation treats self as a regular parameter. For instance methods, you'll typically define tools as module-level functions or static methods to avoid including self in the LLM-facing schema. The build_schema function inspects all parameters including self unless excluded.

How does Needle handle default values in the generated schema?

Parameters with default values are marked as optional in the required array, and their defaults appear in the property definitions. This matches the OpenAI function calling specification where missing arguments assume documented defaults.

Where can I customize the generated description?

The function docstring drives the description field. Use Google-style docstrings with Args: sections; build_schema parses these to populate individual parameter descriptions. There's no separate API to override the description—keep docstrings informative and current.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →