How to Use `typing.Annotated` with `needle.Field` for Tool Argument Constraints in Python

Use typing.Annotated wrapped with needle.Field to attach JSON Schema constraints directly to function parameters, which needle then converts into structured tool definitions for LLM agents.

The needle library automatically generates JSON Schema descriptions for Python functions exposed as tools to LLM agents. According to the cactus-compute/needle source code, this process centers on needle/agent/tools.py, where the Field class captures validation metadata and the tool decorator processes Annotated type hints to build runtime schemas.

How needle.Field Stores Validation Metadata

The Field class in needle/agent/tools.py acts as a container for JSON Schema validation keywords. Its __init__ method (lines 18–31) accepts standard schema constraints including ge, le, min_length, max_length, and pattern:


# From needle/agent/tools.py

class Field:
    def __init__(self, *, ge=None, le=None, min_length=None, max_length=None, pattern=None, **kwargs):
        self.ge = ge
        self.le = le
        self.min_length = min_length
        self.max_length = max_length
        self.pattern = pattern
        # ... additional schema keywords

When attached to a parameter via typing.Annotated, these values persist until schema generation. The Field.apply method (lines 36–49) later writes these constraints into the final JSON Schema object by mapping each attribute to its corresponding schema key.

Extracting Field Data from Annotated Types

The _field_of function (lines 89–101) handles the extraction logic. When needle encounters a parameter annotated as Annotated[int, needle.Field(ge=0, le=100)], _field_of inspects the __metadata__ tuple of the Annotated type, locates the Field instance, and returns it for processing.

If no Field is present, the function returns None, and the schema generator proceeds with only the base type information.

Building the Complete Tool Schema

The build_schema function orchestrates the conversion from Python function to JSON Schema. It executes four critical steps according to the source:

  1. Type Inspection: Uses Python's inspect module to retrieve type hints, including metadata from Annotated wrappers.

  2. Type Mapping: The _json_type helper (lines 57–86) translates Python types to JSON Schema types, handling special cases like Literal, Enum, list, and dict.

  3. Constraint Application: If _field_of returns a Field instance, build_schema calls Field.apply to inject constraint keys (e.g., minimum, maximum, pattern) into the parameter's schema definition.

  4. Required Field Logic: Parameters without default values and not wrapped in typing.Optional are marked as required in the schema's required array (lines 38–44).

Finally, the tool decorator (lines 73–76) attaches the resulting schema to the function via the _needle_tool attribute, making it discoverable by the Needle runtime.

Practical Examples of Annotated Constraints

Basic Numeric Constraints

Define integer bounds using ge (greater than or equal) and le (less than or equal):

import needle
from typing import Annotated

@needle.tool
def set_volume(level: Annotated[int, needle.Field(ge=0, le=100)]) -> str:
    """Set the speaker volume."""
    return f"Volume set to {level}%"

# Inspect the generated schema

print(set_volume._needle_tool)

Output:

{
  "name": "set_volume",
  "description": "Set the speaker volume.",
  "parameters": {
    "type": "object",
    "properties": {
      "level": {
        "type": "integer",
        "minimum": 0,
        "maximum": 100
      }
    },
    "required": ["level"]
  }
}

The schema includes minimum and maximum keys populated from the needle.Field arguments.

Optional Parameters with Defaults

Parameters with default values are automatically excluded from the required array:

@needle.tool
def log_water_intake(
    amount_ml: Annotated[int, needle.Field(ge=1, le=5000)] = 250
) -> str:
    """Log water intake in millilitres."""
    return f"Logged {amount_ml} ml"

Because amount_ml has a default value of 250, the generated schema omits it from the required list while retaining the minimum (1) and maximum (5000) constraints.

String Constraints

String parameters support min_length, max_length, and pattern as implemented in needle/environments/productivity.py:

@needle.tool
def create_reminder(
    title: Annotated[str, needle.Field(min_length=1, max_length=100)],
    description: Annotated[str, needle.Field(min_length=0)] = ""
) -> str:
    """Create a new reminder."""
    return f"Reminder '{title}' created"

Key Implementation Files

Summary

  • needle.Field captures JSON Schema validation keywords like ge, le, and pattern in needle/agent/tools.py.
  • typing.Annotated attaches Field instances to type hints without altering the runtime type.
  • _field_of extracts Field metadata from Annotated wrappers during schema generation.
  • The tool decorator stores the final JSON Schema in fn._needle_tool, making functions discoverable by the Needle agent runtime.
  • Default values automatically mark parameters as optional in the generated schema.

Frequently Asked Questions

What JSON Schema constraints does needle.Field support?

needle.Field supports numeric constraints (ge, le, gt, lt), string constraints (min_length, max_length, pattern), and array constraints through its constructor in needle/agent/tools.py (lines 18–31). These map directly to JSON Schema keywords minimum, maximum, minLength, maxLength, and pattern.

How does Needle handle optional tool parameters?

Needle treats parameters as optional if they have a default value or are wrapped in typing.Optional. The build_schema function checks for defaults in the function signature (lines 38–44 of needle/agent/tools.py) and excludes those parameter names from the schema's required array.

Where is the generated schema stored after applying the @needle.tool decorator?

The tool decorator attaches the schema dictionary to the function object via the _needle_tool attribute (lines 73–76 of needle/agent/tools.py). Access it directly with function_name._needle_tool to inspect the JSON Schema output.

Can I use needle.Field without typing.Annotated?

No, needle.Field instances must be attached to type hints using typing.Annotated. The _field_of function specifically searches the __metadata__ tuple of Annotated types to locate Field objects (lines 89–101). Without Annotated, the schema builder cannot associate the Field with a specific parameter.

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 →