mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: [BREAKING] Python: Rename workflow to workflows (#1007)
* Rename workflow to workflows * Update occurence of workflow to new name
This commit is contained in:
committed by
GitHub
Unverified
parent
189434dd4b
commit
b42bb700fb
@@ -0,0 +1,92 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from contextlib import AsyncExitStack
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import AgentRunUpdateEvent, WorkflowBuilder, WorkflowOutputEvent
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
|
||||
"""
|
||||
Sample: Agents in a workflow with streaming
|
||||
|
||||
A Writer agent generates content, then a Reviewer agent critiques it.
|
||||
The workflow uses streaming so you can observe incremental AgentRunUpdateEvent chunks as each agent produces tokens.
|
||||
|
||||
Purpose:
|
||||
Show how to wire chat agents directly into a WorkflowBuilder pipeline where agents are auto wrapped as executors.
|
||||
|
||||
Demonstrate:
|
||||
- Automatic streaming of agent deltas via AgentRunUpdateEvent.
|
||||
- A simple console aggregator that groups updates by executor id and prints them as they arrive.
|
||||
- The workflow completes when idle and outputs are available in events.get_outputs().
|
||||
|
||||
Prerequisites:
|
||||
- Azure AI Agent Service configured, along with the required environment variables.
|
||||
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
|
||||
- Basic familiarity with WorkflowBuilder, edges, events, and streaming runs.
|
||||
"""
|
||||
|
||||
|
||||
async def create_azure_ai_agent() -> tuple[Callable[..., Awaitable[Any]], Callable[[], Awaitable[None]]]:
|
||||
"""Helper method to create a Azure AI agent factory and a close function.
|
||||
|
||||
This makes sure the async context managers are properly handled.
|
||||
"""
|
||||
stack = AsyncExitStack()
|
||||
cred = await stack.enter_async_context(AzureCliCredential())
|
||||
|
||||
client = await stack.enter_async_context(AzureAIAgentClient(async_credential=cred))
|
||||
|
||||
async def agent(**kwargs: Any) -> Any:
|
||||
return await stack.enter_async_context(client.create_agent(**kwargs))
|
||||
|
||||
async def close() -> None:
|
||||
await stack.aclose()
|
||||
|
||||
return agent, close
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
agent, close = await create_azure_ai_agent()
|
||||
try:
|
||||
writer = await agent(
|
||||
name="Writer",
|
||||
instructions=(
|
||||
"You are an excellent content writer. You create new content and edit contents based on the feedback."
|
||||
),
|
||||
)
|
||||
reviewer = await agent(
|
||||
name="Reviewer",
|
||||
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."
|
||||
),
|
||||
)
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(writer).add_edge(writer, reviewer).build()
|
||||
|
||||
last_executor_id: str | None = None
|
||||
|
||||
events = workflow.run_stream("Create a slogan for a new electric SUV that is affordable and fun to drive.")
|
||||
async for event in events:
|
||||
if isinstance(event, AgentRunUpdateEvent):
|
||||
eid = event.executor_id
|
||||
if eid != last_executor_id:
|
||||
if last_executor_id is not None:
|
||||
print()
|
||||
print(f"{eid}:", end=" ", flush=True)
|
||||
last_executor_id = eid
|
||||
print(event.data, end="", flush=True)
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
print("\n===== Final output =====")
|
||||
print(event.data)
|
||||
finally:
|
||||
await close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,88 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import AgentRunUpdateEvent, WorkflowBuilder, WorkflowOutputEvent
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""
|
||||
Sample: Agents in a workflow with streaming
|
||||
|
||||
A Writer agent generates content, then a Reviewer agent critiques it.
|
||||
The workflow uses streaming so you can observe incremental AgentRunUpdateEvent chunks as each agent produces tokens.
|
||||
|
||||
Purpose:
|
||||
Show how to wire chat agents directly into a WorkflowBuilder pipeline where agents are auto wrapped as executors.
|
||||
|
||||
Demonstrate:
|
||||
- Automatic streaming of agent deltas via AgentRunUpdateEvent.
|
||||
- A simple console aggregator that groups updates by executor id and prints them as they arrive.
|
||||
- The workflow completes when idle and outputs are available in events.get_outputs().
|
||||
|
||||
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, edges, events, and streaming runs.
|
||||
"""
|
||||
|
||||
|
||||
async def main():
|
||||
"""Build and run a simple two node agent workflow: Writer then Reviewer."""
|
||||
# Create the Azure chat client. AzureCliCredential uses your current az login.
|
||||
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
|
||||
|
||||
# Define two domain specific chat agents. The builder will wrap these as executors.
|
||||
writer_agent = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You are an excellent content writer. You create new content and edit contents based on the feedback."
|
||||
),
|
||||
name="writer_agent",
|
||||
)
|
||||
|
||||
reviewer_agent = chat_client.create_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_agent",
|
||||
)
|
||||
|
||||
# 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_agent).add_edge(writer_agent, reviewer_agent).build()
|
||||
|
||||
# Stream events from the workflow. We aggregate partial token updates per executor for readable output.
|
||||
last_executor_id = None
|
||||
|
||||
events = workflow.run_stream("Create a slogan for a new electric SUV that is affordable and fun to drive.")
|
||||
async for event in events:
|
||||
if isinstance(event, AgentRunUpdateEvent):
|
||||
# AgentRunUpdateEvent contains incremental text deltas from the underlying agent.
|
||||
# Print a prefix when the executor changes, then append updates on the same line.
|
||||
eid = event.executor_id
|
||||
if eid != last_executor_id: # type: ignore[reportUnnecessaryComparison]
|
||||
if last_executor_id is not None:
|
||||
print()
|
||||
print(f"{eid}:", end=" ", flush=True)
|
||||
last_executor_id = eid
|
||||
print(event.data, end="", flush=True)
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
print("===== Final Output =====")
|
||||
print(event.data)
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
writer_agent: Charge Up Your Journey. Fun, Affordable, Electric.
|
||||
reviewer_agent: Clear message, but consider highlighting SUV specific benefits (space, versatility) for stronger
|
||||
impact. Try more vivid language to evoke excitement. Example: "Big on Space. Big on Fun. Electric for Everyone."
|
||||
===== Final Output =====
|
||||
Clear message, but consider highlighting SUV specific benefits (space, versatility) for stronger impact. Try more
|
||||
vivid language to evoke excitement. Example: "Big on Space. Big on Fun. Electric for Everyone."
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,131 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import (
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
Executor,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
handler,
|
||||
)
|
||||
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.
|
||||
|
||||
Purpose:
|
||||
Show how to wrap chat agents created by AzureOpenAIChatClient inside workflow executors. Demonstrate the @handler pattern
|
||||
with typed inputs and typed WorkflowContext[T] outputs, connect executors with the fluent WorkflowBuilder, and finish
|
||||
by yielding outputs from the terminal node.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
class Writer(Executor):
|
||||
"""Custom executor that owns a domain specific agent responsible for generating content.
|
||||
|
||||
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.create_agent(
|
||||
instructions=(
|
||||
"You are an excellent content writer. You create new content and edit contents based on the feedback."
|
||||
),
|
||||
)
|
||||
# Associate the agent with this 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], str]) -> None:
|
||||
"""Generate content using the agent 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.
|
||||
|
||||
This class demonstrates:
|
||||
- Consuming a typed payload produced upstream.
|
||||
- Yielding the final text outcome to complete 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.create_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[list[ChatMessage], str]) -> None:
|
||||
"""Review the full conversation transcript and complete with a final string.
|
||||
|
||||
This node consumes all messages so far. It uses its agent to produce the final text,
|
||||
then signals completion by yielding the output.
|
||||
"""
|
||||
response = await self.agent.run(messages)
|
||||
await ctx.yield_output(response.text)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Build and run a simple two node agent workflow: Writer then Reviewer."""
|
||||
# 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)
|
||||
|
||||
# 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()
|
||||
|
||||
# Run the workflow with the user's initial message.
|
||||
# For foundational clarity, use run (non streaming) and print the workflow output.
|
||||
events = await workflow.run(
|
||||
ChatMessage(role="user", text="Create a slogan for a new electric SUV that is affordable and fun to drive.")
|
||||
)
|
||||
# The terminal node yields output; print its contents.
|
||||
outputs = events.get_outputs()
|
||||
if outputs:
|
||||
print(outputs[-1])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
# Ensure local getting_started package can be imported when running as a script.
|
||||
_SAMPLES_ROOT = Path(__file__).resolve().parents[3]
|
||||
if str(_SAMPLES_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_SAMPLES_ROOT))
|
||||
|
||||
from agent_framework import ( # noqa: E402
|
||||
ChatMessage,
|
||||
Executor,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
RequestInfoExecutor,
|
||||
RequestInfoMessage,
|
||||
RequestResponse,
|
||||
Role,
|
||||
WorkflowAgent,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
handler,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient # noqa: E402
|
||||
from getting_started.workflows.agents.workflow_as_agent_reflection_pattern import ( # noqa: E402
|
||||
ReviewRequest,
|
||||
ReviewResponse,
|
||||
Worker,
|
||||
)
|
||||
|
||||
"""
|
||||
Sample: Workflow Agent with Human-in-the-Loop
|
||||
|
||||
Purpose:
|
||||
This sample demonstrates how to build a workflow agent that escalates uncertain
|
||||
decisions to a human manager. A Worker generates results, while a Reviewer
|
||||
evaluates them. When the Reviewer is not confident, it escalates the decision
|
||||
to a human via RequestInfoExecutor, receives the human response, and then
|
||||
forwards that response back to the Worker. The workflow completes when idle.
|
||||
|
||||
Prerequisites:
|
||||
- OpenAI account configured and accessible for OpenAIChatClient.
|
||||
- Familiarity with WorkflowBuilder, Executor, and WorkflowContext from agent_framework.
|
||||
- Understanding of request-response message handling (RequestInfoMessage, RequestResponse).
|
||||
- (Optional) Review of reflection and escalation patterns, such as those in
|
||||
workflow_as_agent_reflection.py.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class HumanReviewRequest(RequestInfoMessage):
|
||||
"""A request message type for escalation to a human reviewer."""
|
||||
|
||||
agent_request: ReviewRequest | None = None
|
||||
|
||||
|
||||
class ReviewerWithHumanInTheLoop(Executor):
|
||||
"""Executor that always escalates reviews to a human manager."""
|
||||
|
||||
def __init__(self, worker_id: str, request_info_id: str, reviewer_id: str | None = None) -> None:
|
||||
unique_id = reviewer_id or f"{worker_id}-reviewer"
|
||||
super().__init__(id=unique_id)
|
||||
self._worker_id = worker_id
|
||||
self._request_info_id = request_info_id
|
||||
|
||||
@handler
|
||||
async def review(self, request: ReviewRequest, ctx: WorkflowContext[ReviewResponse | HumanReviewRequest]) -> None:
|
||||
# In this simplified example, we always escalate to a human manager.
|
||||
# See workflow_as_agent_reflection.py for an implementation
|
||||
# using an automated agent to make the review decision.
|
||||
print(f"Reviewer: Evaluating response for request {request.request_id[:8]}...")
|
||||
print("Reviewer: Escalating to human manager...")
|
||||
|
||||
# Forward the request to a human manager by sending a HumanReviewRequest.
|
||||
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:
|
||||
# Accept the human review response and forward it back to the Worker.
|
||||
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 for the workflow.
|
||||
print("Creating chat client and executors...")
|
||||
mini_chat_client = OpenAIChatClient(ai_model_id="gpt-4.1-nano")
|
||||
worker = Worker(id="sub-worker", chat_client=mini_chat_client)
|
||||
request_info_executor = RequestInfoExecutor(id="request_info")
|
||||
reviewer = ReviewerWithHumanInTheLoop(worker_id=worker.id, request_info_id=request_info_executor.id)
|
||||
|
||||
print("Building workflow with Worker ↔ Reviewer cycle...")
|
||||
# Build a workflow with bidirectional communication between Worker and Reviewer,
|
||||
# and escalation paths for human review.
|
||||
agent = (
|
||||
WorkflowBuilder()
|
||||
.add_edge(worker, reviewer) # Worker sends requests to Reviewer
|
||||
.add_edge(reviewer, worker) # Reviewer sends feedback to Worker
|
||||
.add_edge(reviewer, request_info_executor) # Reviewer requests human input
|
||||
.add_edge(request_info_executor, reviewer) # Human input forwarded back to Reviewer
|
||||
.set_start_executor(worker)
|
||||
.build()
|
||||
.as_agent() # Convert workflow into an agent interface
|
||||
)
|
||||
|
||||
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 with an initial query.
|
||||
response = await agent.run(
|
||||
"Write code for parallel reading 1 million Files on disk and write to a sorted output file."
|
||||
)
|
||||
|
||||
# Locate the human review function call in the response messages.
|
||||
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 the human review if required.
|
||||
if human_review_function_call:
|
||||
# Parse the human review request arguments.
|
||||
human_request_args = human_review_function_call.arguments
|
||||
if isinstance(human_request_args, str):
|
||||
request: WorkflowAgent.RequestInfoFunctionArgs = WorkflowAgent.RequestInfoFunctionArgs.from_json(
|
||||
human_request_args
|
||||
)
|
||||
elif isinstance(human_request_args, Mapping):
|
||||
request = WorkflowAgent.RequestInfoFunctionArgs.from_dict(dict(human_request_args))
|
||||
else:
|
||||
raise TypeError("Unexpected argument type for human review function call.")
|
||||
|
||||
request_payload_obj: Any = request.data
|
||||
if not isinstance(request_payload_obj, Mapping):
|
||||
raise ValueError("Human review request payload must be a mapping.")
|
||||
request_payload = cast(Mapping[str, Any], request_payload_obj)
|
||||
|
||||
agent_request_obj = request_payload.get("agent_request")
|
||||
if not isinstance(agent_request_obj, Mapping):
|
||||
raise ValueError("Human review request must include agent_request mapping data.")
|
||||
agent_request_data = cast(Mapping[str, Any], agent_request_obj)
|
||||
|
||||
request_id_obj = agent_request_data.get("request_id")
|
||||
if not isinstance(request_id_obj, str):
|
||||
raise ValueError("Human review request_id must be a string.")
|
||||
request_id_value = request_id_obj
|
||||
|
||||
# Mock a human response approval for demonstration purposes.
|
||||
human_response = ReviewResponse(request_id=request_id_value, feedback="Approved", approved=True)
|
||||
|
||||
# Create the function call result object to send back to the agent.
|
||||
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=Role.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())
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from uuid import uuid4
|
||||
|
||||
from agent_framework import (
|
||||
AgentRunResponseUpdate,
|
||||
AgentRunUpdateEvent,
|
||||
ChatClientProtocol,
|
||||
ChatMessage,
|
||||
Contents,
|
||||
Executor,
|
||||
Role,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
handler,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
"""
|
||||
Sample: Workflow as Agent with Reflection and Retry Pattern
|
||||
|
||||
Purpose:
|
||||
This sample demonstrates how to wrap a workflow as an agent using WorkflowAgent.
|
||||
It uses a reflection pattern where a Worker executor generates responses and a
|
||||
Reviewer executor evaluates them. If the response is not approved, the Worker
|
||||
regenerates the output based on feedback until the Reviewer approves it. Only
|
||||
approved responses are emitted to the external consumer. The workflow completes when idle.
|
||||
|
||||
Key Concepts Demonstrated:
|
||||
- WorkflowAgent: Wraps a workflow to behave like a regular agent.
|
||||
- Cyclic workflow design (Worker ↔ Reviewer) for iterative improvement.
|
||||
- AgentRunUpdateEvent: Mechanism for emitting approved responses externally.
|
||||
- Structured output parsing for review feedback using Pydantic.
|
||||
- State management for pending requests and retry logic.
|
||||
|
||||
Prerequisites:
|
||||
- OpenAI account configured and accessible for OpenAIChatClient.
|
||||
- Familiarity with WorkflowBuilder, Executor, WorkflowContext, and event handling.
|
||||
- Understanding of how agent messages are generated, reviewed, and re-submitted.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReviewRequest:
|
||||
"""Structured request passed from Worker to Reviewer for evaluation."""
|
||||
|
||||
request_id: str
|
||||
user_messages: list[ChatMessage]
|
||||
agent_messages: list[ChatMessage]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReviewResponse:
|
||||
"""Structured response from Reviewer back to Worker."""
|
||||
|
||||
request_id: str
|
||||
feedback: str
|
||||
approved: bool
|
||||
|
||||
|
||||
class Reviewer(Executor):
|
||||
"""Executor that reviews agent responses and provides structured feedback."""
|
||||
|
||||
def __init__(self, id: str, chat_client: ChatClientProtocol) -> None:
|
||||
super().__init__(id=id)
|
||||
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]}...")
|
||||
|
||||
# Define structured schema for the LLM to return.
|
||||
class _Response(BaseModel):
|
||||
feedback: str
|
||||
approved: bool
|
||||
|
||||
# Construct review instructions and context.
|
||||
messages = [
|
||||
ChatMessage(
|
||||
role=Role.SYSTEM,
|
||||
text=(
|
||||
"You are a reviewer for an AI agent. Provide feedback on the "
|
||||
"exchange between a user and the agent. Indicate approval only if:\n"
|
||||
"- Relevance: response addresses the query\n"
|
||||
"- Accuracy: information is correct\n"
|
||||
"- Clarity: response is easy to understand\n"
|
||||
"- Completeness: response covers all aspects\n"
|
||||
"Do not approve until all criteria are satisfied."
|
||||
),
|
||||
)
|
||||
]
|
||||
# Add conversation history.
|
||||
messages.extend(request.user_messages)
|
||||
messages.extend(request.agent_messages)
|
||||
|
||||
# Add explicit review instruction.
|
||||
messages.append(ChatMessage(role=Role.USER, text="Please review the agent's responses."))
|
||||
|
||||
print("Reviewer: Sending review request to LLM...")
|
||||
response = await self._chat_client.get_response(messages=messages, response_format=_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 structured review result to Worker.
|
||||
await ctx.send_message(
|
||||
ReviewResponse(request_id=request.request_id, feedback=parsed.feedback, approved=parsed.approved)
|
||||
)
|
||||
|
||||
|
||||
class Worker(Executor):
|
||||
"""Executor that generates responses and incorporates feedback when necessary."""
|
||||
|
||||
def __init__(self, id: str, chat_client: ChatClientProtocol) -> None:
|
||||
super().__init__(id=id)
|
||||
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...")
|
||||
|
||||
# Initialize chat with system prompt.
|
||||
messages = [ChatMessage(role=Role.SYSTEM, text="You are a helpful assistant.")]
|
||||
messages.extend(user_messages)
|
||||
|
||||
print("Worker: Calling LLM to generate response...")
|
||||
response = await self._chat_client.get_response(messages=messages)
|
||||
print(f"Worker: Response generated: {response.messages[-1].text}")
|
||||
|
||||
# Add agent messages to context.
|
||||
messages.extend(response.messages)
|
||||
|
||||
# Create review request and send to Reviewer.
|
||||
request = ReviewRequest(request_id=str(uuid4()), user_messages=user_messages, agent_messages=response.messages)
|
||||
print(f"Worker: Sending response for review (ID: {request.request_id[:8]})")
|
||||
await ctx.send_message(request)
|
||||
|
||||
# Track request for possible retry.
|
||||
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}")
|
||||
|
||||
if review.request_id not in self._pending_requests:
|
||||
raise ValueError(f"Unknown request ID in review: {review.request_id}")
|
||||
|
||||
request, messages = self._pending_requests.pop(review.request_id)
|
||||
|
||||
if review.approved:
|
||||
print("Worker: Response approved. Emitting to external consumer...")
|
||||
contents: list[Contents] = []
|
||||
for message in request.agent_messages:
|
||||
contents.extend(message.contents)
|
||||
|
||||
# Emit approved result to external consumer via AgentRunUpdateEvent.
|
||||
await ctx.add_event(
|
||||
AgentRunUpdateEvent(self.id, data=AgentRunResponseUpdate(contents=contents, role=Role.ASSISTANT))
|
||||
)
|
||||
return
|
||||
|
||||
print(f"Worker: Response not approved. Feedback: {review.feedback}")
|
||||
print("Worker: Regenerating response with feedback...")
|
||||
|
||||
# Incorporate review feedback.
|
||||
messages.append(ChatMessage(role=Role.SYSTEM, text=review.feedback))
|
||||
messages.append(
|
||||
ChatMessage(role=Role.SYSTEM, text="Please incorporate the feedback and regenerate the response.")
|
||||
)
|
||||
messages.extend(request.user_messages)
|
||||
|
||||
# Retry with updated prompt.
|
||||
response = await self._chat_client.get_response(messages=messages)
|
||||
print(f"Worker: New response generated: {response.messages[-1].text}")
|
||||
|
||||
messages.extend(response.messages)
|
||||
|
||||
# Send updated request for re-review.
|
||||
new_request = ReviewRequest(
|
||||
request_id=review.request_id, user_messages=request.user_messages, agent_messages=response.messages
|
||||
)
|
||||
await ctx.send_message(new_request)
|
||||
|
||||
# Track new request for further evaluation.
|
||||
self._pending_requests[new_request.request_id] = (new_request, messages)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("Starting Workflow Agent Demo")
|
||||
print("=" * 50)
|
||||
|
||||
# Initialize chat clients and 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(id="reviewer", chat_client=chat_client)
|
||||
worker = Worker(id="worker", chat_client=mini_chat_client)
|
||||
|
||||
print("Building workflow with Worker ↔ Reviewer cycle...")
|
||||
agent = (
|
||||
WorkflowBuilder()
|
||||
.add_edge(worker, reviewer) # Worker sends responses to Reviewer
|
||||
.add_edge(reviewer, worker) # Reviewer provides feedback to Worker
|
||||
.set_start_executor(worker)
|
||||
.build()
|
||||
.as_agent() # Wrap workflow as 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 agent in streaming mode to observe incremental updates.
|
||||
async for event in agent.run_stream(
|
||||
"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())
|
||||
Reference in New Issue
Block a user