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
@@ -72,22 +72,20 @@ class Aggregator(Executor):
async def main() -> None:
# 1) Create the executors
dispatcher = Dispatcher(id="dispatcher")
average = Average(id="average")
summation = Sum(id="summation")
aggregator = Aggregator(id="aggregator")
# 2) Build a simple fan out and fan in workflow
# 1) Build a simple fan out and fan in workflow
workflow = (
WorkflowBuilder()
.set_start_executor(dispatcher)
.add_fan_out_edges(dispatcher, [average, summation])
.add_fan_in_edges([average, summation], aggregator)
.register_executor(lambda: Dispatcher(id="dispatcher"), name="dispatcher")
.register_executor(lambda: Average(id="average"), name="average")
.register_executor(lambda: Sum(id="summation"), name="summation")
.register_executor(lambda: Aggregator(id="aggregator"), name="aggregator")
.set_start_executor("dispatcher")
.add_fan_out_edges("dispatcher", ["average", "summation"])
.add_fan_in_edges(["average", "summation"], "aggregator")
.build()
)
# 3) Run the workflow
# 2) Run the workflow
output: list[int | float] | None = None
async for event in workflow.run_stream([random.randint(1, 100) for _ in range(10)]):
if isinstance(event, WorkflowOutputEvent):
@@ -4,10 +4,10 @@ import asyncio
from dataclasses import dataclass
from agent_framework import ( # Core chat primitives to build LLM requests
AgentExecutor, # Wraps an LLM agent for use inside a workflow
AgentExecutorRequest, # The message bundle sent to an AgentExecutor
AgentExecutorResponse, # The structured result returned by an AgentExecutor
AgentRunEvent, # Tracing event for agent execution steps
AgentRunEvent,
ChatAgent, # Tracing event for agent execution steps
ChatMessage, # Chat message structure
Executor, # Base class for custom Python executors
Role, # Enum of chat roles (user, assistant, system)
@@ -16,7 +16,7 @@ from agent_framework import ( # Core chat primitives to build LLM requests
WorkflowOutputEvent, # Event emitted when workflow yields output
handler, # Decorator to mark an Executor method as invokable
)
from agent_framework.azure import AzureOpenAIChatClient # Client wrapper for Azure OpenAI chat models
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential # Uses your az CLI login for credentials
from typing_extensions import Never
@@ -42,20 +42,11 @@ Prerequisites:
class DispatchToExperts(Executor):
"""Dispatches the incoming prompt to all expert agent executors for parallel processing (fan out)."""
def __init__(self, expert_ids: list[str], id: str | None = None):
super().__init__(id=id or "dispatch_to_experts")
self._expert_ids = expert_ids
@handler
async def dispatch(self, prompt: str, ctx: WorkflowContext[AgentExecutorRequest]) -> None:
# Wrap the incoming prompt as a user message for each expert and request a response.
# Each send_message targets a different AgentExecutor by id so that branches run in parallel.
initial_message = ChatMessage(Role.USER, text=prompt)
for expert_id in self._expert_ids:
await ctx.send_message(
AgentExecutorRequest(messages=[initial_message], should_respond=True),
target_id=expert_id,
)
await ctx.send_message(AgentExecutorRequest(messages=[initial_message], should_respond=True))
@dataclass
@@ -70,10 +61,6 @@ class AggregatedInsights:
class AggregateInsights(Executor):
"""Aggregates expert agent responses into a single consolidated result (fan in)."""
def __init__(self, expert_ids: list[str], id: str | None = None):
super().__init__(id=id or "aggregate_insights")
self._expert_ids = expert_ids
@handler
async def aggregate(self, results: list[AgentExecutorResponse], ctx: WorkflowContext[Never, str]) -> None:
# Map responses to text by executor id for a simple, predictable demo.
@@ -104,49 +91,51 @@ class AggregateInsights(Executor):
await ctx.yield_output(consolidated)
def create_researcher_agent() -> ChatAgent:
"""Creates a research domain expert agent."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
instructions=(
"You're an expert market and product researcher. Given a prompt, provide concise, factual insights,"
" opportunities, and risks."
),
name="researcher",
)
def create_marketer_agent() -> ChatAgent:
"""Creates a marketing domain expert agent."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
instructions=(
"You're a creative marketing strategist. Craft compelling value propositions and target messaging"
" aligned to the prompt."
),
name="marketer",
)
def create_legal_agent() -> ChatAgent:
"""Creates a legal/compliance domain expert agent."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
instructions=(
"You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns"
" based on the prompt."
),
name="legal",
)
async def main() -> None:
# 1) Create agent executors for domain experts
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
researcher = AgentExecutor(
chat_client.create_agent(
instructions=(
"You're an expert market and product researcher. Given a prompt, provide concise, factual insights,"
" opportunities, and risks."
),
),
id="researcher",
)
marketer = AgentExecutor(
chat_client.create_agent(
instructions=(
"You're a creative marketing strategist. Craft compelling value propositions and target messaging"
" aligned to the prompt."
),
),
id="marketer",
)
legal = AgentExecutor(
chat_client.create_agent(
instructions=(
"You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns"
" based on the prompt."
),
),
id="legal",
)
expert_ids = [researcher.id, marketer.id, legal.id]
dispatcher = DispatchToExperts(expert_ids=expert_ids, id="dispatcher")
aggregator = AggregateInsights(expert_ids=expert_ids, id="aggregator")
# 2) Build a simple fan out and fan in workflow
# 1) Build a simple fan out and fan in workflow
workflow = (
WorkflowBuilder()
.set_start_executor(dispatcher)
.add_fan_out_edges(dispatcher, [researcher, marketer, legal]) # Parallel branches
.add_fan_in_edges([researcher, marketer, legal], aggregator) # Join at the aggregator
.register_agent(create_researcher_agent, name="researcher")
.register_agent(create_marketer_agent, name="marketer")
.register_agent(create_legal_agent, name="legal")
.register_executor(lambda: DispatchToExperts(id="dispatcher"), name="dispatcher")
.register_executor(lambda: AggregateInsights(id="aggregator"), name="aggregator")
.set_start_executor("dispatcher")
.add_fan_out_edges("dispatcher", ["researcher", "marketer", "legal"]) # Parallel branches
.add_fan_in_edges(["researcher", "marketer", "legal"], "aggregator") # Join at the aggregator
.build()
)
@@ -259,27 +259,50 @@ class CompletionExecutor(Executor):
async def main():
"""Construct the map reduce workflow, visualize it, then run it over a sample file."""
# Step 1: Create the executors.
map_operations = [Map(id=f"map_executor_{i}") for i in range(3)]
split_operation = Split(
[map_operation.id for map_operation in map_operations],
id="split_data_executor",
# Step 1: Create the workflow builder and register executors.
workflow_builder = (
WorkflowBuilder()
.register_executor(lambda: Map(id="map_executor_0"), name="map_executor_0")
.register_executor(lambda: Map(id="map_executor_1"), name="map_executor_1")
.register_executor(lambda: Map(id="map_executor_2"), name="map_executor_2")
.register_executor(
lambda: Split(["map_executor_0", "map_executor_1", "map_executor_2"], id="split_data_executor"),
name="split_data_executor",
)
.register_executor(lambda: Reduce(id="reduce_executor_0"), name="reduce_executor_0")
.register_executor(lambda: Reduce(id="reduce_executor_1"), name="reduce_executor_1")
.register_executor(lambda: Reduce(id="reduce_executor_2"), name="reduce_executor_2")
.register_executor(lambda: Reduce(id="reduce_executor_3"), name="reduce_executor_3")
.register_executor(
lambda: Shuffle(
["reduce_executor_0", "reduce_executor_1", "reduce_executor_2", "reduce_executor_3"],
id="shuffle_executor",
),
name="shuffle_executor",
)
.register_executor(lambda: CompletionExecutor(id="completion_executor"), name="completion_executor")
)
reduce_operations = [Reduce(id=f"reduce_executor_{i}") for i in range(4)]
shuffle_operation = Shuffle(
[reduce_operation.id for reduce_operation in reduce_operations],
id="shuffle_executor",
)
completion_executor = CompletionExecutor(id="completion_executor")
# Step 2: Build the workflow graph using fan out and fan in edges.
workflow = (
WorkflowBuilder()
.set_start_executor(split_operation)
.add_fan_out_edges(split_operation, map_operations) # Split -> many mappers
.add_fan_in_edges(map_operations, shuffle_operation) # All mappers -> shuffle
.add_fan_out_edges(shuffle_operation, reduce_operations) # Shuffle -> many reducers
.add_fan_in_edges(reduce_operations, completion_executor) # All reducers -> completion
workflow_builder.set_start_executor("split_data_executor")
.add_fan_out_edges(
"split_data_executor",
["map_executor_0", "map_executor_1", "map_executor_2"],
) # Split -> many mappers
.add_fan_in_edges(
["map_executor_0", "map_executor_1", "map_executor_2"],
"shuffle_executor",
) # All mappers -> shuffle
.add_fan_out_edges(
"shuffle_executor",
["reduce_executor_0", "reduce_executor_1", "reduce_executor_2", "reduce_executor_3"],
) # Shuffle -> many reducers
.add_fan_in_edges(
["reduce_executor_0", "reduce_executor_1", "reduce_executor_2", "reduce_executor_3"],
"completion_executor",
) # All reducers -> completion
.build()
)