mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
84b721ee40
commit
3577508a20
+248
@@ -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())
|
||||
Reference in New Issue
Block a user