mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
[BREAKING] Python: Refactor workflow events to unified discriminated union pattern (#3690)
* Refactor events * Merge main * Fixes * Cleanup * Update samples and tests * Remove unused imports * PR feedback * Merge main. Add properties for events to help typing * Formatting * Cleanup * use builtins.type to avoid shadowing by WorkflowEvent.type attribute * Final improvements
This commit is contained in:
committed by
GitHub
Unverified
parent
09f59b21ad
commit
0f3f4dbcaf
@@ -11,12 +11,9 @@ from agent_framework import (
|
||||
AgentResponseUpdate,
|
||||
ChatMessage,
|
||||
Executor,
|
||||
RequestInfoEvent,
|
||||
Role,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
handler,
|
||||
response_handler,
|
||||
)
|
||||
@@ -30,13 +27,13 @@ Sample: AzureOpenAI Chat Agents in workflow with human feedback
|
||||
Pipeline layout:
|
||||
writer_agent -> Coordinator -> writer_agent -> Coordinator -> final_editor_agent -> Coordinator -> output
|
||||
|
||||
The writer agent drafts marketing copy. A custom executor emits a RequestInfoEvent so a human can comment,
|
||||
then relays the human guidance back into the conversation before the final editor agent produces the polished
|
||||
output.
|
||||
The writer agent drafts marketing copy. A custom executor emits a request_info event (type='request_info') so a
|
||||
human can comment, then relays the human guidance back into the conversation before the final editor agent
|
||||
produces the polished output.
|
||||
|
||||
Demonstrates:
|
||||
- Capturing agent responses in a custom executor.
|
||||
- Emitting RequestInfoEvent to request human input.
|
||||
- Emitting request_info events (type='request_info') to request human input.
|
||||
- Handling human feedback and routing it to the appropriate agents.
|
||||
|
||||
Prerequisites:
|
||||
@@ -103,8 +100,7 @@ class Coordinator(Executor):
|
||||
# Human approved the draft as-is; forward it unchanged.
|
||||
await ctx.send_message(
|
||||
AgentExecutorRequest(
|
||||
messages=original_request.conversation
|
||||
+ [ChatMessage(Role.USER, text="The draft is approved as-is.")],
|
||||
messages=original_request.conversation + [ChatMessage("user", text="The draft is approved as-is.")],
|
||||
should_respond=True,
|
||||
),
|
||||
target_id=self.final_editor_name,
|
||||
@@ -119,7 +115,7 @@ class Coordinator(Executor):
|
||||
"Rewrite the draft from the previous assistant message into a polished final version. "
|
||||
"Keep the response under 120 words and reflect any requested tone adjustments."
|
||||
)
|
||||
conversation.append(ChatMessage(Role.USER, text=instruction))
|
||||
conversation.append(ChatMessage("user", text=instruction))
|
||||
await ctx.send_message(
|
||||
AgentExecutorRequest(messages=conversation, should_respond=True), target_id=self.writer_name
|
||||
)
|
||||
@@ -132,9 +128,9 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
|
||||
|
||||
requests: list[tuple[str, DraftFeedbackRequest]] = []
|
||||
async for event in stream:
|
||||
if isinstance(event, RequestInfoEvent) and isinstance(event.data, DraftFeedbackRequest):
|
||||
if event.type == "request_info" and isinstance(event.data, DraftFeedbackRequest):
|
||||
requests.append((event.request_id, event.data))
|
||||
elif isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
|
||||
elif event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
# This workflow should only produce AgentResponseUpdate as outputs.
|
||||
# Streaming updates from an agent will be consecutive, because no two agents run simultaneously
|
||||
# in this workflow. So we can use last_author to format output nicely.
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ Demonstrate:
|
||||
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, RequestInfoEvent, and streaming runs.
|
||||
- Basic familiarity with WorkflowBuilder, edges, events, request_info events (type='request_info'), and streaming runs.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
+3
-6
@@ -26,12 +26,10 @@ from collections.abc import AsyncIterable
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import (
|
||||
AgentExecutorResponse,
|
||||
ChatMessage,
|
||||
RequestInfoEvent,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
)
|
||||
from agent_framework._workflows._agent_executor import AgentExecutorResponse
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.orchestrations import AgentRequestInfoResponse, ConcurrentBuilder
|
||||
from azure.identity import AzureCliCredential
|
||||
@@ -97,11 +95,10 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
|
||||
|
||||
requests: dict[str, AgentExecutorResponse] = {}
|
||||
async for event in stream:
|
||||
if isinstance(event, RequestInfoEvent) and isinstance(event.data, AgentExecutorResponse):
|
||||
# Display agent output for review and potential modification
|
||||
if event.type == "request_info" and isinstance(event.data, AgentExecutorResponse):
|
||||
requests[event.request_id] = event.data
|
||||
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
# The output of the workflow comes from the aggregator and it's a single string
|
||||
print("\n" + "=" * 60)
|
||||
print("ANALYSIS COMPLETE")
|
||||
|
||||
+2
-4
@@ -29,9 +29,7 @@ from typing import cast
|
||||
from agent_framework import (
|
||||
AgentExecutorResponse,
|
||||
ChatMessage,
|
||||
RequestInfoEvent,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.orchestrations import AgentRequestInfoResponse, GroupChatBuilder
|
||||
@@ -43,10 +41,10 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
|
||||
|
||||
requests: dict[str, AgentExecutorResponse] = {}
|
||||
async for event in stream:
|
||||
if isinstance(event, RequestInfoEvent) and isinstance(event.data, AgentExecutorResponse):
|
||||
if event.type == "request_info" and isinstance(event.data, AgentExecutorResponse):
|
||||
requests[event.request_id] = event.data
|
||||
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
# The output of the workflow comes from the orchestrator and it's a list of messages
|
||||
print("\n" + "=" * 60)
|
||||
print("DISCUSSION COMPLETE")
|
||||
|
||||
+9
-9
@@ -10,11 +10,9 @@ from agent_framework import (
|
||||
AgentResponseUpdate,
|
||||
ChatMessage,
|
||||
Executor,
|
||||
RequestInfoEvent,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
handler,
|
||||
response_handler,
|
||||
)
|
||||
@@ -46,7 +44,7 @@ Prerequisites:
|
||||
|
||||
# How human-in-the-loop is achieved via `request_info` and `send_responses_streaming`:
|
||||
# - An executor (TurnManager) calls `ctx.request_info` with a payload (HumanFeedbackRequest).
|
||||
# - The workflow run pauses and emits a RequestInfoEvent with the payload and the request_id.
|
||||
# - The workflow run pauses and emits a with the payload and the request_id.
|
||||
# - The application captures the event, prompts the user, and collects replies.
|
||||
# - The application calls `send_responses_streaming` with a map of request_ids to replies.
|
||||
# - The workflow resumes, and the response is delivered to the executor method decorated with @response_handler.
|
||||
@@ -132,11 +130,13 @@ class TurnManager(Executor):
|
||||
return
|
||||
|
||||
# Provide feedback to the agent to try again.
|
||||
# We keep the agent's output strictly JSON to ensure stable parsing on the next turn.
|
||||
user_msg = ChatMessage(
|
||||
"user",
|
||||
text=(f'Feedback: {reply}. Return ONLY a JSON object matching the schema {{"guess": <int 1..10>}}.'),
|
||||
# response_format=GuessOutput on the agent ensures JSON output, so we just need to guide the logic.
|
||||
last_guess = original_request.prompt.split(": ")[1].split(".")[0]
|
||||
feedback_text = (
|
||||
f"Feedback: {reply}. Your last guess was {last_guess}. "
|
||||
f"Use this feedback to adjust and make your next guess (1-10)."
|
||||
)
|
||||
user_msg = ChatMessage("user", text=feedback_text)
|
||||
await ctx.send_message(AgentExecutorRequest(messages=[user_msg], should_respond=True))
|
||||
|
||||
|
||||
@@ -147,9 +147,9 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
|
||||
|
||||
requests: list[tuple[str, HumanFeedbackRequest]] = []
|
||||
async for event in stream:
|
||||
if isinstance(event, RequestInfoEvent) and isinstance(event.data, HumanFeedbackRequest):
|
||||
if event.type == "request_info" and isinstance(event.data, HumanFeedbackRequest):
|
||||
requests.append((event.request_id, event.data))
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
elif event.type == "output":
|
||||
if isinstance(event.data, AgentResponseUpdate):
|
||||
update = event.data
|
||||
response_id = update.response_id
|
||||
|
||||
+3
-5
@@ -13,7 +13,7 @@ using the standard request_info pattern for consistency.
|
||||
|
||||
Demonstrate:
|
||||
- Configuring request info with `.with_request_info()`
|
||||
- Handling RequestInfoEvent with AgentInputRequest data
|
||||
- Handling with AgentInputRequest data
|
||||
- Injecting responses back into the workflow via send_responses_streaming
|
||||
|
||||
Prerequisites:
|
||||
@@ -28,9 +28,7 @@ from typing import cast
|
||||
from agent_framework import (
|
||||
AgentExecutorResponse,
|
||||
ChatMessage,
|
||||
RequestInfoEvent,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.orchestrations import AgentRequestInfoResponse, SequentialBuilder
|
||||
@@ -42,10 +40,10 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
|
||||
|
||||
requests: dict[str, AgentExecutorResponse] = {}
|
||||
async for event in stream:
|
||||
if isinstance(event, RequestInfoEvent) and isinstance(event.data, AgentExecutorResponse):
|
||||
if event.type == "request_info" and isinstance(event.data, AgentExecutorResponse):
|
||||
requests[event.request_id] = event.data
|
||||
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
elif event.type == "output":
|
||||
# The output of the sequential workflow is a list of ChatMessages
|
||||
print("\n" + "=" * 60)
|
||||
print("WORKFLOW COMPLETE")
|
||||
|
||||
Reference in New Issue
Block a user