Python: [BREAKING] Python: Intro group chat and refactor orchestrations. Fix as_agent(). Standardize orchestration start msg types. (#1538)

* Intro group chat and refactor magentic. Fix as_agent()

* Cleanup and improvements

* Add as_agent docstring clarification

* Standardize orchestration messages to use agent-style inputs.

* Simplify group chat constructs

* Further cleanup

* Add sk to af group chat migration sample. Update README.

* Improvements and simplifications

* consolidating shared orchestration logic

* Further clean up

* Add group chat sample

* Improve typing

* Fix test imports

* Fix readme links

* Cleanup per PR Feedback
This commit is contained in:
Evan Mattson
2025-10-25 09:14:06 +09:00
committed by GitHub
Unverified
parent 899d8ff775
commit e3aad8e4e0
38 changed files with 5024 additions and 814 deletions
@@ -39,6 +39,9 @@ Once comfortable with these, explore the rest of the samples below.
| Azure Chat Agents (Function Bridge) | [agents/azure_chat_agents_function_bridge.py](./agents/azure_chat_agents_function_bridge.py) | Chain two agents with a function executor that injects external context |
| Azure Chat Agents (Tools + HITL) | [agents/azure_chat_agents_tool_calls_with_feedback.py](./agents/azure_chat_agents_tool_calls_with_feedback.py) | Tool-enabled writer/editor pipeline with human feedback gating via RequestInfoExecutor |
| Custom Agent Executors | [agents/custom_agent_executors.py](./agents/custom_agent_executors.py) | Create executors to handle agent run methods |
| Sequential Workflow as Agent | [agents/sequential_workflow_as_agent.py](./agents/sequential_workflow_as_agent.py) | Build a sequential workflow orchestrating agents, then expose it as a reusable agent |
| Concurrent Workflow as Agent | [agents/concurrent_workflow_as_agent.py](./agents/concurrent_workflow_as_agent.py) | Build a concurrent fan-out/fan-in workflow, then expose it as a reusable agent |
| Magentic Workflow as Agent | [agents/magentic_workflow_as_agent.py](./agents/magentic_workflow_as_agent.py) | Configure Magentic orchestration with callbacks, then expose the workflow as an agent |
| Workflow as Agent (Reflection Pattern) | [agents/workflow_as_agent_reflection_pattern.py](./agents/workflow_as_agent_reflection_pattern.py) | Wrap a workflow so it can behave like an agent (reflection pattern) |
| Workflow as Agent + HITL | [agents/workflow_as_agent_human_in_the_loop.py](./agents/workflow_as_agent_human_in_the_loop.py) | Extend workflow-as-agent with human-in-the-loop capability |
@@ -89,6 +92,8 @@ Once comfortable with these, explore the rest of the samples below.
| 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 |
| Group Chat Orchestration with Prompt Based Manager | [orchestration/group_chat_prompt_based_manager.py](./orchestration/group_chat_prompt_based_manager.py) | LLM Manager-directed conversation using GroupChatBuilder |
| Group Chat with Simple Function Selector | [orchestration/group_chat_simple_selector.py](./orchestration/group_chat_simple_selector.py) | Group chat with a simple function selector for next speaker |
| Handoff (Simple) | [orchestration/handoff_simple.py](./orchestration/handoff_simple.py) | Single-tier routing: triage agent routes to specialists, control returns to user after each specialist response |
| Handoff (Specialist-to-Specialist) | [orchestration/handoff_specialist_to_specialist.py](./orchestration/handoff_specialist_to_specialist.py) | Multi-tier routing: specialists can hand off to other specialists using `.add_handoff()` fluent API |
| Magentic Workflow (Multi-Agent) | [orchestration/magentic.py](./orchestration/magentic.py) | Orchestrate multiple agents with Magentic manager and streaming |
@@ -0,0 +1,126 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import ConcurrentBuilder
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential
"""
Sample: Build a concurrent workflow orchestration and wrap it as an agent.
This script wires up a fan-out/fan-in workflow using `ConcurrentBuilder`, and then
invokes the entire orchestration through the `workflow.as_agent(...)` interface so
downstream coordinators can reuse the orchestration as a single agent.
Demonstrates:
- Fan-out to multiple agents, fan-in aggregation of final ChatMessages.
- Reusing the orchestrated workflow as an agent entry point with `workflow.as_agent(...)`.
- Workflow completion when idle with no pending work
Prerequisites:
- Azure OpenAI access configured for AzureOpenAIChatClient (use az login + env vars)
- Familiarity with Workflow events (AgentRunEvent, WorkflowOutputEvent)
"""
async def main() -> None:
# 1) Create three domain agents using AzureOpenAIChatClient
chat_client = AzureOpenAIChatClient(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
workflow = ConcurrentBuilder().participants([researcher, marketer, legal]).build()
# 3) Expose the concurrent workflow as an agent for easy reuse
agent = workflow.as_agent(name="ConcurrentWorkflowAgent")
prompt = "We are launching a new budget-friendly electric bike for urban commuters."
agent_response = await agent.run(prompt)
if agent_response.messages:
print("\n===== Aggregated Messages =====")
for i, msg in enumerate(agent_response.messages, start=1):
role = getattr(msg.role, "value", msg.role)
name = msg.author_name if msg.author_name else role
print(f"{'-' * 60}\n\n{i:02d} [{name}]:\n{msg.text}")
"""
Sample Output:
===== Aggregated 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())
@@ -0,0 +1,67 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
from agent_framework import ChatAgent, GroupChatBuilder
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
logging.basicConfig(level=logging.INFO)
"""
Sample: Group Chat Orchestration (manager-directed)
What it does:
- Demonstrates the generic GroupChatBuilder with a language-model manager directing two agents.
- The manager coordinates a researcher (chat completions) and a writer (responses API) to solve a task.
- Uses the default group chat orchestration pipeline shared with Magentic.
Prerequisites:
- OpenAI environment variables configured for `OpenAIChatClient` and `OpenAIResponsesClient`.
"""
async def main() -> None:
researcher = ChatAgent(
name="Researcher",
description="Collects relevant background information.",
instructions="Gather concise facts that help a teammate answer the question.",
chat_client=OpenAIChatClient(model_id="gpt-4o-mini"),
)
writer = ChatAgent(
name="Writer",
description="Synthesizes a polished answer using the gathered notes.",
instructions="Compose clear and structured answers using any notes provided.",
chat_client=OpenAIResponsesClient(),
)
workflow = (
GroupChatBuilder()
.set_prompt_based_manager(chat_client=OpenAIChatClient(), display_name="Coordinator")
.participants(researcher=researcher, writer=writer)
.build()
)
task = "Outline the core considerations for planning a community hackathon, and finish with a concise action plan."
print("\nStarting Group Chat Workflow...\n")
print(f"Input: {task}\n")
try:
workflow_agent = workflow.as_agent(name="GroupChatWorkflowAgent")
agent_result = await workflow_agent.run(task)
if agent_result.messages:
print("\n===== as_agent() Transcript =====")
for i, msg in enumerate(agent_result.messages, start=1):
role_value = getattr(msg.role, "value", msg.role)
speaker = msg.author_name or role_value
print(f"{'-' * 50}\n{i:02d} [{speaker}]\n{msg.text}")
except Exception as e:
print(f"Workflow execution failed: {e}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,139 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
from agent_framework import (
ChatAgent,
HostedCodeInterpreterTool,
MagenticAgentDeltaEvent,
MagenticAgentMessageEvent,
MagenticBuilder,
MagenticFinalResultEvent,
MagenticOrchestratorMessageEvent,
WorkflowOutputEvent,
)
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
"""
Sample: Build a Magentic orchestration and wrap it as an agent.
The script configures a Magentic workflow with streaming callbacks, then invokes the
orchestration through `workflow.as_agent(...)` so the entire Magentic loop can be reused
like any other agent while still emitting callback telemetry.
Prerequisites:
- OpenAI credentials configured for `OpenAIChatClient` and `OpenAIResponsesClient`.
"""
async def main() -> None:
researcher_agent = ChatAgent(
name="ResearcherAgent",
description="Specialist in research and information gathering",
instructions=(
"You are a Researcher. You find information without additional computation or quantitative analysis."
),
# This agent requires the gpt-4o-search-preview model to perform web searches.
# Feel free to explore with other agents that support web search, for example,
# the `OpenAIResponseAgent` or `AzureAgentProtocol` with bing grounding.
chat_client=OpenAIChatClient(model_id="gpt-4o-search-preview"),
)
coder_agent = ChatAgent(
name="CoderAgent",
description="A helpful assistant that writes and executes code to process and analyze data.",
instructions="You solve questions using code. Please provide detailed analysis and computation process.",
chat_client=OpenAIResponsesClient(),
tools=HostedCodeInterpreterTool(),
)
print("\nBuilding Magentic Workflow...")
workflow = (
MagenticBuilder()
.participants(researcher=researcher_agent, coder=coder_agent)
.with_standard_manager(
chat_client=OpenAIChatClient(),
max_round_count=10,
max_stall_count=3,
max_reset_count=2,
)
.build()
)
task = (
"I am preparing a report on the energy efficiency of different machine learning model architectures. "
"Compare the estimated training and inference energy consumption of ResNet-50, BERT-base, and GPT-2 "
"on standard datasets (e.g., ImageNet for ResNet, GLUE for BERT, WebText for GPT-2). "
"Then, estimate the CO2 emissions associated with each, assuming training on an Azure Standard_NC6s_v3 "
"VM for 24 hours. Provide tables for clarity, and recommend the most energy-efficient model "
"per task type (image classification, text classification, and text generation)."
)
print(f"\nTask: {task}")
print("\nStarting workflow execution...")
try:
last_stream_agent_id: str | None = None
stream_line_open: bool = False
final_output: str | None = None
async for event in workflow.run_stream(task):
if isinstance(event, MagenticOrchestratorMessageEvent):
print(f"\n[ORCH:{event.kind}]\n\n{getattr(event.message, 'text', '')}\n{'-' * 26}")
elif isinstance(event, MagenticAgentDeltaEvent):
if last_stream_agent_id != event.agent_id or not stream_line_open:
if stream_line_open:
print()
print(f"\n[STREAM:{event.agent_id}]: ", end="", flush=True)
last_stream_agent_id = event.agent_id
stream_line_open = True
if event.text:
print(event.text, end="", flush=True)
elif isinstance(event, MagenticAgentMessageEvent):
if stream_line_open:
print(" (final)")
stream_line_open = False
print()
msg = event.message
if msg is not None:
response_text = (msg.text or "").replace("\n", " ")
print(f"\n[AGENT:{event.agent_id}] {msg.role.value}\n\n{response_text}\n{'-' * 26}")
elif isinstance(event, MagenticFinalResultEvent):
print("\n" + "=" * 50)
print("FINAL RESULT:")
print("=" * 50)
if event.message is not None:
print(event.message.text)
print("=" * 50)
elif isinstance(event, WorkflowOutputEvent):
final_output = str(event.data) if event.data is not None else None
if stream_line_open:
print()
stream_line_open = False
if final_output is not None:
print(f"\nWorkflow completed with result:\n\n{final_output}\n")
# Wrap the workflow as an agent for composition scenarios
workflow_agent = workflow.as_agent(name="MagenticWorkflowAgent")
agent_result = await workflow_agent.run(task)
if agent_result.messages:
print("\n===== as_agent() Transcript =====")
for i, msg in enumerate(agent_result.messages, start=1):
role_value = getattr(msg.role, "value", msg.role)
speaker = msg.author_name or role_value
print(f"{'-' * 50}\n{i:02d} [{speaker}]\n{msg.text}")
except Exception as e:
print(f"Workflow execution failed: {e}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,87 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import Role, SequentialBuilder
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential
"""
Sample: Build a sequential workflow orchestration and wrap it as an agent.
The script assembles a sequential conversation flow with `SequentialBuilder`, then
invokes the entire orchestration through the `workflow.as_agent(...)` interface so
other coordinators can reuse the chain as a single participant.
Note on internal adapters:
- Sequential orchestration includes small adapter nodes for input normalization
("input-conversation"), agent-response conversion ("to-conversation:<participant>"),
and completion ("complete"). These may appear as ExecutorInvoke/Completed events in
the stream—similar to how concurrent orchestration includes a dispatcher/aggregator.
You can safely ignore them when focusing on agent progress.
Prerequisites:
- Azure OpenAI access configured for AzureOpenAIChatClient (use az login + env vars)
"""
async def main() -> None:
# 1) Create agents
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
writer = chat_client.create_agent(
instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."),
name="writer",
)
reviewer = chat_client.create_agent(
instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."),
name="reviewer",
)
# 2) Build sequential workflow: writer -> reviewer
workflow = SequentialBuilder().participants([writer, reviewer]).build()
# 3) Treat the workflow itself as an agent for follow-up invocations
agent = workflow.as_agent(name="SequentialWorkflowAgent")
prompt = "Write a tagline for a budget-friendly eBike."
agent_response = await agent.run(prompt)
if agent_response.messages:
print("\n===== Conversation =====")
for i, msg in enumerate(agent_response.messages, start=1):
role_value = getattr(msg.role, "value", msg.role)
normalized_role = str(role_value).lower() if role_value is not None else "assistant"
name = msg.author_name or ("assistant" if normalized_role == Role.ASSISTANT.value else "user")
print(f"{'-' * 60}\n{i:02d} [{name}]\n{msg.text}")
"""
Sample Output:
===== Final Conversation =====
------------------------------------------------------------
01 [user]
Write a tagline for a budget-friendly eBike.
------------------------------------------------------------
02 [writer]
Ride farther, spend less—your affordable eBike adventure starts here.
------------------------------------------------------------
03 [reviewer]
This tagline clearly communicates affordability and the benefit of extended travel, making it
appealing to budget-conscious consumers. It has a friendly and motivating tone, though it could
be slightly shorter for more punch. Overall, a strong and effective suggestion!
===== as_agent() Conversation =====
------------------------------------------------------------
01 [writer]
Go electric, save big—your affordable ride awaits!
------------------------------------------------------------
02 [reviewer]
Catchy and straightforward! The tagline clearly emphasizes both the electric aspect and the affordability of the
eBike. It's inviting and actionable. For even more impact, consider making it slightly shorter:
"Go electric, save big." Overall, this is an effective and appealing suggestion for a budget-friendly eBike.
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,75 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
from agent_framework import AgentRunUpdateEvent, ChatAgent, GroupChatBuilder, WorkflowOutputEvent
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
logging.basicConfig(level=logging.INFO)
"""
Sample: Group Chat Orchestration (manager-directed)
What it does:
- Demonstrates the generic GroupChatBuilder with a language-model manager directing two agents.
- The manager coordinates a researcher (chat completions) and a writer (responses API) to solve a task.
- Uses the default group chat orchestration pipeline shared with Magentic.
Prerequisites:
- OpenAI environment variables configured for `OpenAIChatClient` and `OpenAIResponsesClient`.
"""
async def main() -> None:
researcher = ChatAgent(
name="Researcher",
description="Collects relevant background information.",
instructions="Gather concise facts that help a teammate answer the question.",
chat_client=OpenAIChatClient(model_id="gpt-4o-mini"),
)
writer = ChatAgent(
name="Writer",
description="Synthesizes a polished answer using the gathered notes.",
instructions="Compose clear and structured answers using any notes provided.",
chat_client=OpenAIResponsesClient(),
)
workflow = (
GroupChatBuilder()
.set_prompt_based_manager(chat_client=OpenAIChatClient(), display_name="Coordinator")
.participants(researcher=researcher, writer=writer)
.build()
)
task = "Outline the core considerations for planning a community hackathon, and finish with a concise action plan."
print("\nStarting Group Chat Workflow...\n")
print(f"TASK: {task}\n")
final_response = None
last_executor_id: str | None = None
async for event in workflow.run_stream(task):
if isinstance(event, AgentRunUpdateEvent):
# Handle the streaming agent update as it's produced
eid = event.executor_id
if eid != last_executor_id:
if last_executor_id is not None:
print()
print(f"{eid}:", end=" ", flush=True)
last_executor_id = eid
print(event.data, end="", flush=True)
elif isinstance(event, WorkflowOutputEvent):
final_response = getattr(event.data, "text", str(event.data))
if final_response:
print("=" * 60)
print("FINAL RESPONSE")
print("=" * 60)
print(final_response)
print("=" * 60)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,110 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
from agent_framework import ChatAgent, GroupChatBuilder, GroupChatStateSnapshot, WorkflowOutputEvent
from agent_framework.openai import OpenAIChatClient
logging.basicConfig(level=logging.INFO)
"""
Sample: Group Chat with Simple Speaker Selector Function
What it does:
- Demonstrates the select_speakers() API for GroupChat orchestration
- Uses a pure Python function to control speaker selection based on conversation state
- Alternates between researcher and writer agents in a simple round-robin pattern
- Shows how to access conversation history, round index, and participant metadata
Key pattern:
def select_next_speaker(state: GroupChatStateSnapshot) -> str | None:
# state contains: task, participants, conversation, history, round_index
# Return participant name to continue, or None to finish
...
Prerequisites:
- OpenAI environment variables configured for OpenAIChatClient
"""
def select_next_speaker(state: GroupChatStateSnapshot) -> str | None:
"""Simple speaker selector that alternates between researcher and writer.
This function demonstrates the core pattern:
1. Examine the current state of the group chat
2. Decide who should speak next
3. Return participant name or None to finish
Args:
state: Immutable snapshot containing:
- task: ChatMessage - original user task
- participants: dict[str, str] - participant names → descriptions
- conversation: tuple[ChatMessage, ...] - full conversation history
- history: tuple[GroupChatTurn, ...] - turn-by-turn with speaker attribution
- round_index: int - number of selection rounds so far
- pending_agent: str | None - currently active agent (if any)
Returns:
Name of next speaker, or None to finish the conversation
"""
round_idx = state["round_index"]
history = state["history"]
# Finish after 4 turns (researcher → writer → researcher → writer)
if round_idx >= 4:
return None
# Get the last speaker from history
last_speaker = history[-1].speaker if history else None
# Simple alternation: researcher → writer → researcher → writer
if last_speaker == "Researcher":
return "Writer"
return "Researcher"
async def main() -> None:
researcher = ChatAgent(
name="Researcher",
description="Collects relevant background information.",
instructions="Gather concise facts that help answer the question. Be brief.",
chat_client=OpenAIChatClient(model_id="gpt-4o-mini"),
)
writer = ChatAgent(
name="Writer",
description="Synthesizes a polished answer using the gathered notes.",
instructions="Compose a clear, structured answer using any notes provided.",
chat_client=OpenAIChatClient(model_id="gpt-4o-mini"),
)
# Two ways to specify participants:
# 1. List form - uses agent.name attribute: .participants([researcher, writer])
# 2. Dict form - explicit names: .participants(researcher=researcher, writer=writer)
workflow = (
GroupChatBuilder()
.select_speakers(select_next_speaker, display_name="Orchestrator")
.participants([researcher, writer]) # Uses agent.name for participant names
.build()
)
task = "What are the key benefits of using async/await in Python?"
print("\nStarting Group Chat with Simple Speaker Selector...\n")
print(f"TASK: {task}\n")
print("=" * 80)
async for event in workflow.run_stream(task):
if isinstance(event, WorkflowOutputEvent):
final_message = event.data
author = getattr(final_message, "author_name", "Unknown")
text = getattr(final_message, "text", str(final_message))
print(f"\n[{author}]\n{text}\n")
print("-" * 80)
print("\nWorkflow completed.")
if __name__ == "__main__":
asyncio.run(main())
@@ -9,8 +9,6 @@ from agent_framework import (
MagenticAgentDeltaEvent,
MagenticAgentMessageEvent,
MagenticBuilder,
MagenticCallbackEvent,
MagenticCallbackMode,
MagenticFinalResultEvent,
MagenticOrchestratorMessageEvent,
WorkflowOutputEvent,
@@ -66,40 +64,6 @@ async def main() -> None:
tools=HostedCodeInterpreterTool(),
)
# Unified callback
async def on_event(event: MagenticCallbackEvent) -> None:
"""
The `on_event` callback processes events emitted by the workflow.
Events include: orchestrator messages, agent delta updates, agent messages, and final result events.
"""
nonlocal last_stream_agent_id, stream_line_open
if isinstance(event, MagenticOrchestratorMessageEvent):
print(f"\n[ORCH:{event.kind}]\n\n{getattr(event.message, 'text', '')}\n{'-' * 26}")
elif isinstance(event, MagenticAgentDeltaEvent):
if last_stream_agent_id != event.agent_id or not stream_line_open:
if stream_line_open:
print()
print(f"\n[STREAM:{event.agent_id}]: ", end="", flush=True)
last_stream_agent_id = event.agent_id
stream_line_open = True
print(event.text, end="", flush=True)
elif isinstance(event, MagenticAgentMessageEvent):
if stream_line_open:
print(" (final)")
stream_line_open = False
print()
msg = event.message
if msg is not None:
response_text = (msg.text or "").replace("\n", " ")
print(f"\n[AGENT:{event.agent_id}] {msg.role.value}\n\n{response_text}\n{'-' * 26}")
elif isinstance(event, MagenticFinalResultEvent):
print("\n" + "=" * 50)
print("FINAL RESULT:")
print("=" * 50)
if event.message is not None:
print(event.message.text)
print("=" * 50)
print("\nBuilding Magentic Workflow...")
# State used by on_agent_stream callback
@@ -109,7 +73,6 @@ async def main() -> None:
workflow = (
MagenticBuilder()
.participants(researcher=researcher_agent, coder=coder_agent)
.on_event(on_event, mode=MagenticCallbackMode.STREAMING)
.with_standard_manager(
chat_client=OpenAIChatClient(),
max_round_count=10,
@@ -134,9 +97,39 @@ async def main() -> None:
try:
output: str | None = None
async for event in workflow.run_stream(task):
print(event)
if isinstance(event, WorkflowOutputEvent):
output = str(event.data)
if isinstance(event, MagenticOrchestratorMessageEvent):
print(f"\n[ORCH:{event.kind}]\n\n{getattr(event.message, 'text', '')}\n{'-' * 26}")
elif isinstance(event, MagenticAgentDeltaEvent):
if last_stream_agent_id != event.agent_id or not stream_line_open:
if stream_line_open:
print()
print(f"\n[STREAM:{event.agent_id}]: ", end="", flush=True)
last_stream_agent_id = event.agent_id
stream_line_open = True
if event.text:
print(event.text, end="", flush=True)
elif isinstance(event, MagenticAgentMessageEvent):
if stream_line_open:
print(" (final)")
stream_line_open = False
print()
msg = event.message
if msg is not None:
response_text = (msg.text or "").replace("\n", " ")
print(f"\n[AGENT:{event.agent_id}] {msg.role.value}\n\n{response_text}\n{'-' * 26}")
elif isinstance(event, MagenticFinalResultEvent):
print("\n" + "=" * 50)
print("FINAL RESULT:")
print("=" * 50)
if event.message is not None:
print(event.message.text)
print("=" * 50)
elif isinstance(event, WorkflowOutputEvent):
output = str(event.data) if event.data is not None else None
if stream_line_open:
print()
stream_line_open = False
if output is not None:
print(f"Workflow completed with result:\n\n{output}")
@@ -113,7 +113,7 @@ async def main() -> None:
print("No plan review request emitted; nothing to resume.")
return
checkpoints = await checkpoint_storage.list_checkpoints(workflow.workflow.id)
checkpoints = await checkpoint_storage.list_checkpoints(workflow.id)
if not checkpoints:
print("No checkpoints persisted.")
return
@@ -141,7 +141,7 @@ async def main() -> None:
# and then continues the workflow. Because we only captured the initial plan review
# checkpoint, the resumed run should complete almost immediately.
final_event: WorkflowOutputEvent | None = None
async for event in resumed_workflow.workflow.run_stream_from_checkpoint(
async for event in resumed_workflow.run_stream_from_checkpoint(
resume_checkpoint.checkpoint_id,
responses={plan_review_request_id: approval},
):
@@ -204,7 +204,7 @@ async def main() -> None:
final_event_post: WorkflowOutputEvent | None = None
post_emitted_events = False
post_plan_workflow = build_workflow(checkpoint_storage)
async for event in post_plan_workflow.workflow.run_stream_from_checkpoint(
async for event in post_plan_workflow.run_stream_from_checkpoint(
post_plan_checkpoint.checkpoint_id,
responses={},
):
@@ -10,8 +10,6 @@ from agent_framework import (
MagenticAgentDeltaEvent,
MagenticAgentMessageEvent,
MagenticBuilder,
MagenticCallbackEvent,
MagenticCallbackMode,
MagenticFinalResultEvent,
MagenticOrchestratorMessageEvent,
MagenticPlanReviewDecision,
@@ -77,43 +75,11 @@ async def main() -> None:
last_stream_agent_id: str | None = None
stream_line_open: bool = False
# Unified callback
async def on_event(event: MagenticCallbackEvent) -> None:
nonlocal last_stream_agent_id, stream_line_open
if isinstance(event, MagenticOrchestratorMessageEvent):
print(f"\n[ORCH:{event.kind}]\n\n{getattr(event.message, 'text', '')}\n{'-' * 26}")
elif isinstance(event, MagenticAgentDeltaEvent):
if last_stream_agent_id != event.agent_id or not stream_line_open:
if stream_line_open:
print()
print(f"\n[STREAM:{event.agent_id}]: ", end="", flush=True)
last_stream_agent_id = event.agent_id
stream_line_open = True
print(event.text, end="", flush=True)
elif isinstance(event, MagenticAgentMessageEvent):
if stream_line_open:
print(" (final)")
stream_line_open = False
print()
msg = event.message
if msg is not None:
response_text = (msg.text or "").replace("\n", " ")
print(f"\n[AGENT:{event.agent_id}] {msg.role.value}\n\n{response_text}\n{'-' * 26}")
elif isinstance(event, MagenticFinalResultEvent):
print("\n" + "=" * 50)
print("FINAL RESULT:")
print("=" * 50)
if event.message is not None:
print(event.message.text)
print("=" * 50)
print("\nBuilding Magentic Workflow...")
workflow = (
MagenticBuilder()
.participants(researcher=researcher_agent, coder=coder_agent)
.on_exception(on_exception)
.on_event(on_event, mode=MagenticCallbackMode.STREAMING)
.with_standard_manager(
chat_client=OpenAIChatClient(),
max_round_count=10,
@@ -150,11 +116,34 @@ async def main() -> None:
stream = workflow.run_stream(task)
# Collect events from the stream
events = [event async for event in stream]
pending_responses = None
# Process events to find request info events, outputs, and completion status
for event in events:
async for event in stream:
if isinstance(event, MagenticOrchestratorMessageEvent):
print(f"\n[ORCH:{event.kind}]\n\n{getattr(event.message, 'text', '')}\n{'-' * 26}")
elif isinstance(event, MagenticAgentDeltaEvent):
if last_stream_agent_id != event.agent_id or not stream_line_open:
if stream_line_open:
print()
print(f"\n[STREAM:{event.agent_id}]: ", end="", flush=True)
last_stream_agent_id = event.agent_id
stream_line_open = True
if event.text:
print(event.text, end="", flush=True)
elif isinstance(event, MagenticAgentMessageEvent):
if stream_line_open:
print(" (final)")
stream_line_open = False
print()
msg = event.message
if msg is not None:
response_text = (msg.text or "").replace("\n", " ")
print(f"\n[AGENT:{event.agent_id}] {msg.role.value}\n\n{response_text}\n{'-' * 26}")
elif isinstance(event, MagenticFinalResultEvent):
print("\n" + "=" * 50)
print("FINAL RESULT:")
print("=" * 50)
if event.message is not None:
print(event.message.text)
print("=" * 50)
if isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest:
pending_request = event
review_req = cast(MagenticPlanReviewRequest, event.data)
@@ -162,9 +151,14 @@ async def main() -> None:
print(f"\n=== PLAN REVIEW REQUEST ===\n{review_req.plan_text}\n")
elif isinstance(event, WorkflowOutputEvent):
# Capture workflow output during streaming
workflow_output = str(event.data)
workflow_output = str(event.data) if event.data else None
completed = True
if stream_line_open:
print()
stream_line_open = False
pending_responses = None
# Handle pending plan review request
if pending_request is not None:
# Get human input for plan review decision