mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
[BREAKING] Python: Fix workflow as agent streaming output (#3649)
* WIP: with_output_from * Add with_output_from to other modules; next: workflow as agent * WIP: remove agent run events * orchestrations * WIP: update samples; next start at guessing_game_With_human_input.py * Update all samples * WIP: consolidate workflow as agent streaming vs non-streaming * Consolidate workflow as agent streaming vs non-streaming * Move request info event processing to a share method * Final pass on the samples * Fix mypy * Fix mypy * Comments --------- Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
907654a489
commit
a971d24f1e
+20
-28
@@ -1,26 +1,26 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import cast
|
||||
|
||||
from agent_framework import AgentRunEvent, WorkflowBuilder
|
||||
from agent_framework import AgentResponse, WorkflowBuilder
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""
|
||||
Step 2: Agents in a Workflow non-streaming
|
||||
|
||||
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.
|
||||
This sample creates two agents: a Writer agent creates or edits content, and a Reviewer agent which
|
||||
evaluates and provides feedback.
|
||||
|
||||
Purpose:
|
||||
Show how to wrap chat agents created by AzureOpenAIChatClient 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.
|
||||
Show how to create agents from AzureOpenAIChatClient and use them directly in a workflow. Demonstrate
|
||||
how agents can be used in a workflow.
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables.
|
||||
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
|
||||
- Basic familiarity with WorkflowBuilder, executors, edges, events, and streaming or non streaming runs.
|
||||
- Basic familiarity with WorkflowBuilder, edges, events, and streaming or non-streaming runs.
|
||||
"""
|
||||
|
||||
|
||||
@@ -51,34 +51,26 @@ 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.")
|
||||
# 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}\nWorkflow Outputs: {events.get_outputs()}")
|
||||
outputs = events.get_outputs()
|
||||
# The outputs of the workflow are whatever the agents produce. So the outputs are expected to be a list
|
||||
# of `AgentResponse` from the agents in the workflow.
|
||||
outputs = cast(list[AgentResponse], outputs)
|
||||
for output in outputs:
|
||||
# TODO: author_name should be available in AgentResponse
|
||||
print(f"{output.messages[0].author_name}: {output.text}\n")
|
||||
|
||||
# Summarize the final run state (e.g., COMPLETED)
|
||||
print("Final state:", events.get_final_state())
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
writer: "Charge Ahead: Affordable Adventure Awaits!"
|
||||
|
||||
writer: "Charge Up Your Adventure—Affordable Fun, Electrified!"
|
||||
reviewer: Slogan: "Plug Into Fun—Affordable Adventure, Electrified."
|
||||
reviewer: - Consider emphasizing both affordability and fun in a more dynamic way.
|
||||
- Try using a catchy phrase that includes a play on words, like “Electrify Your Drive: Fun Meets Affordability!”
|
||||
- Ensure the slogan is succinct while capturing the essence of the car's unique selling proposition.
|
||||
|
||||
**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.
|
||||
============================================================
|
||||
Workflow Outputs: ['Slogan: "Plug Into Fun—Affordable Adventure, Electrified."
|
||||
|
||||
**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.']
|
||||
Final state: WorkflowRunState.IDLE
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -2,36 +2,20 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import (
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
Executor,
|
||||
ExecutorFailedEvent,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowFailedEvent,
|
||||
WorkflowRunState,
|
||||
WorkflowStatusEvent,
|
||||
handler,
|
||||
)
|
||||
from agent_framework import AgentResponseUpdate, ChatMessage, WorkflowBuilder
|
||||
from agent_framework._workflows._events import WorkflowOutputEvent
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from typing_extensions import Never
|
||||
|
||||
"""
|
||||
Step 3: Agents in a workflow with streaming
|
||||
|
||||
A Writer agent generates content,
|
||||
then passes the conversation to a Reviewer agent that finalizes the result.
|
||||
The workflow is invoked with run_stream so you can observe events as they occur.
|
||||
This sample creates two agents: a Writer agent creates or edits content, and a Reviewer agent which
|
||||
evaluates and provides feedback.
|
||||
|
||||
Purpose:
|
||||
Show how to wrap chat agents created by AzureOpenAIChatClient 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_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.
|
||||
Show how to create agents from AzureOpenAIChatClient and use them directly in a workflow. Demonstrate
|
||||
how agents can be used in a workflow.
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables.
|
||||
@@ -40,125 +24,59 @@ Prerequisites:
|
||||
"""
|
||||
|
||||
|
||||
class Writer(Executor):
|
||||
"""Custom executor that owns a domain specific agent for content generation.
|
||||
|
||||
This class demonstrates:
|
||||
- Attaching a ChatAgent to an Executor so it participates as a node in a workflow.
|
||||
- Using a @handler method to accept a typed input and forward a typed output via ctx.send_message.
|
||||
"""
|
||||
|
||||
agent: ChatAgent
|
||||
|
||||
def __init__(self, chat_client: AzureOpenAIChatClient, id: str = "writer"):
|
||||
# Create a domain specific agent using your configured AzureOpenAIChatClient.
|
||||
self.agent = chat_client.as_agent(
|
||||
instructions=(
|
||||
"You are an excellent content writer. You create new content and edit contents based on the feedback."
|
||||
),
|
||||
)
|
||||
# Associate this agent with the executor node. The base Executor stores it on self.agent.
|
||||
super().__init__(id=id)
|
||||
|
||||
@handler
|
||||
async def handle(self, message: ChatMessage, ctx: WorkflowContext[list[ChatMessage]]) -> None:
|
||||
"""Generate content and forward the updated conversation.
|
||||
|
||||
Contract for this handler:
|
||||
- message is the inbound user ChatMessage.
|
||||
- ctx is a WorkflowContext that expects a list[ChatMessage] to be sent downstream.
|
||||
|
||||
Pattern shown here:
|
||||
1) Seed the conversation with the inbound message.
|
||||
2) Run the attached agent to produce assistant messages.
|
||||
3) Forward the cumulative messages to the next executor with ctx.send_message.
|
||||
"""
|
||||
# Start the conversation with the incoming user message.
|
||||
messages: list[ChatMessage] = [message]
|
||||
# Run the agent and extend the conversation with the agent's messages.
|
||||
response = await self.agent.run(messages)
|
||||
messages.extend(response.messages)
|
||||
# Forward the accumulated messages to the next executor in the workflow.
|
||||
await ctx.send_message(messages)
|
||||
|
||||
|
||||
class Reviewer(Executor):
|
||||
"""Custom executor that owns a review agent and completes the workflow."""
|
||||
|
||||
agent: ChatAgent
|
||||
|
||||
def __init__(self, chat_client: AzureOpenAIChatClient, id: str = "reviewer"):
|
||||
# Create a domain specific agent that evaluates and refines content.
|
||||
self.agent = chat_client.as_agent(
|
||||
instructions=(
|
||||
"You are an excellent content reviewer. You review the content and provide feedback to the writer."
|
||||
),
|
||||
)
|
||||
super().__init__(id=id)
|
||||
|
||||
@handler
|
||||
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 yields the output. The workflow completes when it becomes idle.
|
||||
"""
|
||||
response = await self.agent.run(messages)
|
||||
await ctx.yield_output(response.text)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Build the two node workflow and run it with streaming to observe events."""
|
||||
# Create the Azure chat client. AzureCliCredential uses your current az login.
|
||||
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
|
||||
# Instantiate the two agent backed executors.
|
||||
writer = Writer(chat_client)
|
||||
reviewer = Reviewer(chat_client)
|
||||
writer_agent = chat_client.as_agent(
|
||||
instructions=(
|
||||
"You are an excellent content writer. You create new content and edit contents based on the feedback."
|
||||
),
|
||||
name="writer",
|
||||
)
|
||||
|
||||
reviewer_agent = chat_client.as_agent(
|
||||
instructions=(
|
||||
"You are an excellent content reviewer."
|
||||
"Provide actionable feedback to the writer about the provided content."
|
||||
"Provide the feedback in the most concise manner possible."
|
||||
),
|
||||
name="reviewer",
|
||||
)
|
||||
|
||||
# Build the workflow using the fluent builder.
|
||||
# Set the start node and connect an edge from writer to reviewer.
|
||||
workflow = WorkflowBuilder().set_start_executor(writer).add_edge(writer, reviewer).build()
|
||||
workflow = WorkflowBuilder().set_start_executor(writer_agent).add_edge(writer_agent, reviewer_agent).build()
|
||||
|
||||
# Track the last author to format streaming output.
|
||||
last_author: str | None = None
|
||||
|
||||
# Run the workflow with the user's initial message and stream events as they occur.
|
||||
# This surfaces executor events, workflow outputs, run-state changes, and errors.
|
||||
async for event in workflow.run_stream(
|
||||
ChatMessage("user", ["Create a slogan for a new electric SUV that is affordable and fun to drive."])
|
||||
):
|
||||
if isinstance(event, WorkflowStatusEvent):
|
||||
prefix = f"State ({event.origin.value}): "
|
||||
if event.state == WorkflowRunState.IN_PROGRESS:
|
||||
print(prefix + "IN_PROGRESS")
|
||||
elif event.state == WorkflowRunState.IN_PROGRESS_PENDING_REQUESTS:
|
||||
print(prefix + "IN_PROGRESS_PENDING_REQUESTS (requests in flight)")
|
||||
elif event.state == WorkflowRunState.IDLE:
|
||||
print(prefix + "IDLE (no active work)")
|
||||
elif event.state == WorkflowRunState.IDLE_WITH_PENDING_REQUESTS:
|
||||
print(prefix + "IDLE_WITH_PENDING_REQUESTS (prompt user or UI now)")
|
||||
# The outputs of the workflow are whatever the agents produce. So the events are expected to
|
||||
# contain `AgentResponseUpdate` from the agents in the workflow.
|
||||
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
|
||||
update = event.data
|
||||
author = update.author_name
|
||||
if author != last_author:
|
||||
if last_author is not None:
|
||||
print() # Newline between different authors
|
||||
print(f"{author}: {update.text}", end="", flush=True)
|
||||
last_author = author
|
||||
else:
|
||||
print(prefix + str(event.state))
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
print(f"Workflow output ({event.origin.value}): {event.data}")
|
||||
elif isinstance(event, ExecutorFailedEvent):
|
||||
print(
|
||||
f"Executor failed ({event.origin.value}): "
|
||||
f"{event.executor_id} {event.details.error_type}: {event.details.message}"
|
||||
)
|
||||
elif isinstance(event, WorkflowFailedEvent):
|
||||
details = event.details
|
||||
print(f"Workflow failed ({event.origin.value}): {details.error_type}: {details.message}")
|
||||
else:
|
||||
print(f"{event.__class__.__name__} ({event.origin.value}): {event}")
|
||||
print(update.text, end="", flush=True)
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
writer: "Electrify Your Journey: Affordable Fun Awaits!"
|
||||
reviewer: Feedback:
|
||||
|
||||
State (RUNNER): IN_PROGRESS
|
||||
ExecutorInvokeEvent (RUNNER): ExecutorInvokeEvent(executor_id=writer)
|
||||
ExecutorCompletedEvent (RUNNER): ExecutorCompletedEvent(executor_id=writer)
|
||||
ExecutorInvokeEvent (RUNNER): ExecutorInvokeEvent(executor_id=reviewer)
|
||||
Workflow output (EXECUTOR): Drive the Future. Affordable Adventure, Electrified.
|
||||
ExecutorCompletedEvent (RUNNER): ExecutorCompletedEvent(executor_id=reviewer)
|
||||
State (RUNNER): IDLE
|
||||
1. **Clarity**: Consider simplifying the message. "Affordable Fun" could be more direct.
|
||||
2. **Emotional Appeal**: Emphasize the thrill of driving more. Try using words that evoke excitement.
|
||||
3. **Unique Selling Proposition**: Highlight the electric aspect more boldly.
|
||||
|
||||
Example revision: "Charge Your Adventure: Affordable SUVs for Fun-Loving Drivers!"
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import asyncio
|
||||
|
||||
from agent_framework import (
|
||||
AgentResponse,
|
||||
AgentResponseUpdate,
|
||||
ChatAgent,
|
||||
Executor,
|
||||
WorkflowBuilder,
|
||||
@@ -77,26 +77,28 @@ async def main():
|
||||
WorkflowBuilder()
|
||||
.register_executor(lambda: UpperCase(id="upper_case_executor"), name="UpperCase")
|
||||
.register_executor(lambda: reverse_text, name="ReverseText")
|
||||
.register_agent(create_agent, name="DecoderAgent", output_response=True)
|
||||
.register_agent(create_agent, name="DecoderAgent")
|
||||
.add_chain(["UpperCase", "ReverseText", "DecoderAgent"])
|
||||
.set_start_executor("UpperCase")
|
||||
.build()
|
||||
)
|
||||
|
||||
output: AgentResponse | None = None
|
||||
first_update = True
|
||||
async for event in workflow.run_stream("hello world"):
|
||||
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponse):
|
||||
output = event.data
|
||||
|
||||
if output:
|
||||
print(f"Decoded output: {output.text}")
|
||||
else:
|
||||
print("No output received.")
|
||||
# The outputs of the workflow are whatever the agents produce. So the events are expected to
|
||||
# contain `AgentResponseUpdate` from the agents in the workflow.
|
||||
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
|
||||
update = event.data
|
||||
if first_update:
|
||||
print(f"{update.author_name}: {update.text}", end="", flush=True)
|
||||
first_update = False
|
||||
else:
|
||||
print(update.text, end="", flush=True)
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
HELLO WORLD
|
||||
decoder: HELLO WORLD
|
||||
"""
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user