Building Code-Executing AI Agents with Smolagents: A Developer’s Blueprint

Traditional JSON tool-calling frameworks force language models to navigate rigid, multi-turn string parsing just to execute basic operations. When an agent needs to fetch data from an API, perform math on the output, and pass that transformed data into a second function, standard LLM tool chains turn into a slow, error-prone game of telephone. Every action requires a separate JSON round-trip, bloating latency, spiking API costs, and increasing failure points.

Hugging Face’s smolagents framework flips this design on its head. Built in approximately 1,000 lines of clean Python code, smolagents centers on a bold premise: language models perform significantly better when they generate and execute standard Python code instead of raw JSON parameters.

This blueprint covers how to build secure, high-performance, code-executing agents with smolagents, complete with dynamic tool building, sandboxed runtime security, and custom model routing.

Why Code Agents Outperform JSON Tool Calling

Standard tool calling asks a model to output a structured JSON schema:

{
  "name": "search_docs",
  "arguments": { "query": "latency metrics" }
}

If the agent needs to loop over five items, filter out non-zero results, and calculate an average, a JSON-based agent must execute five distinct LLM turns.

smolagents uses CodeAgent, allowing the model to express actions directly as executable Python snippets:

results = [search_docs(item) for item in items]
filtered = [r for r in results if r["latency"] > 0]
avg_latency = sum(r["latency"] for r in filtered) / len(filtered)
print(avg_latency)

Key Architectural Advantages

  • Native Control Flow: The model handles conditionals, loops, error handling, and variable storage within a single execution block.
  • State Management: Intermediary object instances (like pandas DataFrames, Pillow images, or custom class instances) stay in memory between steps without getting converted to strings.
  • Reduced Token Footprint: Combining multiple tool interactions into a single code block dramatically reduces round-trip API calls and context consumption.

Step-by-Step Blueprint: Constructing Your First Code-Executing Agent

Building an production-ready agent with smolagents requires three primary components: an LLM engine, tools, and an execution runtime.

Step 1: Environment Setup and Library Installation

First, install smolagents alongside your preferred model runner backend.

pip install smolagents duckduckgo-search requests

Step 2: Defining Custom Tools with Python Decorators

smolagents makes custom tool creation frictionless. You convert standard Python functions into agent tools using the @tool decorator. The library uses function type hints and docstrings to generate descriptions for the agent.

from typing import Optional
import requests
from smolagents import tool

@tool
def fetch_system_status(service_name: str, region: Optional[str] = "us-east-1") -> str:
    """
    Queries internal health endpoints to retrieve service metrics.

    Args:
        service_name: The name of the target microservice (e.g., 'auth-service').
        region: The AWS cloud region where the service is deployed. Defaults to 'us-east-1'.
    """
    # Simulate an internal API call
    api_url = f"https://api.internal.techelite.org/health/{service_name}?region={region}"
    
    # Imports must live inside the function for clean modular isolation
    import json
    
    return json.dumps({
        "service": service_name,
        "region": region,
        "status": "healthy",
        "cpu_utilization": 42.5,
        "memory_usage_mb": 1024
    })

Step 3: Instantiating the Model and Agent

Select an LLM model provider using LiteLLMModel (for OpenAI, Anthropic, or local Ollama instances) or InferenceClientModel for Hugging Face Hub inference.

from smolagents import CodeAgent, DuckDuckGoSearchTool, LiteLLMModel

# Initialize the model engine via LiteLLM
model = LiteLLMModel(model_id="gpt-4o-mini")

# Assemble the CodeAgent with built-in and custom tools
agent = CodeAgent(
    tools=[DuckDuckGoSearchTool(), fetch_system_status],
    model=model,
    additional_authorized_imports=["datetime", "json", "math"]
)

# Run the agent with a complex query
response = agent.run(
    "Check the system status for 'auth-service'. "
    "If CPU utilization is under 50%, search DuckDuckGo for recent "
    "deployment best practices for light workloads."
)

print(response)

Hardening Security: Sandboxing and Import Controls

Allowing an LLM to generate and run arbitrary code on production infrastructure demands strict isolation. smolagents implements multi-tiered security features to prevent unchecked execution.

┌──────────────────────────────────────────────────────────┐
│                   LLM (Code Generation)                  │
└─────────────────────────────┬────────────────────────────┘
                              │ Generated Python Code
                              ▼
┌──────────────────────────────────────────────────────────┐
│                 Smolagents AST Parser                    │
│   • Blocks syntax-level threats                          │
│   • Restricts unauthorized module imports                │
└─────────────────────────────┬────────────────────────────┘
                              │ Sanitized Execution Tree
                              ▼
┌──────────────────────────────────────────────────────────┐
│              Sandboxed Runtime Isolation                 │
│   • Local Restricted Environment                         │
│   • External Containers (Docker / E2B Sandbox API)       │
└──────────────────────────────────────────────────────────┘

Import Whitelisting

By default, smolagents blocks arbitrary standard library and third-party imports. If the generated code attempts to call import os or import subprocess without authorization, the parser halts execution and alerts the agent to rethink its approach. Explicitly grant authorization using additional_authorized_imports:

agent = CodeAgent(
    tools=[fetch_system_status],
    model=model,
    # Restrict permissions strictly to safe, required packages
    additional_authorized_imports=["math", "datetime", "re"]
)

Isolated Sandbox Environments

For enterprise production deployments handling untrusted user input, execution should move entirely off the application host machine. Configure remote sandboxing backends using E2B, Docker, or Blaxel:

# Executing code inside isolated E2B micro-VM containers
agent = CodeAgent(
    tools=[fetch_system_status],
    model=model,
    executor_type="e2b"  # Routes code execution to a remote E2B sandbox container
)

Pro-Tip: Fixing the “Import State Leak” Pitfall

A common trap for developers migrating to smolagents involves defining dependencies at the root level of custom tool modules rather than inside the decorated function.

# ❌ INCORRECT: Root-level import breaks tool portability
import requests
from smolagents import tool

@tool
def bad_weather_tool(location: str) -> str:
    """Fetches current weather for a location."""
    res = requests.get(f"https://wttr.in/{location}?format=3")
    return res.text

Why this fails: When smolagents pushes custom tools across remote sandboxes (such as E2B or Hugging Face Spaces), root-level imports fail to serialize properly.

The Fix: Always write self-contained tools with imports placed strictly inside the function body.

# ✅ CORRECT: Fully self-contained tool design
from smolagents import tool

@tool
def good_weather_tool(location: str) -> str:
    """Fetches current weather for a location.
    
    Args:
        location: City or region name.
    """
    import requests  # Scoped import enables seamless remote sandbox execution
    
    res = requests.get(f"https://wttr.in/{location}?format=3")
    return res.text

Next Steps for Developers

Shifting from standard JSON tool calling to code-executing agents cuts system latency, simplifies multi-step reasoning, and keeps your codebase readable.

To take your setup further:

  1. Audit your current tool set: Identify complex multi-step JSON agent workflows and convert them to CodeAgent tasks.
  2. Lock down execution: Enable executor_type="e2b" or standard Docker isolation before opening endpoint access to production traffic.
  3. Inspect agent execution logs: Trace generated code by inspecting agent.logs during local integration testing.

Leave a Reply

Your email address will not be published. Required fields are marked *