This llamaindex custom tools tutorial walks through giving a LlamaIndex agent the ability to execute your own Python functions as callable tools. The built-in toolset covers common cases, but production agents usually need domain logic, internal APIs, or side effects that only your code can provide. Below is an end-to-end path from a raw function to a running agent that invokes it.
Step 1: Install and import the stack
Start with a clean virtual environment. You need llama-index (the core package) and an LLM client. OpenAI’s SDK is the default for the agent loop, but any OpenAI-compatible endpoint works.
pip install llama-index openai requests
Import the pieces you’ll use:
from llama_index.core.tools import FunctionTool
from llama_index.llms.openai import OpenAI
from llama_index.agent.openai import OpenAIAgent
import requests
If you’re on a recent LlamaIndex version, OpenAIAgent lives in llama_index.agent.openai. Older docs reference AgentRunner with OpenAIAgent from llama_index.agent. The import above is correct for v0.10+.
Step 2: Write the Python function you want to expose
A tool is just a function with a clear signature and docstring. The docstring matters—LlamaIndex forwards it to the model as the tool description. Keep it precise.
Below is a real, keyless call to CoinGecko’s public API for a spot price. No fake endpoints:
def get_btc_price(currency: str = "usd") -> str:
"""Fetch the current Bitcoin price in the given fiat currency.
Args:
currency: ISO currency code, e.g. 'usd', 'eur', 'jpy'.
"""
url = f"https://api.coingecko.com/api/v3/simple/price"
params = {"ids": "bitcoin", "vs_currencies": currency.lower()}
resp = requests.get(url, params=params, timeout=10)
resp.raise_for_status()
data = resp.json()
price = data["bitcoin"][currency.lower()]
return f"1 BTC = {price} {currency.upper()}"
The function returns a string. LlamaIndex tools can return any JSON-serializable object, but strings are easiest for the model to reason over.
Step 3: Wrap the function as a FunctionTool
LlamaIndex does not pass raw functions to the agent. You wrap them with FunctionTool.from_defaults. The wrapper extracts the docstring and type hints to build the OpenAI tool schema.
btc_tool = FunctionTool.from_defaults(
fn=get_btc_price,
name="get_btc_price",
description="Get the current Bitcoin price in a specified fiat currency",
)
You can override name and description if you want tighter control than the docstring gives. For a llamaindex custom tools tutorial, the key point is that the schema is derived automatically—but you are responsible for correct type hints.
Step 4: Configure the LLM (and optionally a gateway)
The agent needs an LLM that supports tool calling. gpt-4o-mini is a sane default for testing.
llm = OpenAI(model="gpt-4o-mini", temperature=0)
If you want provider redundancy, point the LLM at n4n.ai’s OpenAI-compatible endpoint—it fronts 240+ models and automatically falls back when a provider is rate-limited. Set api_base and your gateway key:
llm = OpenAI(
model="gpt-4o-mini",
temperature=0,
api_base="https://api.n4n.ai/v1",
api_key="your-gateway-key",
)
The agent code does not change. You get fallback and per-token metering without rewriting tools.
Step 5: Construct the agent and run a query
Create the agent with from_tools, pass the tool list, and chat.
agent = OpenAIAgent.from_tools(
tools=[btc_tool],
llm=llm,
verbose=True,
)
response = agent.chat("What is the Bitcoin price in euros right now?")
print(str(response))
verbose=True prints the tool call and result in the console. That is your first signal the wiring works.
Step 6: Verify the agent actually called your code
Success is not just a plausible answer—it’s evidence the function executed. With verbose=True you’ll see a log line like:
Calling tool: get_btc_price with args: {'currency': 'eur'}
To assert this programmatically, wrap the function with a side effect:
call_log = []
def get_btc_price(currency: str = "usd") -> str:
call_log.append(currency)
# ... rest unchanged
After agent.chat, check assert call_log == ["eur"]. If the list is empty, the model ignored the tool or the schema was malformed.
A second verification: force the tool via the agent’s ctx is overkill; just ask a question no pretrained model could answer, e.g. get_btc_price with a mock that returns a fixed string, then confirm the response contains that string.
Step 7: Tighten the schema with Pydantic
For production, rely on explicit input models instead of guessing from type hints. FunctionTool accepts a fn_schema built from a Pydantic class.
from pydantic import BaseModel, Field
class PriceArgs(BaseModel):
currency: str = Field(default="usd", description="ISO currency code")
btc_tool = FunctionTool.from_defaults(
fn=get_btc_price,
fn_schema=PriceArgs,
name="get_btc_price",
description="Get the current Bitcoin price in a specified fiat currency",
)
This removes ambiguity for the model and validates inputs before your code runs. In a llamaindex custom tools tutorial, skipping schema validation is the most common source of silent agent failures.
Step 8: Add a second tool and handle errors
Agents earn their keep when they choose between tools. Add a simple calculator:
def multiply(a: int, b: int) -> str:
"""Multiply two integers and return the product as a string."""
return str(a * b)
mul_tool = FunctionTool.from_defaults(fn=multiply, name="multiply")
Register both:
agent = OpenAIAgent.from_tools([btc_tool, mul_tool], llm=llm, verbose=True)
Now ask: “What is 17 times 23, and what’s BTC in usd?” The agent should emit two tool calls. If your function can raise, catch inside and return an error string—the model recovers better from text than from an exception trace.
def get_btc_price(currency: str = "usd") -> str:
try:
# ... request
except requests.RequestException as e:
return f"error: {e}"
Step 9: Run async tools if you need concurrency
LlamaIndex supports async functions. Decorate with async def and use FunctionTool.from_defaults identically; the agent event loop handles it.
import asyncio
async def get_btc_price_async(currency: str = "usd") -> str:
# use httpx.AsyncClient here
return "1 BTC = 60000 USD"
async_tool = FunctionTool.from_defaults(fn=get_btc_price_async)
Do not block the event loop with synchronous I/O in an async tool. Use httpx or aiohttp.
Verifying end-to-end success
A complete run looks like this:
pip installcompletes without conflict.agent.chatwith a tool-related prompt prints aCalling tool:line.call_log(or similar) shows the expected arguments.- The final response text contains data only your function could produce (live price, computed product).
- Adding a second tool yields multiple distinct tool calls in one turn.
If all five hold, your llamaindex custom tools tutorial implementation is correct. From here, extract tools into a module, load them dynamically, and add retry logic at the function level—not the agent level.
Custom tools are the boundary where LLM reasoning meets real systems. Get the schema right, log the calls, and treat the function body as production code.