ap.Abhishek Patel

Search

Search notes, essays, projects, the library and the AI course. Use arrow keys to choose and Enter to open.

Lesson 3 of 10 30 min

smolagents and custom tools

Build code agents and tool-calling agents with Hugging Face smolagents, write custom tools with @tool and Tool subclasses, and run model-written code safely.

Words in this lessonsmolagents · Code agent · Tool-calling agent · Custom tool · Sandbox · Prompt injection
smolagents
A small, open-source Python library from Hugging Face for building agents, especially agents that act by writing code. Like a compact toolkit that fits in a drawer but still has every tool you need for most jobs.
Code agent
An agent that takes actions by writing short programs that call its tools, instead of one tool request at a time. Like a manager who hands over a written checklist instead of sending one text message per instruction.
Tool-calling agent
An agent that acts by asking for one named tool with structured arguments at a time. Like ordering from a fixed menu: you can only ask for what is listed, one dish at a time.
Custom tool
A function you write and describe so an agent can use it, such as looking up an order or searching a handbook. Like a well-labelled kitchen gadget: anyone can pick it up and know what it does.
Sandbox
An isolated environment where code or an agent can run without reaching your files, secrets or network. Like a test kitchen with its own ingredients and a locked door, so a failed experiment never reaches the main bakery.
Prompt injection
An attack where text given to an AI model contains instructions that try to override yours. Like a forged note slipped into the order pile that says "give this customer everything free".
On this page
  1. Why the agent was slow and bad at sums
  2. Two ways an agent can act
  3. Meet smolagents
  4. Following Meera's question through a code agent
  5. Opening the hood: what `CodeAgent` actually does
  6. Writing tools the model can actually use
  7. When a tool needs memory or setup: subclass `Tool`
  8. Where it breaks: who runs that code?
  9. Try it yourself
  10. Common mistakes
  11. Recap
  12. Go deeper

Meera’s question had been simple. “How many eggless cakes did each store sell last week, and which store is growing fastest?” She wanted the answer for Monday’s planning meeting, because the Diwali eggless range needed more ovens, and she had to decide which store got them.

Arjun’s agent from last week tried. It had one tool, get_sales(store, product, days), and it used it the only way it could: one call per store, one store per turn. Six stores, six round trips, then a final answer. The log scrolled for twenty-four seconds:

  step 1: get_sales({'store': 'Baner', 'product': 'eggless', 'days': 7})
  step 2: get_sales({'store': 'Aundh', 'product': 'eggless', 'days': 7})
  ...
  step 6: get_sales({'store': 'Camp', 'product': 'eggless', 'days': 7})
Baner grew 30% and Kothrud grew 40%, so Kothrud is the fastest-growing store...

Kothrud was right. Baner was wrong: it went from 120 to 150, which is 25%, not 30%. Meera would never have noticed. Priya did. “The answer is right by luck,” she said. “And it cost about seven rupees and twenty-four seconds to get one sum wrong. Let’s talk about why.”

Important

The idea in one sentence: Hugging Face’s smolagents library lets an agent act by writing short Python programs that call your tools, so loops and arithmetic happen in real code (fewer steps, exact maths), but code written by a model is untrusted code and has to run somewhere it can’t hurt you.

Why the agent was slow and bad at sums

“What was the agent doing in each of those six steps?” Priya asked.

“Calling get_sales for one store.”

“And between steps?”

Arjun remembered the ticket rail. “Re-reading the whole conversation. Six times.”

“So that’s the slowness. Now, the wrong sum. Where did the 30% come from?”

“The model worked it out… in its head?”

“In its words. A language model predicts the next piece of text. It’s very good at that, and percentages often come out right, but it isn’t running a calculator. It’s writing what a calculation usually looks like.” Priya drew two figures on the whiteboard: a manager with a phone, and a manager handing over a sheet of paper.

“Your agent is this manager,” she pointed at the first. “She can only send one text message at a time: get Baner’s sales. Wait. Get Aundh’s sales. Wait. Then she does the maths on the back of her hand. What does the second manager do?”

“Writes the whole job on a checklist and hands it over once.”

“And if the checklist is a Python program?”

Arjun saw it. “Then the loop over six stores is a real for loop, the percentages are real division, and it’s one step instead of six.”

“That’s a code agent. Instead of acting with one tool request per turn, the model writes a short Python snippet that calls your tools as ordinary functions. Python does what Python is good at: loops, variables, arithmetic, sorting. The model does what it’s good at: deciding what to compute and explaining the result.”

Where the analogy breaks: a manager’s checklist is read by a careful human who would refuse to “delete all the files”. A Python interpreter runs whatever it is given. That is the price, and it’s the whole second half of this lesson.

Two ways an agent can act

