[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
@@ -1,168 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Any
from agent_framework import (
ChatAgent,
ChatMessage,
Executor,
Workflow,
WorkflowContext,
handler,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import ConcurrentBuilder
from azure.identity import AzureCliCredential
from typing_extensions import Never
"""
Sample: Concurrent Orchestration with participant factories and Custom Aggregator
Build a concurrent workflow with ConcurrentBuilder that fans out one prompt to
multiple domain agents and fans in their responses.
Override the default aggregator with a custom Executor class that uses
AzureOpenAIChatClient.get_response() to synthesize a concise, consolidated summary
from the experts' outputs.
All participants and the aggregator are created via factory functions that return
their respective ChatAgent or Executor instances.
Using participant factories allows you to set up proper state isolation between workflow
instances created by the same builder. This is particularly useful when you need to handle
requests or tasks in parallel with stateful participants.
Demonstrates:
- ConcurrentBuilder(participant_factories=[...]).with_aggregator(callback)
- Fan-out to agents and fan-in at an aggregator
- Aggregation implemented via an LLM call (chat_client.get_response)
- Workflow output yielded with the synthesized summary string
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient (az login + required env vars)
"""
def create_researcher() -> ChatAgent:
"""Factory function to create a researcher agent instance."""
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() -> ChatAgent:
"""Factory function to create a marketer agent instance."""
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() -> ChatAgent:
"""Factory function to create a legal/compliance agent instance."""
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",
)
class SummarizationExecutor(Executor):
"""Custom aggregator executor that synthesizes expert outputs into a concise summary."""
def __init__(self) -> None:
super().__init__(id="summarization_executor")
self.chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
@handler
async def summarize_results(self, results: list[Any], ctx: WorkflowContext[Never, str]) -> None:
expert_sections: list[str] = []
for r in results:
try:
messages = getattr(r.agent_response, "messages", [])
final_text = messages[-1].text if messages and hasattr(messages[-1], "text") else "(no content)"
expert_sections.append(f"{getattr(r, 'executor_id', 'expert')}:\n{final_text}")
except Exception as e:
expert_sections.append(f"{getattr(r, 'executor_id', 'expert')}: (error: {type(e).__name__}: {e})")
# Ask the model to synthesize a concise summary of the experts' outputs
system_msg = ChatMessage(
"system",
text=(
"You are a helpful assistant that consolidates multiple domain expert outputs "
"into one cohesive, concise summary with clear takeaways. Keep it under 200 words."
),
)
user_msg = ChatMessage("user", text="\n\n".join(expert_sections))
response = await self.chat_client.get_response([system_msg, user_msg])
await ctx.yield_output(response.messages[-1].text if response.messages else "")
async def run_workflow(workflow: Workflow, query: str) -> None:
events = await workflow.run(query)
outputs = events.get_outputs()
if outputs:
print(outputs[0]) # Get the first (and typically only) output
else:
raise RuntimeError("No outputs received from the workflow.")
async def main() -> None:
# Create a concurrent builder with participant factories and a custom aggregator
# - register_participants([...]) accepts factory functions that return
# SupportsAgentRun (agents) or Executor instances.
# - register_aggregator(...) takes a factory function that returns an Executor instance.
concurrent_builder = (
ConcurrentBuilder(participant_factories=[create_researcher, create_marketer, create_legal])
.register_aggregator(SummarizationExecutor)
)
# Build workflow_a
workflow_a = concurrent_builder.build()
# Run workflow_a
# Context is maintained across runs
print("=== First Run on workflow_a ===")
await run_workflow(workflow_a, "We are launching a new budget-friendly electric bike for urban commuters.")
print("\n=== Second Run on workflow_a ===")
await run_workflow(workflow_a, "Refine your response to focus on the California market.")
# Build workflow_b
# This will create new instances of all participants and the aggregator
# The agents will also get new threads
workflow_b = concurrent_builder.build()
# Run workflow_b
# Context is not maintained across instances
# Should not expect mentions of electric bikes in the results
print("\n=== First Run on workflow_b ===")
await run_workflow(workflow_b, "Refine your response to focus on the California market.")
"""
Sample Output:
=== First Run on workflow_a ===
The budget-friendly electric bike market is poised for significant growth, driven by urbanization, ...
=== Second Run on workflow_a ===
Launching a budget-friendly electric bike in California presents significant opportunities, driven ...
=== First Run on workflow_b ===
To successfully penetrate the California market, consider these tailored strategies focused on ...
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -1,271 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
from typing import Annotated, cast
from agent_framework import (
AgentResponse,
ChatAgent,
ChatMessage,
Workflow,
WorkflowEvent,
WorkflowRunState,
tool,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
from azure.identity import AzureCliCredential
logging.basicConfig(level=logging.ERROR)
"""Sample: Handoff workflow with participant factories for state isolation.
This sample demonstrates how to use participant factories in HandoffBuilder to create
agents dynamically.
Using participant factories allows you to set up proper state isolation between workflow
instances created by the same builder. This is particularly useful when you need to handle
requests or tasks in parallel with stateful participants.
Routing Pattern:
User -> Triage Agent -> Specialist (Refund/Order Status/Return) -> User
Prerequisites:
- `az login` (Azure CLI authentication)
- Environment variables for AzureOpenAIChatClient (AZURE_OPENAI_ENDPOINT, etc.)
Key Concepts:
- Participant factories: create agents via factory functions for isolation
- State isolation: each workflow instance gets its own agent instances
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# See:
# samples/getting_started/tools/function_tool_with_approval.py
# samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def process_refund(order_number: Annotated[str, "Order number to process refund for"]) -> str:
"""Simulated function to process a refund for a given order number."""
return f"Refund processed successfully for order {order_number}."
@tool(approval_mode="never_require")
def check_order_status(order_number: Annotated[str, "Order number to check status for"]) -> str:
"""Simulated function to check the status of a given order number."""
return f"Order {order_number} is currently being processed and will ship in 2 business days."
@tool(approval_mode="never_require")
def process_return(order_number: Annotated[str, "Order number to process return for"]) -> str:
"""Simulated function to process a return for a given order number."""
return f"Return initiated successfully for order {order_number}. You will receive return instructions via email."
def create_triage_agent() -> ChatAgent:
"""Factory function to create a triage agent instance."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
instructions=(
"You are frontline support triage. Route customer issues to the appropriate specialist agents "
"based on the problem described."
),
name="triage_agent",
)
def create_refund_agent() -> ChatAgent:
"""Factory function to create a refund agent instance."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
instructions="You process refund requests.",
name="refund_agent",
# In a real application, an agent can have multiple tools; here we keep it simple
tools=[process_refund],
)
def create_order_status_agent() -> ChatAgent:
"""Factory function to create an order status agent instance."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
instructions="You handle order and shipping inquiries.",
name="order_agent",
# In a real application, an agent can have multiple tools; here we keep it simple
tools=[check_order_status],
)
def create_return_agent() -> ChatAgent:
"""Factory function to create a return agent instance."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
instructions="You manage product return requests.",
name="return_agent",
# In a real application, an agent can have multiple tools; here we keep it simple
tools=[process_return],
)
def _handle_events(events: list[WorkflowEvent]) -> list[WorkflowEvent[HandoffAgentUserRequest]]:
"""Process workflow events and extract any pending user input requests.
This function inspects each event type and:
- Prints workflow status changes (IDLE, IDLE_WITH_PENDING_REQUESTS, etc.)
- Displays final conversation snapshots when workflow completes
- Prints user input request prompts
- Collects all request_info events for response handling
Args:
events: List of WorkflowEvent to process
Returns:
List of WorkflowEvent[HandoffAgentUserRequest] representing pending user input requests
"""
requests: list[WorkflowEvent[HandoffAgentUserRequest]] = []
for event in events:
if event.type == "handoff_sent":
# handoff_sent event: Indicates a handoff has been initiated
print(f"\n[Handoff from {event.data.source} to {event.data.target} initiated.]")
elif event.type == "status" and event.state in {
WorkflowRunState.IDLE,
WorkflowRunState.IDLE_WITH_PENDING_REQUESTS,
}:
# Status event: Indicates workflow state changes
print(f"\n[Workflow Status] {event.state.name}")
elif event.type == "output":
# Output event: Contains contents generated by the workflow
data = event.data
if isinstance(data, AgentResponse):
for message in data.messages:
if not message.text:
# Skip messages without text (e.g., tool calls)
continue
speaker = message.author_name or message.role
print(f"- {speaker}: {message.text}")
elif event.type == "output":
# The output of the handoff workflow is a collection of chat messages from all participants
conversation = cast(list[ChatMessage], event.data)
if isinstance(conversation, list):
print("\n=== Final Conversation Snapshot ===")
for message in conversation:
speaker = message.author_name or message.role
print(f"- {speaker}: {message.text or [content.type for content in message.contents]}")
print("===================================")
elif event.type == "request_info" and isinstance(event.data, HandoffAgentUserRequest):
# Request info event: Workflow is requesting user input
_print_handoff_agent_user_request(event.data.agent_response)
requests.append(cast(WorkflowEvent[HandoffAgentUserRequest], event))
return requests
def _print_handoff_agent_user_request(response: AgentResponse) -> None:
"""Display the agent's response messages when requesting user input.
This will happen when an agent generates a response that doesn't trigger
a handoff, i.e., the agent is asking the user for more information.
Args:
response: The AgentResponse from the agent requesting user input
"""
if not response.messages:
raise RuntimeError("Cannot print agent responses: response has no messages.")
print("\n[Agent is requesting your input...]")
# Print agent responses
for message in response.messages:
if not message.text:
# Skip messages without text (e.g., tool calls)
continue
speaker = message.author_name or message.role
print(f"- {speaker}: {message.text}")
async def _run_workflow(workflow: Workflow, user_inputs: list[str]) -> None:
"""Run the workflow with the given user input and display events."""
print(f"- User: {user_inputs[0]}")
workflow_result = await workflow.run(user_inputs[0])
pending_requests = _handle_events(workflow_result)
# Process the request/response cycle
# The workflow will continue requesting input until:
# 1. The termination condition is met (4 user messages in this case), OR
# 2. We run out of scripted responses
while pending_requests:
if user_inputs[1:]:
# Get the next scripted response
user_response = user_inputs.pop(1)
print(f"\n- User: {user_response}")
# Send response(s) to all pending requests
# In this demo, there's typically one request per cycle, but the API supports multiple
responses = {
req.request_id: HandoffAgentUserRequest.create_response(user_response) for req in pending_requests
}
else:
# No more scripted responses; terminate the workflow
responses = {req.request_id: HandoffAgentUserRequest.terminate() for req in pending_requests}
# Send responses and get new events
# We use run(responses=...) to get events, allowing us to
# display agent responses and handle new requests as they arrive
workflow_result = await workflow.run(responses=responses)
pending_requests = _handle_events(workflow_result)
async def main() -> None:
"""Run the autonomous handoff workflow with participant factories."""
# Build the handoff workflow using participant factories
# termination_condition: Custom termination that checks if the triage agent has provided a closing message.
# This looks for the last message being from triage_agent and containing "welcome",
# which indicates the conversation has concluded naturally.
workflow_builder = (
HandoffBuilder(
name="Autonomous Handoff with Participant Factories",
participant_factories={
"triage": create_triage_agent,
"refund": create_refund_agent,
"order_status": create_order_status_agent,
"return": create_return_agent,
},
termination_condition=lambda conversation: (
len(conversation) > 0
and conversation[-1].author_name == "triage_agent"
and "welcome" in conversation[-1].text.lower()
),
)
.with_start_agent("triage")
)
# Scripted user responses for reproducible demo
# In a console application, replace this with:
# user_input = input("Your response: ")
# or integrate with a UI/chat interface
user_inputs = [
"Hello, I need assistance with my recent purchase.",
"My order 1234 arrived damaged and the packaging was destroyed. I'd like to return it.",
"Is my return being processed?",
"Thanks for resolving this.",
]
workflow_a = workflow_builder.build()
print("=== Running workflow_a ===")
await _run_workflow(workflow_a, list(user_inputs))
workflow_b = workflow_builder.build()
print("=== Running workflow_b ===")
# Only provide the last two inputs to workflow_b to demonstrate state isolation
# The agents in this workflow have no prior context thus should not have knowledge of
# order 1234 or previous interactions.
await _run_workflow(workflow_b, user_inputs[2:])
"""
Expected behavior:
- workflow_a and workflow_b maintain separate states for their participants.
- Each workflow processes its requests independently without interference.
- workflow_a will answer the follow-up request based on its own conversation history,
while workflow_b will provide a general answer without prior context.
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -1,126 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import (
ChatAgent,
ChatMessage,
Executor,
Workflow,
WorkflowContext,
handler,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
"""
Sample: Sequential workflow with participant factories
This sample demonstrates how to create a sequential workflow with participant factories.
Using participant factories allows you to set up proper state isolation between workflow
instances created by the same builder. This is particularly useful when you need to handle
requests or tasks in parallel with stateful participants.
In this example, we create a sequential workflow with two participants: an accumulator
and a content producer. The accumulator is stateful and maintains a list of all messages it has
received. Context is maintained across runs of the same workflow instance but not across different
workflow instances.
"""
class Accumulate(Executor):
"""Simple accumulator.
Accumulates all messages from the conversation and prints them out.
"""
def __init__(self, id: str):
super().__init__(id)
# Some internal state to accumulate messages
self._accumulated: list[str] = []
@handler
async def accumulate(self, conversation: list[ChatMessage], ctx: WorkflowContext[list[ChatMessage]]) -> None:
self._accumulated.extend([msg.text for msg in conversation])
print(f"Number of queries received so far: {len(self._accumulated)}")
await ctx.send_message(conversation)
def create_agent() -> ChatAgent:
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
instructions="Produce a concise paragraph answering the user's request.",
name="ContentProducer",
)
async def run_workflow(workflow: Workflow, query: str) -> None:
events = await workflow.run(query)
outputs = events.get_outputs()
if outputs:
messages: list[ChatMessage] = outputs[0]
for message in messages:
name = message.author_name or ("assistant" if message.role == "assistant" else "user")
print(f"{name}: {message.text}")
else:
raise RuntimeError("No outputs received from the workflow.")
async def main() -> None:
# 1) Create a builder with participant factories
builder = SequentialBuilder(participant_factories=[
lambda: Accumulate("accumulator"),
create_agent,
])
# 2) Build workflow_a
workflow_a = builder.build()
# 3) Run workflow_a
# Context is maintained across runs
print("=== First Run on workflow_a ===")
await run_workflow(workflow_a, "Why is the sky blue?")
print("\n=== Second Run on workflow_a ===")
await run_workflow(workflow_a, "Repeat my previous question.")
# 4) Build workflow_b
# This will create a new instance of the accumulator and content producer
# using the same workflow builder
workflow_b = builder.build()
# 5) Run workflow_b
# Context is not maintained across instances
print("\n=== First Run on workflow_b ===")
await run_workflow(workflow_b, "Repeat my previous question.")
"""
Sample Output:
=== First Run on workflow_a ===
Number of queries received so far: 1
user: Why is the sky blue?
ContentProducer: The sky appears blue due to a phenomenon called Rayleigh scattering.
When sunlight enters the Earth's atmosphere, it collides with gases
and particles, scattering shorter wavelengths of light (blue and violet)
more than the longer wavelengths (red and yellow). Although violet light
is scattered even more than blue, our eyes are more sensitive to blue
light, and some violet light is absorbed by the ozone layer. As a result,
we perceive the sky as predominantly blue during the day.
=== Second Run on workflow_a ===
Number of queries received so far: 2
user: Repeat my previous question.
ContentProducer: Why is the sky blue?
=== First Run on workflow_b ===
Number of queries received so far: 1
user: Repeat my previous question.
ContentProducer: I'm sorry, but I can't repeat your previous question as I don't have
access to your past queries. However, feel free to ask anything again,
and I'll be happy to help!
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -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()
)