Jangada AIJangada AI

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 (via to_tool(...)).

tool_choice controls the selection: "auto" (default), "none", "required", or the name of a tool to force it.

Pieces

  • Completion.tool_calls: list of ToolCall(id, name, args).
  • comp.assistant_message(): rebuilds the assistant message (text + tool calls) for the history.
  • call.result(output): creates the corresponding ToolResultPart.
  • 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:

ToolWhat it does
calculatorevaluates arithmetic expressions (safe, via ast)
current_datetimecurrent date/time (IANA timezone)
fetch_urldownloads a page and returns the readable text (blocks internal networks by default)
wikipedia_searchWikipedia summary (no key)
http_requestgeneric HTTP request (GET/POST/...; blocks internal networks by default)

With a key (read from the environment, or pass api_key=):

ToolKey
tavily_searchTAVILY_API_KEY (or tavily_tool(api_key=...))
brave_searchBRAVE_API_KEY
openweatherOPENWEATHER_API_KEY

Keyword-only parameters (after *, such as api_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; a Union of several types becomes anyOf; a parameter annotated with a Pydantic BaseModel becomes the model's schema, and jangada_ai.coerce_args(fn, args) turns the received dict into the instance (Agent already 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 {}: the ToolCall carries metadata["raw_arguments"] and metadata["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_request block internal networks by default. Only http/https; the host is resolved and loopback, private networks, link-local (including the cloud metadata endpoint 169.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 .env or your company network. To call an internal service on purpose: allow_private=True on the call or the JANGADA_HTTP_ALLOW_PRIVATE=1 env var.
  • Network errors become a message for the model (malformed URL, connection reset, incomplete response) instead of an exception that breaks the loop.
  • fetch_url honours the server charset and decodes HTML entities.
  • calculator caps exponents (±1000) and number size (10k digits) — 9**9**9 no longer hangs the process.
  • Alphanumeric CNPJ. validar_cnpj, formatar_cnpj, validar_documento and consultar_cnpj accept 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.

On this page