mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: [Breaking] Remove WorkflowCompletedEvent, introduce workflow output and migrate to ctx.yield_output() + a huge refactoring (#845)
* Introduce input and output types for executor and workflow * WorkflowOutputContext handles two types * Remove can_handle_types from Executor * Update validation * Move workflow executor * Move workflow executor * Fix issues in WorkflowExecutor * refactor executor * update execute signature to create workflow context within Executor * fix simple sub workflow test; fix validation * fix output types in WorkflowExecutor * fix issue in Executor handling of SubWorkflowRequestInfo * update tests to use proper workflow output * update orchestration patterns to use output * Update sample -- not finished * Update python/packages/main/tests/workflow/test_workflow_states.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update python/packages/main/tests/workflow/test_concurrent.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * address comments * WorkflowOutputContext --> WorkflowContext * remove WorkflowCompletedEvent * update samples * Update doc string for important classes; update WorkflowExecutor to support concurrent execution * use Never instead of None for default type * Update usage of WorkflowContext[None to WorkflowContext[Never * address comments * remove filter for None * address comments, minor fixes * quality of life improvement on interceptor types --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
0f913bcdeb
commit
2133043f11
@@ -3,7 +3,7 @@
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import ChatMessage, ConcurrentBuilder, WorkflowCompletedEvent
|
||||
from agent_framework import ChatMessage, ConcurrentBuilder
|
||||
from agent_framework.azure import AzureChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
@@ -12,13 +12,13 @@ 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.
|
||||
The default aggregator fans in their results and yields output containing
|
||||
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
|
||||
- Workflow completion when idle with no pending work
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI access configured for AzureChatClient (use az login + env vars)
|
||||
@@ -58,18 +58,17 @@ async def main() -> None:
|
||||
# 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
|
||||
# 3) Run with a single prompt and pretty-print the final combined messages
|
||||
events = await workflow.run("We are launching a new budget-friendly electric bike for urban commuters.")
|
||||
outputs = events.get_outputs()
|
||||
|
||||
if completion:
|
||||
if outputs:
|
||||
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}")
|
||||
for output in outputs:
|
||||
messages: list[ChatMessage] | Any = output
|
||||
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:
|
||||
|
||||
+5
-7
@@ -10,7 +10,6 @@ from agent_framework import (
|
||||
ChatMessage,
|
||||
ConcurrentBuilder,
|
||||
Executor,
|
||||
WorkflowCompletedEvent,
|
||||
WorkflowContext,
|
||||
handler,
|
||||
)
|
||||
@@ -30,6 +29,7 @@ Demonstrates:
|
||||
- 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)
|
||||
- Workflow completion when all participants become idle
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI configured for AzureChatClient (az login + required env vars)
|
||||
@@ -105,14 +105,12 @@ async def main() -> None:
|
||||
|
||||
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
|
||||
events = await workflow.run("We are launching a new budget-friendly electric bike for urban commuters.")
|
||||
outputs = events.get_outputs()
|
||||
|
||||
if completion:
|
||||
if outputs:
|
||||
print("===== Final Aggregated Conversation (messages) =====")
|
||||
messages: list[ChatMessage] | Any = completion.data
|
||||
messages: list[ChatMessage] | Any = outputs[0] # Get the first (and typically only) output
|
||||
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}")
|
||||
|
||||
+8
-9
@@ -3,7 +3,7 @@
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import ChatMessage, ConcurrentBuilder, Role, WorkflowCompletedEvent
|
||||
from agent_framework import ChatMessage, ConcurrentBuilder, Role
|
||||
from agent_framework.azure import AzureChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
@@ -14,12 +14,13 @@ 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.
|
||||
The workflow completes when all participants become idle.
|
||||
|
||||
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
|
||||
- Workflow output yielded with the synthesized summary string
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI configured for AzureChatClient (az login + required env vars)
|
||||
@@ -82,20 +83,18 @@ async def main() -> None:
|
||||
# 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)
|
||||
# • Custom callback -> return value becomes workflow output (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
|
||||
events = await workflow.run("We are launching a new budget-friendly electric bike for urban commuters.")
|
||||
outputs = events.get_outputs()
|
||||
|
||||
if completion:
|
||||
if outputs:
|
||||
print("===== Final Consolidated Output =====")
|
||||
print(completion.data)
|
||||
print(outputs[0]) # Get the first (and typically only) output
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
@@ -13,7 +13,7 @@ from agent_framework import (
|
||||
MagenticCallbackMode,
|
||||
MagenticFinalResultEvent,
|
||||
MagenticOrchestratorMessageEvent,
|
||||
WorkflowCompletedEvent,
|
||||
WorkflowOutputEvent,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
|
||||
|
||||
@@ -38,7 +38,7 @@ The workflow is configured with:
|
||||
|
||||
When run, the script builds the workflow, submits a task about estimating the
|
||||
energy efficiency and CO2 emissions of several ML models, streams intermediate
|
||||
events, and prints the final answer.
|
||||
events, and prints the final answer. The workflow completes when idle.
|
||||
|
||||
Prerequisites:
|
||||
- OpenAI credentials configured for `OpenAIChatClient` and `OpenAIResponsesClient`.
|
||||
@@ -132,17 +132,14 @@ async def main() -> None:
|
||||
print("\nStarting workflow execution...")
|
||||
|
||||
try:
|
||||
completion_event = None
|
||||
output: str | None = None
|
||||
async for event in workflow.run_stream(task):
|
||||
print(f"Event: {event}")
|
||||
print(event)
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
output = str(event.data)
|
||||
|
||||
if isinstance(event, WorkflowCompletedEvent):
|
||||
completion_event = event
|
||||
|
||||
if completion_event is not None:
|
||||
data = getattr(completion_event, "data", None)
|
||||
preview = getattr(data, "text", None) or (str(data) if data is not None else "")
|
||||
print(f"Workflow completed with result:\n\n{preview}")
|
||||
if output is not None:
|
||||
print(f"Workflow completed with result:\n\n{output}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Workflow execution failed: {e}")
|
||||
|
||||
+51
-40
@@ -18,7 +18,7 @@ from agent_framework import (
|
||||
MagenticPlanReviewReply,
|
||||
MagenticPlanReviewRequest,
|
||||
RequestInfoEvent,
|
||||
WorkflowCompletedEvent,
|
||||
WorkflowOutputEvent,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
|
||||
|
||||
@@ -41,6 +41,7 @@ Key behaviors demonstrated:
|
||||
replies with PlanReviewReply (here we auto-approve, but you can edit/collect input)
|
||||
- Callbacks: on_agent_stream (incremental chunks), on_agent_response (final messages),
|
||||
on_result (final answer), and on_exception
|
||||
- Workflow completion when idle
|
||||
|
||||
Prerequisites:
|
||||
- OpenAI credentials configured for `OpenAIChatClient` and `OpenAIResponsesClient`.
|
||||
@@ -73,6 +74,9 @@ async def main() -> None:
|
||||
print(f"Exception occurred: {exception}")
|
||||
logger.exception("Workflow exception", exc_info=exception)
|
||||
|
||||
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
|
||||
@@ -105,9 +109,6 @@ async def main() -> None:
|
||||
|
||||
print("\nBuilding Magentic Workflow...")
|
||||
|
||||
last_stream_agent_id: str | None = None
|
||||
stream_line_open: bool = False
|
||||
|
||||
workflow = (
|
||||
MagenticBuilder()
|
||||
.participants(researcher=researcher_agent, coder=coder_agent)
|
||||
@@ -136,51 +137,61 @@ async def main() -> None:
|
||||
print("\nStarting workflow execution...")
|
||||
|
||||
try:
|
||||
completion_event: WorkflowCompletedEvent | None = None
|
||||
pending_request: RequestInfoEvent | None = None
|
||||
pending_responses: dict[str, MagenticPlanReviewReply] | None = None
|
||||
completed = False
|
||||
workflow_output: str | None = None
|
||||
|
||||
while True:
|
||||
# Phase 1: run until either completion or a HIL request
|
||||
if pending_request is None:
|
||||
async for event in workflow.run_stream(task):
|
||||
print(f"Event: {event}")
|
||||
while not completed:
|
||||
# Use streaming for both initial run and response sending
|
||||
if pending_responses is not None:
|
||||
stream = workflow.send_responses_streaming(pending_responses)
|
||||
else:
|
||||
stream = workflow.run_stream(task)
|
||||
|
||||
if isinstance(event, WorkflowCompletedEvent):
|
||||
completion_event = event
|
||||
# Collect events from the stream
|
||||
events = [event async for event in stream]
|
||||
pending_responses = None
|
||||
|
||||
if isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest:
|
||||
pending_request = event
|
||||
review_req = cast(MagenticPlanReviewRequest, event.data)
|
||||
if review_req.plan_text:
|
||||
print(f"\n=== PLAN REVIEW REQUEST ===\n{review_req.plan_text}\n")
|
||||
# Process events to find request info events, outputs, and completion status
|
||||
for event in events:
|
||||
if isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest:
|
||||
pending_request = event
|
||||
review_req = cast(MagenticPlanReviewRequest, event.data)
|
||||
if review_req.plan_text:
|
||||
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)
|
||||
completed = True
|
||||
|
||||
# Break if completed
|
||||
if completion_event is not None:
|
||||
data = getattr(completion_event, "data", None)
|
||||
preview = getattr(data, "text", None) or (str(data) if data is not None else "")
|
||||
print(f"Workflow completed with result:\n\n{preview}")
|
||||
|
||||
# Phase 2: respond to the pending plan review (HIL) request
|
||||
# Handle pending plan review request
|
||||
if pending_request is not None:
|
||||
# For demo purposes we approve as-is. Replace this with UI input
|
||||
# to collect a human decision/comments/edited plan.
|
||||
reply = MagenticPlanReviewReply(decision=MagenticPlanReviewDecision.APPROVE)
|
||||
# Get human input for plan review decision
|
||||
print("Plan review options:")
|
||||
print("1. approve - Approve the plan as-is")
|
||||
print("2. revise - Request revision of the plan")
|
||||
print("3. exit - Exit the workflow")
|
||||
|
||||
async for event in workflow.send_responses_streaming({pending_request.request_id: reply}):
|
||||
print(f"Event: {event}")
|
||||
while True:
|
||||
choice = input("Enter your choice (approve/revise/exit): ").strip().lower() # noqa: ASYNC250
|
||||
if choice in ["approve", "1"]:
|
||||
reply = MagenticPlanReviewReply(decision=MagenticPlanReviewDecision.APPROVE)
|
||||
break
|
||||
if choice in ["revise", "2"]:
|
||||
reply = MagenticPlanReviewReply(decision=MagenticPlanReviewDecision.REVISE)
|
||||
break
|
||||
if choice in ["exit", "3"]:
|
||||
print("Exiting workflow...")
|
||||
return
|
||||
print("Invalid choice. Please enter 'approve', 'revise', or 'exit'.")
|
||||
|
||||
if isinstance(event, WorkflowCompletedEvent):
|
||||
completion_event = event
|
||||
pending_responses = {pending_request.request_id: reply}
|
||||
pending_request = None
|
||||
|
||||
if isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest:
|
||||
# Another review cycle requested; keep pending
|
||||
pending_request = event
|
||||
review_req = cast(MagenticPlanReviewRequest, event.data)
|
||||
if review_req.plan_text:
|
||||
print(f"\n=== PLAN REVIEW REQUEST ===\n{review_req.plan_text}\n")
|
||||
else:
|
||||
# Clear pending if no immediate new request
|
||||
pending_request = None
|
||||
# Show final result from captured workflow output
|
||||
if workflow_output:
|
||||
print(f"Workflow completed with result:\n\n{workflow_output}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Workflow execution failed: {e}")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
|
||||
from agent_framework import ChatMessage, Role, SequentialBuilder, WorkflowCompletedEvent
|
||||
from agent_framework import ChatMessage, Role, SequentialBuilder, WorkflowOutputEvent
|
||||
from agent_framework.azure import AzureChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
@@ -12,8 +12,8 @@ Sample: Sequential workflow (agent-focused API) with shared conversation context
|
||||
|
||||
Build a high-level sequential workflow using SequentialBuilder and two domain agents.
|
||||
The shared conversation (list[ChatMessage]) flows through each participant. Each agent
|
||||
appends its assistant message to the context. The final WorkflowCompletedEvent includes
|
||||
the final conversation list.
|
||||
appends its assistant message to the context. The workflow outputs the final conversation
|
||||
list when complete.
|
||||
|
||||
Note on internal adapters:
|
||||
- Sequential orchestration includes small adapter nodes for input normalization
|
||||
@@ -44,16 +44,15 @@ async def main() -> None:
|
||||
# 2) Build sequential workflow: writer -> reviewer
|
||||
workflow = SequentialBuilder().participants([writer, reviewer]).build()
|
||||
|
||||
# 3) Run and print final conversation
|
||||
completion: WorkflowCompletedEvent | None = None
|
||||
# 3) Run and collect outputs
|
||||
outputs: list[list[ChatMessage]] = []
|
||||
async for event in workflow.run_stream("Write a tagline for a budget-friendly eBike."):
|
||||
if isinstance(event, WorkflowCompletedEvent):
|
||||
completion = event
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
outputs.append(cast(list[ChatMessage], event.data))
|
||||
|
||||
if completion:
|
||||
if outputs:
|
||||
print("===== Final Conversation =====")
|
||||
messages: list[ChatMessage] | Any = completion.data
|
||||
for i, msg in enumerate(messages, start=1):
|
||||
for i, msg in enumerate(outputs[-1], start=1):
|
||||
name = msg.author_name or ("assistant" if msg.role == Role.ASSISTANT else "user")
|
||||
print(f"{'-' * 60}\n{i:02d} [{name}]\n{msg.text}")
|
||||
|
||||
|
||||
+11
-11
@@ -3,12 +3,13 @@
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from typing_extensions import Never
|
||||
|
||||
from agent_framework import (
|
||||
ChatMessage,
|
||||
Executor,
|
||||
Role,
|
||||
SequentialBuilder,
|
||||
WorkflowCompletedEvent,
|
||||
WorkflowContext,
|
||||
handler,
|
||||
)
|
||||
@@ -20,8 +21,8 @@ Sample: Sequential workflow mixing agents and a custom summarizer executor
|
||||
|
||||
This demonstrates how SequentialBuilder chains participants with a shared
|
||||
conversation context (list[ChatMessage]). An agent produces content; a custom
|
||||
executor appends a compact summary to the conversation. The final WorkflowCompletedEvent
|
||||
contains the complete conversation.
|
||||
executor appends a compact summary to the conversation. The workflow completes
|
||||
when idle, and the final output contains the complete conversation.
|
||||
|
||||
Custom executor contract:
|
||||
- Provide at least one @handler accepting list[ChatMessage] and a WorkflowContext[list[ChatMessage]]
|
||||
@@ -42,11 +43,12 @@ class Summarizer(Executor):
|
||||
"""Simple summarizer: consumes full conversation and appends an assistant summary."""
|
||||
|
||||
@handler
|
||||
async def summarize(self, conversation: list[ChatMessage], ctx: WorkflowContext[list[ChatMessage]]) -> None:
|
||||
async def summarize(self, conversation: list[ChatMessage], ctx: WorkflowContext[Never, list[ChatMessage]]) -> None:
|
||||
users = sum(1 for m in conversation if m.role == Role.USER)
|
||||
assistants = sum(1 for m in conversation if m.role == Role.ASSISTANT)
|
||||
summary = ChatMessage(role=Role.ASSISTANT, text=f"Summary -> users:{users} assistants:{assistants}")
|
||||
await ctx.send_message(list(conversation) + [summary])
|
||||
final_conversation = list(conversation) + [summary]
|
||||
await ctx.yield_output(final_conversation)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
@@ -62,14 +64,12 @@ async def main() -> None:
|
||||
workflow = SequentialBuilder().participants([content, summarizer]).build()
|
||||
|
||||
# 3) Run and print final conversation
|
||||
completion: WorkflowCompletedEvent | None = None
|
||||
async for event in workflow.run_stream("Explain the benefits of budget eBikes for commuters."):
|
||||
if isinstance(event, WorkflowCompletedEvent):
|
||||
completion = event
|
||||
events = await workflow.run("Explain the benefits of budget eBikes for commuters.")
|
||||
outputs = events.get_outputs()
|
||||
|
||||
if completion:
|
||||
if outputs:
|
||||
print("===== Final Conversation =====")
|
||||
messages: list[ChatMessage] | Any = completion.data
|
||||
messages: list[ChatMessage] | Any = outputs[0]
|
||||
for i, msg in enumerate(messages, start=1):
|
||||
name = msg.author_name or ("assistant" if msg.role == Role.ASSISTANT else "user")
|
||||
print(f"{'-' * 60}\n{i:02d} [{name}]\n{msg.text}")
|
||||
|
||||
Reference in New Issue
Block a user