How to Apply Field Constraints to Tool Parameters Using `typing.Annotated` in Needle
Use typing.Annotated combined with needle.agent.tools.Field to attach validation constraints directly to function parameters, which Needle automatically converts into JSON Schema for LLM function calling.
Needle’s tool-definition system transforms Python callables into JSON-schema specifications compatible with language-model function-calling APIs. By leveraging typing.Annotated, developers declaratively specify validation rules—such as minimum/maximum values, string lengths, and enumerations—directly on function signatures, keeping code clean while providing precise constraints to the model.
Understanding the Core Components
The constraint system is implemented in needle/agent/tools.py, which provides the machinery to extract metadata from type hints and inject it into generated schemas.
The Field Class
The Field class acts as a container for constraint metadata. According to the source code in needle/agent/tools.py (lines 18-33), it accepts parameters like ge (greater than or equal), le (less than or equal), min_length, max_length, and enum.
The class exposes an apply() method that takes a JSON-schema dictionary and enriches it with the stored constraints. For example, when a Field contains ge=0 and le=5, the apply() method adds "minimum": 0 and "maximum": 5 to the schema properties.
The _field_of() Function
Located at lines 89-100 in needle/agent/tools.py, _field_of() traverses type annotations to extract metadata. When it encounters a typing.Annotated wrapper, it inspects the metadata arguments for instances of Field. If found, it returns the Field object; otherwise, it returns None.
This function enables Needle to distinguish between plain type hints and those carrying additional validation constraints.
How Schema Generation Works
The build_schema() function (lines 20-51 in needle/agent/tools.py) orchestrates the conversion of Python function signatures into OpenAI-compatible function descriptions. For each parameter, it executes the following steps:
- Resolves the type hint, including unpacking any
typing.Annotatedwrappers. - Calls
_json_type()to determine the base JSON Schema type (string, integer, object, etc.). - Calls
_field_of()to retrieve anyFieldinstances attached viaAnnotated. - Applies constraints by calling
field.apply(schema)if aFieldis present, injecting validation rules into the schema dictionary. - Determines requirement status based on whether the parameter has a default value.
The final output is a fully-formed JSON Schema object within the parameters field, enabling the LLM to enforce constraints before invoking the tool.
Practical Implementation Examples
Basic String and Integer Constraints
Use Annotated with Field to constrain string length and numeric ranges:
from typing import Annotated
from needle.agent.tools import Field, tool
@tool
def greet(
name: Annotated[str, Field(min_length=3, max_length=30)],
excitement: Annotated[int, Field(ge=0, le=5)] = 1
) -> str:
"""Generate a greeting."""
return f"{'!' * excitement} Hello, {name}"
This generates the following schema (simplified):
{
"name": "greet",
"description": "Generate a greeting.",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 3,
"maxLength": 30
},
"excitement": {
"type": "integer",
"minimum": 0,
"maximum": 5,
"default": 1
}
},
"required": ["name"]
}
}
Enum Constraints with Literal Types
Restrict parameters to specific allowed values using Literal combined with Field:
from typing import Literal, Annotated
from needle.agent.tools import Field, tool
@tool
def set_mode(
mode: Annotated[Literal["auto", "manual"], Field(enum=["auto", "manual"])]
) -> None:
"""Switch the device mode."""
pass
The generated schema includes "enum": ["auto", "manual"] for the mode parameter, restricting the LLM to only these valid options.
Optional Parameters with Default Values
Combine Annotated with default values to create optional constrained parameters:
from needle.agent.tools import tool
from typing import Annotated
@tool
def multiply(
a: int,
b: Annotated[int, Field(ge=0, le=10)] = 1,
) -> int:
"""Return the product of a and b."""
return a * b
Here, b is optional (defaults to 1) but must be between 0 and 10 when provided. Needle marks a as required in the JSON Schema while omitting b from the required array, yet preserves the numeric constraints.
Summary
needle/agent/tools.pycontains the core implementation of the constraint system, specifically theFieldclass,_field_of()function, andbuild_schema()logic.typing.Annotatedserves as the bridge between Python type hints and Needle's constraint metadata, allowing arbitraryFieldattachments without changing the runtime type.Field.apply()injects constraints into JSON Schema dictionaries, converting Python validation rules into LLM-compatible schema properties.- Parameters with defaults become optional in the schema, while unconstrained parameters use the base type mapping without additional metadata.
Frequently Asked Questions
What is typing.Annotated in Python?
typing.Annotated is a standard Python type hint wrapper introduced in PEP 593 that allows attaching arbitrary metadata to type hints without affecting the actual type. In Needle, it serves as the mechanism to attach Field constraint objects to function parameters while preserving static type checking compatibility.
How does Needle convert Field constraints to JSON Schema?
When build_schema() processes a parameter, it first extracts the base JSON type via _json_type(), then retrieves any Field instance via _field_of(). If present, field.apply(schema) is called, which mutates the schema dictionary to include standard JSON Schema keys like minimum, maximum, minLength, maxLength, or enum based on the Field configuration.
Can I use multiple constraints on a single parameter?
Yes. The Field class accepts multiple constraint arguments simultaneously. For example, Field(ge=0, le=100, description="Percentage") applies both numeric bounds and a description. All constraints stored in the Field instance are applied to the generated schema when build_schema() processes the Annotated type hint.
Where does Needle handle the extraction of Annotated metadata?
The extraction logic resides in the _field_of() function within needle/agent/tools.py (lines 89-100). This function specifically checks for typing.Annotated types, iterates through the metadata arguments, and returns the first Field instance it encounters, enabling the schema builder to access constraint data.
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 →