[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:
Tao Chen
2026-02-04 16:16:45 -08:00
committed by GitHub
Unverified
parent 907654a489
commit a971d24f1e
68 changed files with 2652 additions and 2247 deletions
@@ -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:
@@ -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:
@@ -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: