Needle complete() vs run() vs extract(): When to Use Each API
Needle's three core methods—complete(), run(), and extract()—differ primarily in iteration depth, return type, and whether they handle tool-calling automatically. This guide breaks down exactly how each API works under the hood so you can choose the right one for your use case.
Needle is a Python library that bridges language models with native C performance via cactus-compute/needle. Understanding the distinction between these three entry points is essential for building efficient agentic applications. Let's examine each method's behavior, source implementation, and practical applications.
complete(): Single-Shot Completion
The simplest API—complete() performs exactly one forward pass through the model and returns the raw JSON response envelope.
How It Works Internally
In needle/__init__.py (lines 103–118), complete() is a thin wrapper around the compiled C library:
def complete(self, prompt: str, **kwargs) -> dict:
# Serializes prompt, calls needle_complete(), deserializes JSON
response = self._native.complete(prompt, self._tools, **kwargs)
return json.loads(response)
No iteration occurs. No tool results are fed back to the model. You get exactly what the model generates in one shot.
When to Use complete()
- Simple chat-style responses
- Generation tasks where tools aren't needed
- Scenarios where you want manual control over any tool-calling logic
from needle import Needle
agent = Needle()
resp = agent.complete("Explain quantum entanglement in one sentence.")
print(resp["text"]) # → "Quantum entanglement is..."
run(): Iterative Tool-Calling Loop
The most powerful API—run() keeps the conversation going, automatically executing tools and feeding results back until the model produces a final response or hits a step limit.
How It Works Internally
Per needle/__init__.py (lines 19–40), run() builds on complete() with a loop:
def run(self, prompt: str, max_steps: int = 10) -> dict:
# First completion
response = self.complete(prompt)
results = []
for step in range(max_steps):
calls = response.get("function_calls", [])
if not calls:
break # No more tool calls needed
# Execute each tool, collect results
for call in calls:
func = self._functions[call["name"]]
try:
result = func(**call["arguments"])
except Exception as e:
result = {"error": str(e)}
results.append(result)
# Feed results back for next completion
response = self.complete(prompt + json.dumps(results))
response["results"] = results # Enrich final envelope
return response
Key Characteristics
- Multiple steps: Up to
max_stepscompletions (default: 10) - Automatic tool execution: Looks up Python functions in
self._functions - Result enrichment: Adds a
"results"field with all tool-call outcomes - Error handling: Captures exceptions and feeds them back as results
When to Use run()
- Multi-step reasoning with external tools
- Scenarios where the model needs to fetch data, calculate, then reason about results
- Agentic workflows where you want the engine to manage the conversation loop
def fetch(url: str) -> str:
import requests
return requests.get(url).text
agent = Needle(tools=[fetch])
result = agent.run(
"Fetch the first paragraph of https://example.com and summarise it.",
max_steps=5
)
print(result["results"]) # List of tool execution results
print(result["text"]) # Final summarised response
extract(): One-Shot Structured Extraction
The convenience API—extract() treats a Pydantic schema as the only available "tool" and returns a parsed Python object directly, eliminating boilerplate.
How It Works Internally
Per needle/__init__.py lines 57–68 and 141–142, extract() creates a temporary agent:
def extract(cls, text: str, schema: Type[BaseModel]) -> Optional[BaseModel]:
# Create ephemeral agent with schema as sole tool
agent = cls(tools=[schema])
response = agent.complete(text)
calls = response.get("function_calls", [])
if not calls:
return None
# Parse first function call into schema instance
args = calls[0]["arguments"]
return schema(**args) # Or return raw dict if instantiation fails
Key Characteristics
- No persistent agent: Creates a temporary
Needleinstance internally - Single tool restriction: Schema is the only available function
- Direct object return: Returns a Pydantic instance or
None, not a JSON envelope - No iteration loop: Exactly one completion, like
complete()
When to Use extract()
- Structured data extraction from free-form text (dates, addresses, entities)
- Quick parsing without managing a full agent lifecycle
- One-shot transformation of unstructured → structured data
from pydantic import BaseModel
class PersonInfo(BaseModel):
name: str
email: str
text = "John Doe's email is john.doe@example.com."
extracted = Needle().extract(text, PersonInfo)
print(extracted) # PersonInfo(name='John Doe', email='john.doe@example.com')
Summary: Choosing the Right API
| Aspect | complete() |
run() |
extract() |
|---|---|---|---|
| Steps | 1 | Up to max_steps |
1 |
| Tool handling | None (unless prompt contains schema) | Automatic loop | Schema-only |
| Return type | JSON envelope (dict) | JSON envelope + "results" list |
Pydantic object or None |
| Agent lifecycle | Uses existing instance | Uses existing instance | Creates temporary agent |
| Best for | Simple completions, manual control | Multi-step agentic workflows | Quick structured extraction |
Frequently Asked Questions
Can I use extract() with multiple schemas at once?
No. The extract() design intentionally restricts the tool set to a single schema, as implemented in needle/__init__.py lines 57–68. For multiple extraction targets, create a single Pydantic model with optional fields or use complete()/run() with a broader tool set.
Does run() always execute tools synchronously?
Yes. According to the source in needle/__init__.py lines 19–40, each tool is looked up in self._functions and executed via direct Python invocation. There is no built-in async support in the current implementation—each tool runs to completion before the next complete() call.
Why does extract() return None instead of raising an exception?
This is intentional defensive behavior per lines 57–68. If the model fails to generate a matching function call—due to prompt ambiguity, schema mismatch, or model refusal—extract() returns None rather than crashing. Always check the return value or wrap calls in your own validation logic.
Can I access the raw JSON from extract() if needed?
No. The extract() API deliberately discards the envelope. If you need raw response details (confidence scores, token usage, alternative calls), use complete() directly with your schema as a tool and handle parsing yourself, as shown in the extract() implementation pattern.
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 →