How to Add Per-Argument Constraints Using needle.Field
Attach JSON Schema constraints to function parameters using needle.Field, either as default values or via typing.Annotated, and the @needle.tool decorator automatically merges them into the generated OpenAI function schema.
The cactus-compute/needle library provides a lightweight mechanism for defining per-argument constraints through the Field class. When you wrap a function with the @tool decorator, the library inspects the signature, extracts type hints, and integrates any Field metadata into the final JSON Schema. This allows you to specify validation rules like numeric bounds, regex patterns, and enum restrictions directly in your Python code.
Understanding the Field Class
The Field class is implemented in needle/agent/tools.py (lines 18-50) and serves as a container for JSON Schema constraint parameters. When instantiated, it stores validation metadata that later gets applied to the schema dictionary during tool registration.
Key methods include:
__init__– Accepts constraint parameters (e.g.,ge,max_length,pattern) and stores them as instance attributes.has_default– ReturnsTruewhen the field was created with an explicit default value, distinguishing it from the internal_MISSINGsentinel.apply– Merges non-Noneconstraint values into the target schema dictionary, converting enums to lists and conditionally addingconstvalues.
Available Constraint Parameters
needle.Field supports the full range of JSON Schema validation keywords. When constructing a Field instance, you can combine any of the following arguments:
description– Human-readable description appearing in the OpenAI schema.enum– List of allowed values for the parameter.const– Fixed constant value.ge/le– Inclusive minimum and maximum values (>=,<=).gt/lt– Exclusive minimum and maximum values (>,<).multiple_of– Value must be a multiple of this number.min_length/max_length– String length boundaries.pattern– Regular expression the string must match.format– JSON Schema format hint (e.g.,"email").min_items/max_items– Array length boundaries.unique_items– WhenTrue, requires array elements to be unique.
Schema Construction Pipeline
The integration of Field metadata into the final schema occurs through a three-stage pipeline defined in needle/agent/tools.py:
-
Detection via
_field_of(line 85) – Inspects each parameter to detectFieldinstances, whether provided directly as default values or wrapped insidetyping.Annotatedhints. -
Base Type Generation – The
build_schemafunction (line 11) constructs the base JSON type for each parameter using_json_type. -
Constraint Merging – If a
Fieldis detected,build_schemacallsfield.apply(schema)to merge constraints into the properties dictionary. The function also adjusts therequiredarray based on whether parameters have defaults or are optional.
Usage Patterns
You can attach per-argument constraints using two primary syntaxes, depending on whether you need to preserve the Python type annotation independently of the constraints.
Direct Default Value Syntax
Provide a Field instance as the parameter default when the constraint itself serves as the default value.
from needle.agent.tools import Field, tool
@tool
def summarize(text: str, max_words: int = Field(default=100, description="Maximum words")):
"""Summarize the given text."""
pass
In this pattern, Field(default=...) both provides the default value and attaches metadata. The has_default method returns True, and build_schema recognizes this parameter as optional in the required array.
typing.Annotated Syntax
Use typing.Annotated to keep the Python type separate from the constraint metadata, which is essential when you need to apply constraints without providing a runtime default.
from typing import Annotated
from needle.agent.tools import Field, tool
@tool
def translate(
text: str,
target_lang: Annotated[
str,
Field(
description="ISO-639-1 language code",
pattern="^[a-z]{2}$",
enum=["en", "es", "fr", "de"]
)
],
) -> str:
"""Translate text to the specified language."""
pass
Here, _field_of extracts the Field instance from the Annotated metadata, allowing apply to inject the pattern and enum constraints into the generated schema:
{
"name": "translate",
"description": "Translate text to the specified language.",
"parameters": {
"type": "object",
"properties": {
"text": { "type": "string" },
"target_lang": {
"type": "string",
"description": "ISO-639-1 language code",
"pattern": "^[a-z]{2}$",
"enum": ["en", "es", "fr", "de"]
}
},
"required": ["text", "target_lang"]
}
}
Combining Multiple Constraints
Complex validation rules are created by stacking multiple constraint arguments in a single Field call.
@tool
def upload_file(
path: Annotated[
str,
Field(
description="Path to the file",
pattern=r"^/data/.*\.csv$",
max_length=200
)
],
size_mb: Annotated[int, Field(ge=0, le=10, description="File size in MB")],
):
"""Upload a CSV file smaller than 10 MB."""
pass
The apply method filters out None values, so you only need to specify the constraints relevant to your use case.
Summary
needle.Fieldprovides a declarative way to define JSON Schema constraints for function arguments incactus-compute/needle.- Constraints are defined in
needle/agent/tools.pyand support validation keywords includingge,pattern,enum, andmax_length. - The
@tooldecorator automatically detectsFieldmetadata via_field_ofand merges constraints using theapplymethod during schema generation. - Use
Fieldas a default value for simple cases, or wrap it intyping.Annotatedwhen you need to separate type hints from validation rules.
Frequently Asked Questions
What is the difference between using Field as a default and using Annotated?
Using Field as a default value (e.g., param: int = Field(default=5)) assigns that value at runtime and marks the parameter as optional in the schema. Using typing.Annotated (e.g., param: Annotated[int, Field(ge=0)]) attaches constraints without changing the parameter's required status or runtime default, which is necessary when you want the LLM to always provide the value but still validate it against rules.
Does needle.Field support custom JSON Schema formats?
Yes, the format parameter accepts any JSON Schema format string (e.g., "email", "uri", "date-time"). The apply method in needle/agent/tools.py passes this directly into the schema dictionary, making it compatible with validators that respect the JSON Schema specification.
How does needle handle conflicting constraints?
needle.Field performs no runtime validation of constraint combinations; it simply merges provided values into the schema via the apply method. The OpenAI API or your downstream JSON Schema validator will enforce the constraints. If you provide logically incompatible rules (e.g., gt=10 and lt=5), the resulting schema will contain both constraints, and validation will fail when the function is called.
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 →