The autogen assistantagent userproxyagent setup is the foundation for most Microsoft AutoGen workflows. These two agent types handle the split between model-driven reasoning and human or tool-mediated action, and getting their configuration right saves hours of debugging later.
Installation and Prerequisites
Install the framework and a way to load secrets. Use Python 3.10 or newer.
pip install pyautogen python-dotenv
Pin a version if you need reproducibility: pyautogen==0.2.32 is a known stable release for the AgentChat API. The import paths below assume that line.
Create a .env file for keys and never commit it:
OPENAI_API_KEY=sk-...
If you run inside a container, mount the secret as an environment variable instead of a file.
Configuring the LLM Client
AutoGen expects a config_list: a list of dicts with at least model and api_key. You can add base_url to target any OpenAI-compatible server.
import os
from dotenv import load_dotenv
load_dotenv()
config_list = [
{
"model": "gpt-4o-mini",
"api_key": os.getenv("OPENAI_API_KEY"),
"base_url": "https://api.openai.com/v1",
"temperature": 0.2,
"max_tokens": 1024,
}
]
If you want a single OpenAI-compatible endpoint that addresses 240+ models with automatic fallback when a provider is rate-limited or degraded, point base_url at n4n.ai and use your metering key. Client routing directives and provider cache-control hints pass through unchanged.
A common mistake is mixing models with different capabilities in one config_list without specifying model per call. AutoGen picks the first entry by default. Use filter_config or set model explicitly in llm_config to avoid silent mismatches.
Defining the AssistantAgent
The AssistantAgent wraps the LLM. It sends messages and parses replies. Keep the system message specific.
from autogen import AssistantAgent
assistant = AssistantAgent(
name="assistant",
llm_config={"config_list": config_list, "timeout": 60},
system_message=(
"You are a senior Python engineer. "
"Respond with minimal prose. "
"When asked for code, output a single fenced block."
),
)
The timeout prevents hung requests from blocking the event loop. Set max_retries if you see transient 429s.
Pitfall: AssistantAgent does not execute code or call tools by itself. It returns text. If you need function-calling schema enforcement, you must implement a custom agent or rely on the UserProxyAgent’s code-extraction path.
Defining the UserProxyAgent
UserProxyAgent is the bridge to the outside world. It can solicit human input and run generated code.
from autogen import UserProxyAgent
user_proxy = UserProxyAgent(
name="user_proxy",
human_input_mode="NEVER",
max_consecutive_auto_reply=10,
is_termination_msg=lambda m: "TERMINATE" in m.get("content", ""),
code_execution_config={
"work_dir": "coding",
"use_docker": False,
},
)
human_input_mode has three settings:
ALWAYS: prompts the console for every step. Useful for debugging, fatal for headless scripts.TERMINATE: asks only when the assistant suggests termination.NEVER: fully autonomous; relies onis_termination_msgand reply limits.
Tradeoff: NEVER gives autonomy but can loop. Set max_consecutive_auto_reply to bound cost and avoid runaway bills.
code_execution_config controls where files are written and whether Docker isolates execution. use_docker=False is convenient but runs code on your host. For anything untrusted, set use_docker=True and pre-pull python:3.11-slim.
Running a Basic Conversation
Initiate from the proxy side. The proxy sends the first message; the assistant replies; the proxy may execute code and feed output back.
chat_result = user_proxy.initiate_chat(
assistant,
message="Write a function to compute the nth Fibonacci number iteratively.",
)
print(chat_result.summary)
If human_input_mode="ALWAYS", the script blocks waiting for stdin. In a notebook this appears as an input widget.
The assistant’s reply containing a code block triggers the proxy’s executor. Stdout from the executed script is returned as a message, letting the assistant refine the solution.
Handling Code Execution Safely
The default executor writes to work_dir and runs via python in the current environment. A malicious prompt can delete files. Mitigate:
- Use Docker. Set
use_docker=True. - Restrict
work_dirto a temp path outside your repo. - Override
code_execution_configwithtimeout(seconds) to kill long loops.
code_execution_config={
"work_dir": "/tmp/autogen_sandbox",
"use_docker": True,
"timeout": 30,
}
Pitfall: Docker on macOS requires granting file sharing for the work dir. Otherwise you get silent mount errors that look like empty output.
Termination and Control Flow
Without explicit termination, the pair will bounce messages until max_consecutive_auto_reply hits. Define a clear stop condition.
def stop_on_done(msg):
return "DONE" in msg.get("content", "").upper()
user_proxy = UserProxyAgent(
name="user_proxy",
human_input_mode="NEVER",
max_consecutive_auto_reply=5,
is_termination_msg=stop_on_done,
code_execution_config={"work_dir": "coding", "use_docker": False},
)
Instruct the assistant in its system message to emit DONE when the task is complete. This avoids ambiguous TERMINATE parsing across models.
Inspecting and Debugging Messages
AutoGen logs each message. Set the logger to DEBUG to see raw payloads.
import autogen.runtime.logging as logging
logging.set_logger_level("DEBUG")
If the assistant ignores your system message, check that llm_config is actually passed. A frequent bug: creating the agent with llm_config=None then wondering why it errors on first call.
Advanced LLM Configuration
You can place multiple models in config_list and let AutoGen fall back on failure:
config_list = [
{"model": "gpt-4o", "api_key": os.getenv("OPENAI_API_KEY")},
{"model": "gpt-4o-mini", "api_key": os.getenv("OPENAI_API_KEY")},
]
AutoGen tries the first, then the next on error. This is coarse. For finer control, use filter_config with a lambda on model at call time.
If you need structured outputs, pass response_format in the config dict where the provider supports it. Not all OpenAI-compatible gateways honor it, so test before relying on it.
Example: End-to-End Script
A single file that ties the autogen assistantagent userproxyagent setup together:
import os
from dotenv import load_dotenv
from autogen import AssistantAgent, UserProxyAgent
load_dotenv()
config_list = [{
"model": "gpt-4o-mini",
"api_key": os.getenv("OPENAI_API_KEY"),
"temperature": 0.1,
}]
assistant = AssistantAgent(
name="assistant",
llm_config={"config_list": config_list},
system_message="You are a concise coder. End with DONE.",
)
user_proxy = UserProxyAgent(
name="user_proxy",
human_input_mode="NEVER",
max_consecutive_auto_reply=4,
is_termination_msg=lambda m: "DONE" in m.get("content", ""),
code_execution_config={"work_dir": "out", "use_docker": False},
)
user_proxy.initiate_chat(
assistant,
message="Write a CLI that prints 'hello' and runs without error.",
)
Run it with python main.py. Watch the out/ directory for generated scripts.
Tradeoffs of the Two-Agent Pattern
The autogen assistantagent userproxyagent setup couples reasoning and action in a minimal graph. It is fast to prototype. But the UserProxyAgent conflates human-in-the-loop, code execution, and orchestration. In larger systems that becomes a bottleneck: you cannot have two assistants collaborate without a third group-chat agent.
For production, consider splitting concerns: a pure executor service, a separate human approval gateway, and stateless assistant calls. The two-agent pattern is a starting point, not a final architecture.
Moving Beyond the Basics
Once the autogen assistantagent userproxyagent setup runs locally, add a GroupChat with a GroupChatManager to involve multiple specialists. Keep the same config_list but vary system_message per agent.
Monitor token usage. If you used a gateway with per-token metering, read the usage field from the final chat result to reconcile cost. The pattern is stable, but the framework evolves. Pin your version and read migration notes before upgrading.