AutoGen Essentials · Lesson 5 of 11
Registering Functions as Agent Tools
Beyond Inline Code Generation
In the previous lesson, the assistant generated code inline — it wrote Python in a code block, and the user proxy executed it. This works well for ad-hoc programming tasks.
But sometimes you want agents to call pre-built, trusted functions rather than generating arbitrary code. Reasons include:
- Security: restrict agents to approved operations only (no arbitrary code execution)
- Reliability: tested, production-ready functions with proper error handling
- Integration: call internal APIs, databases, or services with authentication
- Consistency: ensure agents use the same logic as the rest of your system
AutoGen's tool registration system lets you expose Python functions to the LLM as callable tools, complete with automatic schema generation.
How Tool Registration Works
AutoGen uses two decorators to register tools:
| Decorator | Applied to | Purpose |
|---|---|---|
| @register_for_llm | AssistantAgent | Tells the LLM this function exists and what it does |
| @register_for_execution | UserProxyAgent | Tells the user proxy to actually execute the function |
The separation is intentional: the LLM knows about the function (via the schema in its system prompt), but the actual execution happens in the user proxy — maintaining the separation of thinking and doing.
Tool Registration: The Decorator Pattern
import autogen
import os
llm_config = {
"config_list": [{"model": "gpt-4o-mini", "api_key": os.environ["OPENAI_API_KEY"]}],
"temperature": 0,
}
assistant = autogen.AssistantAgent(
name="assistant",
llm_config=llm_config,
system_message="You are a helpful assistant. Use tools to answer questions accurately.",
)
user_proxy = autogen.UserProxyAgent(
name="user_proxy",
human_input_mode="NEVER",
max_consecutive_auto_reply=5,
is_termination_msg=lambda msg: "TERMINATE" in msg.get("content", ""),
code_execution_config=False, # we are using tools, not inline code execution
)
# ─── Tool 1: Stock Price Lookup ───────────────────────────────────────────────
@user_proxy.register_for_execution()
@assistant.register_for_llm(
name="get_stock_price",
description=(
"Get the current stock price for a given ticker symbol. "
"Returns the price in USD. Use this for real-time stock queries."
),
)
def get_stock_price(ticker: str) -> dict:
"""
Fetch the current stock price for a given ticker symbol.
In production this would call a real market data API.
For this demo we use static data.
Args:
ticker: The stock ticker symbol (e.g., 'AAPL', 'MSFT', 'GOOGL').
Returns:
A dict with 'ticker', 'price', 'currency', and 'change_pct'.
"""
# Mock data — replace with real API call in production
prices = {
"AAPL": {"price": 211.45, "change_pct": 1.23},
"MSFT": {"price": 415.87, "change_pct": -0.54},
"GOOGL": {"price": 178.32, "change_pct": 2.01},
"AMZN": {"price": 192.76, "change_pct": 0.88},
"NVDA": {"price": 912.34, "change_pct": 3.45},
}
ticker = ticker.upper().strip()
if ticker not in prices:
return {"error": f"Ticker '{ticker}' not found. Supported: {list(prices.keys())}"}
data = prices[ticker]
return {
"ticker": ticker,
"price": data["price"],
"currency": "USD",
"change_pct": data["change_pct"],
}
# ─── Tool 2: Database Query ───────────────────────────────────────────────────
@user_proxy.register_for_execution()
@assistant.register_for_llm(
name="query_sales_db",
description=(
"Query the sales database for revenue data. "
"Provide a product name and optional date range. "
"Returns a list of sale records."
),
)
def query_sales_db(
product: str,
start_date: str = "2026-01-01",
end_date: str = "2026-12-31",
) -> dict:
"""
Query the sales database.
Args:
product: Product name to filter by.
start_date: Start date in YYYY-MM-DD format.
end_date: End date in YYYY-MM-DD format.
Returns:
A dict with 'product', 'total_revenue', 'units_sold', and 'records'.
"""
# Mock database — replace with real DB call in production
mock_db = {
"widget_pro": [
{"date": "2026-01-15", "units": 145, "revenue": 14500.00},
{"date": "2026-02-20", "units": 212, "revenue": 21200.00},
{"date": "2026-03-08", "units": 178, "revenue": 17800.00},
],
"gadget_lite": [
{"date": "2026-01-22", "units": 89, "revenue": 4450.00},
{"date": "2026-02-14", "units": 134, "revenue": 6700.00},
],
}
product_key = product.lower().replace(" ", "_")
records = mock_db.get(product_key, [])
if not records:
return {
"error": f"No data found for product '{product}'. "
f"Available products: {[k.replace('_', ' ') for k in mock_db.keys()]}"
}
total_revenue = sum(r["revenue"] for r in records)
units_sold = sum(r["units"] for r in records)
return {
"product": product,
"start_date": start_date,
"end_date": end_date,
"total_revenue": total_revenue,
"units_sold": units_sold,
"records": records,
}How the LLM Discovers Tools
When you use @register_for_llm, AutoGen automatically:
- Inspects the function signature and type annotations
- Reads the
descriptionparameter and the docstring - Generates a JSON schema for the function arguments
- Injects the tool schema into the LLM config as function definitions
You can inspect the generated schema:
# After registration, inspect the LLM config to see tool schemas
import json
print(json.dumps(assistant.llm_config.get("functions", []), indent=2))Output:
[
{
"name": "get_stock_price",
"description": "Get the current stock price for a given ticker symbol. Returns the price in USD. Use this for real-time stock queries.",
"parameters": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "The stock ticker symbol (e.g., 'AAPL', 'MSFT', 'GOOGL')."
}
},
"required": ["ticker"]
}
},
{
"name": "query_sales_db",
"description": "Query the sales database for revenue data. ...",
"parameters": {
"type": "object",
"properties": {
"product": {"type": "string"},
"start_date": {"type": "string", "default": "2026-01-01"},
"end_date": {"type": "string", "default": "2026-12-31"}
},
"required": ["product"]
}
}
]The LLM now knows about these tools and can decide when to call them based on the user's question.
Running the Tool-Enabled Conversation
# Ask a question that requires using tools
user_proxy.initiate_chat(
assistant,
message=(
"What is the current price of AAPL and MSFT? "
"Also, how much revenue did Widget Pro generate this year? "
"Summarise everything and then say TERMINATE."
),
)Expected conversation flow:
user_proxy → assistant:
"What is the current price of AAPL and MSFT? Also, how much revenue
did Widget Pro generate this year? Summarise everything and then say TERMINATE."
assistant → user_proxy:
[Calls tool: get_stock_price(ticker="AAPL")]
user_proxy → assistant:
[Tool result: {"ticker": "AAPL", "price": 211.45, "currency": "USD", "change_pct": 1.23}]
assistant → user_proxy:
[Calls tool: get_stock_price(ticker="MSFT")]
user_proxy → assistant:
[Tool result: {"ticker": "MSFT", "price": 415.87, "currency": "USD", "change_pct": -0.54}]
assistant → user_proxy:
[Calls tool: query_sales_db(product="Widget Pro")]
user_proxy → assistant:
[Tool result: {"product": "Widget Pro", "total_revenue": 53500.0, "units_sold": 535, ...}]
assistant → user_proxy:
"Here is the summary:
- AAPL: $211.45 (+1.23%)
- MSFT: $415.87 (-0.54%)
- Widget Pro YTD revenue: $53,500 across 535 units sold
TERMINATE"Tool Registration Without Decorators
If you prefer an explicit API over decorators, AutoGen also supports registering tools directly:
def calculate_compound_interest(
principal: float,
annual_rate: float,
years: int,
compounds_per_year: int = 12,
) -> dict:
"""
Calculate compound interest.
Args:
principal: Initial investment in dollars.
annual_rate: Annual interest rate as a decimal (0.05 = 5%).
years: Number of years.
compounds_per_year: How many times per year interest compounds.
Returns:
A dict with 'final_amount' and 'total_interest_earned'.
"""
amount = principal * (1 + annual_rate / compounds_per_year) ** (compounds_per_year * years)
return {
"principal": principal,
"final_amount": round(amount, 2),
"total_interest_earned": round(amount - principal, 2),
"years": years,
"annual_rate_pct": annual_rate * 100,
}
# Register without decorators
autogen.register_function(
calculate_compound_interest,
caller=assistant, # the agent that can decide to call this tool
executor=user_proxy, # the agent that actually executes it
name="calculate_compound_interest",
description=(
"Calculate compound interest given a principal, rate, and time period. "
"Use this for investment or loan calculations."
),
)The autogen.register_function approach is equivalent to the decorator pattern but is easier to use in dynamic scenarios where you need to register tools conditionally or in a loop.
Error Handling in Tools
Always return structured errors from tools — never raise unhandled exceptions:
@user_proxy.register_for_execution()
@assistant.register_for_llm(
name="get_exchange_rate",
description="Get the exchange rate between two currencies.",
)
def get_exchange_rate(from_currency: str, to_currency: str) -> dict:
"""Get exchange rate between two currencies."""
try:
# Simulate an API call
supported = {"USD", "EUR", "GBP", "JPY", "AUD"}
from_currency = from_currency.upper()
to_currency = to_currency.upper()
if from_currency not in supported:
return {
"error": f"Unsupported currency: {from_currency}. Supported: {list(supported)}"
}
if to_currency not in supported:
return {
"error": f"Unsupported currency: {to_currency}. Supported: {list(supported)}"
}
# Mock rates (base: USD)
rates = {"USD": 1.0, "EUR": 0.92, "GBP": 0.79, "JPY": 149.5, "AUD": 1.53}
rate = rates[to_currency] / rates[from_currency]
return {
"from_currency": from_currency,
"to_currency": to_currency,
"rate": round(rate, 6),
"example": f"1 {from_currency} = {round(rate, 4)} {to_currency}",
}
except Exception as e:
# Return error as data — let the LLM handle it gracefully
return {"error": f"Unexpected error: {str(e)}"}The LLM will read the error field and either retry with corrected parameters or explain the failure to the user.
Multiple Tools in One Agent Setup
Here is a complete, production-style configuration with three tools, proper error handling, and clean termination:
import autogen
import os
from datetime import datetime
llm_config = {
"config_list": [{"model": "gpt-4o-mini", "api_key": os.environ["OPENAI_API_KEY"]}],
"temperature": 0,
}
assistant = autogen.AssistantAgent(
name="financial_assistant",
llm_config=llm_config,
system_message="""You are a financial assistant with access to market data tools.
Available tools:
- get_stock_price: current price for a ticker
- get_exchange_rate: exchange rate between two currencies
- calculate_compound_interest: investment growth calculator
Always use tools rather than making up numbers.
After answering the user's question completely, say TERMINATE.
""",
)
user_proxy = autogen.UserProxyAgent(
name="user_proxy",
human_input_mode="NEVER",
max_consecutive_auto_reply=10,
is_termination_msg=lambda msg: "TERMINATE" in msg.get("content", ""),
code_execution_config=False,
)
# Register all three tools
@user_proxy.register_for_execution()
@assistant.register_for_llm(
name="get_stock_price",
description="Get current stock price for a ticker symbol.",
)
def get_stock_price(ticker: str) -> dict:
prices = {"AAPL": 211.45, "MSFT": 415.87, "GOOGL": 178.32}
t = ticker.upper()
return (
{"ticker": t, "price": prices[t], "currency": "USD"}
if t in prices
else {"error": f"Unknown ticker: {t}"}
)
@user_proxy.register_for_execution()
@assistant.register_for_llm(
name="get_exchange_rate",
description="Get exchange rate between two currencies (e.g., USD to EUR).",
)
def get_exchange_rate(from_currency: str, to_currency: str) -> dict:
rates = {"USD": 1.0, "EUR": 0.92, "GBP": 0.79}
f, t = from_currency.upper(), to_currency.upper()
if f not in rates or t not in rates:
return {"error": f"Unsupported currencies. Supported: {list(rates.keys())}"}
return {"from": f, "to": t, "rate": round(rates[t] / rates[f], 6)}
@user_proxy.register_for_execution()
@assistant.register_for_llm(
name="calculate_compound_interest",
description="Calculate compound interest. Returns final amount and interest earned.",
)
def calculate_compound_interest(
principal: float, annual_rate: float, years: int
) -> dict:
amount = principal * (1 + annual_rate) ** years
return {
"principal": principal,
"final_amount": round(amount, 2),
"interest_earned": round(amount - principal, 2),
}
# Run a multi-tool query
user_proxy.initiate_chat(
assistant,
message=(
"I want to invest $10,000 in AAPL stock. "
"What is the current price? How many shares can I buy? "
"If the stock grows at 8% per year for 10 years, what will my investment be worth? "
"Also, what is the USD to EUR exchange rate so I can convert the final amount?"
),
)Summary
@register_for_llmexposes a function's schema to the LLM so it knows the tool exists@register_for_executiontells the user proxy to actually run the function when called- Tool registration generates a JSON schema from your type annotations and docstrings — write them carefully
autogen.register_function()is the non-decorator alternative for dynamic registration- Always return structured dicts (including error cases) — never raise unhandled exceptions
- Tool-based agents are safer than inline code execution when you need to restrict agent capabilities
Next: we go deeper into code execution — AutoGen's most powerful (and riskiest) feature.