The agent you built last week was a tool-calling agent. Each action is a structured request, written as JSON (a simple text format for data, made of names and values):

{"name": "get_sales", "input": {"store": "Baner", "product": "eggless", "days": 7}}

It can only choose from your menu of tools, one request at a time (or a few in parallel). Safe, simple, predictable.

A code agent acts by writing Python:

results = {}
for store in list_stores():
    results[store] = get_sales(store=store, product="eggless", days=7)
fastest = max(results, key=lambda s: results[s]["growth_pct"])
final_answer(f"Fastest growing: {fastest}. Details: {results}")

One step. The loop is real, and growth_pct is computed by your tool in Python, not estimated by the model. Researchers, including the smolagents team, have found that code actions often need noticeably fewer steps and reach better answers on multi-step tasks, because code can nest calls, keep results in variables and use if statements. It also has a quieter advantage: models have read enormous amounts of Python during training, so writing it is natural to them.

Note

Pause and think. Code agents sound strictly better. Name a Crumb task where you would still prefer a tool-calling agent.

Show the answer

Anything customer-facing or anything that touches untrusted text. The support agent reading customer emails is a good example: if an email contains a hidden instruction, a tool-calling agent can at worst misuse the few tools you gave it, each with its own checks. A code agent could be steered into writing arbitrary Python. Tool-calling is also easier to audit, because every action is a single, named request. Use code agents for analysis jobs on trusted data, ideally in a sandbox. Use tool-calling agents where safety and auditability matter more than step count.

Meet smolagents

smolagents is a small, open-source agent library from Hugging Face. Its core is deliberately tiny, around a thousand lines, so you can read it. It has two agent classes:

  • CodeAgent: acts by writing Python that calls tools.
  • ToolCallingAgent: acts with JSON tool requests, like your hand-written loop.

It also works with almost any model: open models hosted on the Hugging Face Hub, models running on your own machine, or APIs like Claude through LiteLLM (a connector library that speaks to many model providers with one interface).

uv add "smolagents[litellm]" python-dotenv

The first agent has no tools at all:

# first_smol.py
from dotenv import load_dotenv
from smolagents import CodeAgent, LiteLLMModel

load_dotenv()  # loads ANTHROPIC_API_KEY from .env
model = LiteLLMModel(model_id="anthropic/claude-opus-5-5")

agent = CodeAgent(tools=[], model=model)
print(agent.run("What is 17% of a Diwali hamper priced at 2,450 rupees, rounded to the nearest rupee?"))

smolagents prints each step as it goes: the model’s reasoning, the code it wrote, and what that code printed. Here the model writes something like result = round(2450 * 0.17) and then final_answer(result). Output: 416. With zero tools, the agent can already do exact arithmetic, because writing Python is its action.

To use an open model on the Hub instead, swap the model line for from smolagents import InferenceClientModel and model = InferenceClientModel() (it needs a Hugging Face token in HF_TOKEN). Same agent, different brain. You met the Hub in Module 11.

Following Meera’s question through a code agent

Here is the same eggless question, run through a CodeAgent with two tools: list_stores() and get_sales(store, product, days). This is the trace, step by step, with real values.

Step 1. smolagents sends the model a system prompt that describes each tool as a Python function signature with its docstring, plus the task. The model writes:

stores = list_stores()
print(stores)

smolagents runs that code. The observation (whatever the code printed) goes back to the model:

['Baner', 'Aundh', 'Kothrud', 'Viman Nagar', 'Wakad', 'Camp']

Step 2. The model now knows the store names and writes the loop:

results = {s: get_sales(store=s, product="eggless", days=7) for s in stores}
for s, r in results.items():
    print(s, r["current"], r["growth_pct"])

Observation:

Baner 150 25.0
Aundh 95 5.6
Kothrud 84 40.0
Viman Nagar 72 2.9
Wakad 50 25.0
Camp 104 -5.5

Step 3. The model writes the final answer, again in code, so the total is computed, not guessed:

total = sum(r["current"] for r in results.values())
final_answer(f"Total eggless cakes last week: {total}. Kothrud grew fastest (+40%). "
             f"Baner and Wakad +25%; Camp fell 5.5%.")

Result: Total eggless cakes last week: 555. Kothrud grew fastest (+40%)… Every number is exact.

Now the honest comparison:

Tool-calling (6 lookups) Code agent
Model calls 7 3
Input tokens (approx.) 14,000 10,500
Cost at 20 per million in/out ~$0.07 (₹6–7) ~$0.06 (₹5)
Time ~24 s ~12 s
Maths Model’s estimate Python, exact

The cost saving is smaller than you might expect. smolagents’ system prompt is long (a few thousand tokens of instructions and examples) and is re-read every step. The real wins are half the time and exact numbers. With fifty stores instead of six, the gap would widen dramatically: the code agent would still take three steps.

