mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
899d8ff775
commit
e3aad8e4e0
+75
@@ -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={},
|
||||
):
|
||||
|
||||
+34
-40
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user