Introduction to Tool Use Patterns with AutoGen
AutoGen, Microsoft's multi-agent conversation framework, has become one of the most popular libraries for building LLM-powered applications. One of its most powerful features is the ability to equip agents with tools — external functions they can call to interact with the world, retrieve data, or perform computations. Understanding tool use patterns is essential for building robust, production-grade agentic systems.
In this guide, we'll explore the core tool use patterns in AutoGen, from basic function registration to advanced multi-agent orchestration with shared and specialized tools. Whether you're building a simple assistant or a complex multi-agent pipeline, these patterns will help you design systems that are reliable, maintainable, and extensible.
What Is Tool Use in AutoGen?
Tool use in AutoGen refers to the mechanism by which agents can invoke external Python functions during a conversation. When an agent is configured with tools, the underlying LLM can decide to call those functions based on the user's request. AutoGen handles the execution of the function and feeds the result back into the conversation, allowing the agent to reason about the output and take further action.
This is fundamentally different from simple prompt-based interactions. Instead of just generating text, the agent can:
- Query databases or APIs for real-time information
- Perform precise mathematical calculations
- Execute code in a sandboxed environment
- Interact with file systems, web browsers, or other services
- Trigger downstream workflows and pipelines
The key abstraction in AutoGen is the ConversableAgent, which supports tool registration through decorators or explicit method calls. When tools are registered, AutoGen automatically generates the appropriate function schemas and passes them to the LLM as callable functions.
Why Tool Use Patterns Matter
LLMs alone are powerful but limited. They hallucinate facts, struggle with precise arithmetic, have knowledge cutoffs, and cannot interact with external systems. Tools bridge these gaps. However, how you structure tool use across your agents dramatically affects the behavior, reliability, and cost of your system.
Consider these scenarios where tool use patterns become critical:
- Single-agent with many tools: A personal assistant that can search the web, read emails, and manage a calendar. Too many tools can confuse the LLM and increase token costs.
- Multi-agent with specialized tools: A research team where one agent searches the web, another analyzes data, and a third writes reports. Each agent has a focused toolset.
- Shared tools across agents: Multiple agents that can all access a common database but use it for different purposes.
- Tool chaining: The output of one tool feeds into another, requiring careful orchestration.
Choosing the right pattern for your use case is what separates a toy demo from a production system.
Getting Started: Basic Tool Registration
Let's start with the simplest pattern: registering a single tool with a single agent. AutoGen provides the @user_proxy.register_for_execution() and @assistant.register_for_llm() decorators, or you can use the unified register_for_llm and register_for_execution methods.
Prerequisites and Setup
First, install AutoGen and set up your environment:
pip install "pyautogen>=0.2.0"
Configure your LLM. You'll need an OpenAI API key (or another supported backend):
import os
import autogen
os.environ["OPENAI_API_KEY"] = "your-api-key-here"
llm_config = {
"config_list": [
{
"model": "gpt-4o",
"api_key": os.environ["OPENAI_API_KEY"],
}
],
}
Your First Tool
Let's create a simple agent that can calculate compound interest. We'll define a Python function and register it as a tool:
import autogen
# Define the LLM configuration
llm_config = {
"config_list": [{"model": "gpt-4o", "api_key": os.environ["OPENAI_API_KEY"]}],
}
# Create the assistant agent
assistant = autogen.ConversableAgent(
name="FinancialAssistant",
system_message="You are a helpful financial assistant. "
"Use the compound_interest tool for any interest calculations. "
"Always show your work and explain the results clearly.",
llm_config=llm_config,
)
# Create the user proxy agent (executes tools)
user_proxy = autogen.UserProxyAgent(
name="User",
human_input_mode="NEVER",
is_termination_msg=lambda msg: msg.get("content") is not None and "TERMINATE" in msg["content"],
code_execution_config=False, # We're using explicit tools, not code execution
)
# Define the tool function
@user_proxy.register_for_execution()
@assistant.register_for_llm(description="Calculate compound interest given principal, rate, time, and compounding frequency.")
def compound_interest(
principal: float,
annual_rate: float,
years: float,
compounds_per_year: int = 12,
) -> str:
"""Calculate the future value of an investment with compound interest.
Args:
principal: The initial investment amount in dollars.
annual_rate: The annual interest rate as a decimal (e.g., 0.05 for 5%).
years: The number of years the money is invested.
compounds_per_year: How many times per year interest is compounded.
Returns:
A string describing the result.
"""
amount = principal * (1 + annual_rate / compounds_per_year) ** (compounds_per_year * years)
interest_earned = amount - principal
return (
f"Future value: ${amount:,.2f}\n"
f"Interest earned: ${interest_earned:,.2f}\n"
f"Principal: ${principal:,.2f}\n"
f"Rate: {annual_rate*100:.2f}%\n"
f"Time: {years} years\n"
f"Compounding: {compounds_per_year}x per year"
)
# Start the conversation
user_proxy.initiate_chat(
assistant,
message="I want to invest $10,000 at 5% annual interest for 10 years, compounded monthly. How much will I have?",
)
When you run this, the assistant recognizes the financial question, calls the compound_interest tool with the appropriate arguments, receives the result, and presents it to the user in a natural way. The key elements here are:
- The
descriptionparameter tells the LLM when to use the tool. - The type hints and docstring help the LLM understand what arguments to pass.
- The function is registered for both execution (on the user proxy) and for the LLM (on the assistant).
Pattern 1: The Single-Agent Multi-Tool Pattern
The most common pattern is a single assistant agent equipped with multiple tools. This works well for focused applications where one agent handles a variety of related tasks. The key challenge is keeping the toolset coherent and not overwhelming the LLM.
import autogen
import requests
import json
from datetime import datetime
llm_config = {
"config_list": [{"model": "gpt-4o", "api_key": os.environ["OPENAI_API_KEY"]}],
}
assistant = autogen.ConversableAgent(
name="WeatherAssistant",
system_message=(
"You are a weather and travel assistant. You can check current weather, "
"get forecasts, and suggest what to pack. Always use the available tools "
"for weather data — never guess. Respond concisely."
),
llm_config=llm_config,
)
user_proxy = autogen.UserProxyAgent(
name="User",
human_input_mode="NEVER",
is_termination_msg=lambda msg: "TERMINATE" in (msg.get("content") or ""),
code_execution_config=False,
)
@user_proxy.register_for_execution()
@assistant.register_for_llm(description="Get current weather for a given city.")
def get_current_weather(city: str) -> str:
"""Fetch current weather conditions for a city.
Args:
city: The name of the city, e.g., 'San Francisco'.
"""
# Using a free weather API
url = f"https://wttr.in/{city}?format=j1"
resp = requests.get(url, timeout=10)
data = resp.json()
current = data["current_condition"][0]
return (
f"City: {city}\n"
f"Temperature: {current['temp_C']}°C / {current['temp_F']}°F\n"
f"Condition: {current['weatherDesc'][0]['value']}\n"
f"Humidity: {current['humidity']}%\n"
f"Wind: {current['windspeedKmph']} km/h"
)
@user_proxy.register_for_execution()
@assistant.register_for_llm(description="Get a multi-day weather forecast for a city.")
def get_forecast(city: str, days: int = 3) -> str:
"""Fetch a weather forecast for a city.
Args:
city: The name of the city.
days: Number of days to forecast (1-7).
"""
url = f"https://wttr.in/{city}?format=j1"
resp = requests.get(url, timeout=10)
data = resp.json()
forecasts = []
for day in data["weather"][:days]:
date = day["date"]
max_temp = day["maxtempC"]
min_temp = day["mintempC"]
desc = day["hourly"][4]["weatherDesc"][0]["value"]
forecasts.append(f"{date}: {min_temp}°C - {max_temp}°C, {desc}")
return f"Forecast for {city}:\n" + "\n".join(forecasts)
@user_proxy.register_for_execution()
@assistant.register_for_llm(description="Suggest what to pack based on weather conditions.")
def suggest_packing(temperature_c: float, condition: str, duration_days: int) -> str:
"""Generate a packing list based on weather.
Args:
temperature_c: Temperature in Celsius.
condition: Weather condition description.
duration_days: Trip duration in days.
"""
items = []
if temperature_c < 5:
items.extend(["Heavy coat", "Thermal underwear", "Gloves", "Scarf", "Warm hat"])
elif temperature_c < 15:
items.extend(["Jacket", "Long pants", "Light sweater"])
elif temperature_c < 25:
items.extend(["Light jacket", "T-shirts", "Jeans"])
else:
items.extend(["T-shirts", "Shorts", "Sunscreen", "Sunglasses"])
if "rain" in condition.lower() or "drizzle" in condition.lower():
items.extend(["Umbrella", "Waterproof shoes"])
if "snow" in condition.lower():
items.extend(["Waterproof boots", "Snow gloves"])
items.extend([f"{duration_days} pairs of socks", f"{duration_days} sets of underwear"])
return "Packing list:\n- " + "\n- ".join(items)
user_proxy.initiate_chat(
assistant,
message="I'm traveling to London for 5 days. What's the weather like and what should I pack?",
)
In this example, the assistant has three related tools. The LLM can chain them together: first checking the current weather, then getting a forecast, and finally suggesting a packing list. This chaining happens naturally through the conversation flow — the assistant calls one tool, reasons about the result, and decides whether to call another.
Pattern 2: The Multi-Agent Specialized Tool Pattern
As your application grows, a single agent with many tools becomes unwieldy. The LLM may struggle to choose the right tool, token costs increase with each additional tool schema, and the system becomes harder to debug. The solution is to split responsibilities across multiple agents, each with a focused toolset.
import autogen
llm_config = {
"config_list": [{"model": "gpt-4o", "api_key": os.environ["OPENAI_API_KEY"]}],
}
# --- Define specialized tools ---
def search_web(query: str) -> str:
"""Search the web for information."""
# Simulated web search
return f"Search results for '{query}': [Article 1: ...] [Article 2: ...]"
def analyze_data(data: str, analysis_type: str) -> str:
"""Perform statistical analysis on data."""
return f"Analysis ({analysis_type}) of data: mean=42.5, std=12.3, n=1000"
def generate_chart(data: str, chart_type: str) -> str:
"""Generate a chart from data."""
return f"Chart generated: {chart_type} chart saved to /tmp/chart.png"
def write_report(content: str, format: str = "markdown") -> str:
"""Write a formatted report."""
return f"Report written in {format} format. Length: {len(content)} characters."
# --- Create specialized agents ---
researcher = autogen.ConversableAgent(
name="Researcher",
system_message=(
"You are a research specialist. Your job is to search for information "
"using the search_web tool. Pass your findings to the DataAnalyst or "
"ReportWriter when appropriate. Be thorough and cite your sources."
),
llm_config=llm_config,
)
analyst = autogen.ConversableAgent(
name="DataAnalyst",
system_message=(
"You are a data analysis specialist. Use the analyze_data and generate_chart "
"tools to process data. Share your findings with the ReportWriter."
),
llm_config=llm_config,
)
writer = autogen.ConversableAgent(
name="ReportWriter",
system_message=(
"You are a professional report writer. Use the write_report tool to produce "
"the final deliverable. Synthesize information from other agents into a "
"coherent, well-structured report."
),
llm_config=llm_config,
)
# --- Create the user proxy ---
user_proxy = autogen.UserProxyAgent(
name="User",
human_input_mode="NEVER",
is_termination_msg=lambda msg: "TERMINATE" in (msg.get("content") or ""),
code_execution_config=False,
)
# --- Register tools with the appropriate agents ---
# Researcher gets the search tool
@user_proxy.register_for_execution()
@researcher.register_for_llm(description="Search the web for information on a topic.")
def search_web_tool(query: str) -> str:
return search_web(query)
# Analyst gets analysis and charting tools
@user_proxy.register_for_execution()
@analyst.register_for_llm(description="Perform statistical analysis on data.")
def analyze_data_tool(data: str, analysis_type: str = "summary") -> str:
return analyze_data(data, analysis_type)
@user_proxy.register_for_execution()
@analyst.register_for_llm(description="Generate a chart from data.")
def generate_chart_tool(data: str, chart_type: str = "bar") -> str:
return generate_chart(data, chart_type)
# Writer gets the report tool
@user_proxy.register_for_execution()
@writer.register_for_llm(description="Write a formatted report from content.")
def write_report_tool(content: str, format: str = "markdown") -> str:
return write_report(content, format)
# --- Set up the group chat ---
groupchat = autogen.GroupChat(
agents=[user_proxy, researcher, analyst, writer],
messages=[],
max_round=15,
speaker_selection_method="auto",
)
manager = autogen.GroupChatManager(
groupchat=groupchat,
llm_config=llm_config,
)
# --- Start the workflow ---
user_proxy.initiate_chat(
manager,
message="Research the impact of remote work on productivity, analyze the data, "
"and write a comprehensive report.",
)
This pattern provides several advantages:
- Focused context: Each agent's system prompt and toolset are tailored to its role, improving tool selection accuracy.
- Reduced token usage: Each LLM call only includes the tools relevant to that agent's role.
- Parallel potential: Agents can work on different aspects of the task simultaneously.
- Easier debugging: When something goes wrong, you can isolate which agent's tool use failed.
Pattern 3: The Shared Tool Registry Pattern
Sometimes multiple agents need access to the same tools. Rather than duplicating tool definitions, you can create a shared tool registry and register tools across multiple agents. This is useful when agents have overlapping capabilities but different specializations.
import autogen
from functools import wraps
llm_config = {
"config_list": [{"model": "gpt-4o", "api_key": os.environ["OPENAI_API_KEY"]}],
}
# --- Shared tool functions ---
def get_database_record(record_id: str) -> str:
"""Retrieve a record from the database."""
# Simulated database lookup
records = {
"001": "Customer: Acme Corp, Revenue: $1.2M, Status: Active",
"002": "Customer: Globex Inc, Revenue: $850K, Status: Churned",
"003": "Customer: Initech, Revenue: $430K, Status: Active",
}
return records.get(record_id, f"Record {record_id} not found.")
def log_action(action: str, agent_name: str, details: str = "") -> str:
"""Log an action to the audit trail."""
timestamp = datetime.now().isoformat()
return f"[{timestamp}] {agent_name}: {action} - {details}"
# --- Create a shared tool registry ---
class ToolRegistry:
"""A registry for sharing tools across multiple agents."""
def __init__(self):
self.tools = []
def register(self, description: str):
"""Decorator to register a tool with its description."""
def decorator(func):
self.tools.append({
"func": func,
"description": description,
"name": func.__name__,
})
return func
return decorator
def register_with_agent(self, agent: autogen.ConversableAgent,
user_proxy: autogen.UserProxyAgent,
tool_names: list = None):
"""Register selected or all tools with an agent."""
for tool in self.tools:
if tool_names and tool["name"] not in tool_names:
continue
func = tool["func"]
desc = tool["description"]
# Create a wrapper that preserves the function signature
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
# Register for both LLM and execution
user_proxy.register_for_execution()(wrapper)
agent.register_for_llm(description=desc)(wrapper)
# --- Set up the registry ---
registry = ToolRegistry()
@registry.register("Retrieve a customer record from the database by ID.")
def get_record(record_id: str) -> str:
return get_database_record(record_id)
@registry.register("Log an action to the audit trail.")
def log_event(action: str, agent_name: str, details: str = "") -> str:
return log_action(action, agent_name, details)
# --- Create agents ---
sales_agent = autogen.ConversableAgent(
name="SalesAgent",
system_message=(
"You are a sales assistant. You can look up customer records and log "
"sales activities. Always log any record lookups you perform."
),
llm_config=llm_config,
)
support_agent = autogen.ConversableAgent(
name="SupportAgent",
system_message=(
"You are a customer support assistant. You can look up customer records "
"to help with support inquiries. Log all support actions."
),
llm_config=llm_config,
)
user_proxy = autogen.UserProxyAgent(
name="User",
human_input_mode="NEVER",
is_termination_msg=lambda msg: "TERMINATE" in (msg.get("content") or ""),
code_execution_config=False,
)
# --- Register shared tools with both agents ---
registry.register_with_agent(sales_agent, user_proxy)
registry.register_with_agent(support_agent, user_proxy)
# --- Use the sales agent ---
user_proxy.initiate_chat(
sales_agent,
message="Look up customer 001 and tell me about them.",
)
This pattern is particularly useful in enterprise settings where multiple agents need to interact with the same backend systems but from different perspectives.
Pattern 4: The Tool Chaining Pattern
Tool chaining occurs when the output of one tool becomes the input to another. While the LLM can sometimes handle this naturally through conversation, explicit chaining gives you more control and reliability. This pattern is especially useful for data pipelines.
import autogen
import json
llm_config = {
"config_list": [{"model": "gpt-4o", "api_key": os.environ["OPENAI_API_KEY"]}],
}
# --- Define pipeline tools ---
def fetch_raw_data(source: str) -> str:
"""Fetch raw data from a source."""
# Simulated data fetch
raw = [
{"name": "Product A", "sales": 150, "region": "North"},
{"name": "Product B", "sales": 230, "region": "South"},
{"name": "Product C", "sales": 95, "region": "North"},
{"name": "Product D", "sales": 310, "region": "West"},
{"name": "Product E", "sales": 180, "region": "South"},
]
return json.dumps(raw)
def transform_data(raw_json: str, operation: str = "filter") -> str:
"""Transform data with a specified operation."""
data = json.loads(raw_json)
if operation == "filter":
# Filter to only high-selling products
result = [d for d in data if d["sales"] > 100]
elif operation == "sort":
result = sorted(data, key=lambda x: x["sales"], reverse=True)
elif operation == "aggregate":
from collections import defaultdict
agg = defaultdict(int)
for d in data:
agg[d["region"]] += d["sales"]
result = [{"region": k, "total_sales": v} for k, v in agg.items()]
else:
result = data
return json.dumps(result)
def summarize_data(data_json: str) -> str:
"""Generate a summary of the data."""
data = json.loads(data_json)
if not data:
return "No data to summarize."
summary_lines = [f"Total records: {len(data)}"]
if "sales" in data[0]:
total = sum(d["sales"] for d in data)
avg = total / len(data)
summary_lines.append(f"Total sales: {total}")
summary_lines.append(f"Average sales: {avg:.1f}")
if "total_sales" in data[0]:
total = sum(d["total_sales"] for d in data)
summary_lines.append(f"Grand total: {total}")
for d in data:
summary_lines.append(f" {d['region']}: {d['total_sales']}")
return "\n".join(summary_lines)
# --- Set up agents ---
orchestrator = autogen.ConversableAgent(
name="Orchestrator",
system_message=(
"You are a data pipeline orchestrator. You have access to three tools: "
"fetch_raw_data, transform_data, and summarize_data. "
"Always follow this sequence: "
"1. Fetch raw data using fetch_raw_data "
"2. Transform the data using transform_data (pass the JSON output from step 1) "
"3. Summarize the transformed data using summarize_data (pass the JSON output from step 2) "
"Present the final summary to the user. Do not skip steps."
),
llm_config=llm_config,
)
user_proxy = autogen.UserProxyAgent(
name="User",
human_input_mode="NEVER",
is_termination_msg=lambda msg: "TERMINATE" in (msg.get("content") or ""),
code_execution_config=False,
)
@user_proxy.register_for_execution()
@orchestrator.register_for_llm(description="Fetch raw sales data from a data source.")
def fetch_raw_data_tool(source: str) -> str:
return fetch_raw_data(source)
@user_proxy.register_for_execution()
@orchestrator.register_for_llm(description="Transform JSON data. Operations: filter, sort, aggregate.")
def transform_data_tool(raw_json: str, operation: str = "filter") -> str:
return transform_data(raw_json, operation)
@user_proxy.register_for_execution()
@orchestrator.register_for_llm(description="Generate a text summary from JSON data.")
def summarize_data_tool(data_json: str) -> str:
return summarize_data(data_json)
user_proxy.initiate_chat(
orchestrator,
message="Fetch data from 'sales_db', aggregate it by region, and give me a summary.",
)
The critical design choice here is the system prompt. By explicitly instructing the agent on the sequence and the data flow, you significantly increase the reliability of the chaining. The agent knows to pass JSON output from one tool directly as input to the next.
Pattern 5: The Human-in-the-Loop Tool Pattern
Some tools require human approval before execution — for example, sending emails, making payments, or modifying production data. AutoGen supports this through the human_input_mode setting and custom execution wrappers.
import autogen
llm_config = {
"config_list": [{"model": "gpt-4o", "api_key": os.environ["OPENAI_API_KEY"]}],
}
assistant = autogen.ConversableAgent(
name="EmailAssistant",
system_message=(
"You are an email assistant. You can draft and send emails. "
"Always draft the email first and confirm with the user before sending."
),
llm_config=llm_config,
)
# Use "ALWAYS" to require human approval for every step
user_proxy = autogen.UserProxyAgent(
name="User",
human_input_mode="ALWAYS", # Prompt user for input at every turn
is_termination_msg=lambda msg: "TERMINATE" in (msg.get("content") or ""),
code_execution_config=False,
)
sent_emails = []
@user_proxy.register_for_execution()
@assistant.register_for_llm(
description="Send an email to a recipient. Use only after user confirmation."
)
def send_email(recipient: str, subject: str, body: str) -> str:
"""Send an email.
Args:
recipient: Email address of the recipient.
subject: Email subject line.
body: Email body content.
"""
sent_emails.append({"to": recipient, "subject": subject, "body": body})
return f"Email sent to {recipient} with subject '{subject}'."
@user_proxy.register_for_execution()
@assistant.register_for_llm(description="Draft an email without sending it.")
def draft_email(recipient: str, subject: str, body: str) -> str:
"""Draft an email for review.
Args:
recipient: Email address of the recipient.
subject: Email subject line.
body: Email body content.
"""
return (
f"DRAFT EMAIL\n"
f"To: {recipient}\n"
f"Subject: {subject}\n"
f"---\n"
f"{body}\n"
f"---\n"
f"Reply 'yes' to send, or provide feedback to revise."
)
user_proxy.initiate_chat(
assistant,
message="Draft an email to boss@company.com requesting next Friday off. "
"Keep it professional and brief.",
)
With human_input_mode="ALWAYS", the user proxy will prompt you for input after every agent response. This gives you the opportunity to approve, modify, or reject any action before it executes. For a more granular approach, you can use "TERMINATE" in the is_termination_msg check and implement custom approval logic in your tool functions.
Pattern 6: The Dynamic Tool Selection Pattern
In some applications, the available tools change based on context. For example, a coding assistant might offer different tools depending on the programming language or project type. You can implement dynamic tool selection by reconfiguring agents at runtime.
import autogen
llm_config = {
"config_list": [{"model": "gpt-4o", "api_key": os.environ["OPENAI_API_KEY"]}],
}
# --- Language-specific tools ---
def run_python(code: str) -> str:
"""Execute Python code."""
import subprocess
result = subprocess.run(["python3", "-c", code], capture_output=True, text=True, timeout=10)
return result.stdout or result.stderr
def run_javascript(code: str) -> str:
"""Execute JavaScript code."""
import subprocess
result = subprocess.run(["node", "-e", code], capture_output=True, text=True, timeout=10)
return result.stdout or result.stderr
def lint_python(code: str) -> str:
"""Lint Python code."""
return "Python lint: No issues found." # Simplified
def lint_javascript(code: str) -> str:
"""Lint JavaScript code."""
return "JS lint: No issues found." # Simplified
# --- Router agent that determines the language ---
router = autogen.ConversableAgent(
name="Router",
system_message=(
"You are a routing agent. Analyze the user's code request and determine "
"the programming language. Respond with exactly 'PYTHON' or 'JAVASCRIPT'. "
"Do not add any other text."
),
llm_config=llm_config,
)
# --- Create language-specific agents dynamically ---
def create_coding_agent(language: str, user_proxy: autogen.UserProxyAgent) -> autogen.ConversableAgent:
"""Create a coding agent with language-specific tools."""
agent = autogen.ConversableAgent(
name=f"{language.title()}Coder",
system_message=(
f"You are a {language} coding assistant. You can run and lint {language} code. "
"Always lint code before running it. Explain what the code does."
),
llm_config=llm_config,
)
if language == "python":
@user_proxy.register_for_execution()
@agent.register_for_llm(description="Execute Python code.")
def run_code(code: str) -> str:
return run_python(code)
@user_proxy.register_for_execution()
@agent.register_for_llm(description="Lint Python code for issues.")
def lint_code(code: str) -> str:
return lint_python(code)
elif language == "javascript":
@user_proxy.register_for_execution()
@agent.register_for_llm(description="Execute JavaScript code.")
def run_code(code: str) -> str:
return run_javascript(code)
@user_proxy.register_for_execution()
@agent.register_for_llm(description="Lint JavaScript code for issues.")
def lint_code(code: str) -> str:
return lint_javascript(code)
return agent
# --- Main interaction ---
user_proxy = autogen.UserProxyAgent(
name="User",
human_input_mode="NEVER",
is_termination_msg=lambda msg: "TERMINATE" in (msg.get("content") or ""),
code_execution_config=False,
)
# First, determine the language
user_proxy.initiate_chat(
router,
message="I want to write a function that reverses a string.",
)
# Extract the language from the router's response
# (In practice, you'd parse the conversation history)
last_message = router.last_message(user_proxy)["content"].strip().upper()
language = "python" if "PYTHON" in last_message else "javascript"
# Create the appropriate agent and continue
coder = create_coding_agent(language, user_proxy)
user_proxy.initiate_chat(
coder,
message="Write and run a function that reverses a string.",
clear_history=False,
)
This pattern allows you to keep each agent's toolset small and focused while still supporting a wide range of capabilities across your system.
Best Practices for Tool Use in AutoGen
1. Write Excellent Tool Descriptions
The tool description is the single most important factor in whether the LLM uses your tool correctly. A good description should explain what the tool does and when to use it. Be specific about the expected inputs and what the tool returns.
# Bad description
@assistant.register_for_llm(description="Search function.")
def search(query: str) -> str:
...
# Good description
@assistant.register_for_llm(
description="Search the company knowledge base for documents matching a query. "
"Use this when the user asks about company policies, procedures, "
"or historical decisions. Returns up to 10 matching document summaries."
)
def search_knowledge_base(query: str) -> str:
...
2. Use Strong Type Hints and Docstrings
AutoGen uses type hints to generate the function schema for the LLM. Clear types reduce errors in argument passing. Docstrings provide additional context that the LLM uses to understand parameter semantics.
@assistant.register_for_llm(description="Calculate loan monthly payment.")
def calculate_loan_payment(
principal: float,
annual_interest_rate: float,
loan_term_years: int,
) -> dict:
"""Calculate the monthly payment for a fixed-rate loan.
Args:
principal: The loan amount in dollars (e.g., 250000.0).
annual_interest_rate: Annual rate as decimal (e.g., 0.065 for 6.5%).
loan_term_years: Duration of the loan in years (e.g., 30).
Returns:
A dictionary with 'monthly_payment', 'total_interest', and 'total_paid'.
"""
monthly_rate = annual_interest_rate / 12
num_payments = loan_term_years * 12
monthly_payment = principal * (monthly_rate * (1 + monthly_rate) ** num_payments) / \
((1 + monthly_rate) ** num_payments - 1)
total_paid = monthly_payment * num_payments
return {
"monthly_payment": round(monthly_payment, 2),
"total_interest": round(total_paid - principal, 2),
"total_paid": round(total_paid, 2),
}
3. Return Structured, Informative Results
Tools should return results that are easy for the LLM to reason about. Return structured data (JSON or formatted strings) that includes context. Avoid returning raw binary data or extremely long outputs.
# Bad: returns raw data with no context
def get_stock_price(symbol: str) -> str:
return "145.32"
# Good: returns structured data with context
def get_stock_price(symbol: str) -> str:
price = fetch_price(symbol)
change = fetch_change(symbol)
return (
f"Stock: {symbol}\n"
f"Current price: ${price}\n"
f"Daily change: {change:+.2f}%\n"
f"Timestamp: {datetime.now().isoformat()}"
)
4. Handle Errors Gracefully
Tools should never raise unhandled exceptions. Return error messages as strings so the LLM can reason about the failure and try an alternative approach.
@assistant.register_for_llm(description="Fetch data from a URL.")
def fetch_url(url: str) -> str:
"""Fetch content from a URL."""
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
return f"Status: {response.status_code}\nContent length: {len(response.text)}\n\n{response.text[:5000]}"
except requests.Timeout:
return f"Error: Request to {url} timed out after 10 seconds."
except requests.ConnectionError:
return f"Error: Could not connect to {url}. Check the URL or network."
except requests.HTTPError as e:
return f"Error: HTTP {response.status_code} - {str(e)}"
except Exception as e:
return f"Error