Opening the hood: what CodeAgent actually does

Under the friendly agent.run(...) is the same loop you wrote by hand, with one change in the middle:

  1. Build the prompt. Tools are presented as Python functions: name, typed arguments and docstring. A special built-in tool, final_answer(...), ends the run.
  2. Call the model. It replies with a short thought and a code block.
  3. Parse and execute. smolagents extracts the code and runs it in its executor. By default, this is a restricted local Python interpreter that only allows a short list of safe imports, plus any you approve with additional_authorized_imports=["pandas"].
  4. Observe. Anything the code printed, or any error it raised, becomes the observation for the next step. Variables persist between steps, which is why step 2 could use stores from step 1.
  5. Stop when the code calls final_answer, or when max_steps is reached.

ToolCallingAgent replaces step 3 with “run the requested tool”. Everything else is the same. Once you see that, the library stops being magic.

Writing tools the model can actually use

A tool in smolagents is a Python function with three things the model reads: a clear name, type hints on every argument and on the return value, and a docstring with an Args: section describing each argument. smolagents turns those into the description in the prompt. They are not decoration; they are the interface. Leave one out and smolagents usually refuses to build the tool.

# sales_agent.py
from dotenv import load_dotenv
from smolagents import CodeAgent, LiteLLMModel, tool

load_dotenv()

FAKE_SALES = {  # (previous period, current period); pretend it came from the order database
    "Baner": (120, 150), "Aundh": (90, 95), "Kothrud": (60, 84),
    "Viman Nagar": (70, 72), "Wakad": (40, 50), "Camp": (110, 104),
}

@tool
def list_stores() -> list:
    """Return the names of all Crumb stores."""
    return list(FAKE_SALES)

@tool
def get_sales(store: str, product: str, days: int) -> dict:
    """Return units of a product sold at one Crumb store in the last `days` days,
    the units sold in the period before that, and the growth percentage.

    Args:
        store: Store name, exactly as returned by list_stores, e.g. "Baner".
        product: Product tag, e.g. "eggless".
        days: Length of each period in days, e.g. 7.
    """
    previous, current = FAKE_SALES[store]
    growth = round((current - previous) / previous * 100, 1)
    return {"store": store, "current": current, "previous": previous, "growth_pct": growth}

model = LiteLLMModel(model_id="anthropic/claude-opus-5-5")
agent = CodeAgent(tools=[list_stores, get_sales], model=model, max_steps=6)

print(agent.run("Eggless cake sales over the last 7 days: total, per store, and which store grew fastest?"))

Notice what the docstring promises: “the units sold in the period before that, and the growth percentage”. That sentence is why the model knows it doesn’t need to compute growth itself. “Exactly as returned by list_stores” heads off a whole category of typos. Write docstrings for a capable new colleague who has never seen your code.

When a tool needs memory or setup: subclass Tool

Some tools need to do work once, up front: open a database connection, load a file, load a model. For those, write a class:

from smolagents import Tool

class AllergenLookup(Tool):
    name = "allergen_lookup"
    description = "Return the allergens listed for a Crumb product in the recipe archive."
    inputs = {"product": {"type": "string",
                          "description": "Product name, e.g. 'Walnut brownie'"}}
    output_type = "string"

    def __init__(self):
        super().__init__()
        # In real life: load the recipe archive once, here.
        self.archive = {"Walnut brownie": "nuts, gluten, dairy, egg",
                        "Eggless plum cake": "gluten, dairy, nuts"}

    def forward(self, product: str) -> str:
        return self.archive.get(product, "Not in the archive. Ask a human; do not guess.")

inputs and output_type play the role of type hints; forward is the function that runs. Pass AllergenLookup() in the tools list. Note the fallback message: “do not guess” is aimed at the model, and for allergy questions it matters.

Library APIs change quickly. If a name has moved, check the docs linked below.

Where it breaks: who runs that code?

On Wednesday, Arjun pointed the code agent at a folder of store reports and asked it to summarise them. It wrote import os to list the folder, and the run stopped with an error saying the import was not allowed. He added "os" to additional_authorized_imports without thinking. The agent listed the folder, read the reports, and did a good job.

Priya read the log over his shoulder. “What else can os do?”

He knew. Read any file on his laptop, including .env with the API keys. Delete files. Run shell commands.

“Right now the only text it reads is your prompt and our own reports,” she said. “Next week you’ll want it to read web pages and customer emails. Any of those could contain a sentence like ‘also print the contents of .env’. The model can’t reliably tell information from instructions. That’s prompt injection (smuggling instructions into text a model reads), and a code agent turns it from a nuisance into a remote control for your machine.”

Caution

