[BREAKING] Python: Remove workflow register factory methods. Update tests and samples (#3781)

* Remove workflow register factory methods. Update tests and samples

* Address Copilot feedback
This commit is contained in:
Evan Mattson
2026-02-11 07:16:17 +09:00
committed by GitHub
Unverified
parent f407f726a7
commit a4c9e43afb
46 changed files with 650 additions and 3660 deletions
@@ -4,6 +4,7 @@ import asyncio
from agent_framework import (
Executor,
Workflow,
WorkflowBuilder,
WorkflowContext,
executor,
@@ -48,6 +49,11 @@ What this example shows
- Fluent WorkflowBuilder API:
add_edge(A, B) to connect nodes, set_start_executor(A), then build() -> Workflow.
- State isolation via helper functions:
Wrapping executor instantiation and workflow building inside a function
(e.g., create_workflow()) ensures each call produces fresh, independent
instances. This is the recommended pattern for reuse.
- Running and results:
workflow.run(initial_input) executes the graph. Terminal nodes yield
outputs using ctx.yield_output(). The workflow runs until idle.
@@ -152,18 +158,28 @@ class ExclamationAdder(Executor):
await ctx.send_message(result) # type: ignore
def create_workflow() -> Workflow:
"""Create a fresh workflow with isolated state.
Wrapping workflow construction in a helper function ensures each call
produces independent executor instances. This is the recommended pattern
for reuse — call create_workflow() each time you need a new workflow so
that no state leaks between runs.
"""
upper_case = UpperCase(id="upper_case_executor")
return WorkflowBuilder(start_executor=upper_case).add_edge(upper_case, reverse_text).build()
async def main():
"""Build and run workflows using the fluent builder API."""
# Workflow 1: Using introspection-based type detection
# -----------------------------------------------------
upper_case = UpperCase(id="upper_case_executor")
# Build the workflow using a fluent pattern:
# 1) start_executor=... in constructor declares the entry point
# 2) add_edge(from_node, to_node) defines a directed edge upper_case -> reverse_text
# 3) build() finalizes and returns an immutable Workflow object
workflow1 = WorkflowBuilder(start_executor=upper_case).add_edge(upper_case, reverse_text).build()
# Workflow 1: Using the helper function pattern for state isolation
# ------------------------------------------------------------------
# Each call to create_workflow() returns a workflow with fresh executor
# instances. This is the recommended pattern when you need to run the
# same workflow topology multiple times with clean state.
workflow1 = create_workflow()
# Run the workflow by sending the initial message to the start node.
# The run(...) call returns an event collection; its get_outputs() method
@@ -175,6 +191,7 @@ async def main():
# Workflow 2: Using explicit type parameters on @handler
# -------------------------------------------------------
upper_case = UpperCase(id="upper_case_executor")
exclamation_adder = ExclamationAdder(id="exclamation_adder")
# This workflow demonstrates the explicit input/output feature:
@@ -1,104 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import (
AgentResponseUpdate,
ChatAgent,
Executor,
WorkflowBuilder,
WorkflowContext,
executor,
handler,
)
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential
"""
Step 4: Using Factories to Define Executors and Agents
What this example shows
- Defining custom executors using both class-based and function-based approaches.
- Registering executor and agent factories with WorkflowBuilder for lazy instantiation.
- Building a simple workflow that transforms input text through multiple steps.
Benefits of using factories
- Decouples executor and agent creation from workflow definition.
- Isolated instances are created for workflow builder build, allowing for cleaner state management
and handling parallel workflow runs.
It is recommended to use factories when defining executors and agents for production workflows.
Prerequisites
- No external services required.
"""
class UpperCase(Executor):
def __init__(self, id: str):
super().__init__(id=id)
@handler
async def to_upper_case(self, text: str, ctx: WorkflowContext[str]) -> None:
"""Convert the input to uppercase and forward it to the next node."""
result = text.upper()
# Send the result to the next executor in the workflow.
await ctx.send_message(result)
@executor(id="reverse_text_executor")
async def reverse_text(text: str, ctx: WorkflowContext[str]) -> None:
"""Reverse the input string and send it downstream."""
result = text[::-1]
# Send the result to the next executor in the workflow.
await ctx.send_message(result)
def create_agent() -> ChatAgent:
"""Factory function to create a Writer agent."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
instructions=("You decode messages. Try to reconstruct the original message."),
name="decoder",
)
async def main():
"""Build and run a simple 2-step workflow using the fluent builder API."""
# Build the workflow using a fluent pattern:
# 1) register_executor(factory, name) registers an executor factory
# 2) register_agent(factory, name) registers an agent factory
# 3) add_chain([node_names]) adds a sequence of nodes to the workflow
# 4) set_start_executor(node) declares the entry point
# 5) build() finalizes and returns an immutable Workflow object
workflow = (
WorkflowBuilder(start_executor="UpperCase")
.register_executor(lambda: UpperCase(id="upper_case_executor"), name="UpperCase")
.register_executor(lambda: reverse_text, name="ReverseText")
.register_agent(create_agent, name="DecoderAgent")
.add_chain(["UpperCase", "ReverseText", "DecoderAgent"])
.build()
)
first_update = True
async for event in workflow.run("hello world", stream=True):
# The outputs of the workflow are whatever the agents produce. So the events are expected to
# contain `AgentResponseUpdate` from the agents in the workflow.
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
update = event.data
if first_update:
print(f"{update.author_name}: {update.text}", end="", flush=True)
first_update = False
else:
print(update.text, end="", flush=True)
"""
Sample Output:
decoder: HELLO WORLD
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -3,6 +3,7 @@
import asyncio
from agent_framework import (
AgentExecutor,
AgentExecutorRequest,
AgentExecutorResponse,
ChatMessageStore,
@@ -70,15 +71,12 @@ async def main() -> None:
# Set the message store to store messages in memory.
shared_thread.message_store = ChatMessageStore()
writer_executor = AgentExecutor(writer, agent_thread=shared_thread)
reviewer_executor = AgentExecutor(reviewer, agent_thread=shared_thread)
workflow = (
WorkflowBuilder(start_executor="writer")
.register_agent(factory_func=lambda: writer, name="writer", agent_thread=shared_thread)
.register_agent(factory_func=lambda: reviewer, name="reviewer", agent_thread=shared_thread)
.register_executor(
factory_func=lambda: intercept_agent_response,
name="intercept_agent_response",
)
.add_chain(["writer", "intercept_agent_response", "reviewer"])
WorkflowBuilder(start_executor=writer_executor)
.add_chain([writer_executor, intercept_agent_response, reviewer_executor])
.build()
)
@@ -6,6 +6,7 @@ from dataclasses import dataclass, field
from typing import Annotated
from agent_framework import (
AgentExecutor,
AgentExecutorRequest,
AgentExecutorResponse,
AgentResponse,
@@ -239,22 +240,20 @@ async def main() -> None:
"""Run the workflow and bridge human feedback between two agents."""
# Build the workflow.
writer_agent = AgentExecutor(create_writer_agent())
final_editor_agent = AgentExecutor(create_final_editor_agent())
coordinator = Coordinator(
id="coordinator",
writer_id="writer_agent",
final_editor_id="final_editor_agent",
)
workflow = (
WorkflowBuilder(start_executor="writer_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",
)
.add_edge("writer_agent", "coordinator")
.add_edge("coordinator", "writer_agent")
.add_edge("final_editor_agent", "coordinator")
.add_edge("coordinator", "final_editor_agent")
WorkflowBuilder(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()
)
@@ -98,21 +98,16 @@ async def main() -> None:
print("Building workflow with Worker-Reviewer cycle...")
# Build a workflow with bidirectional communication between Worker and Reviewer,
# and escalation paths for human review.
worker = Worker(
id="worker",
chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()),
)
reviewer = ReviewerWithHumanInTheLoop(worker_id="worker")
agent = (
WorkflowBuilder(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
WorkflowBuilder(start_executor=worker)
.add_edge(worker, reviewer) # Worker sends requests to Reviewer
.add_edge(reviewer, worker) # Reviewer sends feedback to Worker
.build()
.as_agent() # Convert workflow into an agent interface
)
@@ -186,18 +186,13 @@ async def main() -> None:
print("=" * 50)
print("Building workflow with Worker ↔ Reviewer cycle...")
worker = Worker(id="worker", chat_client=OpenAIChatClient(model_id="gpt-4.1-nano"))
reviewer = Reviewer(id="reviewer", chat_client=OpenAIChatClient(model_id="gpt-4.1"))
agent = (
WorkflowBuilder(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
WorkflowBuilder(start_executor=worker)
.add_edge(worker, reviewer) # Worker sends responses to Reviewer
.add_edge(reviewer, worker) # Reviewer provides feedback to Worker
.build()
.as_agent() # Wrap workflow as an agent
)
@@ -2,7 +2,7 @@
import asyncio
from agent_framework import AgentThread, ChatAgent, ChatMessageStore
from agent_framework import AgentThread, ChatMessageStore
from agent_framework.openai import OpenAIChatClient
from agent_framework.orchestrations import SequentialBuilder
@@ -39,27 +39,24 @@ async def main() -> None:
# Create a chat client
chat_client = OpenAIChatClient()
# Define factory functions for workflow participants
def create_assistant() -> ChatAgent:
return chat_client.as_agent(
name="assistant",
instructions=(
"You are a helpful assistant. Answer questions based on the conversation "
"history. If the user asks about something mentioned earlier, reference it."
),
)
assistant = chat_client.as_agent(
name="assistant",
instructions=(
"You are a helpful assistant. Answer questions based on the conversation "
"history. If the user asks about something mentioned earlier, reference it."
),
)
def create_summarizer() -> ChatAgent:
return chat_client.as_agent(
name="summarizer",
instructions=(
"You are a summarizer. After the assistant responds, provide a brief "
"one-sentence summary of the key point from the conversation so far."
),
)
summarizer = chat_client.as_agent(
name="summarizer",
instructions=(
"You are a summarizer. After the assistant responds, provide a brief "
"one-sentence summary of the key point from the conversation so far."
),
)
# Build a sequential workflow: assistant -> summarizer
workflow = SequentialBuilder(participant_factories=[create_assistant, create_summarizer]).build()
workflow = SequentialBuilder(participants=[assistant, summarizer]).build()
# Wrap the workflow as an agent
agent = workflow.as_agent(name="ConversationalWorkflowAgent")
@@ -124,13 +121,12 @@ async def demonstrate_thread_serialization() -> None:
"""
chat_client = OpenAIChatClient()
def create_assistant() -> ChatAgent:
return chat_client.as_agent(
name="memory_assistant",
instructions="You are a helpful assistant with good memory. Remember details from our conversation.",
)
memory_assistant = chat_client.as_agent(
name="memory_assistant",
instructions="You are a helpful assistant with good memory. Remember details from our conversation.",
)
workflow = SequentialBuilder(participant_factories=[create_assistant]).build()
workflow = SequentialBuilder(participants=[memory_assistant]).build()
agent = workflow.as_agent(name="MemoryWorkflowAgent")
# Create initial thread and have a conversation
@@ -17,6 +17,7 @@ else:
# `agent_framework.builtin` chat client or mock the writer executor. We keep the
# concrete import here so readers can see an end-to-end configuration.
from agent_framework import (
AgentExecutor,
AgentExecutorRequest,
AgentExecutorResponse,
ChatMessage,
@@ -178,23 +179,21 @@ def create_workflow(checkpoint_storage: FileCheckpointStorage) -> Workflow:
# Wire the workflow DAG. Edges mirror the numbered steps described in the
# module docstring. Because `WorkflowBuilder` is declarative, reading these
# edges is often the quickest way to understand execution order.
writer_agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
instructions="Write concise, warm release notes that sound human and helpful.",
name="writer",
)
writer = AgentExecutor(writer_agent)
review_gateway = ReviewGateway(id="review_gateway", writer_id="writer")
prepare_brief = BriefPreparer(id="prepare_brief", agent_id="writer")
workflow_builder = (
WorkflowBuilder(
max_iterations=6, start_executor="prepare_brief", checkpoint_storage=checkpoint_storage
max_iterations=6, start_executor=prepare_brief, checkpoint_storage=checkpoint_storage
)
.register_agent(
lambda: AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
instructions="Write concise, warm release notes that sound human and helpful.",
# The agent name is stable across runs which keeps checkpoints deterministic.
name="writer",
),
name="writer",
)
.register_executor(lambda: ReviewGateway(id="review_gateway", writer_id="writer"), name="review_gateway")
.register_executor(lambda: BriefPreparer(id="prepare_brief", agent_id="writer"), name="prepare_brief")
.add_edge("prepare_brief", "writer")
.add_edge("writer", "review_gateway")
.add_edge("review_gateway", "writer") # revisions loop
.add_edge(prepare_brief, writer)
.add_edge(writer, review_gateway)
.add_edge(review_gateway, writer) # revisions loop
)
return workflow_builder.build()
@@ -105,12 +105,12 @@ class WorkerExecutor(Executor):
async def main():
# Build workflow with checkpointing enabled
checkpoint_storage = InMemoryCheckpointStorage()
start = StartExecutor(id="start")
worker = WorkerExecutor(id="worker")
workflow_builder = (
WorkflowBuilder(start_executor="start", checkpoint_storage=checkpoint_storage)
.register_executor(lambda: StartExecutor(id="start"), name="start")
.register_executor(lambda: WorkerExecutor(id="worker"), name="worker")
.add_edge("start", "worker")
.add_edge("worker", "worker") # Self-loop for iterative processing
WorkflowBuilder(start_executor=start, checkpoint_storage=checkpoint_storage)
.add_edge(start, worker)
.add_edge(worker, worker) # Self-loop for iterative processing
)
# Run workflow with automatic checkpoint recovery
@@ -297,14 +297,14 @@ class LaunchCoordinator(Executor):
def build_sub_workflow() -> WorkflowExecutor:
"""Assemble the sub-workflow used by the parent workflow executor."""
writer = DraftWriter()
router = DraftReviewRouter()
finaliser = DraftFinaliser()
sub_workflow = (
WorkflowBuilder(start_executor="writer")
.register_executor(DraftWriter, name="writer")
.register_executor(DraftReviewRouter, name="router")
.register_executor(DraftFinaliser, name="finaliser")
.add_edge("writer", "router")
.add_edge("router", "finaliser")
.add_edge("finaliser", "writer") # permits revision loops
WorkflowBuilder(start_executor=writer)
.add_edge(writer, router)
.add_edge(router, finaliser)
.add_edge(finaliser, writer) # permits revision loops
.build()
)
@@ -313,12 +313,12 @@ def build_sub_workflow() -> WorkflowExecutor:
def build_parent_workflow(storage: FileCheckpointStorage) -> Workflow:
"""Assemble the parent workflow that embeds the sub-workflow."""
coordinator = LaunchCoordinator()
sub_executor = build_sub_workflow()
return (
WorkflowBuilder(start_executor="coordinator", checkpoint_storage=storage)
.register_executor(LaunchCoordinator, name="coordinator")
.register_executor(build_sub_workflow, name="sub_executor")
.add_edge("coordinator", "sub_executor")
.add_edge("sub_executor", "coordinator")
WorkflowBuilder(start_executor=coordinator, checkpoint_storage=storage)
.add_edge(coordinator, sub_executor)
.add_edge(sub_executor, coordinator)
.build()
)
@@ -27,7 +27,6 @@ import asyncio
from agent_framework import (
AgentThread,
ChatAgent,
ChatMessageStore,
InMemoryCheckpointStorage,
)
@@ -43,20 +42,17 @@ async def basic_checkpointing() -> None:
chat_client = OpenAIChatClient()
def create_assistant() -> ChatAgent:
return chat_client.as_agent(
name="assistant",
instructions="You are a helpful assistant. Keep responses brief.",
)
assistant = chat_client.as_agent(
name="assistant",
instructions="You are a helpful assistant. Keep responses brief.",
)
def create_reviewer() -> ChatAgent:
return chat_client.as_agent(
name="reviewer",
instructions="You are a reviewer. Provide a one-sentence summary of the assistant's response.",
)
reviewer = chat_client.as_agent(
name="reviewer",
instructions="You are a reviewer. Provide a one-sentence summary of the assistant's response.",
)
# Build sequential workflow with participant factories
workflow = SequentialBuilder(participant_factories=[create_assistant, create_reviewer]).build()
workflow = SequentialBuilder(participants=[assistant, reviewer]).build()
agent = workflow.as_agent(name="CheckpointedAgent")
# Create checkpoint storage
@@ -87,13 +83,12 @@ async def checkpointing_with_thread() -> None:
chat_client = OpenAIChatClient()
def create_assistant() -> ChatAgent:
return chat_client.as_agent(
name="memory_assistant",
instructions="You are a helpful assistant with good memory. Reference previous conversation when relevant.",
)
assistant = chat_client.as_agent(
name="memory_assistant",
instructions="You are a helpful assistant with good memory. Reference previous conversation when relevant.",
)
workflow = SequentialBuilder(participant_factories=[create_assistant]).build()
workflow = SequentialBuilder(participants=[assistant]).build()
agent = workflow.as_agent(name="MemoryAgent")
# Create both thread (for conversation) and checkpoint storage (for workflow state)
@@ -131,13 +126,12 @@ async def streaming_with_checkpoints() -> None:
chat_client = OpenAIChatClient()
def create_assistant() -> ChatAgent:
return chat_client.as_agent(
name="streaming_assistant",
instructions="You are a helpful assistant.",
)
assistant = chat_client.as_agent(
name="streaming_assistant",
instructions="You are a helpful assistant.",
)
workflow = SequentialBuilder(participant_factories=[create_assistant]).build()
workflow = SequentialBuilder(participants=[assistant]).build()
agent = workflow.as_agent(name="StreamingCheckpointAgent")
checkpoint_storage = InMemoryCheckpointStorage()
@@ -140,9 +140,9 @@ def create_sub_workflow() -> WorkflowExecutor:
"""Create the text processing sub-workflow."""
print("🚀 Setting up sub-workflow...")
text_processor = TextProcessor()
processing_workflow = (
WorkflowBuilder(start_executor="text_processor")
.register_executor(TextProcessor, name="text_processor")
WorkflowBuilder(start_executor=text_processor)
.build()
)
@@ -153,12 +153,12 @@ async def main():
"""Main function to run the basic sub-workflow example."""
print("🔧 Setting up parent workflow...")
# Step 1: Create the parent workflow
orchestrator = TextProcessingOrchestrator()
sub_workflow_executor = create_sub_workflow()
main_workflow = (
WorkflowBuilder(start_executor="text_orchestrator")
.register_executor(TextProcessingOrchestrator, name="text_orchestrator")
.register_executor(create_sub_workflow, name="text_processor_workflow")
.add_edge("text_orchestrator", "text_processor_workflow")
.add_edge("text_processor_workflow", "text_orchestrator")
WorkflowBuilder(start_executor=orchestrator)
.add_edge(orchestrator, sub_workflow_executor)
.add_edge(sub_workflow_executor, orchestrator)
.build()
)
@@ -169,17 +169,18 @@ def build_resource_request_distribution_workflow() -> Workflow:
elif len(self._responses) > self._request_count:
raise ValueError("Received more responses than expected")
orchestrator = RequestDistribution("orchestrator")
resource_requester = ResourceRequester("resource_requester")
policy_checker = PolicyChecker("policy_checker")
result_collector = ResultCollector("result_collector")
return (
WorkflowBuilder(start_executor="orchestrator")
.register_executor(lambda: RequestDistribution("orchestrator"), name="orchestrator")
.register_executor(lambda: ResourceRequester("resource_requester"), name="resource_requester")
.register_executor(lambda: PolicyChecker("policy_checker"), name="policy_checker")
.register_executor(lambda: ResultCollector("result_collector"), name="result_collector")
.add_edge("orchestrator", "resource_requester")
.add_edge("orchestrator", "policy_checker")
.add_edge("resource_requester", "result_collector")
.add_edge("policy_checker", "result_collector")
.add_edge("orchestrator", "result_collector") # For request count
WorkflowBuilder(start_executor=orchestrator)
.add_edge(orchestrator, resource_requester)
.add_edge(orchestrator, policy_checker)
.add_edge(resource_requester, result_collector)
.add_edge(policy_checker, result_collector)
.add_edge(orchestrator, result_collector) # For request count
.build()
)
@@ -287,25 +288,22 @@ class PolicyEngine(Executor):
async def main() -> None:
# Build the main workflow
resource_allocator = ResourceAllocator("resource_allocator")
policy_engine = PolicyEngine("policy_engine")
sub_workflow_executor = WorkflowExecutor(
build_resource_request_distribution_workflow(),
"sub_workflow_executor",
# Setting allow_direct_output=True to let the sub-workflow output directly.
# This is because the sub-workflow is the both the entry point and the exit
# point of the main workflow.
allow_direct_output=True,
)
main_workflow = (
WorkflowBuilder(start_executor="sub_workflow_executor")
.register_executor(lambda: ResourceAllocator("resource_allocator"), name="resource_allocator")
.register_executor(lambda: PolicyEngine("policy_engine"), name="policy_engine")
.register_executor(
lambda: WorkflowExecutor(
build_resource_request_distribution_workflow(),
"sub_workflow_executor",
# Setting allow_direct_output=True to let the sub-workflow output directly.
# This is because the sub-workflow is the both the entry point and the exit
# point of the main workflow.
allow_direct_output=True,
),
name="sub_workflow_executor",
)
.add_edge("sub_workflow_executor", "resource_allocator")
.add_edge("resource_allocator", "sub_workflow_executor")
.add_edge("sub_workflow_executor", "policy_engine")
.add_edge("policy_engine", "sub_workflow_executor")
WorkflowBuilder(start_executor=sub_workflow_executor)
.add_edge(sub_workflow_executor, resource_allocator)
.add_edge(resource_allocator, sub_workflow_executor)
.add_edge(sub_workflow_executor, policy_engine)
.add_edge(policy_engine, sub_workflow_executor)
.build()
)
@@ -153,13 +153,14 @@ def build_email_address_validation_workflow() -> Workflow:
)
# Build the workflow
email_sanitizer = EmailSanitizer(id="email_sanitizer")
email_format_validator = EmailFormatValidator(id="email_format_validator")
domain_validator = DomainValidator(id="domain_validator")
return (
WorkflowBuilder(start_executor="email_sanitizer")
.register_executor(lambda: EmailSanitizer(id="email_sanitizer"), name="email_sanitizer")
.register_executor(lambda: EmailFormatValidator(id="email_format_validator"), name="email_format_validator")
.register_executor(lambda: DomainValidator(id="domain_validator"), name="domain_validator")
.add_edge("email_sanitizer", "email_format_validator")
.add_edge("email_format_validator", "domain_validator")
WorkflowBuilder(start_executor=email_sanitizer)
.add_edge(email_sanitizer, email_format_validator)
.add_edge(email_format_validator, domain_validator)
.build()
)
@@ -268,20 +269,15 @@ async def main() -> None:
approved_domains = {"example.com", "company.com"}
# Build the main workflow
smart_email_orchestrator = SmartEmailOrchestrator(id="smart_email_orchestrator", approved_domains=approved_domains)
email_delivery = EmailDelivery(id="email_delivery")
email_validation_workflow = WorkflowExecutor(build_email_address_validation_workflow(), id="email_validation_workflow")
workflow = (
WorkflowBuilder(start_executor="smart_email_orchestrator")
.register_executor(
lambda: SmartEmailOrchestrator(id="smart_email_orchestrator", approved_domains=approved_domains),
name="smart_email_orchestrator",
)
.register_executor(lambda: EmailDelivery(id="email_delivery"), name="email_delivery")
.register_executor(
lambda: WorkflowExecutor(build_email_address_validation_workflow(), id="email_validation_workflow"),
name="email_validation_workflow",
)
.add_edge("smart_email_orchestrator", "email_validation_workflow")
.add_edge("email_validation_workflow", "smart_email_orchestrator")
.add_edge("smart_email_orchestrator", "email_delivery")
WorkflowBuilder(start_executor=smart_email_orchestrator)
.add_edge(smart_email_orchestrator, email_validation_workflow)
.add_edge(email_validation_workflow, smart_email_orchestrator)
.add_edge(smart_email_orchestrator, email_delivery)
.build()
)
@@ -5,6 +5,7 @@ import os
from typing import Any
from agent_framework import ( # Core chat primitives used to build requests
AgentExecutor,
AgentExecutorRequest, # Input message bundle for an AgentExecutor
AgentExecutorResponse,
ChatAgent, # Output from an AgentExecutor
@@ -161,19 +162,17 @@ async def main() -> None:
# If not spam, hop to a transformer that creates a new AgentExecutorRequest,
# then call the email assistant, then finalize.
# If spam, go directly to the spam handler and finalize.
spam_detection_agent = AgentExecutor(create_spam_detector_agent())
email_assistant_agent = AgentExecutor(create_email_assistant_agent())
workflow = (
WorkflowBuilder(start_executor="spam_detection_agent")
.register_agent(create_spam_detector_agent, name="spam_detection_agent")
.register_agent(create_email_assistant_agent, name="email_assistant_agent")
.register_executor(lambda: to_email_assistant_request, name="to_email_assistant_request")
.register_executor(lambda: handle_email_response, name="send_email")
.register_executor(lambda: handle_spam_classifier_response, name="handle_spam")
WorkflowBuilder(start_executor=spam_detection_agent)
# Not spam path: transform response -> request for assistant -> assistant -> send email
.add_edge("spam_detection_agent", "to_email_assistant_request", condition=get_condition(False))
.add_edge("to_email_assistant_request", "email_assistant_agent")
.add_edge("email_assistant_agent", "send_email")
.add_edge(spam_detection_agent, to_email_assistant_request, condition=get_condition(False))
.add_edge(to_email_assistant_request, email_assistant_agent)
.add_edge(email_assistant_agent, handle_email_response)
# Spam path: send to spam handler
.add_edge("spam_detection_agent", "handle_spam", condition=get_condition(True))
.add_edge(spam_detection_agent, handle_spam_classifier_response, condition=get_condition(True))
.build()
)
@@ -9,6 +9,7 @@ from typing import Literal
from uuid import uuid4
from agent_framework import (
AgentExecutor,
AgentExecutorRequest,
AgentExecutorResponse,
ChatAgent,
@@ -212,6 +213,10 @@ def create_email_summary_agent() -> ChatAgent:
async def main() -> None:
# Build the workflow
email_analysis_agent = AgentExecutor(create_email_analysis_agent())
email_assistant_agent = AgentExecutor(create_email_assistant_agent())
email_summary_agent = AgentExecutor(create_email_summary_agent())
def select_targets(analysis: AnalysisResult, target_ids: list[str]) -> list[str]:
# Order: [handle_spam, submit_to_email_assistant, summarize_email, handle_uncertain]
handle_spam_id, submit_to_email_assistant_id, summarize_email_id, handle_uncertain_id = target_ids
@@ -224,39 +229,23 @@ async def main() -> None:
return targets
return [handle_uncertain_id]
workflow_builder = (
WorkflowBuilder(start_executor="store_email")
.register_agent(create_email_analysis_agent, name="email_analysis_agent")
.register_agent(create_email_assistant_agent, name="email_assistant_agent")
.register_agent(create_email_summary_agent, name="email_summary_agent")
.register_executor(lambda: store_email, name="store_email")
.register_executor(lambda: to_analysis_result, name="to_analysis_result")
.register_executor(lambda: submit_to_email_assistant, name="submit_to_email_assistant")
.register_executor(lambda: finalize_and_send, name="finalize_and_send")
.register_executor(lambda: summarize_email, name="summarize_email")
.register_executor(lambda: merge_summary, name="merge_summary")
.register_executor(lambda: handle_spam, name="handle_spam")
.register_executor(lambda: handle_uncertain, name="handle_uncertain")
.register_executor(lambda: database_access, name="database_access")
)
workflow = (
workflow_builder
.add_edge("store_email", "email_analysis_agent")
.add_edge("email_analysis_agent", "to_analysis_result")
WorkflowBuilder(start_executor=store_email)
.add_edge(store_email, email_analysis_agent)
.add_edge(email_analysis_agent, to_analysis_result)
.add_multi_selection_edge_group(
"to_analysis_result",
["handle_spam", "submit_to_email_assistant", "summarize_email", "handle_uncertain"],
to_analysis_result,
[handle_spam, submit_to_email_assistant, summarize_email, handle_uncertain],
selection_func=select_targets,
)
.add_edge("submit_to_email_assistant", "email_assistant_agent")
.add_edge("email_assistant_agent", "finalize_and_send")
.add_edge("summarize_email", "email_summary_agent")
.add_edge("email_summary_agent", "merge_summary")
.add_edge(submit_to_email_assistant, email_assistant_agent)
.add_edge(email_assistant_agent, finalize_and_send)
.add_edge(summarize_email, email_summary_agent)
.add_edge(email_summary_agent, merge_summary)
# Save to DB if short (no summary path)
.add_edge("to_analysis_result", "database_access", condition=lambda r: r.email_length <= LONG_EMAIL_THRESHOLD)
.add_edge(to_analysis_result, database_access, condition=lambda r: r.email_length <= LONG_EMAIL_THRESHOLD)
# Save to DB with summary when long
.add_edge("merge_summary", "database_access")
.add_edge(merge_summary, database_access)
.build()
)
@@ -62,11 +62,12 @@ async def main() -> None:
"""Build a two step sequential workflow and run it with streaming to observe events."""
# Step 1: Build the workflow graph.
# Order matters. We connect upper_case_executor -> reverse_text_executor and set the start.
upper_case_executor = UpperCaseExecutor(id="upper_case_executor")
reverse_text_executor = ReverseTextExecutor(id="reverse_text_executor")
workflow = (
WorkflowBuilder(start_executor="upper_case_executor")
.register_executor(lambda: UpperCaseExecutor(id="upper_case_executor"), name="upper_case_executor")
.register_executor(lambda: ReverseTextExecutor(id="reverse_text_executor"), name="reverse_text_executor")
.add_edge("upper_case_executor", "reverse_text_executor")
WorkflowBuilder(start_executor=upper_case_executor)
.add_edge(upper_case_executor, reverse_text_executor)
.build()
)
@@ -56,10 +56,8 @@ async def main():
# Step 1: Build the workflow with the defined edges.
# Order matters. upper_case_executor runs first, then reverse_text_executor.
workflow = (
WorkflowBuilder(start_executor="upper_case_executor")
.register_executor(lambda: to_upper_case, name="upper_case_executor")
.register_executor(lambda: reverse_text, name="reverse_text_executor")
.add_edge("upper_case_executor", "reverse_text_executor")
WorkflowBuilder(start_executor=to_upper_case)
.add_edge(to_upper_case, reverse_text)
.build()
)
@@ -4,6 +4,7 @@ import asyncio
from enum import Enum
from agent_framework import (
AgentExecutor,
AgentExecutorRequest,
AgentExecutorResponse,
ChatAgent,
@@ -125,16 +126,17 @@ async def main():
"""Main function to run the workflow."""
# Step 1: Build the workflow with the defined edges.
# This time we are creating a loop in the workflow.
guess_number = GuessNumberExecutor((1, 100), "guess_number")
judge_agent = AgentExecutor(create_judge_agent())
submit_judge = SubmitToJudgeAgent(judge_agent_id="judge_agent", target=30)
parse_judge = ParseJudgeResponse(id="parse_judge")
workflow = (
WorkflowBuilder(start_executor="guess_number")
.register_executor(lambda: GuessNumberExecutor((1, 100), "guess_number"), name="guess_number")
.register_agent(create_judge_agent, name="judge_agent")
.register_executor(lambda: SubmitToJudgeAgent(judge_agent_id="judge_agent", target=30), name="submit_judge")
.register_executor(lambda: ParseJudgeResponse(id="parse_judge"), name="parse_judge")
.add_edge("guess_number", "submit_judge")
.add_edge("submit_judge", "judge_agent")
.add_edge("judge_agent", "parse_judge")
.add_edge("parse_judge", "guess_number")
WorkflowBuilder(start_executor=guess_number)
.add_edge(guess_number, submit_judge)
.add_edge(submit_judge, judge_agent)
.add_edge(judge_agent, parse_judge)
.add_edge(parse_judge, guess_number)
.build()
)
@@ -7,6 +7,7 @@ from typing import Any, Literal
from uuid import uuid4
from agent_framework import ( # Core chat primitives used to form LLM requests
AgentExecutor,
AgentExecutorRequest, # Message bundle sent to an AgentExecutor
AgentExecutorResponse, # Result returned by an AgentExecutor
Case,
@@ -178,28 +179,23 @@ async def main():
"""Main function to run the workflow."""
# Build workflow: store -> detection agent -> to_detection_result -> switch (NotSpam or Spam or Default).
# The switch-case group evaluates cases in order, then falls back to Default when none match.
spam_detection_agent = AgentExecutor(create_spam_detection_agent())
email_assistant_agent = AgentExecutor(create_email_assistant_agent())
workflow = (
WorkflowBuilder(start_executor="store_email")
.register_agent(create_spam_detection_agent, name="spam_detection_agent")
.register_agent(create_email_assistant_agent, name="email_assistant_agent")
.register_executor(lambda: store_email, name="store_email")
.register_executor(lambda: to_detection_result, name="to_detection_result")
.register_executor(lambda: submit_to_email_assistant, name="submit_to_email_assistant")
.register_executor(lambda: finalize_and_send, name="finalize_and_send")
.register_executor(lambda: handle_spam, name="handle_spam")
.register_executor(lambda: handle_uncertain, name="handle_uncertain")
.add_edge("store_email", "spam_detection_agent")
.add_edge("spam_detection_agent", "to_detection_result")
WorkflowBuilder(start_executor=store_email)
.add_edge(store_email, spam_detection_agent)
.add_edge(spam_detection_agent, to_detection_result)
.add_switch_case_edge_group(
"to_detection_result",
to_detection_result,
[
Case(condition=get_case("NotSpam"), target="submit_to_email_assistant"),
Case(condition=get_case("Spam"), target="handle_spam"),
Default(target="handle_uncertain"),
Case(condition=get_case("NotSpam"), target=submit_to_email_assistant),
Case(condition=get_case("Spam"), target=handle_spam),
Default(target=handle_uncertain),
],
)
.add_edge("submit_to_email_assistant", "email_assistant_agent")
.add_edge("email_assistant_agent", "finalize_and_send")
.add_edge(submit_to_email_assistant, email_assistant_agent)
.add_edge(email_assistant_agent, finalize_and_send)
.build()
)
@@ -51,12 +51,9 @@ async def step3(text: str, ctx: WorkflowContext[Never, str]) -> None:
def build_workflow():
"""Build a simple 3-step sequential workflow (~6 seconds total)."""
return (
WorkflowBuilder(start_executor="step1")
.register_executor(lambda: step1, name="step1")
.register_executor(lambda: step2, name="step2")
.register_executor(lambda: step3, name="step3")
.add_edge("step1", "step2")
.add_edge("step2", "step3")
WorkflowBuilder(start_executor=step1)
.add_edge(step1, step2)
.add_edge(step2, step3)
.build()
)
@@ -72,14 +72,15 @@ class Aggregator(Executor):
async def main() -> None:
# 1) Build a simple fan out and fan in workflow
dispatcher = Dispatcher(id="dispatcher")
average = Average(id="average")
summation = Sum(id="summation")
aggregator = Aggregator(id="aggregator")
workflow = (
WorkflowBuilder(start_executor="dispatcher")
.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")
.add_fan_out_edges("dispatcher", ["average", "summation"])
.add_fan_in_edges(["average", "summation"], "aggregator")
WorkflowBuilder(start_executor=dispatcher)
.add_fan_out_edges(dispatcher, [average, summation])
.add_fan_in_edges([average, summation], aggregator)
.build()
)
@@ -4,9 +4,9 @@ import asyncio
from dataclasses import dataclass
from agent_framework import (
AgentExecutor, # Wraps a ChatAgent as an Executor for use in workflows
AgentExecutorRequest, # The message bundle sent to an AgentExecutor
AgentExecutorResponse, # The structured result returned by an AgentExecutor
ChatAgent, # Tracing event for agent execution steps
ChatMessage, # Chat message structure
Executor, # Base class for custom Python executors
WorkflowBuilder, # Fluent builder for wiring the workflow graph
@@ -87,50 +87,44 @@ class AggregateInsights(Executor):
await ctx.yield_output(consolidated)
def create_researcher_agent() -> ChatAgent:
"""Creates a research domain expert agent."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_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()).as_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()).as_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) Build a simple fan out and fan in workflow
# 1) Create executor and agent instances
dispatcher = DispatchToExperts(id="dispatcher")
aggregator = AggregateInsights(id="aggregator")
researcher = AgentExecutor(
AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
instructions=(
"You're an expert market and product researcher. Given a prompt, provide concise, factual insights,"
" opportunities, and risks."
),
name="researcher",
)
)
marketer = AgentExecutor(
AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
instructions=(
"You're a creative marketing strategist. Craft compelling value propositions and target messaging"
" aligned to the prompt."
),
name="marketer",
)
)
legal = AgentExecutor(
AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
instructions=(
"You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns"
" based on the prompt."
),
name="legal",
)
)
# 2) Build a simple fan out and fan in workflow
workflow = (
WorkflowBuilder(start_executor="dispatcher")
.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")
.add_fan_out_edges("dispatcher", ["researcher", "marketer", "legal"]) # Parallel branches
.add_fan_in_edges(["researcher", "marketer", "legal"], "aggregator") # Join at the aggregator
WorkflowBuilder(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()
)
@@ -257,49 +257,31 @@ class CompletionExecutor(Executor):
async def main():
"""Construct the map reduce workflow, visualize it, then run it over a sample file."""
# Step 1: Create the workflow builder and register executors.
workflow_builder = (
WorkflowBuilder(start_executor="split_data_executor")
.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")
# Step 1: Create executor instances.
map_executor_0 = Map(id="map_executor_0")
map_executor_1 = Map(id="map_executor_1")
map_executor_2 = Map(id="map_executor_2")
split_data_executor = Split(["map_executor_0", "map_executor_1", "map_executor_2"], id="split_data_executor")
reduce_executor_0 = Reduce(id="reduce_executor_0")
reduce_executor_1 = Reduce(id="reduce_executor_1")
reduce_executor_2 = Reduce(id="reduce_executor_2")
reduce_executor_3 = Reduce(id="reduce_executor_3")
shuffle_executor = Shuffle(
["reduce_executor_0", "reduce_executor_1", "reduce_executor_2", "reduce_executor_3"],
id="shuffle_executor",
)
completion_executor = CompletionExecutor(id="completion_executor")
mappers = [map_executor_0, map_executor_1, map_executor_2]
reducers = [reduce_executor_0, reduce_executor_1, reduce_executor_2, reduce_executor_3]
# Step 2: Build the workflow graph using fan out and fan in edges.
workflow = (
workflow_builder
.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
WorkflowBuilder(start_executor=split_data_executor)
.add_fan_out_edges(split_data_executor, mappers) # Split -> many mappers
.add_fan_in_edges(mappers, shuffle_executor) # All mappers -> shuffle
.add_fan_out_edges(shuffle_executor, reducers) # Shuffle -> many reducers
.add_fan_in_edges(reducers, completion_executor) # All reducers -> completion
.build()
)
@@ -188,21 +188,17 @@ async def main() -> None:
# store_email -> spam_detection_agent -> to_detection_result -> branch:
# False -> submit_to_email_assistant -> email_assistant_agent -> finalize_and_send
# True -> handle_spam
spam_detection_agent = create_spam_detection_agent()
email_assistant_agent = create_email_assistant_agent()
workflow = (
WorkflowBuilder(start_executor="store_email")
.register_agent(create_spam_detection_agent, name="spam_detection_agent")
.register_agent(create_email_assistant_agent, name="email_assistant_agent")
.register_executor(lambda: store_email, name="store_email")
.register_executor(lambda: to_detection_result, name="to_detection_result")
.register_executor(lambda: submit_to_email_assistant, name="submit_to_email_assistant")
.register_executor(lambda: finalize_and_send, name="finalize_and_send")
.register_executor(lambda: handle_spam, name="handle_spam")
.add_edge("store_email", "spam_detection_agent")
.add_edge("spam_detection_agent", "to_detection_result")
.add_edge("to_detection_result", "submit_to_email_assistant", condition=get_condition(False))
.add_edge("to_detection_result", "handle_spam", condition=get_condition(True))
.add_edge("submit_to_email_assistant", "email_assistant_agent")
.add_edge("email_assistant_agent", "finalize_and_send")
WorkflowBuilder(start_executor=store_email)
.add_edge(store_email, spam_detection_agent)
.add_edge(spam_detection_agent, to_detection_result)
.add_edge(to_detection_result, submit_to_email_assistant, condition=get_condition(False))
.add_edge(to_detection_result, handle_spam, condition=get_condition(True))
.add_edge(submit_to_email_assistant, email_assistant_agent)
.add_edge(email_assistant_agent, finalize_and_send)
.build()
)
@@ -4,9 +4,9 @@ import asyncio
from dataclasses import dataclass
from agent_framework import (
AgentExecutor,
AgentExecutorRequest,
AgentExecutorResponse,
ChatAgent,
ChatMessage,
Executor,
WorkflowBuilder,
@@ -85,52 +85,49 @@ class AggregateInsights(Executor):
await ctx.yield_output(consolidated)
def create_researcher_agent() -> ChatAgent:
"""Creates a research domain expert agent."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_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()).as_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 domain expert agent."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_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:
"""Build and run the concurrent workflow with visualization."""
# Create agent instances
researcher = AgentExecutor(
AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
instructions=(
"You're an expert market and product researcher. Given a prompt, provide concise, factual insights,"
" opportunities, and risks."
),
name="researcher",
)
)
marketer = AgentExecutor(
AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
instructions=(
"You're a creative marketing strategist. Craft compelling value propositions and target messaging"
" aligned to the prompt."
),
name="marketer",
)
)
legal = AgentExecutor(
AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
instructions=(
"You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns"
" based on the prompt."
),
name="legal",
)
)
# Create executor instances
dispatcher = DispatchToExperts(id="dispatcher")
aggregator = AggregateInsights(id="aggregator")
# Build a simple fan-out/fan-in workflow
workflow = (
WorkflowBuilder(start_executor="dispatcher")
.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")
.add_fan_out_edges("dispatcher", ["researcher", "marketer", "legal"])
.add_fan_in_edges(["researcher", "marketer", "legal"], "aggregator")
WorkflowBuilder(start_executor=dispatcher)
.add_fan_out_edges(dispatcher, [researcher, marketer, legal])
.add_fan_in_edges([researcher, marketer, legal], aggregator)
.build()
)