[BREAKING] Python: Refactor orchestrations (#3023)

* Group chat refactoring Part 1; Next: HIL and handoff

* Add agent approval flow; next samples

* WIP: samples

* WIP: HIL samples

* Group chat HIL working; next: handoff

* Fix group chat tool approval sample

* WIP: refactor handoff; next handoff handling

* Handoff done; next handoff samples and concurrent and sequential

* Handoff samples, concurrent, and sequential done; next Magentic

* WIP: magentic; next test with samples + HIL

* Magentic Working; next fix all samples and tests

* Fix handoff samples; next tests

* WIP: fixing tests; some orchestration as agent samples are failing

* Group chat unit tests done

* Handoff  unit tests done

* Remove old orchestration_request_info and fix related tests

* Magentic unit tests done

* Fix samples

* Fix test

* Fix test 2

* mypy

* Address comments

* Update readme

* Address comments

* Address comments 2

* Replace display name
This commit is contained in:
Tao Chen
2026-01-13 10:40:26 -08:00
committed by GitHub
Unverified
parent 3e97425245
commit 0b152418b6
54 changed files with 5106 additions and 10245 deletions
@@ -4,17 +4,17 @@
Sample: Request Info with ConcurrentBuilder
This sample demonstrates using the `.with_request_info()` method to pause a
ConcurrentBuilder workflow AFTER all parallel agents complete but BEFORE
aggregation, allowing human review and modification of the combined results.
ConcurrentBuilder workflow for specific agents, allowing human review and
modification of individual agent outputs before aggregation.
Purpose:
Show how to use the request info API that pauses after concurrent agents run,
allowing review and steering of results before they are aggregated.
Show how to use the request info API that pauses for selected concurrent agents,
allowing review and steering of their results.
Demonstrate:
- Configuring request info with `.with_request_info()`
- Reviewing outputs from multiple concurrent agents
- Injecting human guidance after agents execute but before aggregation
- Configuring request info with `.with_request_info()` for specific agents
- Reviewing output from individual agents during concurrent execution
- Injecting human guidance for specific agents before aggregation
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables
@@ -25,7 +25,7 @@ import asyncio
from typing import Any
from agent_framework import (
AgentInputRequest,
AgentRequestInfoResponse,
ChatMessage,
ConcurrentBuilder,
RequestInfoEvent,
@@ -131,12 +131,13 @@ async def main() -> None:
ConcurrentBuilder()
.participants([technical_analyst, business_analyst, user_experience_analyst])
.with_aggregator(aggregate_with_synthesis)
.with_request_info()
# Only enable request info for the technical analyst agent
.with_request_info(agents=["technical_analyst"])
.build()
)
# Run the workflow with human-in-the-loop
pending_responses: dict[str, str] | None = None
pending_responses: dict[str, AgentRequestInfoResponse] | None = None
workflow_complete = False
print("Starting multi-perspective analysis workflow...")
@@ -155,26 +156,34 @@ async def main() -> None:
# Process events
async for event in stream:
if isinstance(event, RequestInfoEvent):
if isinstance(event.data, AgentInputRequest):
# Display pre-execution context for steering concurrent agents
if isinstance(event.data, AgentExecutorResponse):
# Display agent output for review and potential modification
print("\n" + "-" * 40)
print("INPUT REQUESTED (BEFORE CONCURRENT AGENTS)")
print("-" * 40)
print(f"About to call agents: {event.data.target_agent_id}")
print("Conversation context:")
recent = (
event.data.conversation[-2:] if len(event.data.conversation) > 2 else event.data.conversation
print("INPUT REQUESTED")
print(
f"Agent {event.source_executor_id} just responded with: '{event.data.agent_run_response.text}'. "
"Please provide your feedback."
)
for msg in recent:
role = msg.role.value if msg.role else "unknown"
text = (msg.text or "")[:150]
print(f" [{role}]: {text}...")
print("-" * 40)
if event.data.full_conversation:
print("Conversation context:")
recent = (
event.data.full_conversation[-2:]
if len(event.data.full_conversation) > 2
else event.data.full_conversation
)
for msg in recent:
name = msg.author_name or msg.role.value
text = (msg.text or "")[:150]
print(f" [{name}]: {text}...")
print("-" * 40)
# Get human input to steer all agents
user_input = input("Your guidance for the analysts (or 'skip' to continue): ") # noqa: ASYNC250
# Get human input to steer this agent's contribution
user_input = input("Your guidance for the analysts (or 'skip' to approve): ") # noqa: ASYNC250
if user_input.lower() == "skip":
user_input = "Please analyze objectively from your unique perspective."
user_input = AgentRequestInfoResponse.approve()
else:
user_input = AgentRequestInfoResponse.from_strings([user_input])
pending_responses = {event.request_id: user_input}
print("(Resuming workflow...)")
@@ -189,9 +198,8 @@ async def main() -> None:
print(event.data)
workflow_complete = True
elif isinstance(event, WorkflowStatusEvent):
if event.state == WorkflowRunState.IDLE:
workflow_complete = True
elif isinstance(event, WorkflowStatusEvent) and event.state == WorkflowRunState.IDLE:
workflow_complete = True
if __name__ == "__main__":
@@ -25,7 +25,9 @@ Prerequisites:
import asyncio
from agent_framework import (
AgentInputRequest,
AgentExecutorResponse,
AgentRequestInfoResponse,
AgentRunResponse,
AgentRunUpdateEvent,
ChatMessage,
GroupChatBuilder,
@@ -69,18 +71,17 @@ async def main() -> None:
),
)
# Manager orchestrates the discussion
manager = chat_client.create_agent(
name="manager",
# Orchestrator coordinates the discussion
orchestrator = chat_client.create_agent(
name="orchestrator",
instructions=(
"You are a discussion manager coordinating a team conversation between optimist, "
"pragmatist, and creative. Your job is to select who speaks next.\n\n"
"You are a discussion manager coordinating a team conversation between participants. "
"Your job is to select who speaks next.\n\n"
"RULES:\n"
"1. Rotate through ALL participants - do not favor any single participant\n"
"2. Each participant should speak at least once before any participant speaks twice\n"
"3. If human feedback redirects the topic, acknowledge it and continue rotating\n"
"4. Continue for at least 5 participant turns before concluding\n"
"5. Do NOT select the same participant twice in a row"
"3. Continue for at least 5 rounds before ending the discussion\n"
"4. Do NOT select the same participant twice in a row"
),
)
@@ -88,7 +89,7 @@ async def main() -> None:
# Using agents= filter to only pause before pragmatist speaks (not every turn)
workflow = (
GroupChatBuilder()
.set_manager(manager=manager, display_name="Discussion Manager")
.with_agent_orchestrator(orchestrator)
.participants([optimist, pragmatist, creative])
.with_max_rounds(6)
.with_request_info(agents=[pragmatist]) # Only pause before pragmatist speaks
@@ -96,7 +97,7 @@ async def main() -> None:
)
# Run the workflow with human-in-the-loop
pending_responses: dict[str, str] | None = None
pending_responses: dict[str, AgentRequestInfoResponse] | None = None
workflow_complete = False
current_agent: str | None = None # Track current streaming agent
@@ -130,28 +131,28 @@ async def main() -> None:
elif isinstance(event, RequestInfoEvent):
current_agent = None # Reset for next agent
if isinstance(event.data, AgentInputRequest):
if isinstance(event.data, AgentExecutorResponse):
# Display pre-agent context for human input
print("\n" + "-" * 40)
print("INPUT REQUESTED")
print(f"About to call agent: {event.data.target_agent_id}")
print(f"About to call agent: {event.source_executor_id}")
print("-" * 40)
print("Conversation context:")
recent = (
event.data.conversation[-3:] if len(event.data.conversation) > 3 else event.data.conversation
)
agent_run_response: AgentRunResponse = event.data.agent_run_response
messages: list[ChatMessage] = agent_run_response.messages
recent: list[ChatMessage] = messages[-3:] if len(messages) > 3 else messages # type: ignore
for msg in recent:
role = msg.role.value if msg.role else "unknown"
name = msg.author_name or "unknown"
text = (msg.text or "")[:100]
print(f" [{role}]: {text}...")
print(f" [{name}]: {text}...")
print("-" * 40)
# Get human input to steer the agent
user_input = input("Steer the discussion (or 'skip' to continue): ") # noqa: ASYNC250
user_input = input(f"Feedback for {event.source_executor_id} (or 'skip' to approve): ") # noqa: ASYNC250
if user_input.lower() == "skip":
user_input = "Please continue the discussion naturally."
pending_responses = {event.request_id: user_input}
pending_responses = {event.request_id: AgentRequestInfoResponse.approve()}
else:
pending_responses = {event.request_id: AgentRequestInfoResponse.from_strings([user_input])}
print("(Resuming discussion...)")
elif isinstance(event, WorkflowOutputEvent):
@@ -160,11 +161,12 @@ async def main() -> None:
print("=" * 60)
print("Final conversation:")
if event.data:
messages: list[ChatMessage] = event.data[-4:]
messages: list[ChatMessage] = event.data
for msg in messages:
role = msg.role.value if msg.role else "unknown"
role = msg.role.value.capitalize()
name = msg.author_name or "unknown"
text = (msg.text or "")[:200]
print(f"[{role}]: {text}...")
print(f"[{role}][{name}]: {text}...")
workflow_complete = True
elif isinstance(event, WorkflowStatusEvent) and event.state == WorkflowRunState.IDLE:
@@ -4,11 +4,11 @@
Sample: Request Info with SequentialBuilder
This sample demonstrates using the `.with_request_info()` method to pause a
SequentialBuilder workflow BEFORE each agent runs, allowing external input
(e.g., human steering) before the agent responds.
SequentialBuilder workflow AFTER each agent runs, allowing external input
(e.g., human feedback) for review and optional iteration.
Purpose:
Show how to use the request info API that pauses before every agent response,
Show how to use the request info API that pauses after every agent response,
using the standard request_info pattern for consistency.
Demonstrate:
@@ -24,7 +24,8 @@ Prerequisites:
import asyncio
from agent_framework import (
AgentInputRequest,
AgentExecutorResponse,
AgentRequestInfoResponse,
ChatMessage,
RequestInfoEvent,
SequentialBuilder,
@@ -48,7 +49,7 @@ async def main() -> None:
editor = chat_client.create_agent(
name="editor",
instructions=(
"You are an editor. Review the draft and suggest improvements. "
"You are an editor. Review the draft and make improvements. "
"Incorporate any human feedback that was provided."
),
)
@@ -61,11 +62,17 @@ async def main() -> None:
),
)
# Build workflow with request info enabled (pauses before each agent)
workflow = SequentialBuilder().participants([drafter, editor, finalizer]).with_request_info().build()
# Build workflow with request info enabled (pauses after each agent responds)
workflow = (
SequentialBuilder()
.participants([drafter, editor, finalizer])
# Only enable request info for the editor agent
.with_request_info(agents=["editor"])
.build()
)
# Run the workflow with request info handling
pending_responses: dict[str, str] | None = None
pending_responses: dict[str, AgentRequestInfoResponse] | None = None
workflow_complete = False
print("Starting document review workflow...")
@@ -84,26 +91,34 @@ async def main() -> None:
# Process events
async for event in stream:
if isinstance(event, RequestInfoEvent):
if isinstance(event.data, AgentInputRequest):
# Display pre-agent context for steering
if isinstance(event.data, AgentExecutorResponse):
# Display agent response and conversation context for review
print("\n" + "-" * 40)
print("REQUEST INFO: INPUT REQUESTED")
print(f"About to call agent: {event.data.target_agent_id}")
print("-" * 40)
print("Conversation context:")
recent = (
event.data.conversation[-2:] if len(event.data.conversation) > 2 else event.data.conversation
print(
f"Agent {event.source_executor_id} just responded with: '{event.data.agent_run_response.text}'. "
"Please provide your feedback."
)
for msg in recent:
role = msg.role.value if msg.role else "unknown"
text = (msg.text or "")[:150]
print(f" [{role}]: {text}...")
print("-" * 40)
if event.data.full_conversation:
print("Conversation context:")
recent = (
event.data.full_conversation[-2:]
if len(event.data.full_conversation) > 2
else event.data.full_conversation
)
for msg in recent:
name = msg.author_name or msg.role.value
text = (msg.text or "")[:150]
print(f" [{name}]: {text}...")
print("-" * 40)
# Get input to steer the agent
user_input = input("Your guidance (or 'skip' to continue): ") # noqa: ASYNC250
# Get feedback on the agent's response (approve or request iteration)
user_input = input("Your guidance (or 'skip' to approve): ") # noqa: ASYNC250
if user_input.lower() == "skip":
user_input = "Please continue naturally."
user_input = AgentRequestInfoResponse.approve()
else:
user_input = AgentRequestInfoResponse.from_strings([user_input])
pending_responses = {event.request_id: user_input}
print("(Resuming workflow...)")