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
@@ -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)
|
||||
"""
|
||||
|
||||
|
||||
|
||||
+24
-29
@@ -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__":
|
||||
|
||||
+23
-30
@@ -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:
|
||||
|
||||
+24
-29
@@ -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:
|
||||
|
||||
+30
-28
@@ -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)
|
||||
|
||||
+40
-20
@@ -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__":
|
||||
|
||||
+74
-61
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user