Python: Introduce WorkflowAgent (#424)

* start a new implementation based on .net

* add response handling

* update init files

* remove handling of WorkflowCompletedEvent

* clean up implemenation

* fix bug

* update tests for merge_updates

* WorkflowAgent validation

* add a sample and fix bug

* revert pre-commit config

* revert pre-commit

* add human in the loop sample

* add comment

* fix type issue in Executor

* fix type errors and rename Executor.type to Executor.type_ with field alias

* fix test

---------

Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
This commit is contained in:
Eric Zhu
2025-09-02 06:25:03 -10:00
committed by GitHub
Unverified
parent 84b721ee40
commit 3577508a20
13 changed files with 1313 additions and 21 deletions
@@ -0,0 +1,248 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from dataclasses import dataclass
from uuid import uuid4
from agent_framework import AgentRunResponseUpdate, AIContents, ChatClient, ChatMessage, ChatRole
from agent_framework.openai import OpenAIChatClient
from agent_framework.workflow import AgentRunUpdateEvent, Executor, WorkflowBuilder, WorkflowContext, handler
from pydantic import BaseModel
"""
The following sample demonstrates how to wrap a workflow as an agent using WorkflowAgent.
This sample shows how to:
1. Create a workflow with a reflection pattern (Worker + Reviewer executors)
2. Wrap the workflow as an agent using the .as_agent() method
3. Stream responses from the workflow agent like a regular agent
4. Implement a review-retry mechanism where responses are iteratively improved
The example implements a quality-controlled AI assistant where:
- Worker executor generates responses to user queries
- Reviewer executor evaluates the responses and provides feedback
- If not approved, the Worker incorporates feedback and regenerates the response
- The cycle continues until the response is approved
- Only approved responses are emitted to the external consumer
Key concepts demonstrated:
- WorkflowAgent: Wraps a workflow to make it behave as an agent
- Bidirectional workflow with cycles (Worker ↔ Reviewer)
- AgentRunUpdateEvent: How workflows communicate with external consumers
- Structured output parsing for review feedback
- State management with pending requests tracking
"""
@dataclass
class ReviewRequest:
request_id: str
user_messages: list[ChatMessage]
agent_messages: list[ChatMessage]
@dataclass
class ReviewResponse:
request_id: str
feedback: str
approved: bool
class Reviewer(Executor):
"""An executor that reviews messages and provides feedback."""
def __init__(self, chat_client: ChatClient) -> None:
super().__init__()
self._chat_client = chat_client
@handler
async def review(self, request: ReviewRequest, ctx: WorkflowContext[ReviewResponse]) -> None:
print(f"🔍 Reviewer: Evaluating response for request {request.request_id[:8]}...")
# Use the chat client to review the message and use structured output.
# NOTE: this can be modified to use an evaluation framework.
class _Response(BaseModel):
feedback: str
approved: bool
# Define the system prompt.
messages = [
ChatMessage(
role=ChatRole.SYSTEM,
text="You are a reviewer for an AI agent, please provide feedback on the "
"following exchange between a user and the AI agent, "
"and indicate if the agent's responses are approved or not.\n"
"Use the following criteria for your evaluation:\n"
"- Relevance: Does the response address the user's query?\n"
"- Accuracy: Is the information provided correct?\n"
"- Clarity: Is the response easy to understand?\n"
"- Completeness: Does the response cover all aspects of the query?\n"
"Be critical in your evaluation and provide constructive feedback.\n"
"Do not approve until all criteria are met.",
)
]
# Add user and agent messages to the chat history.
messages.extend(request.user_messages)
# Add agent messages to the chat history.
messages.extend(request.agent_messages)
# Add add one more instruction for the assistant to follow.
messages.append(
ChatMessage(role=ChatRole.USER, text="Please provide a review of the agent's responses to the user.")
)
print("🔍 Reviewer: Sending review request to LLM...")
# Get the response from the chat client.
response = await self._chat_client.get_response(messages=messages, response_format=_Response)
# Parse the response.
parsed = _Response.model_validate_json(response.messages[-1].text)
print(f"🔍 Reviewer: Review complete - Approved: {parsed.approved}")
print(f"🔍 Reviewer: Feedback: {parsed.feedback}")
# Send the review response.
await ctx.send_message(
ReviewResponse(request_id=request.request_id, feedback=parsed.feedback, approved=parsed.approved)
)
class Worker(Executor):
"""An executor that performs tasks for the user."""
def __init__(self, chat_client: ChatClient) -> None:
super().__init__()
self._chat_client = chat_client
self._pending_requests: dict[str, tuple[ReviewRequest, list[ChatMessage]]] = {}
@handler
async def handle_user_messages(self, user_messages: list[ChatMessage], ctx: WorkflowContext[ReviewRequest]) -> None:
print("🔧 Worker: Received user messages, generating response...")
# Handle user messages and prepare a review request for the reviewer.
# Define the system prompt.
messages = [ChatMessage(role=ChatRole.SYSTEM, text="You are a helpful assistant.")]
# Add user messages.
messages.extend(user_messages)
print("🔧 Worker: Calling LLM to generate response...")
# Get the response from the chat client.
response = await self._chat_client.get_response(messages=messages)
print(f"🔧 Worker: Response generated: {response.messages[-1].text}")
# Add agent messages.
messages.extend(response.messages)
# Create the review request.
request = ReviewRequest(request_id=str(uuid4()), user_messages=user_messages, agent_messages=response.messages)
print(f"🔧 Worker: Generated response, sending to reviewer (ID: {request.request_id[:8]})")
# Send the review request.
await ctx.send_message(request)
# Add to pending requests.
self._pending_requests[request.request_id] = (request, messages)
@handler
async def handle_review_response(self, review: ReviewResponse, ctx: WorkflowContext[ReviewRequest]) -> None:
print(f"🔧 Worker: Received review for request {review.request_id[:8]} - Approved: {review.approved}")
# Handle the review response. Depending on the approval status,
# either emit the approved response as AgentRunUpdateEvent, or
# retry given the feedback.
if review.request_id not in self._pending_requests:
raise ValueError(f"Received review response for unknown request ID: {review.request_id}")
# Remove the request from pending requests.
request, messages = self._pending_requests.pop(review.request_id)
if review.approved:
print("✅ Worker: Response approved! Emitting to external consumer...")
# If approved, emit the agent run response update to the workflow's
# external consumer.
contents: list[AIContents] = []
for message in request.agent_messages:
contents.extend(message.contents)
# Emitting an AgentRunUpdateEvent in a workflow wrapped by a WorkflowAgent
# will send the AgentRunResponseUpdate to the WorkflowAgent's
# event stream.
await ctx.add_event(
AgentRunUpdateEvent(self.id, data=AgentRunResponseUpdate(contents=contents, role=ChatRole.ASSISTANT))
)
return
print(f"❌ Worker: Response not approved. Feedback: {review.feedback}")
print("🔧 Worker: Incorporating feedback and regenerating response...")
# Construct new messages with feedback.
messages.append(ChatMessage(role=ChatRole.SYSTEM, text=review.feedback))
# Add additional instruction to address the feedback.
messages.append(
ChatMessage(
role=ChatRole.SYSTEM,
text="Please incorporate the feedback above, and provide a response to user's next message.",
)
)
messages.extend(request.user_messages)
# Get the new response from the chat client.
response = await self._chat_client.get_response(messages=messages)
print(f"🔧 Worker: New response generated after feedback: {response.messages[-1].text}")
# Process the response.
messages.extend(response.messages)
print(f"🔧 Worker: Generated improved response, sending for re-review (ID: {review.request_id[:8]})")
# Send an updated review request.
new_request = ReviewRequest(
request_id=review.request_id, user_messages=request.user_messages, agent_messages=response.messages
)
await ctx.send_message(new_request)
# Add to pending requests.
self._pending_requests[new_request.request_id] = (new_request, messages)
async def main() -> None:
print("🚀 Starting Workflow Agent Demo")
print("=" * 50)
# Create executors.
print("📝 Creating chat client and executors...")
mini_chat_client = OpenAIChatClient(ai_model_id="gpt-4.1-nano")
chat_client = OpenAIChatClient(ai_model_id="gpt-4.1")
reviewer = Reviewer(chat_client=chat_client)
worker = Worker(chat_client=mini_chat_client)
print("🏗️ Building workflow with Worker ↔ Reviewer cycle...")
# Create the workflow agent with an underlying reflection workflow.
agent = (
WorkflowBuilder()
.add_edge(worker, reviewer) # <--- This edge allows the worker to send requests to the reviewer
.add_edge(reviewer, worker) # <--- This edge allows the reviewer to send feedback back to the worker
.set_start_executor(worker)
.build()
.as_agent() # Convert the workflow to an agent.
)
print("🎯 Running workflow agent with user query...")
print("Query: 'Write code for parallel reading 1 million files on disk and write to a sorted output file.'")
print("-" * 50)
# Run the agent and stream events.
async for event in agent.run_streaming(
"Write code for parallel reading 1 million files on disk and write to a sorted output file."
):
print(f"📤 Agent Response: {event}")
print("=" * 50)
print("✅ Workflow completed!")
if __name__ == "__main__":
print("🎬 Initializing Workflow as Agent Sample...")
asyncio.run(main())
@@ -0,0 +1,145 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from dataclasses import dataclass
from agent_framework import (
ChatMessage,
ChatRole,
FunctionCallContent,
FunctionResultContent,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.workflow import (
Executor,
RequestInfoExecutor,
RequestInfoMessage,
RequestResponse,
WorkflowAgent,
WorkflowBuilder,
WorkflowContext,
handler,
)
from step_10a_workflow_agent_reflection_pattern import ReviewRequest, ReviewResponse, Worker
@dataclass
class HumanReviewRequest(RequestInfoMessage):
agent_request: ReviewRequest | None = None
class ReviewerWithHumanInTheLoop(Executor):
"""An executor that raises to human manager for review when not confident."""
def __init__(self, worker_id: str, request_info_id: str) -> None:
super().__init__()
self._worker_id = worker_id
self._request_info_id = request_info_id
@handler
async def review(self, request: ReviewRequest, ctx: WorkflowContext[ReviewResponse | HumanReviewRequest]) -> None:
print(f"🔍 Reviewer: Evaluating response for request {request.request_id[:8]}...")
# NOTE: for simplicity, we always escalate to human manager.
# See step_10a_workflow_agent_reflection_pattern.py for implementation
# using an chat client.
print("🔍 Reviewer: Escalate to human manager")
# Send to human manager
await ctx.send_message(
HumanReviewRequest(agent_request=request),
target_id=self._request_info_id,
)
@handler
async def accept_human_review(
self, response: RequestResponse[HumanReviewRequest, ReviewResponse], ctx: WorkflowContext[ReviewResponse]
) -> None:
human_response = response.data
assert isinstance(human_response, ReviewResponse)
print(f"🔍 Reviewer: Accepting human review for request {human_response.request_id[:8]}...")
print(f"🔍 Reviewer: Human feedback: {human_response.feedback}")
print(f"🔍 Reviewer: Human approved: {human_response.approved}")
print("🔍 Reviewer: Forwarding human review back to worker...")
await ctx.send_message(human_response, target_id=self._worker_id)
async def main() -> None:
print("🚀 Starting Workflow Agent with Human-in-the-Loop Demo")
print("=" * 50)
# Create executors.
print("📝 Creating chat client and executors...")
mini_chat_client = OpenAIChatClient(ai_model_id="gpt-4.1-nano")
worker = Worker(chat_client=mini_chat_client)
request_info_executor = RequestInfoExecutor()
reviewer = ReviewerWithHumanInTheLoop(worker_id=worker.id, request_info_id=request_info_executor.id)
print("🏗️ Building workflow with Worker ↔ Reviewer cycle...")
# Create the workflow agent with an underlying reflection workflow.
agent = (
WorkflowBuilder()
.add_edge(worker, reviewer) # <--- This edge allows the worker to send requests to the reviewer
.add_edge(reviewer, worker) # <--- This edge allows the reviewer to send feedback back to the worker
.add_edge(
reviewer, request_info_executor
) # <--- This edge allows the reviewer to send human input requests through the request info executor
.add_edge(
request_info_executor, reviewer
) # <--- This edge allows the human input to be forwarded back to the reviewer
.set_start_executor(worker)
.build()
.as_agent() # Convert the workflow to an agent.
)
print("🎯 Running workflow agent with user query...")
print("Query: 'Write code for parallel reading 1 million files on disk and write to a sorted output file.'")
print("-" * 50)
# NOTE: you can also run the workflow directly, i.e., without the as_agent().
# Then, you will need to handle RequestInfoEvent and send response to the workflow
# using send_response().
# Run the agent.
response = await agent.run(
"Write code for parallel reading 1 million Files on disk and write to a sorted output file."
)
#
# Find human review function call.
# TODO(ekzhu): update this to FunctionApprovalRequestContent
# monitor: https://github.com/microsoft/agent-framework/issues/285
human_review_function_call: FunctionCallContent | None = None
for message in response.messages:
for content in message.contents:
if isinstance(content, FunctionCallContent) and content.name == WorkflowAgent.REQUEST_INFO_FUNCTION_NAME:
human_review_function_call = content
# Handle human review if needed.
if human_review_function_call:
# Use WorkflowAgent.RequestInfoFunctionArgs to parse the request.
if isinstance(human_review_function_call.arguments, str):
request = WorkflowAgent.RequestInfoFunctionArgs.model_validate_json(human_review_function_call.arguments)
else:
request = WorkflowAgent.RequestInfoFunctionArgs.model_validate(human_review_function_call.arguments)
# Mock a human approval.
human_response = ReviewResponse(
request_id=request.data["agent_request"]["request_id"], feedback="Approved", approved=True
)
# Create the function call result to be sent back.
# TODO(ekzhu): update this to FunctionApprovalResponseContent
# monitor: https://github.com/microsoft/agent-framework/issues/285
human_review_function_result = FunctionResultContent(
call_id=human_review_function_call.call_id,
result=human_response,
)
# Send the human review result back to the agent.
response = await agent.run(ChatMessage(role=ChatRole.TOOL, contents=[human_review_function_result]))
print(f"📤 Agent Response: {response.messages[-1].text}")
print("=" * 50)
print("✅ Workflow completed!")
if __name__ == "__main__":
print("🎬 Initializing Workflow as Agent Sample...")
asyncio.run(main())