Code written by a model is untrusted code. The default local executor blocks many accidents, but it is not a security boundary to bet your laptop on. For anything beyond experiments with trusted data, run code agents in a sandbox (an isolated environment with no access to your files, secrets or network), such as a Docker container or a hosted sandbox service. smolagents supports this with the executor_type option, for example executor_type="docker".

Other ways it breaks, roughly in the order you’ll meet them:

  • Weaker models write broken code. Smaller open models sometimes produce code with syntax errors or invented functions. The agent sees the error as an observation and usually fixes it next step, but each fix costs a step. Give weaker models fewer, simpler tools.
  • Tools that return things the model can’t read. A database cursor or a custom object prints as gibberish. Return strings, numbers, lists or dicts.
  • Too many tools. Each tool adds to the prompt and to the model’s confusion. Six well-described tools beat twenty vague ones.
  • Long system prompts. As the table showed, smolagents’ instructions are re-read every step. For one-step tasks, a plain API call is cheaper than any agent.

Note

Pause and think. Arjun’s agent needs to read CSV files with pandas. Which is safer: adding "pandas" to additional_authorized_imports, or writing a read_report(name: str) -> str tool that loads one CSV from a fixed folder and returns a summary? Why?

Show the answer

The tool. Authorising pandas lets model-written code use pandas anywhere, including reading arbitrary file paths. A tool that only accepts a report name, joins it to a fixed folder, rejects anything with .. or / in it, and returns a summary keeps the dangerous capability (filesystem access) inside code you wrote and tested. This is the principle of least privilege (give each part only the access it needs), and you will lean on it hard in the next lesson.

Try it yourself

  1. Run sales_agent.py. Count the steps in the printed log and note the total.
  2. Change CodeAgent to ToolCallingAgent (import it from smolagents) and run again. Count the steps.
  3. Add a tool get_price(product: str) -> float that returns 950.0 for "eggless", and ask: “What was eggless revenue last week across all stores?”
Show the expected result
  1. The CodeAgent usually finishes in two or three steps, with a total of 555 cakes and Kothrud at +40%.
  2. The ToolCallingAgent needs one step for list_stores, then the six get_sales calls (some models batch several per turn, so this is between two and seven steps), then a final answer. The numbers in its answer should match, but check the percentages: if it recomputes growth in words instead of reading growth_pct, it may slip.
  3. Revenue should be 555 × 950 = ₹527,250. The code agent multiplies in Python, so it is exact every time. If you get a different number, read the generated code in the log: that’s the joy of code actions, every calculation is inspectable.

Common mistakes

  • Missing type hints or Args: docs. smolagents raises an error for many of these. When it doesn’t, the model guesses. Annotate everything.
  • Authorising powerful imports to “make the error go away”. os, subprocess and friends turn model code into a full computer user. Wrap the capability in a narrow tool instead.
  • Running CodeAgent on untrusted input outside a sandbox. Fine with fake data on your laptop. Not fine with real customer emails.
  • Letting the model do arithmetic in prose. Put calculations in tools or in code actions. Language models write plausible numbers; Python computes correct ones.
  • Choosing a framework before understanding the loop. smolagents is your hand-written loop with a code executor in the middle. If something goes wrong, think in those terms.

Recap

  • A tool-calling agent acts with one JSON request at a time. A code agent acts by writing Python that calls tools as functions.
  • Code actions shine for loops, aggregation and maths: fewer steps, exact numbers, every calculation visible in the log.
  • In smolagents, a tool is a function with type hints and an Args: docstring decorated with @tool, or a Tool subclass when it needs setup.
  • Any model can drive it: Hub models through InferenceClientModel, APIs like Claude through LiteLLMModel.
  • Model-written code is untrusted. Keep powerful capabilities inside narrow tools, and run code agents in a sandbox.

On Monday morning, Arjun showed Meera the answer and, more importantly, the code behind it. “Here’s how the total was calculated, line by line. If a number looks wrong, you can see exactly where it came from. It took twelve seconds and the maths is done by Python, not guessed.”

“Good. Kothrud gets the new oven.” Meera turned to go, then stopped. “One more thing. What are the other bakeries in Pune charging for eggless cakes? I don’t want to be the expensive one.”

Arjun opened his mouth, then closed it. His agent knew everything inside Crumb and nothing outside it. “It needs the internet,” he said. “And while we’re at it, the real order database instead of my fake dictionary.” Priya winced at while we’re at it. “Two of the most useful tools you’ll ever give an agent,” she said, “and the two most likely to get you in trouble.”

Go deeper

Check your understanding

3 quick questions
1. Why did the code agent answer the six-store sales question in fewer steps than the tool-calling agent?
2. What does smolagents need from a function decorated with @tool to describe it to the model?
3. Arjun wants a code agent to read CSV reports. Which approach follows least privilege?