How to Define Tools Using Manual JSON Schema for the Needle Agent
Bypass automatic schema generation by assigning a custom OpenAI-compatible JSON schema to function._needle_tool to control validation constraints, enum values, and parameter descriptions.
The Needle agent from cactus-compute/needle exposes Python functions as tools that large language models can invoke. By default, the @tool decorator in needle/agent/tools.py introspects type hints and docstrings to build an OpenAI-compatible JSON schema automatically. When you need finer control—custom validation rules, precise descriptions, or enum constraints—you can define the schema manually.
When to Use a Manual JSON Schema
Automatic schema generation covers most cases, but manual construction becomes essential when you need to:
- Add JSON Schema keywords that
build_schema()does not infer, such asformat,pattern, ormultipleOf - Override required field detection to make parameters optional even without default values
- Supply richer documentation with structured descriptions per property
- Define enum constraints or numeric ranges with specific validation semantics
Step-by-Step: Attaching a Manual Schema
Follow these three steps to supply your own schema for a tool function.
1. Construct the JSON Schema Dictionary
Build a dictionary following the OpenAI "function calling" format. The structure requires name, description, and a parameters object with properties and required arrays.
manual_schema = {
"name": "fetch_weather",
"description": "Retrieve current weather conditions for a location.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. 'Berlin'."
},
"units": {
"type": "string",
"enum": ["metric", "imperial"],
"description": "Temperature unit system."
}
},
"required": ["city"]
}
}
2. Assign the Schema via _needle_tool
Attach the dictionary to your function using the private attribute _needle_tool. Needle's agent discovery mechanism checks for this attribute before falling back to automatic generation.
def fetch_weather(city: str, units: str = "metric") -> str:
"""Fetch weather data for the specified city."""
# Implementation omitted
return f"Weather in {city}: 22°C"
# Override automatic schema with manual definition
fetch_weather._needle_tool = {
"name": "fetch_weather",
"description": "Retrieve current weather conditions for a location.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. 'Berlin'."
},
"units": {
"type": "string",
"enum": ["metric", "imperial"],
"default": "metric",
"description": "Temperature unit system."
}
},
"required": ["city"]
}
}
3. Expose the Tool to the Agent
Return the function from needle.agent.fetch.get_tools() or your agent's tool collection method. The agent will serialize your exact schema when constructing LLM prompts.
# needle/agent/fetch.py
from .custom_tools import fetch_weather
def get_tools():
return [fetch_weather] # Agent receives the manual schema
Complete Working Example
Here is a production-ready pattern combining manual schema definition with proper module organization.
# needle/agent/weather_tools.py
def summarize_forecast(
days: int,
location: str,
include_humidity: bool = False
) -> str:
"""Generate a human-readable weather summary."""
# Implementation logic
return f"Forecast for {location}: sunny for {days} days."
# Manual schema with validation constraints
summarize_forecast._needle_tool = {
"name": "summarize_forecast",
"description": "Generate a human-readable multi-day weather summary.",
"parameters": {
"type": "object",
"properties": {
"days": {
"type": "integer",
"minimum": 1,
"maximum": 14,
"description": "Number of days to forecast (1-14)."
},
"location": {
"type": "string",
"minLength": 2,
"description": "City or geographic location."
},
"include_humidity": {
"type": "boolean",
"default": False,
"description": "Whether to include humidity percentages."
}
},
"required": ["days", "location"]
}
}
Registration in the agent's tool fetcher:
# needle/agent/fetch.py
from .weather_tools import summarize_forecast
def get_tools():
return [
summarize_forecast,
# Additional tools...
]
Hybrid Approach: Combining Field with Manual Overrides
The needle/agent/tools.py module provides a Field class for metadata-rich type annotations. You can use Field for automatic generation, then selectively override the resulting schema.
from typing import Annotated
from needle.agent.tools import Field, tool
def translate(
text: str,
target_lang: Annotated[str, Field(
description="ISO-639-1 language code",
enum=["en", "es", "fr", "de", "ja"]
)],
formality: Annotated[str, Field(
description="Register for translation",
enum=["formal", "informal", "neutral"],
default="neutral"
)] = "neutral"
) -> str:
"""Translate text to the target language with formality control."""
return f"[{target_lang}] {text}"
# Generate base schema, then patch
translate = tool(translate)
# Override specific properties manually
translate._needle_tool["parameters"]["properties"]["text"]["maxLength"] = 5000
translate._needle_tool["parameters"]["properties"]["target_lang"]["description"] = (
"Target language code (ISO-639-1). Supported: en, es, fr, de, ja."
)
Core Components in needle/agent/tools.py
| Component | Purpose |
|---|---|
Field |
Dataclass wrapping metadata (description, enum, ge, le, default) for use with Annotated |
build_schema(fn) |
Generates JSON schema from function signature and Field annotations |
tool(fn) |
Decorator that attaches generated schema to fn._needle_tool |
When _needle_tool is already present on a function, the tool decorator preserves it and skips build_schema, enabling seamless manual override.
Summary
- Manual JSON schema definition for Needle tools requires constructing an OpenAI-compatible dictionary and assigning it to
function._needle_tool - The schema must include
name,description,parameters.type="object",parameters.properties, andparameters.required - Register tools through
needle.agent.fetch.get_tools()for agent discovery - Combine
Fieldannotations with manual patches for a hybrid automation approach - Source implementation resides in
needle/agent/tools.pywith discovery logic inneedle/agent/fetch.py
Frequently Asked Questions
How does Needle detect whether to use a manual or automatic schema?
The tool decorator and agent discovery code check for the _needle_tool attribute before invoking build_schema. If present, the existing dictionary is used unchanged; otherwise, automatic generation proceeds from type hints and docstrings.
Can I mix manual schema properties with Field metadata?
Yes. Apply the @tool decorator first to generate a base schema from Field annotations, then mutate function._needle_tool directly to add or override specific properties such as pattern, format, or complex anyOf structures.
What happens if my manual schema omits the required field?
The LLM receives a schema with no mandatory arguments, treating all parameters as optional. This can lead to ambiguous tool calls if your function implementation expects certain arguments. Always explicitly specify required based on your runtime needs.
Where does Needle store the schema for registered tools?
The schema dictionary lives on the function object itself as the _needle_tool attribute. This design keeps schema and implementation colocated, simplifying introspection in needle/agent/fetch.py without requiring external registries.
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 →