Python: WorkflowBuilder registry (#2486)

* Add workflow builder factory pattern

* Add internal edge groups to registered executors; next samples

* Update samples: Part 1

* register -> register_executor

* update hil samples

* Update other samples

* Update agent  samples

* Update doc string

* Add new sample

* Fix mypy

* Address comments

* Fix mypy
This commit is contained in:
Tao Chen
2025-12-04 21:26:10 -08:00
committed by GitHub
Unverified
parent 6809510413
commit f2ed5b55f6
33 changed files with 1609 additions and 696 deletions
@@ -1,11 +1,8 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from collections.abc import Awaitable, Callable
from contextlib import AsyncExitStack
from typing import Any
from agent_framework import AgentRunUpdateEvent, WorkflowBuilder, WorkflowOutputEvent
from agent_framework import AgentRunUpdateEvent, ChatAgent, WorkflowBuilder, WorkflowOutputEvent
from agent_framework.azure import AzureAIAgentClient
from azure.identity.aio import AzureCliCredential
@@ -29,48 +26,36 @@ Prerequisites:
"""
async def create_azure_ai_agent() -> tuple[Callable[..., Awaitable[Any]], Callable[[], Awaitable[None]]]:
"""Helper method to create a Azure AI agent factory and a close function.
def create_writer_agent(client: AzureAIAgentClient) -> ChatAgent:
return client.create_agent(
name="Writer",
instructions=(
"You are an excellent content writer. You create new content and edit contents based on the feedback."
),
)
This makes sure the async context managers are properly handled.
"""
stack = AsyncExitStack()
cred = await stack.enter_async_context(AzureCliCredential())
client = await stack.enter_async_context(AzureAIAgentClient(async_credential=cred))
async def agent(**kwargs: Any) -> Any:
return await stack.enter_async_context(client.create_agent(**kwargs))
async def close() -> None:
await stack.aclose()
return agent, close
def create_reviewer_agent(client: AzureAIAgentClient) -> ChatAgent:
return client.create_agent(
name="Reviewer",
instructions=(
"You are an excellent content reviewer. "
"Provide actionable feedback to the writer about the provided content. "
"Provide the feedback in the most concise manner possible."
),
)
async def main() -> None:
agent, close = await create_azure_ai_agent()
try:
writer = await agent(
name="Writer",
instructions=(
"You are an excellent content writer. You create new content and edit contents based on the feedback."
),
)
reviewer = await agent(
name="Reviewer",
instructions=(
"You are an excellent content reviewer. "
"Provide actionable feedback to the writer about the provided content. "
"Provide the feedback in the most concise manner possible."
),
)
async with AzureCliCredential() as cred, AzureAIAgentClient(async_credential=cred) as client:
# Build the workflow by adding agents directly as edges.
# Agents adapt to workflow mode: run_stream() for incremental updates, run() for complete responses.
workflow = (
WorkflowBuilder()
.set_start_executor(writer)
.add_edge(writer, reviewer)
.register_agent(lambda: create_writer_agent(client), name="writer")
.register_agent(lambda: create_reviewer_agent(client), name="reviewer", output_response=True)
.set_start_executor("writer")
.add_edge("writer", "reviewer")
.build()
)
@@ -89,8 +74,6 @@ async def main() -> None:
elif isinstance(event, WorkflowOutputEvent):
print("\n===== Final output =====")
print(event.data)
finally:
await close()
if __name__ == "__main__":
@@ -86,18 +86,17 @@ async def enrich_with_references(
await ctx.send_message(AgentExecutorRequest(messages=conversation))
async def main() -> None:
"""Run the workflow and stream combined updates from both agents."""
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
research_agent = chat_client.create_agent(
def create_research_agent():
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
name="research_agent",
instructions=(
"Produce a short, bullet-style briefing with two actionable ideas. Label the section as 'Initial Draft'."
),
)
final_editor_agent = chat_client.create_agent(
def create_final_editor_agent():
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
name="final_editor_agent",
instructions=(
"Use all conversation context (including external notes) to produce the final answer. "
@@ -105,11 +104,17 @@ async def main() -> None:
),
)
async def main() -> None:
"""Run the workflow and stream combined updates from both agents."""
workflow = (
WorkflowBuilder()
.set_start_executor(research_agent)
.add_edge(research_agent, enrich_with_references)
.add_edge(enrich_with_references, final_editor_agent)
.register_agent(create_research_agent, name="research_agent")
.register_agent(create_final_editor_agent, name="final_editor_agent")
.register_executor(lambda: enrich_with_references, name="enrich_with_references")
.set_start_executor("research_agent")
.add_edge("research_agent", "enrich_with_references")
.add_edge("enrich_with_references", "final_editor_agent")
.build()
)
@@ -26,35 +26,37 @@ Prerequisites:
"""
async def main():
"""Build and run a simple two node agent workflow: Writer then Reviewer."""
# Create the Azure chat client. AzureCliCredential uses your current az login.
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
# Define two domain specific chat agents.
writer_agent = chat_client.create_agent(
def create_writer_agent():
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
instructions=(
"You are an excellent content writer. You create new content and edit contents based on the feedback."
),
name="writer_agent",
name="writer",
)
reviewer_agent = chat_client.create_agent(
def create_reviewer_agent():
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
instructions=(
"You are an excellent content reviewer."
"Provide actionable feedback to the writer about the provided content."
"Provide the feedback in the most concise manner possible."
),
name="reviewer_agent",
name="reviewer",
)
async def main():
"""Build and run a simple two node agent workflow: Writer then Reviewer."""
# Build the workflow using the fluent builder.
# Set the start node and connect an edge from writer to reviewer.
# Agents adapt to workflow mode: run_stream() for incremental updates, run() for complete responses.
workflow = (
WorkflowBuilder()
.set_start_executor(writer_agent)
.add_edge(writer_agent, reviewer_agent)
.register_agent(create_writer_agent, name="writer")
.register_agent(create_reviewer_agent, name="reviewer", output_response=True)
.set_start_executor("writer")
.add_edge("writer", "reviewer")
.build()
)
@@ -10,6 +10,7 @@ from agent_framework import (
AgentExecutorResponse,
AgentRunResponse,
AgentRunUpdateEvent,
ChatAgent,
ChatMessage,
Executor,
FunctionCallContent,
@@ -166,6 +167,31 @@ class Coordinator(Executor):
)
def create_writer_agent() -> ChatAgent:
"""Creates a writer agent with tools."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
name="writer_agent",
instructions=(
"You are a marketing writer. Call the available tools before drafting copy so you are precise. "
"Always call both tools once before drafting. Summarize tool outputs as bullet points, then "
"produce a 3-sentence draft."
),
tools=[fetch_product_brief, get_brand_voice_profile],
tool_choice=ToolMode.REQUIRED_ANY,
)
def create_final_editor_agent() -> ChatAgent:
"""Creates a final editor agent."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
name="final_editor_agent",
instructions=(
"You are an editor who polishes marketing copy after human approval. "
"Correct any legal or factual issues. Return the final version even if no changes are made. "
),
)
def display_agent_run_update(event: AgentRunUpdateEvent, last_executor: str | None) -> None:
"""Display an AgentRunUpdateEvent in a readable format."""
printed_tool_calls: set[str] = set()
@@ -211,42 +237,25 @@ def display_agent_run_update(event: AgentRunUpdateEvent, last_executor: str | No
async def main() -> None:
"""Run the workflow and bridge human feedback between two agents."""
# Create agents with tools and instructions.
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
writer_agent = chat_client.create_agent(
name="writer_agent",
instructions=(
"You are a marketing writer. Call the available tools before drafting copy so you are precise. "
"Always call both tools once before drafting. Summarize tool outputs as bullet points, then "
"produce a 3-sentence draft."
),
tools=[fetch_product_brief, get_brand_voice_profile],
tool_choice=ToolMode.REQUIRED_ANY,
)
final_editor_agent = chat_client.create_agent(
name="final_editor_agent",
instructions=(
"You are an editor who polishes marketing copy after human approval. "
"Correct any legal or factual issues. Return the final version even if no changes are made. "
),
)
coordinator = Coordinator(
id="coordinator",
writer_id="writer_agent",
final_editor_id="final_editor_agent",
)
# Build the workflow.
workflow = (
WorkflowBuilder()
.set_start_executor(writer_agent)
.add_edge(writer_agent, coordinator)
.add_edge(coordinator, writer_agent)
.add_edge(final_editor_agent, coordinator)
.add_edge(coordinator, final_editor_agent)
.register_agent(create_writer_agent, name="writer_agent")
.register_agent(create_final_editor_agent, name="final_editor_agent")
.register_executor(
lambda: Coordinator(
id="coordinator",
writer_id="writer_agent",
final_editor_id="final_editor_agent",
),
name="coordinator",
)
.set_start_executor("writer_agent")
.add_edge("writer_agent", "coordinator")
.add_edge("coordinator", "writer_agent")
.add_edge("final_editor_agent", "coordinator")
.add_edge("coordinator", "final_editor_agent")
.build()
)
@@ -41,9 +41,9 @@ class Writer(Executor):
agent: ChatAgent
def __init__(self, chat_client: AzureOpenAIChatClient, id: str = "writer"):
def __init__(self, id: str = "writer"):
# Create a domain specific agent using your configured AzureOpenAIChatClient.
self.agent = chat_client.create_agent(
self.agent = AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
instructions=(
"You are an excellent content writer. You create new content and edit contents based on the feedback."
),
@@ -83,9 +83,9 @@ class Reviewer(Executor):
agent: ChatAgent
def __init__(self, chat_client: AzureOpenAIChatClient, id: str = "reviewer"):
def __init__(self, id: str = "reviewer"):
# Create a domain specific agent that evaluates and refines content.
self.agent = chat_client.create_agent(
self.agent = AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
instructions=(
"You are an excellent content reviewer. You review the content and provide feedback to the writer."
),
@@ -105,16 +105,17 @@ class Reviewer(Executor):
async def main():
"""Build and run a simple two node agent workflow: Writer then Reviewer."""
# Create the Azure chat client. AzureCliCredential uses your current az login.
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
# Instantiate the two agent backed executors.
writer = Writer(chat_client)
reviewer = Reviewer(chat_client)
# Build the workflow using the fluent builder.
# Set the start node and connect an edge from writer to reviewer.
workflow = WorkflowBuilder().set_start_executor(writer).add_edge(writer, reviewer).build()
workflow = (
WorkflowBuilder()
.register_executor(Writer, name="writer")
.register_executor(Reviewer, name="reviewer")
.set_start_executor("writer")
.add_edge("writer", "reviewer")
.build()
)
# Run the workflow with the user's initial message.
# For foundational clarity, use run (non streaming) and print the workflow output.
@@ -5,6 +5,7 @@ from typing import Never
from agent_framework import (
AgentExecutorResponse,
ChatAgent,
Executor,
HostedCodeInterpreterTool,
WorkflowBuilder,
@@ -70,21 +71,39 @@ class Evaluator(Executor):
await ctx.yield_output(f"Correctness: {correctness}, Consumption: {consumption}")
def create_coding_agent(client: AzureAIAgentClient) -> ChatAgent:
"""Create an AI agent with code interpretation capabilities.
This agent can generate and execute Python code to solve problems.
Args:
client: The AzureAIAgentClient used to create the agent
Returns:
A ChatAgent configured with coding instructions and tools
"""
return client.create_agent(
name="CodingAgent",
instructions=("You are a helpful assistant that can write and execute Python code to solve problems."),
tools=HostedCodeInterpreterTool(),
)
async def main():
async with (
AzureCliCredential() as credential,
AzureAIAgentClient(async_credential=credential) as chat_client,
):
# Create an agent with code interpretation capabilities
agent = chat_client.create_agent(
name="CodingAgent",
instructions=("You are a helpful assistant that can write and execute Python code to solve problems."),
tools=HostedCodeInterpreterTool(),
)
# Build a workflow: Agent generates code -> Evaluator assesses results
# The agent will be wrapped in a special agent executor which produces AgentExecutorResponse
workflow = WorkflowBuilder().set_start_executor(agent).add_edge(agent, Evaluator(id="evaluator")).build()
workflow = (
WorkflowBuilder()
.register_agent(lambda: create_coding_agent(chat_client), name="coding_agent")
.register_executor(lambda: Evaluator(id="evaluator"), name="evaluator")
.set_start_executor("coding_agent")
.add_edge("coding_agent", "evaluator")
.build()
)
# Execute the workflow with a specific coding task
results = await workflow.run(
@@ -81,7 +81,7 @@ class ReviewerWithHumanInTheLoop(Executor):
@response_handler
async def accept_human_review(
self,
original_request: ReviewRequest,
original_request: HumanReviewRequest,
response: ReviewResponse,
ctx: WorkflowContext[ReviewResponse],
) -> None:
@@ -97,20 +97,25 @@ async def main() -> None:
print("Starting Workflow Agent with Human-in-the-Loop Demo")
print("=" * 50)
# Create executors for the workflow.
print("Creating chat client and executors...")
mini_chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
worker = Worker(id="sub-worker", chat_client=mini_chat_client)
reviewer = ReviewerWithHumanInTheLoop(worker_id=worker.id)
print("Building workflow with Worker-Reviewer cycle...")
# Build a workflow with bidirectional communication between Worker and Reviewer,
# and escalation paths for human review.
agent = (
WorkflowBuilder()
.add_edge(worker, reviewer) # Worker sends requests to Reviewer
.add_edge(reviewer, worker) # Reviewer sends feedback to Worker
.set_start_executor(worker)
.register_executor(
lambda: Worker(
id="sub-worker",
chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()),
),
name="worker",
)
.register_executor(
lambda: ReviewerWithHumanInTheLoop(worker_id="sub-worker"),
name="reviewer",
)
.add_edge("worker", "reviewer") # Worker sends requests to Reviewer
.add_edge("reviewer", "worker") # Reviewer sends feedback to Worker
.set_start_executor("worker")
.build()
.as_agent() # Convert workflow into an agent interface
)
@@ -195,19 +195,20 @@ async def main() -> None:
print("Starting Workflow Agent Demo")
print("=" * 50)
# Initialize chat clients and executors.
print("Creating chat client and executors...")
mini_chat_client = OpenAIChatClient(model_id="gpt-4.1-nano")
chat_client = OpenAIChatClient(model_id="gpt-4.1")
reviewer = Reviewer(id="reviewer", chat_client=chat_client)
worker = Worker(id="worker", chat_client=mini_chat_client)
print("Building workflow with Worker ↔ Reviewer cycle...")
agent = (
WorkflowBuilder()
.add_edge(worker, reviewer) # Worker sends responses to Reviewer
.add_edge(reviewer, worker) # Reviewer provides feedback to Worker
.set_start_executor(worker)
.register_executor(
lambda: Worker(id="worker", chat_client=OpenAIChatClient(model_id="gpt-4.1-nano")),
name="worker",
)
.register_executor(
lambda: Reviewer(id="reviewer", chat_client=OpenAIChatClient(model_id="gpt-4.1")),
name="reviewer",
)
.add_edge("worker", "reviewer") # Worker sends responses to Reviewer
.add_edge("reviewer", "worker") # Reviewer provides feedback to Worker
.set_start_executor("worker")
.build()
.as_agent() # Wrap workflow as an agent
)