An independent, end-to-end Python walkthrough: install agent-framework, build a real agent with a custom tool and Pydantic structured output, stream it async, and decide honestly whether to adopt MAF over the Semantic Kernel + AutoGen lineage it replaces.
What is the Microsoft Agent Framework, and why a Python tutorial?
This Microsoft Agent Framework tutorial for Python builds a real agent from scratch: you will pip install agent-framework, create an Agent backed by an OpenAI or Azure OpenAI chat client, register a custom Python tool, return a validated Pydantic object as structured output, and stream the whole run asynchronously. Most pages that rank for this topic are either Microsoft’s happy-path docs or a .NET/C# walkthrough. This one is independent, end-to-end Python, and ends with an honest “should I adopt it” verdict.
Microsoft Agent Framework (MAF) reached version 1.0 GA in April 2026, unifying two previously separate Microsoft projects, Semantic Kernel and AutoGen, into a single open-source SDK for Python and .NET. Visual Studio Magazine and Microsoft’s own DevBlogs describe 1.0 as the production-ready, long-term-support milestone with stable APIs and native support for the Model Context Protocol (MCP) and Agent-to-Agent (A2A) interoperability.
The practical pitch: MAF combines AutoGen’s lightweight agent abstractions with Semantic Kernel’s enterprise features, session-based state, type safety, middleware, and telemetry, and adds a typed, graph-based workflow engine for multi-agent orchestration. If you have ever wired AutoGen and Semantic Kernel together by hand, MAF is the framework that stops you from doing that.
One housekeeping note before code: the brief and many early write-ups reference `pip install agent-framework –pre` and a `ChatAgent` class. That was correct during the 1.0 release-candidate window. As of the stable 1.7.0 release on PyPI (May 28, 2026), the package installs without `–pre`, and the public class you instantiate is simply `Agent`. This tutorial uses the current, stable surface and flags the older names where they still appear in the wild.

