[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
@@ -22,7 +22,7 @@ Demonstrates:
Prerequisites:
- Azure OpenAI access configured for AzureOpenAIChatClient (use az login + env vars)
- Familiarity with Workflow events (AgentRunEvent, WorkflowOutputEvent)
- Familiarity with Workflow events (WorkflowOutputEvent)
"""
@@ -1,9 +1,10 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import cast
from agent_framework import (
AgentRunUpdateEvent,
AgentResponseUpdate,
ChatAgent,
ChatMessage,
GroupChatBuilder,
@@ -72,6 +73,9 @@ async def main() -> None:
# Set a hard termination condition: stop after 4 assistant messages
# The agent orchestrator will intelligently decide when to end before this limit but just in case
.with_termination_condition(lambda messages: sum(1 for msg in messages if msg.role == "assistant") >= 4)
# Enable intermediate outputs to observe the conversation as it unfolds
# Intermediate outputs will be emitted as WorkflowOutputEvent events
.with_intermediate_outputs()
.build()
)
@@ -81,35 +85,26 @@ async def main() -> None:
print(f"TASK: {task}\n")
print("=" * 80)
# Keep track of the last executor to format output nicely in streaming mode
last_executor_id: str | None = None
output_event: WorkflowOutputEvent | None = None
# 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(task):
if isinstance(event, AgentRunUpdateEvent):
eid = event.executor_id
if eid != last_executor_id:
if last_executor_id is not None:
print("\n")
print(f"{eid}:", end=" ", flush=True)
last_executor_id = eid
print(event.data, end="", flush=True)
elif isinstance(event, WorkflowOutputEvent):
output_event = event
# The output of the workflow is the full list of messages exchanged
if output_event:
if not isinstance(output_event.data, list) or not all(
isinstance(msg, ChatMessage)
for msg in output_event.data # type: ignore
):
raise RuntimeError("Unexpected output event data format.")
print("\n" + "=" * 80)
print("\nFINAL OUTPUT (The conversation history)\n")
for msg in output_event.data: # type: ignore
assert isinstance(msg, ChatMessage)
print(f"{msg.author_name or msg.role}: {msg.text}\n")
else:
raise RuntimeError("Workflow did not produce a final output event.")
if isinstance(event, WorkflowOutputEvent):
data = event.data
if isinstance(data, AgentResponseUpdate):
rid = data.response_id
if rid != last_response_id:
if last_response_id is not None:
print("\n")
print(f"{data.author_name}:", end=" ", flush=True)
last_response_id = rid
print(data.text, end="", flush=True)
else:
# The output of the group chat workflow is a collection of chat messages from all participants
outputs = cast(list[ChatMessage], event.data)
print("\n" + "=" * 80)
print("\nFinal Conversation Transcript:\n")
for message in outputs:
print(f"{message.author_name or message.role}: {message.text}\n")
if __name__ == "__main__":
@@ -4,13 +4,7 @@ import asyncio
import logging
from typing import cast
from agent_framework import (
AgentRunUpdateEvent,
ChatAgent,
ChatMessage,
GroupChatBuilder,
WorkflowOutputEvent,
)
from agent_framework import AgentResponseUpdate, ChatAgent, ChatMessage, GroupChatBuilder, WorkflowOutputEvent
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential
@@ -213,6 +207,9 @@ Share your perspective authentically. Feel free to:
.with_orchestrator(agent=moderator)
.participants([farmer, developer, teacher, activist, spiritual_leader, artist, immigrant, doctor])
.with_termination_condition(lambda messages: sum(1 for msg in messages if msg.role == "assistant") >= 10)
# Enable intermediate outputs to observe the conversation as it unfolds
# Intermediate outputs will be emitted as WorkflowOutputEvent events
.with_intermediate_outputs()
.build()
)
@@ -235,30 +232,26 @@ Share your perspective authentically. Feel free to:
print("DISCUSSION BEGINS")
print("=" * 80 + "\n")
final_conversation: list[ChatMessage] = []
current_speaker: str | None = None
# 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(f"Please begin the discussion on: {topic}"):
if isinstance(event, AgentRunUpdateEvent):
if event.executor_id != current_speaker:
if current_speaker is not None:
print("\n")
print(f"[{event.executor_id}]", flush=True)
current_speaker = event.executor_id
print(event.data, end="", flush=True)
elif isinstance(event, WorkflowOutputEvent):
final_conversation = cast(list[ChatMessage], event.data)
print("\n\n" + "=" * 80)
print("DISCUSSION SUMMARY")
print("=" * 80)
if final_conversation and isinstance(final_conversation, list) and final_conversation:
final_msg = final_conversation[-1]
if hasattr(final_msg, "author_name") and final_msg.author_name == "Moderator":
print(f"\n{final_msg.text}")
if isinstance(event, WorkflowOutputEvent):
data = event.data
if isinstance(data, AgentResponseUpdate):
rid = data.response_id
if rid != last_response_id:
if last_response_id is not None:
print("\n")
print(f"{data.author_name}:", end=" ", flush=True)
last_response_id = rid
print(data.text, end="", flush=True)
else:
# The output of the group chat workflow is a collection of chat messages from all participants
outputs = cast(list[ChatMessage], event.data)
print("\n" + "=" * 80)
print("\nFinal Conversation Transcript:\n")
for message in outputs:
print(f"{message.author_name or message.role}: {message.text}\n")
"""
Sample Output:
@@ -1,9 +1,10 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import cast
from agent_framework import (
AgentRunUpdateEvent,
AgentResponseUpdate,
ChatAgent,
ChatMessage,
GroupChatBuilder,
@@ -91,6 +92,9 @@ async def main() -> None:
# Note: it's possible that the expert gets it right the first time and the other participants
# have nothing to add, but for demo purposes we want to see at least one full round of interaction.
.with_termination_condition(lambda conversation: len(conversation) >= 6)
# Enable intermediate outputs to observe the conversation as it unfolds
# Intermediate outputs will be emitted as WorkflowOutputEvent events
.with_intermediate_outputs()
.build()
)
@@ -100,35 +104,26 @@ async def main() -> None:
print(f"TASK: {task}\n")
print("=" * 80)
# Keep track of the last executor to format output nicely in streaming mode
last_executor_id: str | None = None
output_event: WorkflowOutputEvent | None = None
# 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(task):
if isinstance(event, AgentRunUpdateEvent):
eid = event.executor_id
if eid != last_executor_id:
if last_executor_id is not None:
print("\n")
print(f"{eid}:", end=" ", flush=True)
last_executor_id = eid
print(event.data, end="", flush=True)
elif isinstance(event, WorkflowOutputEvent):
output_event = event
# The output of the workflow is the full list of messages exchanged
if output_event:
if not isinstance(output_event.data, list) or not all(
isinstance(msg, ChatMessage)
for msg in output_event.data # type: ignore
):
raise RuntimeError("Unexpected output event data format.")
print("\n" + "=" * 80)
print("\nFINAL OUTPUT (The conversation history)\n")
for msg in output_event.data: # type: ignore
assert isinstance(msg, ChatMessage)
print(f"{msg.author_name or msg.role}: {msg.text}\n")
else:
raise RuntimeError("Workflow did not produce a final output event.")
if isinstance(event, WorkflowOutputEvent):
data = event.data
if isinstance(data, AgentResponseUpdate):
rid = data.response_id
if rid != last_response_id:
if last_response_id is not None:
print("\n")
print(f"{data.author_name}:", end=" ", flush=True)
last_response_id = rid
print(data.text, end="", flush=True)
else:
# The output of the group chat workflow is a collection of chat messages from all participants
outputs = cast(list[ChatMessage], event.data)
print("\n" + "=" * 80)
print("\nFinal Conversation Transcript:\n")
for message in outputs:
print(f"{message.author_name or message.role}: {message.text}\n")
if __name__ == "__main__":
@@ -6,12 +6,11 @@ from typing import cast
from agent_framework import (
AgentResponseUpdate,
AgentRunUpdateEvent,
ChatAgent,
ChatMessage,
HandoffBuilder,
HandoffSentEvent,
HostedWebSearchTool,
WorkflowEvent,
WorkflowOutputEvent,
resolve_agent_id,
)
@@ -76,31 +75,6 @@ def create_agents(
return coordinator, research_agent, summary_agent
last_response_id: str | None = None
def _display_event(event: WorkflowEvent) -> None:
"""Print the final conversation snapshot from workflow output events."""
if isinstance(event, AgentRunUpdateEvent) and event.data:
update: AgentResponseUpdate = event.data
if not update.text:
return
global last_response_id
if update.response_id != last_response_id:
last_response_id = update.response_id
print(f"\n- {update.author_name}: ", flush=True, end="")
print(event.data, flush=True, end="")
elif isinstance(event, WorkflowOutputEvent):
conversation = cast(list[ChatMessage], event.data)
print("\n=== Final Conversation (Autonomous with Iteration) ===")
for message in conversation:
speaker = message.author_name or message.role
text_preview = message.text[:200] + "..." if len(message.text) > 200 else message.text
print(f"- {speaker}: {text_preview}")
print(f"\nTotal messages: {len(conversation)}")
print("=====================================================")
async def main() -> None:
"""Run an autonomous handoff workflow with specialist iteration enabled."""
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
@@ -130,16 +104,39 @@ async def main() -> None:
)
.with_termination_condition(
# Terminate after coordinator provides 5 assistant responses
lambda conv: sum(1 for msg in conv if msg.author_name == "coordinator" and msg.role == "assistant")
>= 5
lambda conv: sum(1 for msg in conv if msg.author_name == "coordinator" and msg.role == "assistant") >= 5
)
.build()
)
request = "Perform a comprehensive research on Microsoft Agent Framework."
print("Request:", request)
last_response_id: str | None = None
async for event in workflow.run_stream(request):
_display_event(event)
if isinstance(event, HandoffSentEvent):
print(f"\nHandoff Event: from {event.source} to {event.target}\n")
elif isinstance(event, WorkflowOutputEvent):
data = event.data
if isinstance(data, AgentResponseUpdate):
if not data.text:
# Skip updates that don't have text content
# These can be tool calls or other non-text events
continue
rid = data.response_id
if rid != last_response_id:
if last_response_id is not None:
print("\n")
print(f"{data.author_name}:", end=" ", flush=True)
last_response_id = rid
print(data.text, end="", flush=True)
else:
# The output of the group chat workflow is a collection of chat messages from all participants
outputs = cast(list[ChatMessage], event.data)
print("\n" + "=" * 80)
print("\nFinal Conversation Transcript:\n")
for message in outputs:
print(f"{message.author_name or message.role}: {message.text}\n")
"""
Expected behavior:
@@ -6,7 +6,6 @@ from typing import Annotated, cast
from agent_framework import (
AgentResponse,
AgentRunEvent,
ChatAgent,
ChatMessage,
HandoffAgentUserRequest,
@@ -47,7 +46,10 @@ Key Concepts:
"""
# 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 process_refund(order_number: Annotated[str, "Order number to process refund for"]) -> str:
"""Simulated function to process a refund for a given order number."""
@@ -125,38 +127,36 @@ def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
requests: list[RequestInfoEvent] = []
for event in events:
# AgentRunEvent: Contains messages generated by agents during their turn
if isinstance(event, AgentRunEvent):
for message in event.data.messages:
if not message.text:
# Skip messages without text (e.g., tool calls)
continue
speaker = message.author_name or message.role
print(f"- {speaker}: {message.text}")
# HandoffSentEvent: Indicates a handoff has been initiated
if isinstance(event, HandoffSentEvent):
# HandoffSentEvent: Indicates a handoff has been initiated
print(f"\n[Handoff from {event.source} to {event.target} initiated.]")
# WorkflowStatusEvent: Indicates workflow state changes
if isinstance(event, WorkflowStatusEvent) and event.state in {
elif isinstance(event, WorkflowStatusEvent) and event.state in {
WorkflowRunState.IDLE,
WorkflowRunState.IDLE_WITH_PENDING_REQUESTS,
}:
# WorkflowStatusEvent: Indicates workflow state changes
print(f"\n[Workflow Status] {event.state.name}")
# WorkflowOutputEvent: Contains the final conversation when workflow terminates
elif isinstance(event, WorkflowOutputEvent):
conversation = cast(list[ChatMessage], event.data)
if isinstance(conversation, list):
print("\n=== Final Conversation Snapshot ===")
for message in conversation:
# WorkflowOutputEvent: Contains contents generated by the workflow
data = event.data
if isinstance(data, AgentResponse):
for message in data.messages:
if not message.text:
# Skip messages without text (e.g., tool calls)
continue
speaker = message.author_name or message.role
print(f"- {speaker}: {message.text or [content.type for content in message.contents]}")
print("===================================")
# RequestInfoEvent: Workflow is requesting user input
print(f"- {speaker}: {message.text}")
else:
# The output of the handoff workflow is a collection of chat messages from all participants
conversation = cast(list[ChatMessage], event.data)
if isinstance(conversation, list):
print("\n=== Final Conversation Snapshot ===")
for message in conversation:
speaker = message.author_name or message.role
print(f"- {speaker}: {message.text or [content.type for content in message.contents]}")
print("===================================")
elif isinstance(event, RequestInfoEvent):
# RequestInfoEvent: Workflow is requesting user input
if isinstance(event.data, HandoffAgentUserRequest):
_print_handoff_agent_user_request(event.data.agent_response)
requests.append(event)
@@ -237,9 +237,11 @@ async def main() -> None:
# Custom termination: Check if the triage agent has provided a closing message.
# This looks for the last message being from triage_agent and containing "welcome",
# which indicates the conversation has concluded naturally.
lambda conversation: len(conversation) > 0
and conversation[-1].author_name == "triage_agent"
and "welcome" in conversation[-1].text.lower()
lambda conversation: (
len(conversation) > 0
and conversation[-1].author_name == "triage_agent"
and "welcome" in conversation[-1].text.lower()
)
)
)
@@ -5,7 +5,6 @@ from typing import Annotated, cast
from agent_framework import (
AgentResponse,
AgentRunEvent,
ChatAgent,
ChatMessage,
HandoffAgentUserRequest,
@@ -38,7 +37,10 @@ Key Concepts:
"""
# 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 process_refund(order_number: Annotated[str, "Order number to process refund for"]) -> str:
"""Simulated function to process a refund for a given order number."""
@@ -120,38 +122,36 @@ def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
requests: list[RequestInfoEvent] = []
for event in events:
# AgentRunEvent: Contains messages generated by agents during their turn
if isinstance(event, AgentRunEvent):
for message in event.data.messages:
if not message.text:
# Skip messages without text (e.g., tool calls)
continue
speaker = message.author_name or message.role
print(f"- {speaker}: {message.text}")
# HandoffSentEvent: Indicates a handoff has been initiated
if isinstance(event, HandoffSentEvent):
# HandoffSentEvent: Indicates a handoff has been initiated
print(f"\n[Handoff from {event.source} to {event.target} initiated.]")
# WorkflowStatusEvent: Indicates workflow state changes
if isinstance(event, WorkflowStatusEvent) and event.state in {
elif isinstance(event, WorkflowStatusEvent) and event.state in {
WorkflowRunState.IDLE,
WorkflowRunState.IDLE_WITH_PENDING_REQUESTS,
}:
# WorkflowStatusEvent: Indicates workflow state changes
print(f"\n[Workflow Status] {event.state.name}")
# WorkflowOutputEvent: Contains the final conversation when workflow terminates
elif isinstance(event, WorkflowOutputEvent):
conversation = cast(list[ChatMessage], event.data)
if isinstance(conversation, list):
print("\n=== Final Conversation Snapshot ===")
for message in conversation:
# WorkflowOutputEvent: Contains contents generated by the workflow
data = event.data
if isinstance(data, AgentResponse):
for message in data.messages:
if not message.text:
# Skip messages without text (e.g., tool calls)
continue
speaker = message.author_name or message.role
print(f"- {speaker}: {message.text or [content.type for content in message.contents]}")
print("===================================")
# RequestInfoEvent: Workflow is requesting user input
print(f"- {speaker}: {message.text}")
else:
# The output of the handoff workflow is a collection of chat messages from all participants
conversation = cast(list[ChatMessage], event.data)
if isinstance(conversation, list):
print("\n=== Final Conversation Snapshot ===")
for message in conversation:
speaker = message.author_name or message.role
print(f"- {speaker}: {message.text or [content.type for content in message.contents]}")
print("===================================")
elif isinstance(event, RequestInfoEvent):
# RequestInfoEvent: Workflow is requesting user input
if isinstance(event.data, HandoffAgentUserRequest):
_print_handoff_agent_user_request(event.data.agent_response)
requests.append(event)
@@ -6,7 +6,7 @@ Handoff Workflow with Code Interpreter File Generation Sample
This sample demonstrates retrieving file IDs from code interpreter output
in a handoff workflow context. A triage agent routes to a code specialist
that generates a text file, and we verify the file_id is captured correctly
from the streaming AgentRunUpdateEvent events.
from the streaming WorkflowOutputEvent events.
Verifies GitHub issue #2718: files generated by code interpreter in
HandoffBuilder workflows can be properly retrieved.
@@ -28,17 +28,19 @@ Prerequisites:
import asyncio
from collections.abc import AsyncIterable, AsyncIterator
from contextlib import asynccontextmanager
from typing import cast
from agent_framework import (
AgentRunUpdateEvent,
AgentResponseUpdate,
ChatAgent,
Content,
ChatMessage,
HandoffAgentUserRequest,
HandoffBuilder,
HandoffSentEvent,
HostedCodeInterpreterTool,
HostedFileContent,
RequestInfoEvent,
WorkflowEvent,
WorkflowOutputEvent,
WorkflowRunState,
WorkflowStatusEvent,
)
@@ -63,24 +65,42 @@ def _handle_events(events: list[WorkflowEvent]) -> tuple[list[RequestInfoEvent],
file_ids: list[str] = []
for event in events:
if isinstance(event, WorkflowStatusEvent):
if event.state in {WorkflowRunState.IDLE, WorkflowRunState.IDLE_WITH_PENDING_REQUESTS}:
print(f"[status] {event.state.name}")
if isinstance(event, HandoffSentEvent):
# HandoffSentEvent: Indicates a handoff has been initiated
print(f"\n[Handoff from {event.source} to {event.target} initiated.]")
elif isinstance(event, WorkflowStatusEvent) and event.state in {
WorkflowRunState.IDLE,
WorkflowRunState.IDLE_WITH_PENDING_REQUESTS,
}:
# WorkflowStatusEvent: Indicates workflow state changes
print(f"\n[Workflow Status] {event.state.name}")
elif isinstance(event, WorkflowOutputEvent):
# WorkflowOutputEvent: Contains contents generated by the workflow
data = event.data
if isinstance(data, AgentResponseUpdate):
# AgentResponseUpdate: Intermediate output from an agent
for content in data.contents:
if content.type == "hosted_file":
file_ids.append(content.file_id) # type: ignore
print(f"[Found HostedFileContent: file_id={content.file_id}]")
elif content.type == "text" and content.annotations:
for annotation in content.annotations:
file_id = annotation["file_id"] # type: ignore
file_ids.append(file_id)
print(f"[Found file annotation: file_id={file_id}]")
else:
# The output of the handoff workflow is a collection of chat messages from all participants
conversation = cast(list[ChatMessage], event.data)
if isinstance(conversation, list):
print("\n=== Final Conversation Snapshot ===")
for message in conversation:
speaker = message.author_name or message.role
print(f"- {speaker}: {message.text or [content.type for content in message.contents]}")
print("===================================")
elif isinstance(event, RequestInfoEvent):
# RequestInfoEvent: Workflow is requesting user input
requests.append(event)
elif isinstance(event, AgentRunUpdateEvent):
for content in event.data.contents:
if isinstance(content, HostedFileContent):
file_ids.append(content.file_id)
print(f"[Found HostedFileContent: file_id={content.file_id}]")
elif content.type == "text" and content.annotations:
for annotation in content.annotations:
if hasattr(annotation, "file_id") and annotation.file_id:
file_ids.append(annotation.file_id)
print(f"[Found file annotation: file_id={annotation.file_id}]")
return requests, file_ids
@@ -108,7 +128,7 @@ async def create_agents_v1(credential: AzureCliCredential) -> AsyncIterator[tupl
tools=[HostedCodeInterpreterTool()],
)
yield triage, code_specialist
yield triage, code_specialist # type: ignore
@asynccontextmanager
@@ -6,7 +6,7 @@ import logging
from typing import cast
from agent_framework import (
AgentRunUpdateEvent,
AgentResponseUpdate,
ChatAgent,
ChatMessage,
GroupChatRequestSentEvent,
@@ -86,6 +86,9 @@ async def main() -> None:
max_stall_count=3,
max_reset_count=2,
)
# Enable intermediate outputs to observe the conversation as it unfolds
# Intermediate outputs will be emitted as WorkflowOutputEvent events
.with_intermediate_outputs()
.build()
)
@@ -102,19 +105,9 @@ async def main() -> None:
print("\nStarting workflow execution...")
# Keep track of the last executor to format output nicely in streaming mode
last_message_id: str | None = None
output_event: WorkflowOutputEvent | None = None
last_response_id: str | None = None
async for event in workflow.run_stream(task):
if isinstance(event, AgentRunUpdateEvent):
message_id = event.data.message_id
if message_id != last_message_id:
if last_message_id is not None:
print("\n")
print(f"- {event.executor_id}:", end=" ", flush=True)
last_message_id = message_id
print(event.data, end="", flush=True)
elif isinstance(event, MagenticOrchestratorEvent):
if isinstance(event, MagenticOrchestratorEvent):
print(f"\n[Magentic Orchestrator Event] Type: {event.event_type.name}")
if isinstance(event.data, ChatMessage):
print(f"Please review the plan:\n{event.data.text}")
@@ -132,18 +125,22 @@ async def main() -> None:
print(f"\n[REQUEST SENT ({event.round_index})] to agent: {event.participant_name}")
elif isinstance(event, WorkflowOutputEvent):
output_event = event
if not output_event:
raise RuntimeError("Workflow did not produce a final output event.")
print("\n\nWorkflow completed!")
print("Final Output:")
# The output of the Magentic workflow is a list of ChatMessages with only one final message
# generated by the orchestrator.
output_messages = cast(list[ChatMessage], output_event.data)
if output_messages:
output = output_messages[-1].text
print(output)
data = event.data
if isinstance(data, AgentResponseUpdate):
response_id = 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)
else:
# The output of the magentic workflow is a collection of chat messages from all participants
outputs = cast(list[ChatMessage], event.data)
print("\n" + "=" * 80)
print("\nFinal Conversation Transcript:\n")
for message in outputs:
print(f"{message.author_name or message.role}: {message.text}\n")
if __name__ == "__main__":
@@ -2,15 +2,18 @@
import asyncio
import json
from collections.abc import AsyncIterable
from typing import cast
from agent_framework import (
AgentRunUpdateEvent,
AgentResponseUpdate,
ChatAgent,
ChatMessage,
MagenticBuilder,
MagenticPlanReviewRequest,
MagenticPlanReviewResponse,
RequestInfoEvent,
WorkflowEvent,
WorkflowOutputEvent,
)
from agent_framework.openai import OpenAIChatClient
@@ -35,6 +38,62 @@ Prerequisites:
- OpenAI credentials configured for `OpenAIChatClient`.
"""
# Keep track of the last response to format output nicely in streaming mode
last_response_id: str | None = None
async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, MagenticPlanReviewResponse] | None:
"""Process events from the workflow stream to capture human feedback requests."""
global last_response_id
requests: dict[str, MagenticPlanReviewRequest] = {}
async for event in stream:
if isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest:
requests[event.request_id] = cast(MagenticPlanReviewRequest, event.data)
if isinstance(event, WorkflowOutputEvent):
data = event.data
if isinstance(data, AgentResponseUpdate):
rid = data.response_id
if rid != last_response_id:
if last_response_id is not None:
print("\n")
print(f"{data.author_name}:", end=" ", flush=True)
last_response_id = rid
print(data.text, end="", flush=True)
else:
# The output of the workflow comes from the orchestrator and it's a list of messages
print("\n" + "=" * 60)
print("DISCUSSION COMPLETE")
print("=" * 60)
print("Final discussion summary:")
# To make the type checker happy, we cast event.data to the expected type
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, MagenticPlanReviewResponse] = {}
if requests:
for request_id, request in requests.items():
print("\n\n[Magentic Plan Review Request]")
if request.current_progress is not None:
print("Current Progress Ledger:")
print(json.dumps(request.current_progress.to_dict(), indent=2))
print()
print(f"Proposed Plan:\n{request.plan.text}\n")
print("Please provide your feedback (press Enter to approve):")
reply = input("> ") # noqa: ASYNC250
if reply.strip() == "":
print("Plan approved.\n")
responses[request_id] = request.approve()
else:
print("Plan revised by human.\n")
responses[request_id] = request.revise(reply)
return responses if responses else None
async def main() -> None:
researcher_agent = ChatAgent(
@@ -69,7 +128,11 @@ async def main() -> None:
max_stall_count=1,
max_reset_count=2,
)
.with_plan_review() # Request human input for plan review
# Request human input for plan review
.with_plan_review()
# Enable intermediate outputs to observe the conversation as it unfolds
# Intermediate outputs will be emitted as WorkflowOutputEvent events
.with_intermediate_outputs()
.build()
)
@@ -79,66 +142,16 @@ async def main() -> None:
print("\nStarting workflow execution...")
print("=" * 60)
pending_request: RequestInfoEvent | None = None
pending_responses: dict[str, object] | None = None
output_event: WorkflowOutputEvent | None = None
# 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(task)
while not output_event:
if pending_responses is not None:
stream = workflow.send_responses_streaming(pending_responses)
else:
stream = workflow.run_stream(task)
last_message_id: str | None = None
async for event in stream:
if isinstance(event, AgentRunUpdateEvent):
message_id = event.data.message_id
if message_id != last_message_id:
if last_message_id is not None:
print("\n")
print(f"- {event.executor_id}:", end=" ", flush=True)
last_message_id = message_id
print(event.data, end="", flush=True)
elif isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest:
pending_request = event
elif isinstance(event, WorkflowOutputEvent):
output_event = event
pending_responses = None
# Handle plan review request if any
if pending_request is not None:
event_data = cast(MagenticPlanReviewRequest, pending_request.data)
print("\n\n[Magentic Plan Review Request]")
if event_data.current_progress is not None:
print("Current Progress Ledger:")
print(json.dumps(event_data.current_progress.to_dict(), indent=2))
print()
print(f"Proposed Plan:\n{event_data.plan.text}\n")
print("Please provide your feedback (press Enter to approve):")
reply = await asyncio.get_event_loop().run_in_executor(None, input, "> ")
if reply.strip() == "":
print("Plan approved.\n")
pending_responses = {pending_request.request_id: event_data.approve()}
else:
print("Plan revised by human.\n")
pending_responses = {pending_request.request_id: event_data.revise(reply)}
pending_request = None
print("\n" + "=" * 60)
print("WORKFLOW COMPLETED")
print("=" * 60)
print("Final Output:")
# The output of the Magentic workflow is a list of ChatMessages with only one final message
# generated by the orchestrator.
output_messages = cast(list[ChatMessage], output_event.data)
if output_messages:
output = output_messages[-1].text
print(output)
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)
if __name__ == "__main__":