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
@@ -2,10 +2,11 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
from typing_extensions import Never
|
||||
|
||||
from agent_framework import (
|
||||
Executor,
|
||||
WorkflowBuilder,
|
||||
WorkflowCompletedEvent,
|
||||
WorkflowContext,
|
||||
executor,
|
||||
handler,
|
||||
@@ -17,18 +18,28 @@ Step 1: Foundational patterns: Executors and edges
|
||||
What this example shows
|
||||
- Two ways to define a unit of work (an Executor node):
|
||||
1) Custom class that subclasses Executor with an async method marked by @handler.
|
||||
Signature: (text: str, ctx: WorkflowContext[str]) -> None. The typed ctx
|
||||
advertises the type this node emits via ctx.send_message(...).
|
||||
Possible handler signatures:
|
||||
- (text: str, ctx: WorkflowContext) -> None,
|
||||
- (text: str, ctx: WorkflowContext[str]) -> None, or
|
||||
- (text: str, ctx: WorkflowContext[Never, str]) -> None.
|
||||
The first parameter is the typed input to this node, the input type is str here.
|
||||
The second parameter is a WorkflowContext[T_Out, T_W_Out].
|
||||
WorkflowContext[T_Out] is used for nodes that send messages to downstream nodes with ctx.send_message(T_Out).
|
||||
WorkflowContext[T_Out, T_W_Out] is used for nodes that also yield workflow
|
||||
output with ctx.yield_output(T_W_Out).
|
||||
WorkflowContext without type parameters is equivalent to WorkflowContext[Never, Never], meaning this node
|
||||
neither sends messages to downstream nodes nor yields workflow output.
|
||||
|
||||
2) Standalone async function decorated with @executor using the same signature.
|
||||
Simple steps can use this form; a terminal step can emit a
|
||||
WorkflowCompletedEvent to end the workflow.
|
||||
Simple steps can use this form; a terminal step can yield output
|
||||
using ctx.yield_output() to provide workflow results.
|
||||
|
||||
- Fluent WorkflowBuilder API:
|
||||
add_edge(A, B) to connect nodes, set_start_executor(A), then build() -> Workflow.
|
||||
|
||||
- Running and results:
|
||||
workflow.run(initial_input) executes the graph. The last node emits a
|
||||
WorkflowCompletedEvent that carries the final result.
|
||||
workflow.run(initial_input) executes the graph. Terminal nodes yield
|
||||
outputs using ctx.yield_output(). The workflow runs until idle.
|
||||
|
||||
Prerequisites
|
||||
- No external services required.
|
||||
@@ -43,8 +54,8 @@ Prerequisites
|
||||
#
|
||||
# Handler signature contract:
|
||||
# - First parameter is the typed input to this node (here: text: str)
|
||||
# - Second parameter is a WorkflowContext[T], where T is the type of data this
|
||||
# node will emit via ctx.send_message (here: T is str)
|
||||
# - Second parameter is a WorkflowContext[T_Out], where T_Out is the type of data this
|
||||
# node will emit via ctx.send_message (here: T_Out is str)
|
||||
#
|
||||
# Within a handler you typically:
|
||||
# - Compute a result
|
||||
@@ -70,22 +81,25 @@ class UpperCase(Executor):
|
||||
# -----------------------------------------------
|
||||
#
|
||||
# For simple steps you can skip subclassing and define an async function with the
|
||||
# same signature pattern (typed input + WorkflowContext[T]) and decorate it with
|
||||
# same signature pattern (typed input + WorkflowContext[T_Out, T_W_Out]) and decorate it with
|
||||
# @executor. This creates a fully functional node that can be wired into a flow.
|
||||
|
||||
|
||||
@executor(id="reverse_text_executor")
|
||||
async def reverse_text(text: str, ctx: WorkflowContext[str]) -> None:
|
||||
"""Reverse the input string and signal workflow completion.
|
||||
async def reverse_text(text: str, ctx: WorkflowContext[Never, str]) -> None:
|
||||
"""Reverse the input string and yield the workflow output.
|
||||
|
||||
This node emits a terminal event using ctx.add_event(WorkflowCompletedEvent).
|
||||
The data carried by the WorkflowCompletedEvent becomes the final result of
|
||||
the workflow (returned by workflow.run(...)).
|
||||
This node yields the final output using ctx.yield_output(result).
|
||||
The workflow will complete when it becomes idle (no more work to do).
|
||||
|
||||
The WorkflowContext is parameterized with two types:
|
||||
- T_Out = Never: this node does not send messages to downstream nodes.
|
||||
- T_W_Out = str: this node yields workflow output of type str.
|
||||
"""
|
||||
result = text[::-1]
|
||||
|
||||
# Send the result with a workflow completion event.
|
||||
await ctx.add_event(WorkflowCompletedEvent(result))
|
||||
# Yield the output - the workflow will complete when idle
|
||||
await ctx.yield_output(result)
|
||||
|
||||
|
||||
async def main():
|
||||
@@ -100,17 +114,17 @@ async def main():
|
||||
workflow = WorkflowBuilder().add_edge(upper_case, reverse_text).set_start_executor(upper_case).build()
|
||||
|
||||
# Run the workflow by sending the initial message to the start node.
|
||||
# The run(...) call returns an event collection; its get_completed_event()
|
||||
# provides the WorkflowCompletedEvent emitted by the terminal node.
|
||||
# The run(...) call returns an event collection; its get_outputs() method
|
||||
# retrieves the outputs yielded by any terminal nodes.
|
||||
events = await workflow.run("hello world")
|
||||
print(events.get_completed_event())
|
||||
print(events.get_outputs())
|
||||
# Summarize the final run state (e.g., COMPLETED)
|
||||
print("Final state:", events.get_final_state())
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
WorkflowCompletedEvent(data=DLROW OLLEH)
|
||||
['DLROW OLLEH']
|
||||
Final state: WorkflowRunState.COMPLETED
|
||||
"""
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ This sample uses two custom executors. A Writer agent creates or edits content,
|
||||
then hands the conversation to a Reviewer agent which evaluates and finalizes the result.
|
||||
|
||||
Purpose:
|
||||
Show how to wrap chat agents created by AzureChatClient inside workflow executors. Demonstrate the @handler pattern
|
||||
with typed inputs and typed WorkflowContext[T] outputs, connect executors with the fluent WorkflowBuilder, and finish
|
||||
by emitting a WorkflowCompletedEvent from the terminal node.
|
||||
Show how to wrap chat agents created by AzureChatClient inside workflow executors. Demonstrate how agents
|
||||
automatically yield outputs when they complete, removing the need for explicit completion events.
|
||||
The workflow completes when it becomes idle.
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI configured for AzureChatClient with required environment variables.
|
||||
@@ -51,14 +51,12 @@ async def main():
|
||||
# Run the workflow with the user's initial message.
|
||||
# For foundational clarity, use run (non streaming) and print the terminal event.
|
||||
events = await workflow.run("Create a slogan for a new electric SUV that is affordable and fun to drive.")
|
||||
# The terminal node emits a WorkflowCompletedEvent; print its contents.
|
||||
|
||||
# Print interim-agent run events
|
||||
# Print agent run events and final outputs
|
||||
for event in events:
|
||||
if isinstance(event, AgentRunEvent):
|
||||
print(f"{event.executor_id}: {event.data}")
|
||||
|
||||
print(f"{'=' * 60}\n{events.get_completed_event()}")
|
||||
print(f"{'=' * 60}\nWorkflow Outputs: {events.get_outputs()}")
|
||||
# Summarize the final run state (e.g., COMPLETED)
|
||||
print("Final state:", events.get_final_state())
|
||||
|
||||
@@ -74,14 +72,13 @@ async def main():
|
||||
- Consider specifying "SUV" for clarity in some uses.
|
||||
- Strong, upbeat tone suitable for marketing.
|
||||
============================================================
|
||||
Workflow Completed Event:
|
||||
WorkflowCompletedEvent(data=Slogan: "Plug Into Fun—Affordable Adventure, Electrified."
|
||||
Workflow Outputs: ['Slogan: "Plug Into Fun—Affordable Adventure, Electrified."
|
||||
|
||||
**Feedback:**s
|
||||
**Feedback:**
|
||||
- Clear focus on affordability and enjoyment.
|
||||
- "Plug into fun" connects emotionally and highlights electric nature.
|
||||
- Consider specifying "SUV" for clarity in some uses.
|
||||
- Strong, upbeat tone suitable for marketing.)
|
||||
- Strong, upbeat tone suitable for marketing.']
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -2,19 +2,21 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
from typing_extensions import Never
|
||||
|
||||
from agent_framework import (
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
Executor,
|
||||
ExecutorFailedEvent,
|
||||
WorkflowBuilder,
|
||||
WorkflowCompletedEvent,
|
||||
WorkflowContext,
|
||||
WorkflowFailedEvent,
|
||||
WorkflowRunState,
|
||||
WorkflowStatusEvent,
|
||||
handler,
|
||||
)
|
||||
from agent_framework._workflow._events import WorkflowOutputEvent
|
||||
from agent_framework.azure import AzureChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
@@ -28,9 +30,9 @@ The workflow is invoked with run_stream so you can observe events as they occur.
|
||||
Purpose:
|
||||
Show how to wrap chat agents created by AzureChatClient inside workflow executors, wire them with WorkflowBuilder,
|
||||
and consume streaming events from the workflow. Demonstrate the @handler pattern with typed inputs and typed
|
||||
WorkflowContext[T] outputs, and finish by emitting a WorkflowCompletedEvent from the terminal node while printing
|
||||
intermediate events for observability. The streaming loop also surfaces WorkflowEvent.origin so you can
|
||||
distinguish runner-generated lifecycle events from executor-generated data-plane events.
|
||||
WorkflowContext[T_Out, T_W_Out] outputs. Agents automatically yield outputs when they complete.
|
||||
The streaming loop also surfaces WorkflowEvent.origin so you can distinguish runner-generated lifecycle events
|
||||
from executor-generated data-plane events.
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI configured for AzureChatClient with required environment variables.
|
||||
@@ -96,14 +98,14 @@ class Reviewer(Executor):
|
||||
super().__init__(agent=agent, id=id)
|
||||
|
||||
@handler
|
||||
async def handle(self, messages: list[ChatMessage], ctx: WorkflowContext[str]) -> None:
|
||||
"""Review the full conversation transcript and complete with a final string.
|
||||
async def handle(self, messages: list[ChatMessage], ctx: WorkflowContext[Never, str]) -> None:
|
||||
"""Review the full conversation transcript and yield the final output.
|
||||
|
||||
This node consumes all messages so far. It uses its agent to produce the final text,
|
||||
then signals completion by adding a WorkflowCompletedEvent to the event stream.
|
||||
then yields the output. The workflow completes when it becomes idle.
|
||||
"""
|
||||
response = await self.agent.run(messages)
|
||||
await ctx.add_event(WorkflowCompletedEvent(response.text))
|
||||
await ctx.yield_output(response.text)
|
||||
|
||||
|
||||
async def main():
|
||||
@@ -119,7 +121,7 @@ async def main():
|
||||
workflow = WorkflowBuilder().set_start_executor(writer).add_edge(writer, reviewer).build()
|
||||
|
||||
# Run the workflow with the user's initial message and stream events as they occur.
|
||||
# In addition to executor events and WorkflowCompletedEvent, this also surfaces run-state and errors.
|
||||
# This surfaces executor events, workflow outputs, run-state changes, and errors.
|
||||
async for event in workflow.run_stream(
|
||||
ChatMessage(role="user", text="Create a slogan for a new electric SUV that is affordable and fun to drive.")
|
||||
):
|
||||
@@ -127,8 +129,6 @@ async def main():
|
||||
prefix = f"State ({event.origin.value}): "
|
||||
if event.state == WorkflowRunState.IN_PROGRESS:
|
||||
print(prefix + "IN_PROGRESS")
|
||||
elif event.state == WorkflowRunState.COMPLETED:
|
||||
print(prefix + "COMPLETED")
|
||||
elif event.state == WorkflowRunState.IN_PROGRESS_PENDING_REQUESTS:
|
||||
print(prefix + "IN_PROGRESS_PENDING_REQUESTS (requests in flight)")
|
||||
elif event.state == WorkflowRunState.IDLE:
|
||||
@@ -137,8 +137,8 @@ async def main():
|
||||
print(prefix + "IDLE_WITH_PENDING_REQUESTS (prompt user or UI now)")
|
||||
else:
|
||||
print(prefix + str(event.state))
|
||||
elif isinstance(event, WorkflowCompletedEvent):
|
||||
print(f"Workflow completed ({event.origin.value}): {event.data}")
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
print(f"Workflow output ({event.origin.value}): {event.data}")
|
||||
elif isinstance(event, ExecutorFailedEvent):
|
||||
print(
|
||||
f"Executor failed ({event.origin.value}): "
|
||||
@@ -157,9 +157,9 @@ async def main():
|
||||
ExecutorInvokeEvent (RUNNER): ExecutorInvokeEvent(executor_id=writer)
|
||||
ExecutorCompletedEvent (RUNNER): ExecutorCompletedEvent(executor_id=writer)
|
||||
ExecutorInvokeEvent (RUNNER): ExecutorInvokeEvent(executor_id=reviewer)
|
||||
Workflow completed (EXECUTOR): Drive the Future. Affordable Adventure, Electrified.
|
||||
Workflow output (EXECUTOR): Drive the Future. Affordable Adventure, Electrified.
|
||||
ExecutorCompletedEvent (RUNNER): ExecutorCompletedEvent(executor_id=reviewer)
|
||||
State (RUNNER): COMPLETED
|
||||
State (RUNNER): IDLE
|
||||
"""
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user