Learnixo

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

Python
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:

  1. Inspects the function signature and type annotations
  2. Reads the description parameter and the docstring
  3. Generates a JSON schema for the function arguments
  4. Injects the tool schema into the LLM config as function definitions

You can inspect the generated schema:

Python
# After registration, inspect the LLM config to see tool schemas
import json
print(json.dumps(assistant.llm_config.get("functions", []), indent=2))

Output:

JSON
[
  {
    "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

Python
# 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:

Python
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:

Python
@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:

Python
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_llm exposes a function's schema to the LLM so it knows the tool exists
  • @register_for_execution tells 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.