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:
-
Type Inspection: Uses Python's
inspectmodule to retrieve type hints, including metadata fromAnnotatedwrappers. -
Type Mapping: The
_json_typehelper (lines 57–86) translates Python types to JSON Schema types, handling special cases likeLiteral,Enum,list, anddict. -
Constraint Application: If
_field_ofreturns aFieldinstance,build_schemacallsField.applyto inject constraint keys (e.g.,minimum,maximum,pattern) into the parameter's schema definition. -
Required Field Logic: Parameters without default values and not wrapped in
typing.Optionalare marked as required in the schema'srequiredarray (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
needle/agent/tools.py: Core implementation containingField.__init__,_field_of,build_schema, and thetooldecorator.needle/environments/smart_home.py: Real-world usage demonstratingAnnotated[int, needle.Field(...)]for thermostat controls (lines 23–24).needle/environments/productivity.py: Examples of string constraints usingmin_lengthandmax_length(lines 28–31).tests/test_tools.py: Unit tests verifyingAnnotated+Fieldschema generation (lines 64–66).
Summary
needle.Fieldcaptures JSON Schema validation keywords likege,le, andpatterninneedle/agent/tools.py.typing.AnnotatedattachesFieldinstances to type hints without altering the runtime type._field_ofextractsFieldmetadata fromAnnotatedwrappers during schema generation.- The
tooldecorator stores the final JSON Schema infn._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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →