# Building an AI Agent with AutoGen (Now Microsoft Agent Framework)

- Tool: Microsoft Agent Framework (AutoGen)
- Difficulty: Intermediate
- Time required: 45-60 min
- Compatibility: Python 3.10+, agent-framework + agent-framework-anthropic, an Anthropic API key, any OS
- Last updated: November 2026

## TL;DR

AutoGen's last release was September 2025; Microsoft Agent Framework, built by the same AutoGen and Semantic Kernel teams, is now the official successor. This guide builds with Agent Framework from the start: define tools with @tool, gate one behind approval_mode='always_require', and resolve it through a session-based approval loop. The example is a procurement agent that checks vendor contracts freely and only approves a purchase order once a human signs off. Budget 45-60 minutes.

## What you'll build, and why not AutoGen

If you have an existing AutoGen project: AutoGen pioneered GroupChat and an event-driven agent runtime, and it's a fine piece of research software, but its last release was September 2025. Microsoft Agent Framework is its official successor, built by the same AutoGen and Semantic Kernel teams at Microsoft, and it's the one getting new features and fixes going forward. This guide builds directly with Agent Framework rather than teaching a framework that's no longer actively developed.

You'll build a procurement agent. It can look up a vendor's contract terms and current spend through a look_up_vendor_contract tool, and when a purchase order needs sign-off, it can approve one through approve_purchase_order. The contract lookup runs freely; the purchase-order tool is marked approval_mode='always_require', so the agent's run pauses and returns a pending approval request instead of executing it, no matter how confident the model is.

The core pieces map closely from AutoGen if you already know it: AssistantAgent becomes Agent, FunctionTool becomes the @tool decorator with schemas inferred from type hints, and model clients keep similar names (OpenAIChatCompletionClient exists in both; Agent Framework adds a first-party AnthropicClient and a Responses API client AutoGen never had). The biggest behavioral difference is tool-calling itself: AssistantAgent is single-turn unless you raise max_tool_iterations, while Agent is multi-turn by default and keeps calling tools until it has a real answer. For multi-agent orchestration, AutoGen's Team and GroupChat become a typed, graph-based Workflow, and AutoGen's lack of any built-in pause-for-human-input mechanism becomes Workflow's request_info() and @response_handler, which is a capability AutoGen's Team never had at all.

## Before you start

- Python 3.10+ and a terminal
- An Anthropic API key (ANTHROPIC_API_KEY)
- pip install agent-framework agent-framework-anthropic --pre
- Basic comfort with async/await and Python type hints

## Step-by-step guide

### 1. Install Microsoft Agent Framework and set your key

Install the core agent-framework package plus the Anthropic provider package, which is still pre-release so it needs the --pre flag. If you're coming from AutoGen, this replaces autogen-agentchat and autogen-ext; there's no in-place upgrade, it's a new install alongside your old one while you migrate incrementally.

```
pip install agent-framework agent-framework-anthropic --pre
export ANTHROPIC_API_KEY="sk-ant-..."
```

**Expected result:** pip install finishes with no errors and `python -c "import agent_framework"` runs silently.

### 2. Define the read-only tool: look_up_vendor_contract

A plain Python function with type hints becomes a tool when passed to an agent's tools parameter; the @tool decorator lets you set an explicit name and description instead of relying on the function name and docstring. This replaces AutoGen's FunctionTool wrapper, and it needs no approval mode since it only reads data.

```
from typing import Annotated
from agent_framework import tool

VENDORS = {
    "VEN-220": {"name": "Acme Supply Co", "terms": "Net 30", "ytd_spend": 48000},
}

@tool(approval_mode="never_require")
def look_up_vendor_contract(
    vendor_id: Annotated[str, "The vendor ID, e.g. VEN-220"],
) -> str:
    """Look up a vendor's contract terms and year-to-date spend."""
    vendor = VENDORS.get(vendor_id)
    if not vendor:
        return f"No vendor found for {vendor_id}"
    return f"{vendor['name']}: terms={vendor['terms']}, ytd_spend=${vendor['ytd_spend']}"
```

**Expected result:** A tool function, callable directly for a quick test, that the agent can call on every turn without pausing.

### 3. Define the sensitive tool: approve_purchase_order

approve_purchase_order takes the same shape but is marked approval_mode='always_require'. The function body itself doesn't check anything about approval; it just performs the action when the framework actually calls it, which only happens after a human has approved the pending request.

```
@tool(approval_mode="always_require")
def approve_purchase_order(
    vendor_id: Annotated[str, "The vendor ID"],
    amount: Annotated[float, "Purchase order amount in dollars"],
    reason: Annotated[str, "What the purchase order is for"],
) -> str:
    """Approve a purchase order for a vendor. Always requires human approval before it runs."""
    # In production this posts to your procurement/ERP system
    return f"Approved PO for {vendor_id}: ${amount:.2f} ({reason})"
```

**Expected result:** A second tool, same shape as the first, that will only ever execute after a resumed run carrying an approval.

### 4. Build the Agent with the Anthropic provider

Agent takes a client, instructions, a name, and the tools list. AnthropicClient reads ANTHROPIC_API_KEY from the environment; pass model explicitly rather than relying on a default, since defaults in the SDK's own examples lag behind current model releases. Agent is multi-turn by default, unlike AutoGen's AssistantAgent, so it keeps calling tools across a single run until it has a full answer.

```
from agent_framework import Agent
from agent_framework.anthropic import AnthropicClient

agent = Agent(
    client=AnthropicClient(model="claude-sonnet-5-5"),
    name="ProcurementAgent",
    instructions=(
        "You are a procurement agent. Always call look_up_vendor_contract before making any claim "
        "about a vendor. Never tell someone a purchase order was approved without actually calling "
        "approve_purchase_order."
    ),
    tools=[look_up_vendor_contract, approve_purchase_order],
)
```

**Expected result:** An agent that answers vendor questions on its own and, for anything touching approve_purchase_order, returns a pending approval instead of a final answer.

### 5. Run the agent and resolve approvals in a loop

Create a session with agent.create_session() and reuse it for every call in this conversation; approval responses only bind to pending requests recorded in the same session. After each run, check result.user_input_requests. For every pending request, inspect its function_call, get a decision, and build a response with to_function_approval_response(True or False). Keep looping until user_input_requests is empty.

```
import asyncio

async def handle_approvals(query: str, agent, session) -> str:
    result = await agent.run(query, session=session)
    while result.user_input_requests:
        new_inputs = []
        for request in result.user_input_requests:
            call = request.function_call
            print(f"[APPROVAL NEEDED] {call.name}({call.arguments})")
            approved = True  # swap for a real Slack/ticket check
            new_inputs.append({"role": "user", "contents": [request.to_function_approval_response(approved)]})
        result = await agent.run(new_inputs, session=session)
    return result.text

async def main():
    session = agent.create_session()
    text = await handle_approvals(
        "VEN-220 needs a $4,200 PO for a replacement HVAC unit. Check their terms and approve it.",
        agent,
        session,
    )
    print(text)

asyncio.run(main())
```

**Expected result:** The script prints an [APPROVAL NEEDED] line for the purchase order, then a final confirmation once the approval resolves.

### 6. Migrate multi-agent AutoGen patterns to Workflow

If your AutoGen project used GroupChat or RoundRobinGroupChat to coordinate several agents, the Agent Framework equivalent is a graph-based Workflow built with WorkflowBuilder and add_edge, not a direct drop-in. The win is that a Workflow executor can call ctx.request_info() to pause and wait for a human decision mid-graph, something AutoGen's Team abstraction never supported at all; AutoGen human-in-the-loop always required a custom implementation outside the framework.

```
from agent_framework import WorkflowBuilder, WorkflowContext, Executor, handler, response_handler
from dataclasses import dataclass

@dataclass
class POApprovalRequest:
    vendor_id: str
    amount: float

class FinanceReviewer(Executor):
    @handler
    async def review(self, po: POApprovalRequest, ctx: WorkflowContext) -> None:
        await ctx.request_info(request_data=po, response_type=str)

    @response_handler
    async def on_decision(self, original: POApprovalRequest, decision: str, ctx: WorkflowContext) -> None:
        await ctx.yield_output(f"{'APPROVED' if decision == 'approved' else 'REJECTED'}: {original.vendor_id}")

# workflow = WorkflowBuilder(start_executor=procurement_agent_executor).add_edge(
#     procurement_agent_executor, FinanceReviewer(id="finance-reviewer")
# ).build()
```

**Expected result:** A mental model for porting any AutoGen multi-agent flow: single agents migrate directly, coordination logic gets rebuilt as a Workflow.

### 7. Harden it: real APIs, error handling, and standing approvals at scale

Swap VENDORS and the fake PO approval for real calls to your ERP/procurement API inside each tool, wrapped in try/except so a failure returns a plain error string instead of crashing the process. For teams running this at volume, replace the manual loop in step 5 with create_harness_agent, which installs ToolApprovalMiddleware automatically, supports standing 'always approve this vendor' rules scoped to a session, and queues multiple pending approvals instead of handling them one at a time.

```
@tool(approval_mode="always_require")
def approve_purchase_order(
    vendor_id: Annotated[str, "The vendor ID"],
    amount: Annotated[float, "Purchase order amount in dollars"],
    reason: Annotated[str, "What the purchase order is for"],
) -> str:
    """Approve a purchase order for a vendor. Always requires human approval before it runs."""
    try:
        po = erp_client.create_purchase_order(vendor_id=vendor_id, amount=amount, memo=reason)
        return f"Approved PO {po.id} for {vendor_id}: ${amount:.2f}"
    except Exception as e:
        return f"PO creation failed: {e}"
```

**Expected result:** A handler that degrades gracefully on a failed ERP call, and a clear upgrade path to create_harness_agent once one-off approvals turn into dozens a day.

## Complete code example

File: `procurement_agent.py`

```python
"""Procurement agent built on Microsoft Agent Framework (AutoGen's successor):
checks vendor contracts freely, approves purchase orders only after human approval.
Run: python procurement_agent.py
(pip install agent-framework agent-framework-anthropic --pre; set ANTHROPIC_API_KEY)
"""
import asyncio
from typing import Annotated

from agent_framework import Agent, tool
from agent_framework.anthropic import AnthropicClient

VENDORS = {
    "VEN-220": {"name": "Acme Supply Co", "terms": "Net 30", "ytd_spend": 48000},
}


@tool(approval_mode="never_require")
def look_up_vendor_contract(
    vendor_id: Annotated[str, "The vendor ID, e.g. VEN-220"],
) -> str:
    """Look up a vendor's contract terms and year-to-date spend."""
    vendor = VENDORS.get(vendor_id)
    if not vendor:
        return f"No vendor found for {vendor_id}"
    return f"{vendor['name']}: terms={vendor['terms']}, ytd_spend=${vendor['ytd_spend']}"


@tool(approval_mode="always_require")
def approve_purchase_order(
    vendor_id: Annotated[str, "The vendor ID"],
    amount: Annotated[float, "Purchase order amount in dollars"],
    reason: Annotated[str, "What the purchase order is for"],
) -> str:
    """Approve a purchase order for a vendor. Always requires human approval before it runs."""
    # In production this posts to your procurement/ERP system
    return f"Approved PO for {vendor_id}: ${amount:.2f} ({reason})"


agent = Agent(
    client=AnthropicClient(model="claude-sonnet-5-5"),
    name="ProcurementAgent",
    instructions=(
        "You are a procurement agent. Always call look_up_vendor_contract before making any claim "
        "about a vendor. Never tell someone a purchase order was approved without actually calling "
        "approve_purchase_order."
    ),
    tools=[look_up_vendor_contract, approve_purchase_order],
)


async def handle_approvals(query: str, agent: Agent, session) -> str:
    result = await agent.run(query, session=session)
    while result.user_input_requests:
        new_inputs = []
        for request in result.user_input_requests:
            call = request.function_call
            print(f"\n[APPROVAL NEEDED] {call.name}({call.arguments})")
            approved = True  # swap for a real Slack/ticket check
            new_inputs.append(
                {"role": "user", "contents": [request.to_function_approval_response(approved)]}
            )
        result = await agent.run(new_inputs, session=session)
    return result.text


async def main():
    session = agent.create_session()
    text = await handle_approvals(
        "VEN-220 needs a $4,200 PO for a replacement HVAC unit. Check their terms and approve it.",
        agent,
        session,
    )
    print(f"\nAgent: {text}")


if __name__ == "__main__":
    asyncio.run(main())
```

## Common mistakes

- **Trying to upgrade an AssistantAgent in place instead of rewriting it as Agent with @tool-decorated functions** — undefined Fix: There's no automatic migration. Port one agent at a time: replace AssistantAgent(tools=[FunctionTool(...)]) with Agent(tools=[...]) using plain, type-hinted functions, and re-test before moving to the next one.
- **Assuming approval_mode='never_require' is the same as omitting an approval tool entirely** — undefined Fix: It's an explicit, visible policy on the tool rather than silence. Set it on every read-only tool so a future edit can't accidentally flip a write tool to unsupervised by leaving the parameter off.
- **Reusing a fresh session on every approval round-trip instead of the one from create_session()** — undefined Fix: Approval responses only bind to pending requests recorded in the same AgentSession by default. A new session makes every approval response silently ignored.
- **Porting AutoGen's GroupChat speaker-selection logic literally into Workflow edges** — undefined Fix: Model the actual decision points as a graph instead. Workflow is explicit and typed; trying to replicate GroupChat's dynamic speaker selection one-to-one fights the framework.
- **Setting max_tool_iterations-style caps out of habit and being surprised the agent stops early** — undefined Fix: Agent is multi-turn by default and keeps calling tools until it has an answer, unlike AssistantAgent. If you need a cap, set it explicitly through the client's function-invocation configuration instead of assuming AutoGen's defaults carried over.

## Best practices

- Mark every tool explicitly with approval_mode, even 'never_require' on read-only ones, so the policy is visible in code review.
- Reuse one AgentSession across an entire approval round-trip; never start a fresh session mid-conversation.
- State the tool-use rule explicitly in instructions: never claim an action happened without actually calling the tool that performs it.
- Migrate single agents (AssistantAgent to Agent) before touching multi-agent coordination (Team/GroupChat to Workflow); they're independent migrations.
- Move to create_harness_agent once approvals happen often enough that a one-off loop stops scaling; it adds queuing and standing-rule support for free.
- Log every pending approval and its resolution before you build the next run's input, independent of whether it was approved.

## Use cases

### undefined



### undefined



### undefined



## Frequently asked questions

### Is AutoGen dead, and should I stop building new AutoGen ai agent projects?

AutoGen isn't deleted and existing projects keep running, but its last release was September 2025 and Microsoft has named Agent Framework its official successor, built by the same core team. For any new project, build on Agent Framework directly; it's the one receiving new capabilities.

### What's the single biggest behavior change when migrating from AssistantAgent to Agent?

Tool-calling defaults flip. AssistantAgent runs single-turn unless you raise max_tool_iterations, so it often calls one tool and stops. Agent is multi-turn by default and keeps invoking tools until it has a complete answer, which can surprise code that assumed AutoGen's more conservative default.

### Does Microsoft Agent Framework support Anthropic's Claude models directly, not just through AutoGen compatibility?

Yes. Install agent-framework-anthropic and use AnthropicClient (or AnthropicFoundryClient, AnthropicBedrockClient, AnthropicVertexClient for those routes) as a first-class chat client, the same way you'd use OpenAIChatClient. This is a direct Agent Framework integration, not something inherited from AutoGen.

### What replaces AutoGen's GroupChat for coordinating multiple agents?

A graph-based Workflow built with WorkflowBuilder, where you define executors and the edges between them explicitly. It's more upfront structure than GroupChat's dynamic speaker selection, but it adds request_info()-based human-in-the-loop pauses and checkpoint/resume support that GroupChat never had.

### How much does a conversation like this cost in tokens?

On Claude Sonnet 5.5 ($2/M input, $10/M output), a short procurement exchange with one tool call and a modest system prompt runs about $0.01-0.03 per turn uncached, less once your instructions and tool schemas are stable enough to benefit from prompt caching.

### Can someone handle this migration for me instead of doing it myself?

Yes. RapidDev migrates AutoGen projects to Microsoft Agent Framework and builds new agents on it directly, including the approval-loop and Workflow pieces most tutorials skip. A free 30-minute consultation is the fastest way to scope your migration.

---

Source: https://www.rapidevelopers.com/build-ai-agent/how-to-build-an-ai-agent-with-autogen
© RapidDev — https://www.rapidevelopers.com/build-ai-agent/how-to-build-an-ai-agent-with-autogen