Python 3.10+, an OpenAI API key (or an Azure OpenAI deployment), and a virtual environment. MAF does not auto-load .env files, so you will call load_dotenv() yourself or export variables in your shell.
How do I install agent-framework and get started?
To get started, create a virtual environment and run pip install agent-framework, which pulls in all sub-packages; for OpenAI or Azure OpenAI you then import the client from agent_framework.openai and the Agent class from agent_framework. The umbrella package is the simplest path; if you want a smaller footprint you can install just agent-framework-core plus a specific integration package.
Set up your environment first. The pip install agent-framework getting started step is intentionally boring, which is the point: no compiler, no SDK download, just a Python dependency.
Now confirm the install works with a four-line agent. This is the smallest complete Microsoft Agent Framework Python example that actually calls a model and prints a response. Notice that `Agent.run()` is a coroutine, so the whole program lives inside `asyncio.run()`.
Swap OpenAIChatClient() for AzureOpenAIChatClient from agent_framework.azure, pass your deployment name and endpoint, and authenticate with AzureCliCredential() (run az login first) instead of an API key. The Agent code below is otherwise identical.
# 1) Set up and install
python -m venv .venv && source .venv/bin/activate
pip install agent-framework
# 2) Provide credentials (OpenAI shown; Azure OpenAI covered below)
export OPENAI_API_KEY="sk-..."
export OPENAI_CHAT_MODEL_ID="gpt-4o-mini"
# 3) hello_agent.py — smoke test the install
import asyncio
from agent_framework import Agent
from agent_framework.openai import OpenAIChatClient
async def main() -> None:
agent = Agent(
client=OpenAIChatClient(), # reads OPENAI_API_KEY / OPENAI_CHAT_MODEL_ID
name="HelloAgent",
instructions="You are a concise assistant. Keep answers to one sentence.",
)
result = await agent.run("What is the capital of France?")
print(result.text)
if __name__ == "__main__":
asyncio.run(main())
How do I build an agent with a custom tool (ChatAgent tools tutorial)?
To build an agent with the Microsoft Agent Framework that calls your own code, write a plain typed Python function, describe its parameters with Annotated and Pydantic’s Field, and pass it in the agent’s tools list, MAF generates the JSON schema and handles the tool-call round trip for you. This is the single best ergonomic decision in the framework: your type hints are the schema, so there is no manual function-calling boilerplate.
Here is a complete agent with two custom tools, a fake weather lookup and a calculator. The weather function uses Annotated type hints to give the model a parameter description. The calculator uses the optional `@tool` decorator (renamed from the older `@ai_function`) when you want to override the name or description that the model sees.
When you call `agent.run()`, the agent decides whether to invoke a tool, MAF executes your Python function locally, feeds the result back to the model, and returns the final answer in `result.text`. You never write the tool-dispatch loop yourself. This is the agent framework ChatAgent tools tutorial pattern that the official docs spread across several pages, collapsed into one runnable file.
The calculator uses eval() for brevity. In any real agent, run tool code that touches a parser or shell inside a sandbox. See our companion tutorial on building an AI agent code-execution sandbox before you give an LLM-driven tool real power.
# tool_agent.py
import asyncio
from typing import Annotated
from agent_framework import Agent, tool
from agent_framework.openai import OpenAIChatClient
from pydantic import Field
# A plain typed function becomes a tool automatically.
def get_weather(
location: Annotated[str, Field(description="City and country, e.g. 'Amsterdam, NL'")],
) -> str:
"""Get the current weather for a given location."""
# In production, call a real weather API here.
return f"The weather in {location} is cloudy with a high of 15C."
# Use @tool when you want to control the name/description the model sees.
@tool(name="calculator", description="Evaluate a basic arithmetic expression.")
def calculate(
expression: Annotated[str, Field(description="e.g. '15 * 1.08' or '120 / 4'")],
) -> str:
allowed = set("0123456789+-*/(). ")
if not set(expression) <= allowed:
return "Refused: expression contains unsupported characters."
return str(eval(expression, {"__builtins__": {}})) # demo only; sandbox in prod
async def main() -> None:
agent = Agent(
client=OpenAIChatClient(),
name="AssistantWithTools",
instructions="You are a helpful assistant. Use the tools when relevant.",
tools=[get_weather, calculate],
)
result = await agent.run(
"What's the weather in Amsterdam, and what is 15 times 1.08?"
)
print(result.text)
if __name__ == "__main__":
asyncio.run(main())
How do I get Pydantic structured output from a Microsoft Agent Framework agent?
To get structured output, define a Pydantic BaseModel and pass it as options={“response_format”: YourModel} to agent.run(); MAF instructs the model to conform to that schema, validates the response, and returns a typed instance on result.value. This is how you turn a chatty agent into a reliable component that other code can depend on, no regex parsing, no “please respond in JSON” prompt hacks.
Structured output composes cleanly with tools. The agent below first calls the weather tool, then returns a validated `WeatherReport` object. If parsing fails, `result.value` is falsy and you can fall back to `result.text`, which is the correct way to handle the model occasionally going off-schema.
One sharp edge worth internalizing: `response_format` lives in the `options` dict, not as a top-level keyword on `run()`. Early AutoGen and Semantic Kernel muscle memory will lead you to pass it positionally and get a confusing error. Put it in `options`, every time.
# structured_agent.py
import asyncio
from typing import Annotated
from agent_framework import Agent
from agent_framework.openai import OpenAIChatClient
from pydantic import BaseModel, Field
def get_weather(
location: Annotated[str, Field(description="City and country")],
) -> str:
"""Get the current weather for a given location."""
return f"The weather in {location} is cloudy with a high of 15C."
class WeatherReport(BaseModel):
"""Structured weather summary the rest of the app can rely on."""
location: str
condition: str
high_celsius: int
bring_umbrella: bool
async def main() -> None:
agent = Agent(
client=OpenAIChatClient(),
name="WeatherReporter",
instructions="Report weather as a structured object.",
tools=[get_weather],
)
result = await agent.run(
"What's the weather in Amsterdam?",
options={"response_format": WeatherReport},
)
if report := result.value: # parsed Pydantic instance
print(report.location, report.high_celsius, report.bring_umbrella)
print(type(report).__name__) # -> WeatherReport
else:
print("Model went off-schema:", result.text)
if __name__ == "__main__":
asyncio.run(main())
“Your type hints are the schema and your Pydantic model is the contract. That is the whole appeal of MAF in Python in one sentence.”
Field notes from building on MAF 1.7
How do I stream the agent response asynchronously?
To stream, call agent.run(…, stream=True), which returns a ResponseStream you iterate with async for to print tokens as they arrive; when you also requested structured output, call await stream.get_final_response() afterward to get the parsed .value. This is the async streaming pattern most early tutorials get subtly wrong, so it is worth seeing it correct.
The gotcha: while streaming, individual updates carry text chunks, but the structured `value` is only available after the stream completes. You cannot deserialize half a JSON object. So you iterate for the live feel, then call `get_final_response()` to obtain the validated object. If you do not need the live tokens at all, you can skip the loop entirely and call `get_final_response()` directly, it consumes the stream for you.
A second async streaming gotcha that bites people: every `run()` is a coroutine and every stream is an async iterator, so all of this must run under `asyncio.run()` or inside an existing event loop. Mixing synchronous code that blocks the loop (a `requests` call inside a tool, say) will stall token delivery. Keep tool I/O async or offload it.
# streaming_agent.py
import asyncio
from agent_framework import Agent
from agent_framework.openai import OpenAIChatClient
from pydantic import BaseModel
class CityFacts(BaseModel):
city: str
fun_fact: str
async def main() -> None:
agent = Agent(
client=OpenAIChatClient(),
name="CityAgent",
instructions="Describe cities briefly.",
)
stream = agent.run(
"Tell me about Tokyo, Japan.",
stream=True,
options={"response_format": CityFacts},
)
# 1) Live token feed
async for update in stream:
if update.text:
print(update.text, end="", flush=True)
print()
# 2) Validated object, only after the stream is done
final = await stream.get_final_response()
if facts := final.value:
print(f"{facts.city}: {facts.fun_fact}")
if __name__ == "__main__":
asyncio.run(main())
Troubleshooting: ImportError on agent_framework.openai
Make sure you installed the umbrella package (pip install agent-framework), not just agent-framework-core. The core package omits provider integrations. If you only want OpenAI, pip install agent-framework-core agent-framework-openai also works.Troubleshooting: ‘response_format’ not respected / plain text returned
Confirm response_format is inside the options dict, that your model supports structured outputs (gpt-4o / gpt-4o-mini do), and that your Pydantic model has no unsupported field types. Primitives and bare lists are not supported as a top-level schema; wrap them in a BaseModel.Troubleshooting: agent never calls my tool
Tighten the function docstring and the Annotated Field descriptions, the model uses them to decide. Make the instruction explicit (‘Use the tools when relevant’). If a tool needs runtime-only data, inject it via FunctionInvocationContext rather than as a model-visible parameter.Troubleshooting: .env values not loading
MAF does not auto-load .env. Call load_dotenv() from python-dotenv at the top of your script, or export the variables in your shell before running.Troubleshooting: RuntimeError about the event loop
Do not call asyncio.run() twice or nest it. In notebooks, await the coroutine directly in a cell (Jupyter already runs a loop) instead of wrapping it in asyncio.run().Microsoft Agent Framework vs Semantic Kernel and AutoGen: what changed?
Microsoft Agent Framework vs Semantic Kernel and AutoGen is not a competition, MAF is the successor that absorbs both: Semantic Kernel’s enterprise plumbing and AutoGen’s agent and orchestration ideas now live in one SDK, with Microsoft positioning MAF as the path forward for both. Understanding the lineage tells you exactly how much migration pain to expect.
From Semantic Kernel, the move is gentle. Microsoft’s migration guide frames it as mostly import-path and class-name changes, the `Kernel` plus plugin model maps onto the `Agent` plus tool model, and a typical 5-to-10-plugin app is roughly a 2-to-4-hour port. If you are on SK today, there is no fire drill; migrate when you next touch the code.
From AutoGen, the move is deeper because the programming model changes. AutoGen is conversation-centric: agents exchange messages on a chat thread. MAF’s multi-agent orchestration is a typed, directed graph, executors activate when their inputs are ready, and data flows along explicit edges, conceptually closer to LangGraph than to an AutoGen group chat. AutoGen’s `AssistantAgent` maps to MAF’s agent abstractions, `FunctionTool` becomes the `@tool` decorator, and event-driven teams become graph workflows. Budget real rethinking time, not just a find-and-replace.
The table below summarizes the practical migration shape so you can size the work before committing.
| Dimension | Semantic Kernel (legacy) | AutoGen (legacy) | Microsoft Agent Framework 1.0+ |
|---|---|---|---|
| Core abstraction | Kernel + plugins | Conversation-centric agents | Agent + tools, graph workflows |
| Multi-agent model | Limited / manual | Group chat on a thread | Typed directed-graph workflow |
| Tool definition | KernelFunction plugin | FunctionTool wrapper | Plain typed function or @tool |
| Enterprise features | Strong (state, telemetry) | Lighter | Inherits SK’s, unified |
| Migration effort | ~2-4 hrs (import paths) | Higher (model changes) | N/A (this is the target) |
| Status | Superseded by MAF | Superseded by MAF | 1.0 GA, April 2026, LTS |
Is the Microsoft Agent Framework production ready, and when should you NOT use it?
Adopt MAF for multi-agent and enterprise builds; skip it for a single simple agent
Yes, the core of the Microsoft Agent Framework is production ready: version 1.0 GA (April 2026) commits to stable APIs and long-term support, and the agent runtime, tools, structured output, and the graph-based workflow engine are all stable, but the experimental Functional Workflow API is the one piece to keep out of a critical path. “Production ready” applies to the parts you used in this tutorial; it does not blanket-bless everything in the repo.
The 1.0 sharp edges worth flagging. First, the Python Functional Workflow API, the `@workflow` and `@step` decorators that let you write workflows as plain async functions instead of graph executors, is still marked experimental in 1.0. It produces the same observable results as the stable graph API and is a nice on-ramp, but I would not anchor a launch to it yet; use the graph-based workflow API for anything load-bearing. Second, the async streaming behavior covered above (structured `value` only after `get_final_response()`, no nesting `asyncio.run()`) is a real source of bugs, treat it as a contract. Third, Python and .NET are both first-class, but specific provider integrations and hosted tools land at slightly different times across the two, so verify a given feature in the Python package rather than assuming parity from a .NET sample.
When should you NOT reach for MAF? If you need a single tool-using agent with type-safe outputs and nothing else, a lighter framework like Pydantic AI gets you there with less surface area (see our Pydantic AI review). If you are committed to LangGraph’s ecosystem for graph orchestration, MAF’s workflow engine is comparable but not a reason to switch on its own. MAF earns its weight when you want enterprise telemetry, middleware, session state, MCP/A2A interop, and multi-agent orchestration unified behind one SDK, which is precisely the gap that having Semantic Kernel and AutoGen as separate projects used to leave.
Pros
Cons
Builder’s take
I build agent orchestration for a living at Cyntr, so I came to MAF asking one question: would I rip out my current stack to adopt it? Here is the honest read after shipping a Python agent on it.
- The single-binary-of-SDKs promise is real: one import surface across chat clients, tools, structured output, and workflows beats juggling Semantic Kernel plus AutoGen plus glue code.
- Tools are the best part. A plain typed Python function with an Annotated docstring just works as a tool with zero schema boilerplate, which is exactly how I wish every framework did it.
- The graph-based workflow engine is stable and genuinely good, but the experimental Functional Workflow API is where I would not bet a production deadline yet.
- If you are on Semantic Kernel, migrate when convenient (it is mostly import paths). If you are deep in AutoGen, budget real time, because the mental model changes from chat threads to a directed graph.
- For a single tool-using agent, MAF is overkill versus Pydantic AI. MAF earns its weight when you need enterprise telemetry, middleware, and multi-agent orchestration in one place.
Frequently asked questions
Create a virtual environment and run pip install agent-framework, which installs all sub-packages. For a smaller footprint, install agent-framework-core plus a specific integration such as agent-framework-openai. As of the stable 1.7.0 release (May 28, 2026) the –pre flag is no longer required; it was only needed during the 1.0 release-candidate window.
In the current stable Python SDK you instantiate Agent (from agent_framework) with a chat client, name, instructions, and tools. ChatAgent was the name used during the 1.0 release-candidate period and still appears in older posts; treat Agent as the canonical class for new code on 1.x.
Write a plain Python function with type hints, describe parameters using Annotated and Pydantic’s Field, and pass it in the agent’s tools list. MAF auto-generates the JSON schema from your signature and docstring. Use the @tool decorator (formerly @ai_function) only when you want to override the tool’s name or description.
Define a Pydantic BaseModel and pass it as options={‘response_format’: YourModel} to agent.run(). MAF instructs the model to conform to the schema, validates the result, and returns a typed instance on result.value. If parsing fails, result.value is falsy and you can fall back to result.text.
Yes for its core. Version 1.0 reached GA in April 2026 with stable APIs and long-term support, and the agent runtime, tools, structured output, and graph-based workflow engine are stable. The exception is the Python Functional Workflow API (@workflow/@step decorators), which is still experimental in 1.0, so keep it out of load-bearing paths for now.
From Semantic Kernel, migration is mostly import-path and class-name changes, roughly 2 to 4 hours for a typical 5-to-10-plugin app. From AutoGen it is harder because the model shifts from conversation-centric chat threads to a typed directed-graph workflow, so budget time to rethink orchestration, not just rename APIs.
Primary sources
- Microsoft Agent Framework Version 1.0 — Microsoft DevBlogs
- Microsoft Ships Production-Ready Agent Framework 1.0 for .NET and Python — Visual Studio Magazine
- Step 1: Your First Agent — Microsoft Learn
- Using function tools with an agent — Microsoft Learn
- Producing Structured Outputs with agents — Microsoft Learn
- AutoGen to Microsoft Agent Framework Migration Guide — Microsoft Learn
- agent-framework on PyPI — Python Package Index
- microsoft/agent-framework on GitHub — GitHub
Last updated: June 2, 2026. Related: Agent Infrastructure.