How to Define Constraints for Tool Parameters Using needle.Field and Annotated
Use needle.Field either as a default argument value or inside typing.Annotated to attach JSON Schema constraints directly to function parameters; the @tool decorator automatically extracts these constraints in needle/agent/tools.py and injects them into the generated OpenAI-compatible tool schema.
The cactus-compute/needle library provides a lightweight framework for building LLM-compatible tools in Python. Defining constraints for tool parameters using needle.Field and Annotated ensures that your functions receive validated data that matches specific formats, ranges, or patterns before execution. This approach leverages standard JSON Schema vocabulary while maintaining clean, readable Python type hints.
Understanding needle.Field and JSON Schema Constraints
needle.Field is a lightweight descriptor class that attaches validation metadata to function parameters. When you decorate a function with @tool, the build_schema function in needle/agent/tools.py inspects the signature, extracts Field instances via the _field_of helper, and translates arguments like ge, le, min_length, and pattern into corresponding JSON Schema keywords.
The Field.apply method (lines 18‑31 in needle/agent/tools.py) handles the actual mapping. For example, ge=10 becomes minimum: 10 in the schema, while pattern=r"^\+?[0-9]+" becomes pattern: "^\\+?[0-9]+". This generated schema is stored in the function's ._needle_tool attribute, making the constraints available to LLM runtimes.
Declaring Constraints with Field Defaults
You can attach constraints by using needle.Field as a default value for a parameter. This pattern works best when the parameter is required but needs validation rules.
Basic Syntax Using Default Values
from needle import tool, Field
@tool
def set_thermostat(
temperature: int = Field(description="Target temperature in °C", ge=10, le=30)
) -> None:
"""Set the thermostat to a specific temperature."""
...
In this example, build_schema extracts the Field metadata and produces a JSON Schema fragment specifying that temperature must be an integer between 10 and 30. The resulting schema is accessible via set_thermostat._needle_tool:
{
"name": "set_thermostat",
"description": "Set the thermostat to a specific temperature.",
"parameters": {
"type": "object",
"properties": {
"temperature": {
"type": "integer",
"description": "Target temperature in °C",
"minimum": 10,
"maximum": 30
}
},
"required": ["temperature"]
}
}
Using Annotated for Cleaner Type Hints
The typing.Annotated pattern separates the type hint from the default value, which is essential when you need constraints on a parameter that has an actual default value or when you prefer explicit metadata attachment.
The Annotated Pattern
from typing import Annotated
from needle import tool, Field
@tool
def create_reminder(
title: Annotated[str, Field(min_length=1, max_length=120)],
when: str,
) -> None:
"""Create a reminder with a validated title."""
...
When build_schema processes the create_reminder signature (lines 23‑38 in needle/agent/tools.py), it detects the Field instance inside the Annotated wrapper and applies minLength and maxLength constraints to the title property. This method avoids conflating default values with validation logic.
Combining Multiple Validation Rules
Complex validation scenarios require stacking multiple constraints on a single parameter. You can mix numeric bounds, string length limits, and pattern matching within a single Field definition.
from typing import Annotated
from needle import tool, Field
@tool
def log_water_intake(
amount_ml: Annotated[int, Field(ge=1, le=5000)],
note: Annotated[str, Field(max_length=200)] = ""
) -> None:
"""Log a water-intake event."""
...
Here, amount_ml enforces a valid range for milliliters (1‑5000), while note limits the optional comment length. The @tool decorator processes both parameters through the same _field_of extraction logic, merging all constraints into the final schema.
Pattern and Format Validation
For string parameters requiring specific formats—such as email addresses or phone numbers—use the format and pattern arguments to inject regex-based validation directly into the JSON Schema.
from typing import Annotated
from needle import tool, Field
@tool
def register_user(
email: Annotated[str, Field(format="email")],
phone: Annotated[str, Field(pattern=r"^\+?[0-9][0-9 -]{5,17}$")] = None,
) -> None:
"""Register a new user with validated contact info."""
...
The format="email" constraint hints to the LLM that the input should match the email format, while the pattern argument provides a strict regex for international phone numbers. These constraints are applied in needle/agent/tools.py during the schema assembly phase.
How Schema Generation Works Under the Hood
The constraint injection happens in three stages inside needle/agent/tools.py:
- Signature Inspection: The
build_schemafunction (lines 15‑46) introspects the decorated function using Python'sinspectmodule. - Metadata Extraction: The
_field_ofhelper extractsFieldinstances whether they appear as default values or insideAnnotatedwrappers. - Schema Assembly: The
Field.applymethod (lines 18‑31) translates Python arguments into JSON Schema keywords and merges them into the parameter definition.
After processing, the complete schema dictionary is bound to the function as func_def._needle_tool, allowing the runtime to present constrained parameters to the LLM.
Summary
needle.Fieldacts as a bridge between Python type hints and JSON Schema validation keywords.- Use
Field(...)as a default value for required parameters, or wrap it inAnnotated[<type>, Field(...)]to avoid interfering with actual default values. - Supported constraints include
ge/le(numeric ranges),min_length/max_length(string bounds),pattern(regex),enum(allowed values), andformat(semantic types). - The
@tooldecorator inneedle/agent/tools.pyautomatically extracts these fields viabuild_schemaand stores the final schema in._needle_tool. - Refer to
tests/test_tools.pyfor additional usage examples and edge cases.
Frequently Asked Questions
What is the difference between using Field as a default and using Annotated?
Using Field as a default value (param: int = Field(...)) attaches constraints but requires the parameter to have that specific default, which can be problematic if you need a different default value or want the parameter to be required. Using Annotated[int, Field(...)] separates the type and metadata from the default value, allowing you to specify constraints while keeping the parameter required or assigning it a separate default like = None.
Which JSON Schema keywords does needle.Field support?
According to the apply method in needle/agent/tools.py (lines 18‑31), needle.Field supports ge/le (mapped to minimum/maximum), min_length/max_length (mapped to minLength/maxLength), pattern (mapped to pattern), enum (mapped to enum), and format (mapped to format). Additional Pydantic-compatible arguments are passed through to the schema generator.
How do I make a parameter optional when using Annotated?
To make a parameter optional when using Annotated, simply assign a default value after the annotation: param: Annotated[str, Field(...)] = "" or = None. The presence of the default value in the function signature marks it as optional in the generated JSON Schema, while the Field constraints inside Annotated still apply to any provided value.
Where is the generated schema stored after using the @tool decorator?
The @tool decorator stores the complete JSON Schema in the ._needle_tool attribute of the decorated function. You can inspect this dictionary at runtime to see the exact constraints and types that will be sent to the LLM, as demonstrated in the set_thermostat._needle_tool example.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →