Tools (function calling)
The model can request that tools (functions) be called. The API is
low-level: complete() returns the requested calls in Completion.tool_calls,
you run them and send the result back. Supported on OpenAI, Groq, Anthropic
and Gemini — the same interface across all four.
from jangada_ai import LLM, Message
def get_weather(city: str, units: str = "metric") -> str:
"""Returns the current weather for a city."""
return "25°C, sunny"
llm = LLM("openai", "gpt-4o-mini")
# 1) the model decides to call the tool
comp = llm.complete("What's the weather in Recife?", tools=[get_weather])
# 2) you run each call and build the results
results = []
for call in comp.tool_calls: # call.name, call.args (dict)
output = get_weather(**call.args)
results.append(call.result(output))
# 3) send back: history = question + response-with-tool-calls + results
comp2 = llm.complete(
"What's the weather in Recife?",
history=[comp.assistant_message(), Message.tool_results(*results)],
tools=[get_weather],
)
print(comp2.text)Defining tools
tools=[...] accepts:
- Python function — the schema comes from the signature (type hints) + docstring;
- Pydantic model — becomes the schema of the arguments;
- dict
{"name", "description", "parameters"}(JSON Schema) ready to use; - a
Tool(viato_tool(...)).
tool_choice controls the selection: "auto" (default), "none", "required", or
the name of a tool to force it.
Pieces
Completion.tool_calls: list ofToolCall(id, name, args).comp.assistant_message(): rebuilds the assistant message (text + tool calls) for the history.call.result(output): creates the correspondingToolResultPart.Message.tool_results(*parts): packs the results into a message.
Prebuilt tools
Jangada ships ready-made tools in jangada_ai.prebuilt:
from jangada_ai.prebuilt import tavily_search # web search (Tavily)
llm.complete("What's the dollar exchange rate today?", tools=[tavily_search])
# run: tavily_search(**call.args) (needs TAVILY_API_KEY in the environment)No dependency and no key:
| Tool | What it does |
|---|---|
calculator | evaluates arithmetic expressions (safe, via ast) |
current_datetime | current date/time (IANA timezone) |
fetch_url | downloads a page and returns the readable text (blocks internal networks by default) |
wikipedia_search | Wikipedia summary (no key) |
http_request | generic HTTP request (GET/POST/...; blocks internal networks by default) |
With a key (read from the environment, or pass api_key=):
| Tool | Key |
|---|---|
tavily_search | TAVILY_API_KEY (or tavily_tool(api_key=...)) |
brave_search | BRAVE_API_KEY |
openweather | OPENWEATHER_API_KEY |
Keyword-only parameters (after
*, such asapi_key/timeout) are runtime config and do not appear in the schema the model sees.
The tools that call external APIs handle every response case: rate
limit (429, respecting Retry-After), auth (401/403), 404, 5xx, timeout/
connection and non-JSON body. Transient errors (429/5xx/timeout) get a light
retry with backoff; once exhausted, the tool returns an error message as text
(instead of raising an exception), so the model can decide what to do.
See also Structured output (which on Anthropic already uses tool-forcing under the hood) and Observability (tool calls show up in the trace).
What changed in 1.9.0
- Parameter types.
int | None(Python 3.10+ syntax) now becomes the proper optional type in the schema on Python 3.10–3.13 too; aUnionof several types becomesanyOf; a parameter annotated with a PydanticBaseModelbecomes the model's schema, andjangada_ai.coerce_args(fn, args)turns the received dict into the instance (Agentalready does it for you). Annotations that fail to resolve (broken forward ref) no longer break the whole function. - Arguments with invalid JSON no longer silently become
{}: theToolCallcarriesmetadata["raw_arguments"]andmetadata["args_error"] = True. - Native tools (web search, code execution…) go in the same
tools=list — see Native tools.
Safer built-in tools
fetch_url/http_requestblock internal networks by default. Onlyhttp/https; the host is resolved and loopback, private networks, link-local (including the cloud metadata endpoint169.254.169.254), reserved and multicast are refused — on every redirect too (max 5). The body is read up to 5 MB. This stops a prompt injection in a web page from making the agent read your.envor your company network. To call an internal service on purpose:allow_private=Trueon the call or theJANGADA_HTTP_ALLOW_PRIVATE=1env var.- Network errors become a message for the model (malformed URL, connection reset, incomplete response) instead of an exception that breaks the loop.
fetch_urlhonours the server charset and decodes HTML entities.calculatorcaps exponents (±1000) and number size (10k digits) —9**9**9no longer hangs the process.- Alphanumeric CNPJ.
validar_cnpj,formatar_cnpj,validar_documentoandconsultar_cnpjaccept the new Brazilian IRS format (letters in the first 12 positions, numeric check digits), e.g.12.ABC.345/01DE-35.
Example
examples/tools_example.py — runnable script.
Structured output (Pydantic)
A single parse() call returns a validated Pydantic instance, regardless of how each provider implements it under the hood.
Native tools
Server-side tools (web search, url context, code execution, file search, Google Maps, computer use, image generation) with one API across providers: web_search(), code_execution()… and native_tool() for the SDK's native object. Per-provider matrix, citations, multi-turn and cost.