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
+37
-29
@@ -1,6 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterable
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import (
|
||||
@@ -8,6 +9,7 @@ from agent_framework import (
|
||||
ConcurrentBuilder,
|
||||
Content,
|
||||
RequestInfoEvent,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
tool,
|
||||
)
|
||||
@@ -44,7 +46,10 @@ Prerequisites:
|
||||
|
||||
|
||||
# 1. Define market data tools (no approval required)
|
||||
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
|
||||
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
|
||||
# See:
|
||||
# samples/getting_started/tools/function_tool_with_approval.py
|
||||
# samples/getting_started/tools/function_tool_with_approval_and_threads.py.
|
||||
@tool(approval_mode="never_require")
|
||||
def get_stock_price(symbol: Annotated[str, "The stock ticker symbol"]) -> str:
|
||||
"""Get the current stock price for a given symbol."""
|
||||
@@ -100,6 +105,27 @@ def _print_output(event: WorkflowOutputEvent) -> None:
|
||||
print(f"- {msg.author_name or msg.role}: {msg.text}")
|
||||
|
||||
|
||||
async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, Content] | None:
|
||||
"""Process events from the workflow stream to capture human feedback requests."""
|
||||
requests: dict[str, Content] = {}
|
||||
async for event in stream:
|
||||
if isinstance(event, RequestInfoEvent) and isinstance(event.data, Content):
|
||||
# We are only expecting tool approval requests in this sample
|
||||
requests[event.request_id] = event.data
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
_print_output(event)
|
||||
|
||||
responses: dict[str, Content] = {}
|
||||
if requests:
|
||||
for request_id, request in requests.items():
|
||||
if request.type == "function_approval_request":
|
||||
print(f"\nSimulating human approval for: {request.function_call.name}") # type: ignore
|
||||
# Create approval response
|
||||
responses[request_id] = request.to_function_approval_response(approved=True)
|
||||
|
||||
return responses if responses else None
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# 3. Create two agents focused on different stocks but with the same tool sets
|
||||
chat_client = OpenAIChatClient()
|
||||
@@ -130,37 +156,19 @@ async def main() -> None:
|
||||
print("Starting concurrent workflow with tool approval...")
|
||||
print("-" * 60)
|
||||
|
||||
# Phase 1: Run workflow and collect request info events
|
||||
request_info_events: list[RequestInfoEvent] = []
|
||||
async for event in workflow.run_stream(
|
||||
# Initiate the first run of the workflow.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run or send_responses_streaming.
|
||||
stream = workflow.run_stream(
|
||||
"Manage my portfolio. Use a max of 5000 dollars to adjust my position using "
|
||||
"your best judgment based on market sentiment. No need to confirm trades with me."
|
||||
):
|
||||
if isinstance(event, RequestInfoEvent):
|
||||
request_info_events.append(event)
|
||||
if isinstance(event.data, Content) and event.data.type == "function_approval_request":
|
||||
print(f"\nApproval requested for tool: {event.data.function_call.name}")
|
||||
print(f" Arguments: {event.data.function_call.arguments}")
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
_print_output(event)
|
||||
)
|
||||
|
||||
# 6. Handle approval requests (if any)
|
||||
if request_info_events:
|
||||
responses: dict[str, Content] = {}
|
||||
for request_event in request_info_events:
|
||||
if isinstance(request_event.data, Content) and request_event.data.type == "function_approval_request":
|
||||
print(f"\nSimulating human approval for: {request_event.data.function_call.name}")
|
||||
# Create approval response
|
||||
responses[request_event.request_id] = request_event.data.to_function_approval_response(approved=True)
|
||||
|
||||
if responses:
|
||||
# Phase 2: Send all approvals and continue workflow
|
||||
async for event in workflow.send_responses_streaming(responses):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
_print_output(event)
|
||||
else:
|
||||
print("\nWorkflow completed without requiring approvals.")
|
||||
print("(The agents may have only checked data without executing trades)")
|
||||
pending_responses = await process_event_stream(stream)
|
||||
while pending_responses is not None:
|
||||
# Run the workflow until there is no more human feedback to provide,
|
||||
# in which case this workflow completes.
|
||||
stream = workflow.send_responses_streaming(pending_responses)
|
||||
pending_responses = await process_event_stream(stream)
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
+44
-63
@@ -1,15 +1,17 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Annotated
|
||||
from collections.abc import AsyncIterable
|
||||
from typing import Annotated, cast
|
||||
|
||||
from agent_framework import (
|
||||
AgentRunUpdateEvent,
|
||||
ChatMessage,
|
||||
Content,
|
||||
GroupChatBuilder,
|
||||
GroupChatRequestSentEvent,
|
||||
GroupChatState,
|
||||
RequestInfoEvent,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
@@ -93,6 +95,36 @@ def select_next_speaker(state: GroupChatState) -> str:
|
||||
return "DevOpsEngineer" # Subsequent speakers
|
||||
|
||||
|
||||
async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, Content] | None:
|
||||
"""Process events from the workflow stream to capture human feedback requests."""
|
||||
requests: dict[str, Content] = {}
|
||||
async for event in stream:
|
||||
if isinstance(event, RequestInfoEvent) and isinstance(event.data, Content):
|
||||
# We are only expecting tool approval requests in this sample
|
||||
requests[event.request_id] = event.data
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
# The output of the workflow comes from the orchestrator and it's a list of messages
|
||||
print("\n" + "=" * 60)
|
||||
print("Workflow summary:")
|
||||
outputs = cast(list[ChatMessage], event.data)
|
||||
for msg in outputs:
|
||||
speaker = msg.author_name or msg.role.value
|
||||
print(f"[{speaker}]: {msg.text}")
|
||||
|
||||
responses: dict[str, Content] = {}
|
||||
if requests:
|
||||
for request_id, request in requests.items():
|
||||
if request.type == "function_approval_request":
|
||||
print("\n[APPROVAL REQUIRED]")
|
||||
print(f" Tool: {request.function_call.name}") # type: ignore
|
||||
print(f" Arguments: {request.function_call.arguments}") # type: ignore
|
||||
print(f"Simulating human approval for: {request.function_call.name}") # type: ignore
|
||||
# Create approval response
|
||||
responses[request_id] = request.to_function_approval_response(approved=True)
|
||||
|
||||
return responses if responses else None
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# 3. Create specialized agents
|
||||
chat_client = OpenAIChatClient()
|
||||
@@ -135,67 +167,16 @@ async def main() -> None:
|
||||
print(f"Agents: {[qa_engineer.name, devops_engineer.name]}")
|
||||
print("-" * 60)
|
||||
|
||||
# Phase 1: Run workflow and collect all events (stream ends at IDLE or IDLE_WITH_PENDING_REQUESTS)
|
||||
request_info_events: list[RequestInfoEvent] = []
|
||||
# Keep track of the last response to format output nicely in streaming mode
|
||||
last_response_id: str | None = None
|
||||
async for event in workflow.run_stream(
|
||||
"We need to deploy version 2.4.0 to production. Please coordinate the deployment."
|
||||
):
|
||||
if isinstance(event, RequestInfoEvent):
|
||||
request_info_events.append(event)
|
||||
if isinstance(event.data, Content) and event.data.type == "function_approval_request":
|
||||
print("\n[APPROVAL REQUIRED] From agent:", event.source_executor_id)
|
||||
print(f" Tool: {event.data.function_call.name}")
|
||||
print(f" Arguments: {event.data.function_call.arguments}")
|
||||
elif isinstance(event, AgentRunUpdateEvent):
|
||||
if not event.data.text:
|
||||
continue # Skip empty updates
|
||||
response_id = event.data.response_id
|
||||
if response_id != last_response_id:
|
||||
if last_response_id is not None:
|
||||
print("\n")
|
||||
print(f"- {event.executor_id}:", end=" ", flush=True)
|
||||
last_response_id = response_id
|
||||
print(event.data, end="", flush=True)
|
||||
elif isinstance(event, GroupChatRequestSentEvent):
|
||||
print(f"\n[REQUEST SENT ({event.round_index})] to agent: {event.participant_name}")
|
||||
# Initiate the first run of the workflow.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run or send_responses_streaming.
|
||||
stream = workflow.run_stream("We need to deploy version 2.4.0 to production. Please coordinate the deployment.")
|
||||
|
||||
# 6. Handle approval requests
|
||||
if request_info_events:
|
||||
for request_event in request_info_events:
|
||||
if isinstance(request_event.data, Content) and request_event.data.type == "function_approval_request":
|
||||
print("\n" + "=" * 60)
|
||||
print("Human review required for production deployment!")
|
||||
print("In a real scenario, you would review the deployment details here.")
|
||||
print("Simulating approval for demo purposes...")
|
||||
print("=" * 60)
|
||||
|
||||
# Create approval response
|
||||
approval_response = request_event.data.to_function_approval_response(approved=True)
|
||||
|
||||
# Phase 2: Send approval and continue workflow
|
||||
# Keep track of the response to format output nicely in streaming mode
|
||||
last_response_id: str | None = None
|
||||
async for event in workflow.send_responses_streaming({request_event.request_id: approval_response}):
|
||||
if isinstance(event, AgentRunUpdateEvent):
|
||||
if not event.data.text:
|
||||
continue # Skip empty updates
|
||||
response_id = event.data.response_id
|
||||
if response_id != last_response_id:
|
||||
if last_response_id is not None:
|
||||
print("\n")
|
||||
print(f"- {event.executor_id}:", end=" ", flush=True)
|
||||
last_response_id = response_id
|
||||
print(event.data, end="", flush=True)
|
||||
elif isinstance(event, GroupChatRequestSentEvent):
|
||||
print(f"\n[REQUEST SENT ({event.round_index})] To agent: {event.participant_name}")
|
||||
|
||||
print("\n" + "-" * 60)
|
||||
print("Deployment workflow completed successfully!")
|
||||
print("All agents have finished their tasks.")
|
||||
else:
|
||||
print("\nWorkflow completed without requiring production deployment approval.")
|
||||
pending_responses = await process_event_stream(stream)
|
||||
while pending_responses is not None:
|
||||
# Run the workflow until there is no more human feedback to provide,
|
||||
# in which case this workflow completes.
|
||||
stream = workflow.send_responses_streaming(pending_responses)
|
||||
pending_responses = await process_event_stream(stream)
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
+42
-36
@@ -1,13 +1,15 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Annotated
|
||||
from collections.abc import AsyncIterable
|
||||
from typing import Annotated, cast
|
||||
|
||||
from agent_framework import (
|
||||
ChatMessage,
|
||||
Content,
|
||||
RequestInfoEvent,
|
||||
SequentialBuilder,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
tool,
|
||||
)
|
||||
@@ -65,6 +67,36 @@ def get_database_schema() -> str:
|
||||
"""
|
||||
|
||||
|
||||
async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, Content] | None:
|
||||
"""Process events from the workflow stream to capture human feedback requests."""
|
||||
requests: dict[str, Content] = {}
|
||||
async for event in stream:
|
||||
if isinstance(event, RequestInfoEvent) and isinstance(event.data, Content):
|
||||
# We are only expecting tool approval requests in this sample
|
||||
requests[event.request_id] = event.data
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
# The output of the workflow comes from the orchestrator and it's a list of messages
|
||||
print("\n" + "=" * 60)
|
||||
print("Workflow summary:")
|
||||
outputs = cast(list[ChatMessage], event.data)
|
||||
for msg in outputs:
|
||||
speaker = msg.author_name or msg.role
|
||||
print(f"[{speaker}]: {msg.text}")
|
||||
|
||||
responses: dict[str, Content] = {}
|
||||
if requests:
|
||||
for request_id, request in requests.items():
|
||||
if request.type == "function_approval_request":
|
||||
print("\n[APPROVAL REQUIRED]")
|
||||
print(f" Tool: {request.function_call.name}") # type: ignore
|
||||
print(f" Arguments: {request.function_call.arguments}") # type: ignore
|
||||
print(f"Simulating human approval for: {request.function_call.name}") # type: ignore
|
||||
# Create approval response
|
||||
responses[request_id] = request.to_function_approval_response(approved=True)
|
||||
|
||||
return responses if responses else None
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# 2. Create the agent with tools (approval mode is set per-tool via decorator)
|
||||
chat_client = OpenAIChatClient()
|
||||
@@ -85,42 +117,16 @@ async def main() -> None:
|
||||
print("Starting sequential workflow with tool approval...")
|
||||
print("-" * 60)
|
||||
|
||||
# Phase 1: Run workflow and collect all events (stream ends at IDLE or IDLE_WITH_PENDING_REQUESTS)
|
||||
request_info_events: list[RequestInfoEvent] = []
|
||||
async for event in workflow.run_stream(
|
||||
"Check the schema and then update all orders with status 'pending' to 'processing'"
|
||||
):
|
||||
if isinstance(event, RequestInfoEvent):
|
||||
request_info_events.append(event)
|
||||
if isinstance(event.data, Content) and event.data.type == "function_approval_request":
|
||||
print(f"\nApproval requested for tool: {event.data.function_call.name}")
|
||||
print(f" Arguments: {event.data.function_call.arguments}")
|
||||
# Initiate the first run of the workflow.
|
||||
# Runs are not isolated; state is preserved across multiple calls to run or send_responses_streaming.
|
||||
stream = workflow.run_stream("Check the schema and then update all orders with status 'pending' to 'processing'")
|
||||
|
||||
# 5. Handle approval requests
|
||||
if request_info_events:
|
||||
for request_event in request_info_events:
|
||||
if isinstance(request_event.data, Content) and request_event.data.type == "function_approval_request":
|
||||
# In a real application, you would prompt the user here
|
||||
print("\nSimulating human approval (auto-approving for demo)...")
|
||||
|
||||
# Create approval response
|
||||
approval_response = request_event.data.to_function_approval_response(approved=True)
|
||||
|
||||
# Phase 2: Send approval and continue workflow
|
||||
output: list[ChatMessage] | None = None
|
||||
async for event in workflow.send_responses_streaming({request_event.request_id: approval_response}):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
output = event.data
|
||||
|
||||
if output:
|
||||
print("\n" + "-" * 60)
|
||||
print("Workflow completed. Final conversation:")
|
||||
for msg in output:
|
||||
role = msg.role if hasattr(msg.role, "value") else msg.role
|
||||
text = msg.text[:200] + "..." if len(msg.text) > 200 else msg.text
|
||||
print(f" [{role}]: {text}")
|
||||
else:
|
||||
print("No approval requests were generated (schema check may have been sufficient).")
|
||||
pending_responses = await process_event_stream(stream)
|
||||
while pending_responses is not None:
|
||||
# Run the workflow until there is no more human feedback to provide,
|
||||
# in which case this workflow completes.
|
||||
stream = workflow.send_responses_streaming(pending_responses)
|
||||
pending_responses = await process_event_stream(stream)
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
Reference in New Issue
Block a user