How to Use AutoGen with Tools (Web Scraper, Calculator, File)
Learn how to equip AutoGen agents with custom tools like web scrapers, calculators, and file handlers using register_for_llm and register_for_execution.
Get more content like this on Telegram!
Daily AI tips, notes & resources — free
Tool-using agents are where AutoGen really earns its place in my workflow. A plain conversational agent is impressive, but the moment you hand it a working web scraper, a reliable calculator, and a file reader, the whole thing transforms into something I actually ship to production.
This guide walks through exactly how to give AutoGen agents real tools using register_for_llm and register_for_execution. I'll cover a web scraper, a calculator, and file operations, then show you how they compose together. By the end you'll have a reusable pattern you can drop into any AutoGen project.
If you're new to agents in general, check out AI agents explained before diving in. And if you've already built something with LangChain, Build AI agent with LangChain is a useful comparison point.
Why AutoGen's Tool Registration Matters
Most agent frameworks let you pass tools as a list. AutoGen is more explicit: you register a tool separately for the LLM (so the model knows the tool exists) and for execution (so the runtime can actually call the function). This two-step pattern gives you fine-grained control.
The separation means you can, for instance, describe a tool to the LLM without allowing execution — useful for dry-run testing. It also means the assistant and executor agents can be different objects, which is exactly how AutoGen's human-in-the-loop patterns work.
Project Setup
pip install pyautogen requests beautifulsoup4 python-dotenv
Create a .env file:
OPENAI_API_KEY=sk-...
Base configuration used throughout:
import os
from dotenv import load_dotenv
import autogen
load_dotenv()
llm_config = {
"config_list": [
{
"model": "gpt-4o",
"api_key": os.getenv("OPENAI_API_KEY"),
}
],
"temperature": 0,
}
Setting temperature to zero is intentional. Tool-calling benefits from deterministic output — you want the model to pick the right tool with the right parameters, not explore creative alternatives.
The Two-Agent Pattern
Before adding tools, establish the base pattern AutoGen uses:
assistant = autogen.AssistantAgent(
name="assistant",
llm_config=llm_config,
system_message="You are a helpful assistant. Use the tools available to you to complete tasks accurately.",
)
user_proxy = autogen.UserProxyAgent(
name="user_proxy",
human_input_mode="NEVER",
max_consecutive_auto_reply=10,
code_execution_config=False, # we use function tools, not code execution
llm_config=False,
)
The user_proxy acts as the executor. With human_input_mode="NEVER" it runs autonomously. You can flip that to "ALWAYS" when you want approval before each tool call — a pattern covered in AI agent memory and planning.
Tool 1: Web Scraper
Let's build a scraper that fetches the plain text from any URL.
import requests
from bs4 import BeautifulSoup
from typing import Annotated
def scrape_webpage(
url: Annotated[str, "The full URL of the webpage to scrape, including https://"]
) -> str:
"""
Fetches the visible text content from a webpage.
Returns cleaned text with HTML stripped.
Useful for reading articles, documentation, or any public web page.
"""
try:
headers = {"User-Agent": "Mozilla/5.0 (compatible; AutoGenBot/1.0)"}
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
# Remove scripts, styles, nav clutter
for tag in soup(["script", "style", "nav", "footer", "header"]):
tag.decompose()
text = soup.get_text(separator="\n", strip=True)
# Truncate to avoid overwhelming the context window
return text[:4000] if len(text) > 4000 else text
except Exception as e:
return f"Error scraping {url}: {str(e)}"
Now register it with both agents:
# Register the tool description for the LLM
autogen.register_function(
scrape_webpage,
caller=assistant, # LLM that decides to call the tool
executor=user_proxy, # Agent that actually runs the function
name="scrape_webpage",
description="Fetch and return the visible text content from any webpage URL.",
)
The register_function helper is the cleaner way to do both registrations in one call. Under the hood it calls register_for_llm on the caller and register_for_execution on the executor.
Tool 2: Calculator
LLMs make arithmetic mistakes. A calculator tool is one of the highest-ROI additions you can make to any agent.
import ast
import operator as op
# Safe operator whitelist
SAFE_OPERATORS = {
ast.Add: op.add,
ast.Sub: op.sub,
ast.Mult: op.mul,
ast.Div: op.truediv,
ast.Pow: op.pow,
ast.USub: op.neg,
ast.Mod: op.mod,
}
def safe_eval(node):
if isinstance(node, ast.Constant):
return node.n
elif isinstance(node, ast.BinOp):
return SAFE_OPERATORS[type(node.op)](safe_eval(node.left), safe_eval(node.right))
elif isinstance(node, ast.UnaryOp):
return SAFE_OPERATORS[type(node.op)](safe_eval(node.operand))
else:
raise ValueError(f"Unsupported operation: {type(node)}")
def calculate(
expression: Annotated[str, "A mathematical expression as a string, e.g. '(42 * 3.14) / 2'"]
) -> str:
"""
Evaluates a mathematical expression safely without using eval().
Supports +, -, *, /, **, and % operators.
Returns the numeric result as a string.
"""
try:
tree = ast.parse(expression, mode="eval")
result = safe_eval(tree.body)
return str(result)
except Exception as e:
return f"Calculation error: {str(e)}"
autogen.register_function(
calculate,
caller=assistant,
executor=user_proxy,
name="calculate",
description="Safely evaluate a mathematical expression. Use this for any arithmetic, not mental math.",
)
The ast-based approach avoids the security risks of Python's built-in eval. Only whitelisted operations can run — no imports, no function calls.
Tool 3: File Operations
File tools round out a practical agent toolkit. I'll add three: read, write, and list directory.
import os
from pathlib import Path
OUTPUT_DIR = Path("./agent_output")
OUTPUT_DIR.mkdir(exist_ok=True)
def read_file(
filename: Annotated[str, "Name of the file to read (relative to the output directory)"]
) -> str:
"""
Reads and returns the text content of a file from the agent output directory.
Returns an error message if the file does not exist.
"""
try:
filepath = OUTPUT_DIR / filename
return filepath.read_text(encoding="utf-8")
except FileNotFoundError:
return f"File '{filename}' not found in output directory."
except Exception as e:
return f"Error reading file: {str(e)}"
def write_file(
filename: Annotated[str, "Name of the file to create or overwrite"],
content: Annotated[str, "The text content to write to the file"]
) -> str:
"""
Writes text content to a file in the agent output directory.
Creates the file if it does not exist, overwrites it if it does.
Returns a confirmation message with the file path.
"""
try:
filepath = OUTPUT_DIR / filename
filepath.write_text(content, encoding="utf-8")
return f"Successfully wrote {len(content)} characters to {filepath}"
except Exception as e:
return f"Error writing file: {str(e)}"
def list_files(
pattern: Annotated[str, "Optional glob pattern like '*.txt'. Use '*' for all files."] = "*"
) -> str:
"""
Lists files in the agent output directory matching a glob pattern.
Returns a newline-separated list of filenames.
"""
try:
files = list(OUTPUT_DIR.glob(pattern))
if not files:
return "No files found matching the pattern."
return "\n".join(f.name for f in files)
except Exception as e:
return f"Error listing files: {str(e)}"
for fn, desc in [
(read_file, "Read the text content of a file from the output directory."),
(write_file, "Write text content to a named file in the output directory."),
(list_files, "List files in the output directory, optionally filtered by pattern."),
]:
autogen.register_function(
fn,
caller=assistant,
executor=user_proxy,
name=fn.__name__,
description=desc,
)
Confining writes to OUTPUT_DIR is important. Without that constraint, an agent with enough autonomy could write anywhere on your filesystem.
Putting It All Together
Here's a complete example that chains all three tools:
task = """
1. Scrape the Wikipedia page for Python programming language: https://en.wikipedia.org/wiki/Python_(programming_language)
2. Calculate how many years Python has existed if it was created in 1991 and the current year is 2026.
3. Write a summary file called 'python_summary.txt' with: the first 200 characters of the scraped text, the age calculation result, and today's date.
4. List all files in the output directory to confirm the file was created.
"""
user_proxy.initiate_chat(
assistant,
message=task,
)
Running this produces a conversation where the assistant calls scrape_webpage, then calculate, then write_file, then list_files — each tool returning real data that feeds into the next step.
Using register_for_llm and register_for_execution Directly
Sometimes you want more control than register_function offers. Here's the explicit approach:
from autogen import AssistantAgent, UserProxyAgent
assistant = AssistantAgent(name="assistant", llm_config=llm_config)
user_proxy = UserProxyAgent(name="user_proxy", human_input_mode="NEVER")
# Step 1: Tell the LLM the tool exists
@assistant.register_for_llm(name="get_weather", description="Get current weather for a city.")
def get_weather_schema(city: Annotated[str, "City name"]) -> str:
... # Schema only, not executed here
# Step 2: Bind the actual function for execution
@user_proxy.register_for_execution(name="get_weather")
def get_weather(city: Annotated[str, "City name"]) -> str:
# Real implementation
import requests
r = requests.get(f"https://wttr.in/{city}?format=3")
return r.text if r.ok else f"Could not fetch weather for {city}"
The decorator pattern is useful when you want the schema and implementation in different parts of your codebase.
Tool Performance Comparison
| Tool Type | Avg Latency | Token Cost | Failure Rate | Best Use Case |
|---|---|---|---|---|
| Calculator | <ms | Low | <% | Any arithmetic |
| File read/write | 2-10ms | Low | <% | Persistent storage |
| Web scraper | 500ms-3s | Medium | 5-15% | Live data fetching |
| API call | 100ms-2s | Low-Medium | 3-10% | Structured data |
Web scraping has the highest failure rate because external sites change structure, go offline, or block bots. Always handle those failures gracefully as shown in the error-handling pattern above.
Advanced Pattern: Tool with Validation
For production use, add input validation to your tools:
import re
from urllib.parse import urlparse
def scrape_webpage_safe(
url: Annotated[str, "The HTTPS URL to scrape. Must start with https://"]
) -> str:
"""
Fetches visible text from a webpage. Only HTTPS URLs are accepted.
Rejects localhost, private IPs, and non-HTTP schemes for security.
"""
# Validate URL
parsed = urlparse(url)
if parsed.scheme != "https":
return "Error: Only HTTPS URLs are accepted."
if parsed.hostname in ("localhost", "127.0.0.1", "0.0.0.0"):
return "Error: Local addresses are not allowed."
# Check for private IP ranges (simplified)
if re.match(r"^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)", parsed.hostname or ""):
return "Error: Private IP addresses are not allowed."
return scrape_webpage(url)
This matters if your agent is exposed to user input — you don't want it scraping internal services.
Connecting to Larger Architectures
Tools become even more powerful in multi-agent setups. A CrewAI tutorial shows how multiple specialized agents share tools. For agents that need to remember tool outputs across sessions, the memory patterns in AI agent memory and planning are directly applicable.
If you're planning to deploy this to production, Deploy AI model to production covers infrastructure considerations that apply equally to tool-using agents.
Common Mistakes
Forgetting the executor registration. The LLM calls the tool, but nothing happens. Always check both registrations.
Overly long tool docstrings. Every word in a tool description consumes tokens on every request. Aim for one clear sentence per tool.
No timeout on HTTP tools. Without a timeout, a slow website hangs your entire agent. Always set timeout=10 or lower.
Returning large payloads. Truncate scraper output before returning. Returning 50,000 characters fills your context window immediately.
Using eval in calculator tools. Use the ast approach shown above. eval is a serious security risk in agent contexts.
What's Next
With web scraping, calculation, and file I/O covered, your agents can handle a wide range of real-world tasks. The natural next step is adding structured data sources — APIs, databases, or vector stores. The Vector database guide is a good jumping-off point if you want agents that can search their own memory.
For a complete end-to-end research agent that uses these patterns in a realistic scenario, see AI research agent build.
FAQ
What is the difference between register_for_llm and register_for_execution in AutoGen?
register_for_llm tells the language model that a tool exists and describes what it does so the model can decide when to call it. register_for_execution actually binds the Python function so AutoGen can run it when the model requests it. You need both registrations for a tool to work end-to-end.
Can AutoGen tools make real HTTP requests?
Yes. Any standard Python library or third-party package can be used inside a tool function. You can use requests, httpx, playwright, or BeautifulSoup inside your tool and AutoGen will execute it exactly like any other Python code.
How do I handle tool errors gracefully in AutoGen? Wrap the body of your tool function in a try/except block and return a descriptive error string instead of raising. The LLM will read the error message and can decide to retry with different parameters or report the failure to the user.
Is it safe to give AutoGen file-write permissions? It depends on your use case. In development, sandboxing to a specific output directory is the safest approach. In production, consider using the DockerCommandLineCodeExecutor to isolate file operations inside a container.
How many tools can a single AutoGen agent use? There is no hard limit set by AutoGen itself. Practical limits come from the context window of the underlying LLM — each tool description consumes tokens. Keeping tool docstrings concise and relevant helps you scale to 10-20 tools without issues.
Frequently Asked Questions
AiTechWorlds Team
✓ Verified WriterThe AiTechWorlds team is passionate about AI, technology, and education. We create high-quality, research-backed content to help you learn, grow, and succeed in the modern digital world.
Related Articles
5 AutoGen Agent Roles (Assistant, UserProxy, CodeExecutor)
Understand the 5 core AutoGen agent types — AssistantAgent, UserProxyAgent, CodeExecutorAgent, and more — with code examples and a comparison table for each role.
How to Deploy AutoGen Agents as APIs with FastAPI (2026)
Learn to serve AutoGen multi-agent systems as production REST APIs using FastAPI with async endpoints and real-time streaming responses.
How to Use AutoGen with Azure OpenAI (Enterprise Security)
Connect Microsoft AutoGen to Azure OpenAI for enterprise-grade AI agents. Step-by-step setup with private endpoints, OAI_CONFIG_LIST, and deployment config.
Build a Code Debugging Agent with AutoGen (Auto-Fix PRs)
Build an AutoGen agent that reviews code, analyzes PR diffs, suggests fixes, and automates code quality improvements with a full working implementation.