mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Add Concurrent orchestration builder support. Samples. Tests. (#683)
* Add Concurrent orchestration builder support. Samples. Tests. * Add sample output * Add better docstrings * PR feedback * PR feedback
This commit is contained in:
committed by
GitHub
Unverified
parent
3c0926b670
commit
fbd0c566c2
@@ -78,6 +78,9 @@ Once comfortable with these, explore the rest of the samples below.
|
||||
### orchestration
|
||||
| Sample | File | Concepts |
|
||||
|---|---|---|
|
||||
| Concurrent Orchestration (Default Aggregator) | [orchestration/concurrent_agents.py](./orchestration/concurrent_agents.py) | Fan-out to multiple agents; fan-in with default aggregator returning combined ChatMessages |
|
||||
| Concurrent Orchestration (Custom Aggregator) | [orchestration/concurrent_custom_aggregator.py](./orchestration/concurrent_custom_aggregator.py) | Override aggregator via callback; summarize results with an LLM |
|
||||
| Concurrent Orchestration (Custom Agent Executors) | [orchestration/concurrent_custom_agent_executors.py](./orchestration/concurrent_custom_agent_executors.py) | Child executors own ChatAgents; concurrent fan-out/fan-in via ConcurrentBuilder |
|
||||
| Magentic Workflow (Multi-Agent) | [orchestration/magentic.py](./orchestration/magentic.py) | Orchestrate multiple agents with Magentic manager and streaming |
|
||||
| Magentic + Human Plan Review | [orchestration/magentic_human_plan_update.py](./orchestration/magentic_human_plan_update.py) | Human reviews/updates the plan before execution |
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import ChatMessage
|
||||
from agent_framework.azure import AzureChatClient
|
||||
from agent_framework.workflow import ConcurrentBuilder, WorkflowCompletedEvent
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""
|
||||
Sample: Concurrent fan-out/fan-in (agent-only API) with default aggregator
|
||||
|
||||
Build a high-level concurrent workflow using ConcurrentBuilder and three domain agents.
|
||||
The default dispatcher fans out the same user prompt to all agents in parallel.
|
||||
The default aggregator fans in their results and emits a WorkflowCompletedEvent whose
|
||||
data is a list[ChatMessage] representing the concatenated conversations from all agents.
|
||||
|
||||
Demonstrates:
|
||||
- Minimal wiring with ConcurrentBuilder().participants([...]).build()
|
||||
- Fan-out to multiple agents, fan-in aggregation of final ChatMessages
|
||||
- Streaming of AgentRunEvent for simple progress visibility
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI access configured for AzureChatClient (use az login + env vars)
|
||||
- Familiarity with Workflow events (AgentRunEvent, WorkflowCompletedEvent)
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# 1) Create three domain agents using AzureChatClient
|
||||
chat_client = AzureChatClient(credential=AzureCliCredential())
|
||||
|
||||
researcher = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You're an expert market and product researcher. Given a prompt, provide concise, factual insights,"
|
||||
" opportunities, and risks."
|
||||
),
|
||||
name="researcher",
|
||||
)
|
||||
|
||||
marketer = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You're a creative marketing strategist. Craft compelling value propositions and target messaging"
|
||||
" aligned to the prompt."
|
||||
),
|
||||
name="marketer",
|
||||
)
|
||||
|
||||
legal = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns"
|
||||
" based on the prompt."
|
||||
),
|
||||
name="legal",
|
||||
)
|
||||
|
||||
# 2) Build a concurrent workflow
|
||||
# Participants are either Agents (type of AgentProtocol) or Executors
|
||||
workflow = ConcurrentBuilder().participants([researcher, marketer, legal]).build()
|
||||
|
||||
# 3) Run with a single prompt, stream progress, and pretty-print the final combined messages
|
||||
completion: WorkflowCompletedEvent | None = None
|
||||
async for event in workflow.run_stream("We are launching a new budget-friendly electric bike for urban commuters."):
|
||||
if isinstance(event, WorkflowCompletedEvent):
|
||||
completion = event
|
||||
|
||||
if completion:
|
||||
print("===== Final Aggregated Conversation (messages) =====")
|
||||
messages: list[ChatMessage] | Any = completion.data
|
||||
for i, msg in enumerate(messages, start=1):
|
||||
name = msg.author_name if msg.author_name else "user"
|
||||
print(f"{'-' * 60}\n\n{i:02d} [{name}]:\n{msg.text}")
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
===== Final Aggregated Conversation (messages) =====
|
||||
------------------------------------------------------------
|
||||
|
||||
01 [user]:
|
||||
We are launching a new budget-friendly electric bike for urban commuters.
|
||||
------------------------------------------------------------
|
||||
|
||||
02 [researcher]:
|
||||
**Insights:**
|
||||
|
||||
- **Target Demographic:** Urban commuters seeking affordable, eco-friendly transport;
|
||||
likely to include students, young professionals, and price-sensitive urban residents.
|
||||
- **Market Trends:** E-bike sales are growing globally, with increasing urbanization,
|
||||
higher fuel costs, and sustainability concerns driving adoption.
|
||||
- **Competitive Landscape:** Key competitors include brands like Rad Power Bikes, Aventon,
|
||||
Lectric, and domestic budget-focused manufacturers in North America, Europe, and Asia.
|
||||
- **Feature Expectations:** Customers expect reliability, ease-of-use, theft protection,
|
||||
lightweight design, sufficient battery range for daily city commutes (typically 25-40 miles),
|
||||
and low-maintenance components.
|
||||
|
||||
**Opportunities:**
|
||||
|
||||
- **First-time Buyers:** Capture newcomers to e-biking by emphasizing affordability, ease of
|
||||
operation, and cost savings vs. public transit/car ownership.
|
||||
...
|
||||
------------------------------------------------------------
|
||||
|
||||
03 [marketer]:
|
||||
**Value Proposition:**
|
||||
"Empowering your city commute: Our new electric bike combines affordability, reliability, and
|
||||
sustainable design—helping you conquer urban journeys without breaking the bank."
|
||||
|
||||
**Target Messaging:**
|
||||
|
||||
*For Young Professionals:*
|
||||
...
|
||||
------------------------------------------------------------
|
||||
|
||||
04 [legal]:
|
||||
**Constraints, Disclaimers, & Policy Concerns for Launching a Budget-Friendly Electric Bike for Urban Commuters:**
|
||||
|
||||
**1. Regulatory Compliance**
|
||||
- Verify that the electric bike meets all applicable federal, state, and local regulations
|
||||
regarding e-bike classification, speed limits, power output, and safety features.
|
||||
- Ensure necessary certifications (e.g., UL certification for batteries, CE markings if sold internationally) are obtained.
|
||||
|
||||
**2. Product Safety**
|
||||
- Include consumer safety warnings regarding use, battery handling, charging protocols, and age restrictions.
|
||||
...
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import ChatAgent, ChatMessage
|
||||
from agent_framework.azure import AzureChatClient
|
||||
from agent_framework.workflow import (
|
||||
AgentExecutorRequest,
|
||||
AgentExecutorResponse,
|
||||
ConcurrentBuilder,
|
||||
Executor,
|
||||
WorkflowCompletedEvent,
|
||||
WorkflowContext,
|
||||
handler,
|
||||
)
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""
|
||||
Sample: Concurrent Orchestration with Custom Agent Executors
|
||||
|
||||
This sample shows a concurrent fan-out/fan-in pattern using child Executor classes
|
||||
that each own their ChatAgent. The executors accept AgentExecutorRequest inputs
|
||||
and emit AgentExecutorResponse outputs, which allows reuse of the high-level
|
||||
ConcurrentBuilder API and the default aggregator.
|
||||
|
||||
Demonstrates:
|
||||
- Executors that create their ChatAgent in __init__ (via AzureChatClient)
|
||||
- A @handler that converts AgentExecutorRequest -> AgentExecutorResponse
|
||||
- ConcurrentBuilder().participants([...]) to build fan-out/fan-in
|
||||
- Default aggregator returning list[ChatMessage] (one user + one assistant per agent)
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI configured for AzureChatClient (az login + required env vars)
|
||||
"""
|
||||
|
||||
|
||||
class ResearcherExec(Executor):
|
||||
agent: ChatAgent
|
||||
|
||||
def __init__(self, chat_client: AzureChatClient, id: str = "researcher"):
|
||||
agent = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You're an expert market and product researcher. Given a prompt, provide concise, factual insights,"
|
||||
" opportunities, and risks."
|
||||
),
|
||||
name=id,
|
||||
)
|
||||
super().__init__(agent=agent, id=id)
|
||||
|
||||
@handler
|
||||
async def run(self, request: AgentExecutorRequest, ctx: WorkflowContext[AgentExecutorResponse]) -> None:
|
||||
response = await self.agent.run(request.messages)
|
||||
full_conversation = list(request.messages) + list(response.messages)
|
||||
await ctx.send_message(AgentExecutorResponse(self.id, response, full_conversation=full_conversation))
|
||||
|
||||
|
||||
class MarketerExec(Executor):
|
||||
agent: ChatAgent
|
||||
|
||||
def __init__(self, chat_client: AzureChatClient, id: str = "marketer"):
|
||||
agent = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You're a creative marketing strategist. Craft compelling value propositions and target messaging"
|
||||
" aligned to the prompt."
|
||||
),
|
||||
name=id,
|
||||
)
|
||||
super().__init__(agent=agent, id=id)
|
||||
|
||||
@handler
|
||||
async def run(self, request: AgentExecutorRequest, ctx: WorkflowContext[AgentExecutorResponse]) -> None:
|
||||
response = await self.agent.run(request.messages)
|
||||
full_conversation = list(request.messages) + list(response.messages)
|
||||
await ctx.send_message(AgentExecutorResponse(self.id, response, full_conversation=full_conversation))
|
||||
|
||||
|
||||
class LegalExec(Executor):
|
||||
agent: ChatAgent
|
||||
|
||||
def __init__(self, chat_client: AzureChatClient, id: str = "legal"):
|
||||
agent = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns"
|
||||
" based on the prompt."
|
||||
),
|
||||
name=id,
|
||||
)
|
||||
super().__init__(agent=agent, id=id)
|
||||
|
||||
@handler
|
||||
async def run(self, request: AgentExecutorRequest, ctx: WorkflowContext[AgentExecutorResponse]) -> None:
|
||||
response = await self.agent.run(request.messages)
|
||||
full_conversation = list(request.messages) + list(response.messages)
|
||||
await ctx.send_message(AgentExecutorResponse(self.id, response, full_conversation=full_conversation))
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
chat_client = AzureChatClient(credential=AzureCliCredential())
|
||||
|
||||
researcher = ResearcherExec(chat_client)
|
||||
marketer = MarketerExec(chat_client)
|
||||
legal = LegalExec(chat_client)
|
||||
|
||||
workflow = ConcurrentBuilder().participants([researcher, marketer, legal]).build()
|
||||
|
||||
completion: WorkflowCompletedEvent | None = None
|
||||
async for event in workflow.run_stream("We are launching a new budget-friendly electric bike for urban commuters."):
|
||||
if isinstance(event, WorkflowCompletedEvent):
|
||||
completion = event
|
||||
|
||||
if completion:
|
||||
print("===== Final Aggregated Conversation (messages) =====")
|
||||
messages: list[ChatMessage] | Any = completion.data
|
||||
for i, msg in enumerate(messages, start=1):
|
||||
name = msg.author_name if msg.author_name else "user"
|
||||
print(f"{'-' * 60}\n\n{i:02d} [{name}]:\n{msg.text}")
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
===== Final Aggregated Conversation (messages) =====
|
||||
------------------------------------------------------------
|
||||
|
||||
01 [user]:
|
||||
We are launching a new budget-friendly electric bike for urban commuters.
|
||||
------------------------------------------------------------
|
||||
|
||||
02 [researcher]:
|
||||
**Insights:**
|
||||
|
||||
- **Target Demographic:** Urban commuters seeking affordable, eco-friendly transport;
|
||||
likely to include students, young professionals, and price-sensitive urban residents.
|
||||
- **Market Trends:** E-bike sales are growing globally, with increasing urbanization,
|
||||
higher fuel costs, and sustainability concerns driving adoption.
|
||||
- **Competitive Landscape:** Key competitors include brands like Rad Power Bikes, Aventon,
|
||||
Lectric, and domestic budget-focused manufacturers in North America, Europe, and Asia.
|
||||
- **Feature Expectations:** Customers expect reliability, ease-of-use, theft protection,
|
||||
lightweight design, sufficient battery range for daily city commutes (typically 25-40 miles),
|
||||
and low-maintenance components.
|
||||
|
||||
**Opportunities:**
|
||||
|
||||
- **First-time Buyers:** Capture newcomers to e-biking by emphasizing affordability, ease of
|
||||
operation, and cost savings vs. public transit/car ownership.
|
||||
...
|
||||
------------------------------------------------------------
|
||||
|
||||
03 [marketer]:
|
||||
**Value Proposition:**
|
||||
"Empowering your city commute: Our new electric bike combines affordability, reliability, and
|
||||
sustainable design—helping you conquer urban journeys without breaking the bank."
|
||||
|
||||
**Target Messaging:**
|
||||
|
||||
*For Young Professionals:*
|
||||
...
|
||||
------------------------------------------------------------
|
||||
|
||||
04 [legal]:
|
||||
**Constraints, Disclaimers, & Policy Concerns for Launching a Budget-Friendly Electric Bike for Urban Commuters:**
|
||||
|
||||
**1. Regulatory Compliance**
|
||||
- Verify that the electric bike meets all applicable federal, state, and local regulations
|
||||
regarding e-bike classification, speed limits, power output, and safety features.
|
||||
- Ensure necessary certifications (e.g., UL certification for batteries, CE markings if sold internationally) are obtained.
|
||||
|
||||
**2. Product Safety**
|
||||
- Include consumer safety warnings regarding use, battery handling, charging protocols, and age restrictions.
|
||||
...
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import ChatMessage, Role
|
||||
from agent_framework.azure import AzureChatClient
|
||||
from agent_framework.workflow import ConcurrentBuilder, WorkflowCompletedEvent
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""
|
||||
Sample: Concurrent Orchestration with 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 async callback that uses AzureChatClient.get_response()
|
||||
to synthesize a concise, consolidated summary from the experts' outputs.
|
||||
|
||||
Demonstrates:
|
||||
- ConcurrentBuilder().participants([...]).with_custom_aggregator(callback)
|
||||
- Fan-out to agents and fan-in at an aggregator
|
||||
- Aggregation implemented via an LLM call (chat_client.get_response)
|
||||
- WorkflowCompletedEvent carrying the synthesized summary string
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI configured for AzureChatClient (az login + required env vars)
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
chat_client = AzureChatClient(credential=AzureCliCredential())
|
||||
|
||||
researcher = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You're an expert market and product researcher. Given a prompt, provide concise, factual insights,"
|
||||
" opportunities, and risks."
|
||||
),
|
||||
name="researcher",
|
||||
)
|
||||
marketer = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You're a creative marketing strategist. Craft compelling value propositions and target messaging"
|
||||
" aligned to the prompt."
|
||||
),
|
||||
name="marketer",
|
||||
)
|
||||
legal = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns"
|
||||
" based on the prompt."
|
||||
),
|
||||
name="legal",
|
||||
)
|
||||
|
||||
# Define a custom aggregator callback that uses the chat client to summarize
|
||||
async def summarize_results(results: list[Any]) -> str:
|
||||
# Extract one final assistant message per agent
|
||||
expert_sections: list[str] = []
|
||||
for r in results:
|
||||
try:
|
||||
messages = getattr(r.agent_run_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(
|
||||
Role.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(Role.USER, text="\n\n".join(expert_sections))
|
||||
|
||||
response = await chat_client.get_response([system_msg, user_msg])
|
||||
# Return the model's final assistant text as the completion result
|
||||
return response.messages[-1].text if response.messages else ""
|
||||
|
||||
# Build with a custom aggregator callback function
|
||||
# - participants([...]) accepts AgentProtocol (agents) or Executor instances.
|
||||
# Each participant becomes a parallel branch (fan-out) from an internal dispatcher.
|
||||
# - with_aggregator(...) overrides the default aggregator:
|
||||
# • Default aggregator -> returns list[ChatMessage] (one user + one assistant per agent)
|
||||
# • Custom callback -> return value becomes WorkflowCompletedEvent.data (string here)
|
||||
# The callback can be sync or async; it receives list[AgentExecutorResponse].
|
||||
workflow = (
|
||||
ConcurrentBuilder().participants([researcher, marketer, legal]).with_aggregator(summarize_results).build()
|
||||
)
|
||||
|
||||
completion: WorkflowCompletedEvent | None = None
|
||||
async for event in workflow.run_stream("We are launching a new budget-friendly electric bike for urban commuters."):
|
||||
if isinstance(event, WorkflowCompletedEvent):
|
||||
completion = event
|
||||
|
||||
if completion:
|
||||
print("===== Final Consolidated Output =====")
|
||||
print(completion.data)
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
===== Final Consolidated Output =====
|
||||
Urban e-bike demand is rising rapidly due to eco-awareness, urban congestion, and high fuel costs,
|
||||
with market growth projected at a ~10% CAGR through 2030. Key customer concerns are affordability,
|
||||
easy maintenance, convenient charging, compact design, and theft protection. Differentiation opportunities
|
||||
include integrating smart features (GPS, app connectivity), offering subscription or leasing options, and
|
||||
developing portable, space-saving designs. Partnering with local governments and bike shops can boost visibility.
|
||||
|
||||
Risks include price wars eroding margins, regulatory hurdles, battery quality concerns, and heightened expectations
|
||||
for after-sales support. Accurate, substantiated product claims and transparent marketing (with range disclaimers)
|
||||
are essential. All e-bikes must comply with local and federal regulations on speed, wattage, safety certification,
|
||||
and labeling. Clear warranty, safety instructions (especially regarding batteries), and inclusive, accessible
|
||||
marketing are required. For connected features, data privacy policies and user consents are mandatory.
|
||||
|
||||
Effective messaging should target young professionals, students, eco-conscious commuters, and first-time buyers,
|
||||
emphasizing affordability, convenience, and sustainability. Slogan suggestion: “Charge Ahead—City Commutes Made
|
||||
Affordable.” Legal review in each target market, compliance vetting, and robust customer support policies are
|
||||
critical before launch.
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user