How Needle Extracts Tool Schemas from Python Functions: A Deep Dive into `@tool` and `build_schema`
Needle extracts tool schemas by introspecting Python function signatures, type hints, docstrings, and Field metadata in needle/agent/tools.py, producing OpenAI-compatible JSON schemas through the build_schema() helper.
Needle is a lightweight Python library for structured extraction with LLMs. Its core capability is turning regular Python callables into OpenAI-compatible tool schemas without boilerplate. This article examines the exact mechanism in the cactus-compute/needle repository, walking through the four-step pipeline from decorated function to structured output.
The @tool Decorator: Entry Point for Schema Extraction
When you apply the @tool decorator to a function, Needle immediately triggers schema construction. In needle/agent/tools.py lines 68-70, the decorator stores the schema directly on the function object:
def tool(fn):
fn._needle_tool = build_schema(fn)
return fn
This allows the schema to be computed once at decoration time rather than repeatedly at runtime. The tools= argument in agent initialization uses the same build_schema() helper for undecorated functions.
Step-by-Step Schema Construction in build_schema()
The build_schema() function orchestrates four distinct phases of introspection.
1. Signature and Type Hint Extraction
build_schema() uses inspect.signature() to enumerate parameters and typing.get_type_hints() to resolve forward references and complex annotations. This happens before any JSON-specific processing begins.
2. Type Mapping via _json_type()
For each parameter, _json_type() (lines 57-86) maps Python types to JSON Schema types:
| Python Type | JSON Schema Output |
|---|---|
int |
{"type": "integer"} |
float |
{"type": "number"} |
str |
{"type": "string"} |
bool |
{"type": "boolean"} |
Enum, Literal |
{"type": "string", "enum": [...]} |
list[T] |
{"type": "array", "items": <schema for T>} |
BaseModel |
Merged model schema via pydantic_schema() |
3. Docstring Parsing with _parse_doc()
The _parse_doc() helper (lines 99-112) extracts:
- Summary: First line of the docstring becomes the tool's
description - Argument docs: Google/NumPy-style
Args:sections populate per-parameterdescriptionfields
4. Field Metadata and Constraint Application
Needle supports typing.Annotated for rich constraints:
from typing import Annotated
from needle import Field, tool
@tool
def search(
query: str,
limit: Annotated[int, Field(description="Max results", ge=1, le=100)] = 10
):
"""Search the inventory."""
...
The Field.apply() method (lines 36-49) merges these constraints into the JSON schema, producing minimum, maximum, enum, or description fields as appropriate.
5. Required Field Detection
The _is_optional() helper determines membership in the required array (lines 136-138). An argument is required when it lacks both a default value and an Optional/Union[..., None] type.
First-Class Pydantic Model Support
When _json_type() encounters a Pydantic BaseModel, it delegates to pydantic_schema() (lines 55-72). This extracts the model's JSON schema via model_json_schema() or model.schema(), then merges it into the tool's parameter structure.
from pydantic import BaseModel
from needle import extract
class Invoice(BaseModel):
"""Extracted invoice data."""
vendor: str
amount: float
date: str
# Schema is extracted automatically; result is validated against Invoice
invoice = extract("ACME Corp billed $500 on 2024-01-15", schema=Invoice)
The Complete Schema Shape
The final dictionary matches OpenAI's function-calling specification exactly (lines 39-46):
{
"name": "function_name",
"description": "Docstring summary.",
"parameters": {
"type": "object",
"properties": {
"arg1": {"type": "string", "description": "..."},
...
},
"required": ["arg1", ...]
}
}
Runtime Extraction: needle.extract()
The public extract() function in needle/__init__.py (lines 149-178) creates a temporary Needle agent that treats your schema as the sole available tool. The LLM generates JSON conforming to the schema, which Needle then validates into either a Pydantic instance or plain dict.
Complete Working Example
from typing import Annotated, Literal
from needle import tool, Field, extract
@tool
def get_weather(
city: str,
units: Annotated[
Literal["celsius", "fahrenheit"],
Field(description="Temperature unit")
] = "celsius"
) -> dict:
"""Fetch current weather conditions.
Args:
city: The city to query.
units: Preferred temperature unit.
"""
# Implementation omitted
pass
# Inspect the generated schema
print(get_weather._needle_tool)
# Use in extraction pipeline
agent = Needle()
response = agent.extract(
"What's the weather in Tokyo?",
tools=[get_weather]
)
Summary
@tooldecorator triggers immediate schema construction viabuild_schema()inneedle/agent/tools.py- Type hints map to JSON Schema through
_json_type(), with special handling for Pydantic models - Docstrings provide human-readable descriptions parsed by
_parse_doc() Fieldannotations add constraints like ranges, enums, and custom descriptions- Required fields are detected automatically from defaults and
Optionaltypes needle.extract()leverages the same machinery for one-shot structured extraction
Frequently Asked Questions
What Python versions does Needle's schema extraction support?
Needle relies on typing.get_type_hints() and inspect.signature(), requiring Python 3.8+. Forward references and from __future__ import annotations are fully supported through PEP 563 compatibility in the type hint resolution pipeline.
Can I use Pydantic v1 and v2 models interchangeably?
Yes. The pydantic_schema() helper in needle/agent/tools.py detects the Pydantic version and calls model_json_schema() for v2 or model.schema() for v1, normalizing the output to a consistent dict structure before merging into the tool schema.
How does Needle handle complex nested types like list[BaseModel]?
_json_type() recurses through container types. For list[T] or dict[str, T], it extracts the item type's schema and places it under items or additionalProperties. If T is itself a Pydantic model, pydantic_schema() handles the nested extraction, producing fully specified JSON Schema structures.
What happens if my function has no docstring?
The schema still generates successfully. The description field becomes an empty string, and parameter descriptions are omitted. Needle recommends adding docstrings for production use, as LLM tool selection accuracy depends on clear descriptions.
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 →