Build a Two-Agent Conversation with AutoGen (UserProxy + Assistant)
Build your first AutoGen two-agent conversation from scratch — full working code for UserProxy and AssistantAgent, termination conditions, and what happens under the hood.
Get more content like this on Telegram!
Daily AI tips, notes & resources — free
The two-agent conversation is where every AutoGen journey starts. It sounds simple — two agents talk to each other — but there's more happening under the hood than it appears. Getting this right sets you up for everything more complex.
I'm going to walk through this from absolute scratch, explain what each piece does, and show you the common mistakes beginners make (including ones I made).
The Mental Model Before the Code
Before touching Python, understand the basic architecture.
In AutoGen, a conversation is structured like a turn-based game. Each agent waits for a message, generates a response, and passes it back. The UserProxyAgent typically initiates the conversation (sends the task) and may execute code that the AssistantAgent writes. The AssistantAgent uses an LLM to generate responses.
Think of it this way:
- UserProxyAgent = the project manager / code runner. It has the task, hands it off, and handles execution.
- AssistantAgent = the specialist. It thinks, writes code, makes plans, and responds with content.
This mirrors a real interaction: you (UserProxy) describe what you need, the AI (Assistant) figures out how to do it, you execute and report back, the AI refines. The loop continues until the task is done.
The AI agents explained article covers the conceptual foundation behind this pattern if you want the broader picture before diving into code.
Setting Up Your Environment
# Python 3.10 or 3.11 required
python -m venv autogen-basics
source autogen-basics/bin/activate # Windows: autogen-basics\Scripts\activate
pip install autogen-agentchat autogen-ext[openai]
Set your API key:
# Linux/Mac
export OPENAI_API_KEY="sk-your-key-here"
# Windows PowerShell
$env:OPENAI_API_KEY = "sk-your-key-here"
The Simplest Working Example
Here's a minimal two-agent conversation. Everything else in this tutorial builds from this:
import asyncio
import os
from autogen_agentchat.agents import AssistantAgent, UserProxyAgent
from autogen_agentchat.conditions import TextMentionTermination
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_ext.models import OpenAIChatCompletionClient
async def basic_conversation():
# Set up the model
model_client = OpenAIChatCompletionClient(
model="gpt-4o-mini", # cheaper for learning
api_key=os.environ["OPENAI_API_KEY"]
)
# The AI assistant
assistant = AssistantAgent(
name="assistant",
model_client=model_client,
system_message="You are a helpful assistant. When you have answered the question fully, end your response with DONE."
)
# The user proxy (no human input in this example)
user_proxy = UserProxyAgent(
name="user",
input_func=None # NEVER ask for human input
)
# Stop when assistant says DONE
termination = TextMentionTermination("DONE")
# Create the two-agent team
team = RoundRobinGroupChat(
participants=[user_proxy, assistant],
termination_condition=termination,
max_turns=6 # Safety limit
)
# Run the conversation
result = await team.run(
task="What are three good practices for writing Python unit tests?"
)
# Print the conversation
for message in result.messages:
print(f"\n{'='*40}")
print(f"FROM: {message.source}")
print(f"{'='*40}")
print(message.content)
asyncio.run(basic_conversation())
When I ran this, the output was:
========================================
FROM: user
========================================
What are three good practices for Python unit tests?
========================================
FROM: assistant
========================================
Here are three key practices for Python unit tests:
1. **Test one thing at a time** — each test function should verify exactly one behavior...
2. **Use descriptive test names** — `test_calculate_total_returns_zero_for_empty_cart` tells you exactly what failed...
3. **Arrange-Act-Assert structure** — organize each test into setup, execution, and verification phases...
DONE
Two API calls. Clean output. The DONE keyword triggered the termination condition and the conversation ended.
Understanding initiate_chat (AutoGen 0.2 vs 0.4)
If you've seen AutoGen 0.2 tutorials, they use user_proxy.initiate_chat(assistant, message="..."). That API still works in some contexts but AutoGen 0.4 moved to the team-based model above.
Here's the 0.2 pattern for reference (if you're reading older code):
# AutoGen 0.2 style — still works in compatibility mode
import autogen
config_list = [{"model": "gpt-4o", "api_key": "your-key"}]
assistant = autogen.AssistantAgent(
name="assistant",
llm_config={"config_list": config_list}
)
user_proxy = autogen.UserProxyAgent(
name="user_proxy",
human_input_mode="NEVER",
max_consecutive_auto_reply=5,
is_termination_msg=lambda x: "TERMINATE" in x.get("content", "")
)
# 0.2 way to start a conversation
user_proxy.initiate_chat(
assistant,
message="Write a Python function to sort a list of dictionaries by a key"
)
The key differences in 0.2:
human_input_modewas a direct parameter (notinput_func)max_consecutive_auto_replycontrolled loops instead of termination conditionsis_termination_msgwas a lambda on the agent rather than a separate condition object
The 0.4 approach is cleaner because termination logic is separated from agent definition — you can reuse the same agents with different termination conditions.
Termination Conditions: Your Safety Net
This is probably the most important concept to get right. Without proper termination, conversations can run indefinitely.
AutoGen 0.4 provides several termination conditions you can combine:
from autogen_agentchat.conditions import (
TextMentionTermination,
MaxMessageTermination,
StopMessageTermination,
HandoffTermination
)
# Stop when a specific word/phrase appears
word_stop = TextMentionTermination("TERMINATE")
# Stop after N total messages
count_stop = MaxMessageTermination(10)
# Stop when an agent sends a StopMessage
agent_stop = StopMessageTermination()
# Combine: stop when EITHER condition is met
safe_termination = word_stop | count_stop
# Combine: stop when BOTH conditions are met
strict_termination = word_stop & count_stop # rarely needed
My standard setup uses | between a keyword condition and a max message count:
termination = TextMentionTermination("TERMINATE") | MaxMessageTermination(15)
This way, the conversation ends cleanly when the agent finishes, but you're protected from infinite loops if the agent never says "TERMINATE."
Tip: Always tell your assistant what word to say when it's done. Put it in the system message. Don't assume the LLM will pick something sensible on its own.
human_input_mode Options (and When to Use Each)
In AutoGen 0.4, human input is controlled by the input_func parameter of UserProxyAgent:
# No human input — fully automated
auto_user = UserProxyAgent(
name="user",
input_func=None
)
# Ask for human input every time (blocking)
interactive_user = UserProxyAgent(
name="user",
input_func=input # Python's built-in input()
)
# Custom input function — most flexible
async def my_input(prompt: str) -> str:
# Could read from a queue, webhook, database, etc.
print(f"[AGENT NEEDS INPUT]: {prompt}")
return input("Your response: ")
custom_user = UserProxyAgent(
name="user",
input_func=my_input
)
For production systems, input_func=None is almost always what you want. For development and debugging, input_func=input lets you intervene manually at each step — very useful when you're learning how the conversation flows.
| input_func value | When to use |
|---|---|
None | Production automation, batch processing |
input | Interactive development, debugging |
| Custom async function | Web apps, API integrations, queue-based systems |
Code Execution: UserProxy's Superpower
One thing that makes UserProxyAgent special is code execution. When the AssistantAgent writes Python code, UserProxyAgent can automatically execute it and send the results back.
from autogen_agentchat.agents import AssistantAgent, UserProxyAgent
from autogen_ext.code_executors.local import LocalCommandLineCodeExecutor
async def code_execution_example():
model_client = OpenAIChatCompletionClient(
model="gpt-4o",
api_key=os.environ["OPENAI_API_KEY"]
)
# Set up code executor
code_executor = LocalCommandLineCodeExecutor(
work_dir="./agent_workspace",
timeout=30 # Max seconds per code execution
)
assistant = AssistantAgent(
name="coder",
model_client=model_client,
system_message="Write Python code to solve the problem. Always test your code. Say TERMINATE when done."
)
# UserProxy with code execution enabled
user_proxy = UserProxyAgent(
name="user",
code_executor=code_executor,
input_func=None
)
termination = TextMentionTermination("TERMINATE") | MaxMessageTermination(10)
team = RoundRobinGroupChat(
[user_proxy, assistant],
termination_condition=termination
)
result = await team.run(
task="Write and run a Python script that generates the first 10 Fibonacci numbers"
)
for msg in result.messages:
print(f"\n[{msg.source}]\n{msg.content}")
asyncio.run(code_execution_example())
What happens here is genuinely interesting. The assistant writes code, wraps it in a Python code block. AutoGen detects the code block, executes it locally, captures stdout/stderr, and sends the output back as the next message. The assistant sees the execution output and can refine the code if needed.
This loop — write code → execute → observe output → fix if needed — is the core pattern for AutoGen code generation. It's more reliable than just generating code and hoping it works.
Important security note: LocalCommandLineCodeExecutor runs code on your actual machine. For production or untrusted inputs, use DockerCommandLineCodeExecutor instead, which runs code in an isolated container.
from autogen_ext.code_executors.docker import DockerCommandLineCodeExecutor
safe_executor = DockerCommandLineCodeExecutor(
image="python:3.11-slim",
work_dir="./agent_workspace",
timeout=60
)
What Happens Under the Hood
When you call team.run(task="..."), here's the actual sequence:
- AutoGen converts the task string to an initial
TextMessagefrom the UserProxy - The message is added to the conversation history
- AssistantAgent's turn: it receives the full conversation history, calls the LLM API with system message + history, gets a response
- Response is added to conversation history
- Termination condition is checked — if triggered, run ends
- If not terminated, UserProxy's turn: if code blocks were found in the last message, execute them; if
input_funcis set, call it; send result as next message - Go to step 3
Every message in the conversation is visible to every agent on their turn. This is important — the assistant doesn't just see its own messages, it sees the full dialogue. That's what allows it to build on previous steps, fix errors from execution results, and maintain context.
This is why system messages matter so much. The assistant sees its system message on every LLM call. A well-written system message shapes every response throughout the conversation, not just the first one.
Building a Practical Example: Code Review Bot
Let me put this all together with a real example I use — a simple code review assistant:
import asyncio
import os
from autogen_agentchat.agents import AssistantAgent, UserProxyAgent
from autogen_agentchat.conditions import TextMentionTermination, MaxMessageTermination
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_ext.models import OpenAIChatCompletionClient
async def code_review_bot(code_to_review: str):
model_client = OpenAIChatCompletionClient(
model="gpt-4o",
api_key=os.environ["OPENAI_API_KEY"]
)
reviewer = AssistantAgent(
name="code_reviewer",
model_client=model_client,
system_message="""You are a senior Python developer reviewing code.
For each review, provide:
1. SUMMARY: One sentence on what the code does
2. ISSUES: List any bugs, security issues, or style problems (or 'None found')
3. IMPROVEMENTS: 2-3 specific suggestions
4. RATING: Score 1-10 with justification
Be specific and actionable. End your review with REVIEW_COMPLETE."""
)
user_proxy = UserProxyAgent(
name="developer",
input_func=None
)
termination = (
TextMentionTermination("REVIEW_COMPLETE") |
MaxMessageTermination(4)
)
team = RoundRobinGroupChat(
[user_proxy, reviewer],
termination_condition=termination
)
task = f"""Please review this Python code:
```python
{code_to_review}
Provide a thorough but concise review."""
result = await team.run(task=task)
Return just the review content
for msg in result.messages: if msg.source == "code_reviewer": return msg.content
return "Review failed"
Example usage
sample_code = """ def get_user_data(user_id): query = "SELECT * FROM users WHERE id = " + user_id result = db.execute(query) return result[0] """
review = asyncio.run(code_review_bot(sample_code)) print(review)
This pattern works well. The system message structures the output, the termination condition keeps it clean, and you get a reliable review format every time.
Notice the obvious SQL injection vulnerability in the sample code — the reviewer consistently catches it. That's the kind of value you get from a well-configured code review agent.
## Connecting to the Bigger Picture
Two-agent conversations are the building block. Once you're comfortable with this pattern, you can:
- Add a third agent for specialized tasks (testing, documentation)
- Switch from RoundRobin to Selector for adaptive conversations
- Add tools so agents can call APIs or read files
- Integrate into a FastAPI app for a code review API
The [CrewAI tutorial](/post/crewai-tutorial) shows a different but related approach to multi-agent collaboration if you want to compare frameworks. The [Build AI agent with LangChain](/post/build-ai-agent-langchain) shows how LangChain handles agent conversations differently.
For production deployment of systems like this, the [Deploy AI model to production](/post/deploy-ai-model-production) guide covers infrastructure considerations.
## Conclusion
The UserProxy + AssistantAgent pattern is simple enough to understand in an afternoon but capable enough to power real production tools. I've built code review bots, research assistants, and data processing pipelines on top of this exact pattern.
The key things to take away: always set termination conditions, tell your agents what word means "I'm done," use `input_func=None` for automation and `input_func=input` for debugging, and start with `RoundRobinGroupChat` before reaching for more complex patterns.
Build one two-agent conversation that does something useful to you personally. That hands-on experience will make every subsequent AutoGen concept click much faster.
---
## Frequently Asked Questions
<faqs>
<faq>
<question>What is the difference between UserProxyAgent and AssistantAgent in AutoGen?</question>
<answer>UserProxyAgent represents the human side of a conversation. It can execute code, provide human input (or simulate it), and drive the conversation forward. AssistantAgent is the LLM-powered side — it generates responses, writes code, and solves problems. In a basic setup, UserProxy sends tasks and AssistantAgent fulfills them.</answer>
</faq>
<faq>
<question>What does human_input_mode do in AutoGen?</question>
<answer>human_input_mode controls when AutoGen pauses to ask for human input. ALWAYS means it asks after every message. NEVER means fully automated — no human input at all. TERMINATE means it only asks when the conversation reaches a termination condition. For production automation, use NEVER.</answer>
</faq>
<faq>
<question>How do I stop an AutoGen conversation from running forever?</question>
<answer>Use termination conditions: TextMentionTermination stops when an agent says a specific word (like TERMINATE or DONE). MaxMessageTermination stops after a set number of messages. You can combine them with | (OR) — the conversation ends when either condition is met. Always set at least one termination condition.</answer>
</faq>
</faqs>
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.