mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
[BREAKING] Python: Refactor workflow events to unified discriminated union pattern (#3690)
* Refactor events * Merge main * Fixes * Cleanup * Update samples and tests * Remove unused imports * PR feedback * Merge main. Add properties for events to help typing * Formatting * Cleanup * use builtins.type to avoid shadowing by WorkflowEvent.type attribute * Final improvements
This commit is contained in:
committed by
GitHub
Unverified
parent
09f59b21ad
commit
0f3f4dbcaf
@@ -7,7 +7,7 @@ the task in a round-robin fashion.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import AgentResponseUpdate, WorkflowOutputEvent
|
||||
from agent_framework import AgentResponseUpdate
|
||||
|
||||
|
||||
async def run_autogen() -> None:
|
||||
@@ -55,8 +55,8 @@ async def run_autogen() -> None:
|
||||
|
||||
async def run_agent_framework() -> None:
|
||||
"""Agent Framework's SequentialBuilder for sequential agent orchestration."""
|
||||
from agent_framework import SequentialBuilder
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework.orchestrations import SequentialBuilder
|
||||
|
||||
client = OpenAIChatClient(model_id="gpt-4.1-mini")
|
||||
|
||||
@@ -83,15 +83,14 @@ async def run_agent_framework() -> None:
|
||||
print("[Agent Framework] Sequential conversation:")
|
||||
current_executor = None
|
||||
async for event in workflow.run("Create a brief summary about electric vehicles", stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
# Print executor name header when switching to a new agent
|
||||
if current_executor != event.executor_id:
|
||||
if current_executor is not None:
|
||||
print() # Newline after previous agent's message
|
||||
print(f"---------- {event.executor_id} ----------")
|
||||
current_executor = event.executor_id
|
||||
if isinstance(event.data, AgentResponseUpdate):
|
||||
print(event.data.text, end="", flush=True)
|
||||
print(event.data.text, end="", flush=True)
|
||||
print() # Final newline after conversation
|
||||
|
||||
|
||||
@@ -100,9 +99,9 @@ async def run_agent_framework_with_cycle() -> None:
|
||||
from agent_framework import (
|
||||
AgentExecutorRequest,
|
||||
AgentExecutorResponse,
|
||||
AgentResponseUpdate,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
executor,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
@@ -154,7 +153,10 @@ async def run_agent_framework_with_cycle() -> None:
|
||||
print("[Agent Framework with Cycle] Cyclic conversation:")
|
||||
current_executor = None
|
||||
async for event in workflow.run("Create a brief summary about electric vehicles", stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
|
||||
if event.type == "output" and not isinstance(event.data, AgentResponseUpdate):
|
||||
print("\n---------- Workflow Output ----------")
|
||||
print(event.data)
|
||||
elif event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
# Print executor name header when switching to a new agent
|
||||
if current_executor != event.executor_id:
|
||||
if current_executor is not None:
|
||||
|
||||
@@ -7,7 +7,7 @@ which agent should speak next based on the conversation context.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import AgentResponseUpdate, WorkflowOutputEvent
|
||||
from agent_framework import AgentResponseUpdate
|
||||
|
||||
|
||||
async def run_autogen() -> None:
|
||||
@@ -61,8 +61,8 @@ async def run_autogen() -> None:
|
||||
|
||||
async def run_agent_framework() -> None:
|
||||
"""Agent Framework's GroupChatBuilder with LLM-based speaker selection."""
|
||||
from agent_framework import GroupChatBuilder
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework.orchestrations import GroupChatBuilder
|
||||
|
||||
client = OpenAIChatClient(model_id="gpt-4.1-mini")
|
||||
|
||||
@@ -102,7 +102,7 @@ async def run_agent_framework() -> None:
|
||||
print("[Agent Framework] Group chat conversation:")
|
||||
current_executor = None
|
||||
async for event in workflow.run("How do I connect to a PostgreSQL database using Python?", stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
|
||||
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
# Print executor name header when switching to a new agent
|
||||
if current_executor != event.executor_id:
|
||||
if current_executor is not None:
|
||||
|
||||
@@ -7,7 +7,8 @@ to other specialized agents based on the task requirements.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import AgentResponseUpdate, HandoffAgentUserRequest, WorkflowOutputEvent
|
||||
from agent_framework import WorkflowEvent
|
||||
from orderedmultidict import Any
|
||||
|
||||
|
||||
async def run_autogen() -> None:
|
||||
@@ -98,12 +99,11 @@ async def run_autogen() -> None:
|
||||
async def run_agent_framework() -> None:
|
||||
"""Agent Framework's HandoffBuilder for agent coordination."""
|
||||
from agent_framework import (
|
||||
HandoffBuilder,
|
||||
RequestInfoEvent,
|
||||
AgentResponseUpdate,
|
||||
WorkflowRunState,
|
||||
WorkflowStatusEvent,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
|
||||
|
||||
client = OpenAIChatClient(model_id="gpt-4.1-mini")
|
||||
|
||||
@@ -159,10 +159,10 @@ async def run_agent_framework() -> None:
|
||||
|
||||
current_executor = None
|
||||
stream_line_open = False
|
||||
pending_requests: list[RequestInfoEvent] = []
|
||||
pending_requests: list[WorkflowEvent] = []
|
||||
|
||||
async for event in workflow.run(scripted_responses[0], stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
|
||||
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
# Print executor name header when switching to a new agent
|
||||
if current_executor != event.executor_id:
|
||||
if stream_line_open:
|
||||
@@ -173,10 +173,10 @@ async def run_agent_framework() -> None:
|
||||
stream_line_open = True
|
||||
if event.data:
|
||||
print(event.data.text, end="", flush=True)
|
||||
elif isinstance(event, RequestInfoEvent):
|
||||
elif event.type == "request_info":
|
||||
if isinstance(event.data, HandoffAgentUserRequest):
|
||||
pending_requests.append(event)
|
||||
elif isinstance(event, WorkflowStatusEvent):
|
||||
elif event.type == "status":
|
||||
if event.state in {WorkflowRunState.IDLE_WITH_PENDING_REQUESTS} and stream_line_open:
|
||||
print()
|
||||
stream_line_open = False
|
||||
@@ -188,13 +188,13 @@ async def run_agent_framework() -> None:
|
||||
print("---------- user ----------")
|
||||
print(user_response)
|
||||
|
||||
responses = {req.request_id: user_response for req in pending_requests}
|
||||
responses: dict[str, Any] = {req.request_id: user_response for req in pending_requests} # type: ignore
|
||||
pending_requests = []
|
||||
current_executor = None
|
||||
stream_line_open = False
|
||||
|
||||
async for event in workflow.send_responses_streaming(responses):
|
||||
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
|
||||
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
# Print executor name header when switching to a new agent
|
||||
if current_executor != event.executor_id:
|
||||
if stream_line_open:
|
||||
@@ -205,10 +205,10 @@ async def run_agent_framework() -> None:
|
||||
stream_line_open = True
|
||||
if event.data:
|
||||
print(event.data.text, end="", flush=True)
|
||||
elif isinstance(event, RequestInfoEvent):
|
||||
elif event.type == "request_info":
|
||||
if isinstance(event.data, HandoffAgentUserRequest):
|
||||
pending_requests.append(event)
|
||||
elif isinstance(event, WorkflowStatusEvent):
|
||||
elif event.type == "status":
|
||||
if (
|
||||
event.state in {WorkflowRunState.IDLE_WITH_PENDING_REQUESTS, WorkflowRunState.IDLE}
|
||||
and stream_line_open
|
||||
|
||||
@@ -12,10 +12,9 @@ from typing import cast
|
||||
from agent_framework import (
|
||||
AgentResponseUpdate,
|
||||
ChatMessage,
|
||||
MagenticOrchestratorEvent,
|
||||
MagenticProgressLedger,
|
||||
WorkflowOutputEvent,
|
||||
WorkflowEvent,
|
||||
)
|
||||
from agent_framework.orchestrations import MagenticProgressLedger
|
||||
|
||||
|
||||
async def run_autogen() -> None:
|
||||
@@ -67,8 +66,8 @@ async def run_autogen() -> None:
|
||||
|
||||
async def run_agent_framework() -> None:
|
||||
"""Agent Framework's MagenticBuilder for orchestrated collaboration."""
|
||||
from agent_framework import MagenticBuilder
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework.orchestrations import MagenticBuilder
|
||||
|
||||
client = OpenAIChatClient(model_id="gpt-4.1-mini")
|
||||
|
||||
@@ -110,10 +109,10 @@ async def run_agent_framework() -> None:
|
||||
|
||||
# Run complex task
|
||||
last_message_id: str | None = None
|
||||
output_event: WorkflowOutputEvent | None = None
|
||||
output_event: WorkflowEvent | None = None
|
||||
print("[Agent Framework] Magentic conversation:")
|
||||
async for event in workflow.run("Research Python async patterns and write a simple example", stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
|
||||
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
message_id = event.data.message_id
|
||||
if message_id != last_message_id:
|
||||
if last_message_id is not None:
|
||||
@@ -122,21 +121,21 @@ async def run_agent_framework() -> None:
|
||||
last_message_id = message_id
|
||||
print(event.data, end="", flush=True)
|
||||
|
||||
elif 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}")
|
||||
elif isinstance(event.data, MagenticProgressLedger):
|
||||
print(f"Please review progress ledger:\n{json.dumps(event.data.to_dict(), indent=2)}")
|
||||
elif event.type == "magentic_orchestrator":
|
||||
print(f"\n[Magentic Orchestrator Event] Type: {event.data.event_type.name}")
|
||||
if isinstance(event.data.content, ChatMessage):
|
||||
print(f"Please review the plan:\n{event.data.content.text}")
|
||||
elif isinstance(event.data.content, MagenticProgressLedger):
|
||||
print(f"Please review progress ledger:\n{json.dumps(event.data.content.to_dict(), indent=2)}")
|
||||
else:
|
||||
print(f"Unknown data type in MagenticOrchestratorEvent: {type(event.data)}")
|
||||
print(f"Unknown data type in MagenticOrchestratorEvent: {type(event.data.content)}")
|
||||
|
||||
# Block to allow user to read the plan/progress before continuing
|
||||
# Note: this is for demonstration only and is not the recommended way to handle human interaction.
|
||||
# Please refer to `with_plan_review` for proper human interaction during planning phases.
|
||||
await asyncio.get_event_loop().run_in_executor(None, input, "Press Enter to continue...")
|
||||
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
elif event.type == "output":
|
||||
output_event = event
|
||||
|
||||
if not output_event:
|
||||
|
||||
@@ -48,12 +48,10 @@ from _tools import (
|
||||
from agent_framework import (
|
||||
AgentExecutorResponse,
|
||||
AgentResponseUpdate,
|
||||
AgentRunUpdateEvent,
|
||||
ChatMessage,
|
||||
Executor,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
executor,
|
||||
handler,
|
||||
)
|
||||
@@ -355,7 +353,7 @@ async def _process_workflow_events(events, conversation_ids, response_ids):
|
||||
workflow_output = None
|
||||
|
||||
async for event in events:
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
workflow_output = event.data
|
||||
# Handle Unicode characters that may not be displayable in Windows console
|
||||
try:
|
||||
@@ -364,7 +362,7 @@ async def _process_workflow_events(events, conversation_ids, response_ids):
|
||||
output_str = str(event.data).encode("ascii", "replace").decode("ascii")
|
||||
print(f"\nWorkflow Output: {output_str}\n")
|
||||
|
||||
elif isinstance(event, AgentRunUpdateEvent):
|
||||
elif event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
_track_agent_ids(event, event.executor_id, response_ids, conversation_ids)
|
||||
|
||||
return workflow_output
|
||||
|
||||
@@ -6,7 +6,7 @@ from agent_framework import (
|
||||
Executor,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
|
||||
handler,
|
||||
)
|
||||
from agent_framework.observability import configure_otel_providers, get_tracer
|
||||
@@ -93,7 +93,7 @@ async def run_sequential_workflow() -> None:
|
||||
|
||||
output_event = None
|
||||
async for event in workflow.run("Hello world", stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
# The WorkflowOutputEvent contains the final result.
|
||||
output_event = event
|
||||
|
||||
|
||||
@@ -57,9 +57,9 @@ from agent_framework.orchestrations import (
|
||||
**Sequential orchestration note**: Sequential orchestration uses a few small adapter nodes for plumbing:
|
||||
- `input-conversation` normalizes input to `list[ChatMessage]`
|
||||
- `to-conversation:<participant>` converts agent responses into the shared conversation
|
||||
- `complete` publishes the final `WorkflowOutputEvent`
|
||||
- `complete` publishes the final output event (type='output')
|
||||
|
||||
These may appear in event streams (ExecutorInvoke/Completed). They're analogous to concurrent's dispatcher and aggregator and can be ignored if you only care about agent activity.
|
||||
These may appear in event streams (executor_invoked/executor_completed). They're analogous to concurrent's dispatcher and aggregator and can be ignored if you only care about agent activity.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ Demonstrates:
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI access configured for AzureOpenAIChatClient (use az login + env vars)
|
||||
- Familiarity with Workflow events (WorkflowOutputEvent)
|
||||
- Familiarity with Workflow events (WorkflowEvent)
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ from agent_framework import (
|
||||
AgentResponseUpdate,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
WorkflowOutputEvent,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.orchestrations import GroupChatBuilder
|
||||
@@ -74,7 +73,7 @@ async def main() -> None:
|
||||
# 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
|
||||
# Intermediate outputs will be emitted as WorkflowEvent with type "output" events
|
||||
.with_intermediate_outputs()
|
||||
.build()
|
||||
)
|
||||
@@ -88,7 +87,7 @@ async def main() -> 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(task, stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
data = event.data
|
||||
if isinstance(data, AgentResponseUpdate):
|
||||
rid = data.response_id
|
||||
@@ -98,7 +97,7 @@ async def main() -> None:
|
||||
print(f"{data.author_name}:", end=" ", flush=True)
|
||||
last_response_id = rid
|
||||
print(data.text, end="", flush=True)
|
||||
else:
|
||||
elif event.type == "output":
|
||||
# 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)
|
||||
|
||||
@@ -8,7 +8,6 @@ from agent_framework import (
|
||||
AgentResponseUpdate,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
WorkflowOutputEvent,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.orchestrations import GroupChatBuilder
|
||||
@@ -214,7 +213,7 @@ Share your perspective authentically. Feel free to:
|
||||
.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
|
||||
# Intermediate outputs will be emitted as WorkflowEvent with type "output" events
|
||||
.with_intermediate_outputs()
|
||||
.build()
|
||||
)
|
||||
@@ -241,7 +240,7 @@ Share your perspective authentically. Feel free to:
|
||||
# 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(f"Please begin the discussion on: {topic}", stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
data = event.data
|
||||
if isinstance(data, AgentResponseUpdate):
|
||||
rid = data.response_id
|
||||
@@ -251,7 +250,7 @@ Share your perspective authentically. Feel free to:
|
||||
print(f"{data.author_name}:", end=" ", flush=True)
|
||||
last_response_id = rid
|
||||
print(data.text, end="", flush=True)
|
||||
else:
|
||||
elif event.type == "output":
|
||||
# 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)
|
||||
|
||||
@@ -7,7 +7,6 @@ from agent_framework import (
|
||||
AgentResponseUpdate,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
WorkflowOutputEvent,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.orchestrations import GroupChatBuilder, GroupChatState
|
||||
@@ -92,7 +91,7 @@ async def main() -> None:
|
||||
# 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
|
||||
# Intermediate outputs will be emitted as WorkflowEvent with type "output" events
|
||||
.with_intermediate_outputs()
|
||||
.build()
|
||||
)
|
||||
@@ -106,7 +105,7 @@ async def main() -> 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(task, stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
data = event.data
|
||||
if isinstance(data, AgentResponseUpdate):
|
||||
rid = data.response_id
|
||||
@@ -116,7 +115,7 @@ async def main() -> None:
|
||||
print(f"{data.author_name}:", end=" ", flush=True)
|
||||
last_response_id = rid
|
||||
print(data.text, end="", flush=True)
|
||||
else:
|
||||
elif event.type == "output":
|
||||
# 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)
|
||||
|
||||
@@ -8,8 +8,6 @@ from agent_framework import (
|
||||
AgentResponseUpdate,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
HandoffSentEvent,
|
||||
WorkflowOutputEvent,
|
||||
resolve_agent_id,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
@@ -112,9 +110,9 @@ async def main() -> None:
|
||||
|
||||
last_response_id: str | None = None
|
||||
async for event in workflow.run(request, stream=True):
|
||||
if isinstance(event, HandoffSentEvent):
|
||||
print(f"\nHandoff Event: from {event.source} to {event.target}\n")
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "handoff_sent":
|
||||
print(f"\nHandoff Event: from {event.data.source} to {event.data.target}\n")
|
||||
elif event.type == "output":
|
||||
data = event.data
|
||||
if isinstance(data, AgentResponseUpdate):
|
||||
if not data.text:
|
||||
@@ -128,8 +126,8 @@ async def main() -> None:
|
||||
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
|
||||
elif event.type == "output":
|
||||
# The output of the handoff workflow is a collection of chat messages from all participants
|
||||
outputs = cast(list[ChatMessage], event.data)
|
||||
print("\n" + "=" * 80)
|
||||
print("\nFinal Conversation Transcript:\n")
|
||||
|
||||
@@ -8,16 +8,13 @@ from agent_framework import (
|
||||
AgentResponse,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
RequestInfoEvent,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
WorkflowRunState,
|
||||
WorkflowStatusEvent,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder, HandoffSentEvent
|
||||
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
logging.basicConfig(level=logging.ERROR)
|
||||
@@ -107,35 +104,35 @@ def create_return_agent() -> ChatAgent:
|
||||
)
|
||||
|
||||
|
||||
def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
|
||||
def _handle_events(events: list[WorkflowEvent]) -> list[WorkflowEvent[HandoffAgentUserRequest]]:
|
||||
"""Process workflow events and extract any pending user input requests.
|
||||
|
||||
This function inspects each event type and:
|
||||
- Prints workflow status changes (IDLE, IDLE_WITH_PENDING_REQUESTS, etc.)
|
||||
- Displays final conversation snapshots when workflow completes
|
||||
- Prints user input request prompts
|
||||
- Collects all RequestInfoEvent instances for response handling
|
||||
- Collects all request_info events for response handling
|
||||
|
||||
Args:
|
||||
events: List of WorkflowEvent to process
|
||||
|
||||
Returns:
|
||||
List of RequestInfoEvent representing pending user input requests
|
||||
List of WorkflowEvent[HandoffAgentUserRequest] representing pending user input requests
|
||||
"""
|
||||
requests: list[RequestInfoEvent] = []
|
||||
requests: list[WorkflowEvent[HandoffAgentUserRequest]] = []
|
||||
|
||||
for event in events:
|
||||
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 {
|
||||
if event.type == "handoff_sent":
|
||||
# handoff_sent event: Indicates a handoff has been initiated
|
||||
print(f"\n[Handoff from {event.data.source} to {event.data.target} initiated.]")
|
||||
elif event.type == "status" and event.state in {
|
||||
WorkflowRunState.IDLE,
|
||||
WorkflowRunState.IDLE_WITH_PENDING_REQUESTS,
|
||||
}:
|
||||
# WorkflowStatusEvent: Indicates workflow state changes
|
||||
# Status event: Indicates workflow state changes
|
||||
print(f"\n[Workflow Status] {event.state.name}")
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
# WorkflowOutputEvent: Contains contents generated by the workflow
|
||||
elif event.type == "output":
|
||||
# Output event: Contains contents generated by the workflow
|
||||
data = event.data
|
||||
if isinstance(data, AgentResponse):
|
||||
for message in data.messages:
|
||||
@@ -144,7 +141,7 @@ def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
|
||||
continue
|
||||
speaker = message.author_name or message.role
|
||||
print(f"- {speaker}: {message.text}")
|
||||
else:
|
||||
elif event.type == "output":
|
||||
# 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):
|
||||
@@ -153,11 +150,11 @@ def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
|
||||
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
|
||||
elif event.type == "request_info":
|
||||
# Request info event: Workflow is requesting user input
|
||||
if isinstance(event.data, HandoffAgentUserRequest):
|
||||
_print_handoff_agent_user_request(event.data.agent_response)
|
||||
requests.append(event)
|
||||
requests.append(cast(WorkflowEvent[HandoffAgentUserRequest], event))
|
||||
|
||||
return requests
|
||||
|
||||
|
||||
@@ -7,15 +7,12 @@ from agent_framework import (
|
||||
AgentResponse,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
RequestInfoEvent,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
WorkflowRunState,
|
||||
WorkflowStatusEvent,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder, HandoffSentEvent
|
||||
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""Sample: Simple handoff workflow.
|
||||
@@ -102,35 +99,35 @@ def create_agents(chat_client: AzureOpenAIChatClient) -> tuple[ChatAgent, ChatAg
|
||||
return triage_agent, refund_agent, order_agent, return_agent
|
||||
|
||||
|
||||
def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
|
||||
def _handle_events(events: list[WorkflowEvent]) -> list[WorkflowEvent[HandoffAgentUserRequest]]:
|
||||
"""Process workflow events and extract any pending user input requests.
|
||||
|
||||
This function inspects each event type and:
|
||||
- Prints workflow status changes (IDLE, IDLE_WITH_PENDING_REQUESTS, etc.)
|
||||
- Displays final conversation snapshots when workflow completes
|
||||
- Prints user input request prompts
|
||||
- Collects all RequestInfoEvent instances for response handling
|
||||
- Collects all request_info events for response handling
|
||||
|
||||
Args:
|
||||
events: List of WorkflowEvent to process
|
||||
|
||||
Returns:
|
||||
List of RequestInfoEvent representing pending user input requests
|
||||
List of WorkflowEvent[HandoffAgentUserRequest] representing pending user input requests
|
||||
"""
|
||||
requests: list[RequestInfoEvent] = []
|
||||
requests: list[WorkflowEvent[HandoffAgentUserRequest]] = []
|
||||
|
||||
for event in events:
|
||||
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 {
|
||||
if event.type == "handoff_sent":
|
||||
# handoff_sent event: Indicates a handoff has been initiated
|
||||
print(f"\n[Handoff from {event.data.source} to {event.data.target} initiated.]")
|
||||
elif event.type == "status" 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
|
||||
# Status event: Indicates workflow state changes
|
||||
print(f"\n[Workflow Status] {event.state}")
|
||||
elif event.type == "output":
|
||||
# Output event: Contains contents generated by the workflow
|
||||
data = event.data
|
||||
if isinstance(data, AgentResponse):
|
||||
for message in data.messages:
|
||||
@@ -139,7 +136,7 @@ def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
|
||||
continue
|
||||
speaker = message.author_name or message.role
|
||||
print(f"- {speaker}: {message.text}")
|
||||
else:
|
||||
elif event.type == "output":
|
||||
# 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):
|
||||
@@ -148,11 +145,9 @@ def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
|
||||
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)
|
||||
elif event.type == "request_info" and isinstance(event.data, HandoffAgentUserRequest):
|
||||
_print_handoff_agent_user_request(event.data.agent_response)
|
||||
requests.append(cast(WorkflowEvent[HandoffAgentUserRequest], event))
|
||||
|
||||
return requests
|
||||
|
||||
|
||||
+12
-21
@@ -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 WorkflowOutputEvent events.
|
||||
from the streaming workflow events.
|
||||
|
||||
Verifies GitHub issue #2718: files generated by code interpreter in
|
||||
HandoffBuilder workflows can be properly retrieved.
|
||||
@@ -34,13 +34,9 @@ from agent_framework import (
|
||||
AgentResponseUpdate,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
HandoffSentEvent,
|
||||
HostedCodeInterpreterTool,
|
||||
RequestInfoEvent,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
WorkflowRunState,
|
||||
WorkflowStatusEvent,
|
||||
)
|
||||
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
@@ -54,30 +50,29 @@ async def _drain(stream: AsyncIterable[WorkflowEvent]) -> list[WorkflowEvent]:
|
||||
return [event async for event in stream]
|
||||
|
||||
|
||||
def _handle_events(events: list[WorkflowEvent]) -> tuple[list[RequestInfoEvent], list[str]]:
|
||||
def _handle_events(events: list[WorkflowEvent]) -> tuple[list[WorkflowEvent[HandoffAgentUserRequest]], list[str]]:
|
||||
"""Process workflow events and extract file IDs and pending requests.
|
||||
|
||||
Returns:
|
||||
Tuple of (pending_requests, file_ids_found)
|
||||
"""
|
||||
requests: list[RequestInfoEvent] = []
|
||||
|
||||
requests: list[WorkflowEvent[HandoffAgentUserRequest]] = []
|
||||
file_ids: list[str] = []
|
||||
|
||||
for event in events:
|
||||
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 {
|
||||
if event.type == "handoff_sent":
|
||||
print(f"\n[Handoff from {event.data.source} to {event.data.target} initiated.]")
|
||||
elif event.type == "status" 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
|
||||
print(f"[status] {event.state.name}")
|
||||
elif event.type == "request_info" and isinstance(event.data, HandoffAgentUserRequest):
|
||||
requests.append(cast(WorkflowEvent[HandoffAgentUserRequest], event))
|
||||
elif event.type == "output":
|
||||
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
|
||||
@@ -87,8 +82,7 @@ def _handle_events(events: list[WorkflowEvent]) -> tuple[list[RequestInfoEvent],
|
||||
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
|
||||
elif event.type == "output":
|
||||
conversation = cast(list[ChatMessage], event.data)
|
||||
if isinstance(conversation, list):
|
||||
print("\n=== Final Conversation Snapshot ===")
|
||||
@@ -96,9 +90,6 @@ def _handle_events(events: list[WorkflowEvent]) -> tuple[list[RequestInfoEvent],
|
||||
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)
|
||||
|
||||
return requests, file_ids
|
||||
|
||||
|
||||
@@ -9,12 +9,11 @@ from agent_framework import (
|
||||
AgentResponseUpdate,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
GroupChatRequestSentEvent,
|
||||
HostedCodeInterpreterTool,
|
||||
WorkflowOutputEvent,
|
||||
WorkflowEvent,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
|
||||
from agent_framework.orchestrations import MagenticBuilder, MagenticOrchestratorEvent, MagenticProgressLedger
|
||||
from agent_framework.orchestrations import GroupChatRequestSentEvent, MagenticBuilder, MagenticProgressLedger
|
||||
|
||||
logging.basicConfig(level=logging.WARNING)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -85,7 +84,7 @@ async def main() -> None:
|
||||
max_reset_count=2,
|
||||
)
|
||||
# Enable intermediate outputs to observe the conversation as it unfolds
|
||||
# Intermediate outputs will be emitted as WorkflowOutputEvent events
|
||||
# Intermediate outputs will be emitted as WorkflowEvent events
|
||||
.with_intermediate_outputs()
|
||||
.build()
|
||||
)
|
||||
@@ -104,41 +103,44 @@ async def main() -> None:
|
||||
|
||||
# Keep track of the last executor to format output nicely in streaming mode
|
||||
last_response_id: str | None = None
|
||||
output_event: WorkflowEvent | None = None
|
||||
async for event in workflow.run(task, stream=True):
|
||||
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}")
|
||||
elif isinstance(event.data, MagenticProgressLedger):
|
||||
print(f"Please review progress ledger:\n{json.dumps(event.data.to_dict(), indent=2)}")
|
||||
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
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 event.type == "magentic_orchestrator":
|
||||
print(f"\n[Magentic Orchestrator Event] Type: {event.data.event_type.name}")
|
||||
if isinstance(event.data.content, ChatMessage):
|
||||
print(f"Please review the plan:\n{event.data.content.text}")
|
||||
elif isinstance(event.data.content, MagenticProgressLedger):
|
||||
print(f"Please review progress ledger:\n{json.dumps(event.data.content.to_dict(), indent=2)}")
|
||||
else:
|
||||
print(f"Unknown data type in MagenticOrchestratorEvent: {type(event.data)}")
|
||||
print(f"Unknown data type in MagenticOrchestratorEvent: {type(event.data.content)}")
|
||||
|
||||
# Block to allow user to read the plan/progress before continuing
|
||||
# Note: this is for demonstration only and is not the recommended way to handle human interaction.
|
||||
# Please refer to `with_plan_review` for proper human interaction during planning phases.
|
||||
await asyncio.get_event_loop().run_in_executor(None, input, "Press Enter to continue...")
|
||||
|
||||
elif isinstance(event, GroupChatRequestSentEvent):
|
||||
print(f"\n[REQUEST SENT ({event.round_index})] to agent: {event.participant_name}")
|
||||
elif event.type == "group_chat" and isinstance(event.data, GroupChatRequestSentEvent):
|
||||
print(f"\n[REQUEST SENT ({event.data.round_index})] to agent: {event.data.participant_name}")
|
||||
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
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")
|
||||
elif event.type == "output":
|
||||
output_event = event
|
||||
|
||||
if output_event:
|
||||
# The output of the magentic workflow is a collection of chat messages from all participants
|
||||
outputs = cast(list[ChatMessage], output_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__":
|
||||
|
||||
@@ -9,11 +9,9 @@ from agent_framework import (
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
FileCheckpointStorage,
|
||||
RequestInfoEvent,
|
||||
WorkflowCheckpoint,
|
||||
WorkflowOutputEvent,
|
||||
WorkflowEvent,
|
||||
WorkflowRunState,
|
||||
WorkflowStatusEvent,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.orchestrations import MagenticBuilder, MagenticPlanReviewRequest
|
||||
@@ -105,16 +103,16 @@ async def main() -> None:
|
||||
print("\n=== Stage 1: run until plan review request (checkpointing active) ===")
|
||||
workflow = build_workflow(checkpoint_storage)
|
||||
|
||||
# Run the workflow until the first RequestInfoEvent is surfaced. The event carries the
|
||||
# Run the workflow until the first is surfaced. The event carries the
|
||||
# request_id we must reuse on resume. In a real system this is where the UI would present
|
||||
# the plan for human review.
|
||||
plan_review_request: MagenticPlanReviewRequest | None = None
|
||||
async for event in workflow.run(TASK, stream=True):
|
||||
if isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest:
|
||||
if event.type == "request_info" and event.request_type is MagenticPlanReviewRequest:
|
||||
plan_review_request = event.data
|
||||
print(f"Captured plan review request: {event.request_id}")
|
||||
|
||||
if isinstance(event, WorkflowStatusEvent) and event.state is WorkflowRunState.IDLE_WITH_PENDING_REQUESTS:
|
||||
if event.type == "status" and event.state is WorkflowRunState.IDLE_WITH_PENDING_REQUESTS:
|
||||
break
|
||||
|
||||
if plan_review_request is None:
|
||||
@@ -147,9 +145,9 @@ async def main() -> None:
|
||||
approval = plan_review_request.approve()
|
||||
|
||||
# Resume execution and capture the re-emitted plan review request.
|
||||
request_info_event: RequestInfoEvent | None = None
|
||||
request_info_event: WorkflowEvent | None = None
|
||||
async for event in resumed_workflow.run(checkpoint_id=resume_checkpoint.checkpoint_id, stream=True):
|
||||
if isinstance(event, RequestInfoEvent) and isinstance(event.data, MagenticPlanReviewRequest):
|
||||
if event.type == "request_info" and isinstance(event.data, MagenticPlanReviewRequest):
|
||||
request_info_event = event
|
||||
|
||||
if request_info_event is None:
|
||||
@@ -158,9 +156,9 @@ async def main() -> None:
|
||||
print(f"Resumed plan review request: {request_info_event.request_id}")
|
||||
|
||||
# Supply the approval and continue to run to completion.
|
||||
final_event: WorkflowOutputEvent | None = None
|
||||
final_event: WorkflowEvent | None = None
|
||||
async for event in resumed_workflow.send_responses_streaming({request_info_event.request_id: approval}):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
final_event = event
|
||||
|
||||
if final_event is None:
|
||||
@@ -218,12 +216,12 @@ async def main() -> None:
|
||||
if pending_messages == 0:
|
||||
print("Checkpoint has no pending messages; no additional work expected on resume.")
|
||||
|
||||
final_event_post: WorkflowOutputEvent | None = None
|
||||
final_event_post: WorkflowEvent | None = None
|
||||
post_emitted_events = False
|
||||
post_plan_workflow = build_workflow(checkpoint_storage)
|
||||
async for event in post_plan_workflow.run(checkpoint_id=post_plan_checkpoint.checkpoint_id, stream=True):
|
||||
post_emitted_events = True
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
final_event_post = event
|
||||
|
||||
if final_event_post is None:
|
||||
|
||||
@@ -9,9 +9,7 @@ from agent_framework import (
|
||||
AgentResponseUpdate,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
RequestInfoEvent,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework.orchestrations import MagenticBuilder, MagenticPlanReviewRequest, MagenticPlanReviewResponse
|
||||
@@ -46,10 +44,10 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
|
||||
|
||||
requests: dict[str, MagenticPlanReviewRequest] = {}
|
||||
async for event in stream:
|
||||
if isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest:
|
||||
if event.type == "request_info" and event.request_type is MagenticPlanReviewRequest:
|
||||
requests[event.request_id] = cast(MagenticPlanReviewRequest, event.data)
|
||||
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
data = event.data
|
||||
if isinstance(data, AgentResponseUpdate):
|
||||
rid = data.response_id
|
||||
@@ -68,7 +66,7 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
|
||||
# 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
|
||||
speaker = msg.author_name or msg.role
|
||||
print(f"[{speaker}]: {msg.text}")
|
||||
|
||||
responses: dict[str, MagenticPlanReviewResponse] = {}
|
||||
@@ -129,7 +127,7 @@ async def main() -> None:
|
||||
# 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
|
||||
# Intermediate outputs will be emitted as WorkflowEvent with type "output"
|
||||
.with_intermediate_outputs()
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import asyncio
|
||||
from typing import cast
|
||||
|
||||
from agent_framework import ChatMessage, WorkflowOutputEvent
|
||||
from agent_framework import ChatMessage
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.orchestrations import SequentialBuilder
|
||||
from azure.identity import AzureCliCredential
|
||||
@@ -48,7 +48,7 @@ async def main() -> None:
|
||||
# 3) Run and collect outputs
|
||||
outputs: list[list[ChatMessage]] = []
|
||||
async for event in workflow.run("Write a tagline for a budget-friendly eBike.", stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
outputs.append(cast(list[ChatMessage], event.data))
|
||||
|
||||
if outputs:
|
||||
|
||||
@@ -102,7 +102,7 @@ Tool approval samples demonstrate using `@tool(approval_mode="always_require")`
|
||||
|
||||
| Sample | File | Concepts |
|
||||
| ------------------------ | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| Executor I/O Observation | [observability/executor_io_observation.py](./observability/executor_io_observation.py) | Observe executor input/output data via ExecutorInvokedEvent and ExecutorCompletedEvent without modifying executor code |
|
||||
| Executor I/O Observation | [observability/executor_io_observation.py](./observability/executor_io_observation.py) | Observe executor input/output data via executor_invoked events (type='executor_invoked') and executor_completed events (type='executor_completed') without modifying executor code |
|
||||
|
||||
For additional observability samples in Agent Framework, see the [observability getting started samples](../observability/README.md). The [sample](../observability/workflow_observability.py) demonstrates integrating observability into workflows.
|
||||
|
||||
@@ -162,8 +162,8 @@ Sequential orchestration uses a few small adapter nodes for plumbing:
|
||||
|
||||
- "input-conversation" normalizes input to `list[ChatMessage]`
|
||||
- "to-conversation:<participant>" converts agent responses into the shared conversation
|
||||
- "complete" publishes the final `WorkflowOutputEvent`
|
||||
These may appear in event streams (ExecutorInvoke/Completed). They’re analogous to
|
||||
- "complete" publishes the final output event (type='output')
|
||||
These may appear in event streams (executor_invoked/executor_completed). They're analogous to
|
||||
concurrent’s dispatcher and aggregator and can be ignored if you only care about agent activity.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
@@ -140,7 +140,7 @@ class ExclamationAdder(Executor):
|
||||
super().__init__(id=id)
|
||||
|
||||
@handler(input=str, output=str)
|
||||
async def add_exclamation(self, message: str, ctx: WorkflowContext) -> None:
|
||||
async def add_exclamation(self, message, ctx) -> None: # type: ignore
|
||||
"""Add exclamation marks to the input.
|
||||
|
||||
Note: The input=str and output=str are explicitly specified on @handler,
|
||||
@@ -149,7 +149,7 @@ class ExclamationAdder(Executor):
|
||||
on @handler take precedence.
|
||||
"""
|
||||
result = f"{message}!!!"
|
||||
await ctx.send_message(result)
|
||||
await ctx.send_message(result) # type: ignore
|
||||
|
||||
|
||||
async def main():
|
||||
|
||||
@@ -57,7 +57,6 @@ async def main():
|
||||
# of `AgentResponse` from the agents in the workflow.
|
||||
outputs = cast(list[AgentResponse], outputs)
|
||||
for output in outputs:
|
||||
# TODO: author_name should be available in AgentResponse
|
||||
print(f"{output.messages[0].author_name}: {output.text}\n")
|
||||
|
||||
# Summarize the final run state (e.g., COMPLETED)
|
||||
@@ -66,7 +65,7 @@ async def main():
|
||||
"""
|
||||
writer: "Charge Ahead: Affordable Adventure Awaits!"
|
||||
|
||||
reviewer: - Consider emphasizing both affordability and fun in a more dynamic way.
|
||||
reviewer: - Consider emphasizing both affordability and fun in a more dynamic way.
|
||||
- Try using a catchy phrase that includes a play on words, like “Electrify Your Drive: Fun Meets Affordability!”
|
||||
- Ensure the slogan is succinct while capturing the essence of the car's unique selling proposition.
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import asyncio
|
||||
|
||||
from agent_framework import AgentResponseUpdate, ChatMessage, WorkflowBuilder
|
||||
from agent_framework._workflows._events import WorkflowOutputEvent
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
@@ -58,7 +57,7 @@ async def main():
|
||||
):
|
||||
# The outputs of the workflow are whatever the agents produce. So the events are expected to
|
||||
# contain `AgentResponseUpdate` from the agents in the workflow.
|
||||
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
|
||||
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
update = event.data
|
||||
author = update.author_name
|
||||
if author != last_author:
|
||||
|
||||
@@ -8,7 +8,6 @@ from agent_framework import (
|
||||
Executor,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
executor,
|
||||
handler,
|
||||
)
|
||||
@@ -87,7 +86,7 @@ async def main():
|
||||
async for event in workflow.run("hello world", stream=True):
|
||||
# The outputs of the workflow are whatever the agents produce. So the events are expected to
|
||||
# contain `AgentResponseUpdate` from the agents in the workflow.
|
||||
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
|
||||
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
update = event.data
|
||||
if first_update:
|
||||
print(f"{update.author_name}: {update.text}", end="", flush=True)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import AgentResponseUpdate, WorkflowBuilder, WorkflowOutputEvent
|
||||
from agent_framework import AgentResponseUpdate, WorkflowBuilder
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
|
||||
@@ -50,7 +50,7 @@ async def main() -> None:
|
||||
async for event in events:
|
||||
# The outputs of the workflow are whatever the agents produce. So the events are expected to
|
||||
# contain `AgentResponseUpdate` from the agents in the workflow.
|
||||
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
|
||||
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
update = event.data
|
||||
author = update.author_name
|
||||
if author != last_author:
|
||||
|
||||
@@ -10,7 +10,6 @@ from agent_framework import (
|
||||
ChatMessage,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
executor,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
@@ -128,7 +127,7 @@ async def main() -> None:
|
||||
async for event in events:
|
||||
# The outputs of the workflow are whatever the agents produce. So the events are expected to
|
||||
# contain `AgentResponseUpdate` from the agents in the workflow.
|
||||
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
|
||||
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
update = event.data
|
||||
author = update.author_name
|
||||
if author != last_author:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import AgentResponseUpdate, WorkflowBuilder, WorkflowOutputEvent
|
||||
from agent_framework import AgentResponseUpdate, WorkflowBuilder
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
@@ -49,7 +49,7 @@ async def main():
|
||||
async for event in events:
|
||||
# The outputs of the workflow are whatever the agents produce. So the events are expected to
|
||||
# contain `AgentResponseUpdate` from the agents in the workflow.
|
||||
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
|
||||
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
update = event.data
|
||||
author = update.author_name
|
||||
if author != last_author:
|
||||
|
||||
+19
-17
@@ -9,16 +9,13 @@ from agent_framework import (
|
||||
AgentExecutorRequest,
|
||||
AgentExecutorResponse,
|
||||
AgentResponse,
|
||||
AgentRunUpdateEvent,
|
||||
AgentResponseUpdate,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
Executor,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
RequestInfoEvent,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
WorkflowEvent,
|
||||
handler,
|
||||
response_handler,
|
||||
tool,
|
||||
@@ -36,7 +33,7 @@ writer_agent (uses Azure OpenAI tools) -> Coordinator -> writer_agent
|
||||
-> Coordinator -> final_editor_agent -> Coordinator -> output
|
||||
|
||||
The writer agent calls tools to gather product facts before drafting copy. A custom executor
|
||||
packages the draft and emits a RequestInfoEvent so a human can comment, then replays the human
|
||||
packages the draft and emits a request_info event (type='request_info') so a human can comment, then replays the human
|
||||
guidance back into the conversation before the final editor agent produces the polished output.
|
||||
|
||||
Demonstrates:
|
||||
@@ -50,7 +47,9 @@ Prerequisites:
|
||||
"""
|
||||
|
||||
|
||||
# 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 and
|
||||
# samples/getting_started/tools/function_tool_with_approval_and_threads.py.
|
||||
@tool(approval_mode="never_require")
|
||||
def fetch_product_brief(
|
||||
product_name: Annotated[str, Field(description="Product name to look up.")],
|
||||
@@ -147,8 +146,7 @@ class Coordinator(Executor):
|
||||
# Human approved the draft as-is; forward it unchanged.
|
||||
await ctx.send_message(
|
||||
AgentExecutorRequest(
|
||||
messages=original_request.conversation
|
||||
+ [ChatMessage("user", text="The draft is approved as-is.")],
|
||||
messages=original_request.conversation + [ChatMessage("user", text="The draft is approved as-is.")],
|
||||
should_respond=True,
|
||||
),
|
||||
target_id=self.final_editor_id,
|
||||
@@ -194,15 +192,15 @@ def create_final_editor_agent() -> ChatAgent:
|
||||
)
|
||||
|
||||
|
||||
def display_agent_run_update(event: AgentRunUpdateEvent, last_executor: str | None) -> None:
|
||||
def display_agent_run_update(event: WorkflowEvent, last_executor: str | None) -> None:
|
||||
"""Display an AgentRunUpdateEvent in a readable format."""
|
||||
printed_tool_calls: set[str] = set()
|
||||
printed_tool_results: set[str] = set()
|
||||
executor_id = event.executor_id
|
||||
update = event.data
|
||||
# Extract and print any new tool calls or results from the update.
|
||||
function_calls = [c for c in update.contents if isinstance(c, FunctionCallContent)] # type: ignore[union-attr]
|
||||
function_results = [c for c in update.contents if isinstance(c, FunctionResultContent)] # type: ignore[union-attr]
|
||||
function_calls = [c for c in update.contents if c.type == "function_call"] # type: ignore[union-attr]
|
||||
function_results = [c for c in update.contents if c.type == "function_result"] # type: ignore[union-attr]
|
||||
if executor_id != last_executor:
|
||||
if last_executor is not None:
|
||||
print()
|
||||
@@ -291,18 +289,22 @@ async def main() -> None:
|
||||
requests: list[tuple[str, DraftFeedbackRequest]] = []
|
||||
|
||||
async for event in stream:
|
||||
if isinstance(event, AgentRunUpdateEvent) and display_agent_run_update_switch:
|
||||
if (
|
||||
event.type == "output"
|
||||
and isinstance(event.data, AgentResponseUpdate)
|
||||
and display_agent_run_update_switch
|
||||
):
|
||||
display_agent_run_update(event, last_executor)
|
||||
if isinstance(event, RequestInfoEvent) and isinstance(event.data, DraftFeedbackRequest):
|
||||
if event.type == "request_info" and isinstance(event.data, DraftFeedbackRequest):
|
||||
# Stash the request so we can prompt the human after the stream completes.
|
||||
requests.append((event.request_id, event.data))
|
||||
last_executor = None
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
elif event.type == "output" and not isinstance(event.data, AgentResponseUpdate):
|
||||
# Only mark as completed for final outputs, not streaming updates
|
||||
last_executor = None
|
||||
response = event.data
|
||||
print("\n===== Final output =====")
|
||||
final_text = getattr(response, "text", str(response))
|
||||
print(final_text.strip())
|
||||
print(final_text, flush=True, end="")
|
||||
completed = True
|
||||
|
||||
if requests and not completed:
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import ConcurrentBuilder
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.orchestrations import ConcurrentBuilder
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""
|
||||
@@ -20,7 +20,7 @@ Demonstrates:
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI access configured for AzureOpenAIChatClient (use az login + env vars)
|
||||
- Familiarity with Workflow events (WorkflowOutputEvent)
|
||||
- Familiarity with Workflow events (WorkflowEvent with type "output")
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import ChatAgent, GroupChatBuilder
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
|
||||
from agent_framework.orchestrations import GroupChatBuilder
|
||||
|
||||
"""
|
||||
Sample: Group Chat Orchestration
|
||||
@@ -42,7 +43,7 @@ async def main() -> None:
|
||||
)
|
||||
.participants([researcher, writer])
|
||||
# Enable intermediate outputs to observe the conversation as it unfolds
|
||||
# Intermediate outputs will be emitted as WorkflowOutputEvent events
|
||||
# Intermediate outputs will be emitted as WorkflowEvent with type "output" events
|
||||
.with_intermediate_outputs()
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -8,12 +8,11 @@ from agent_framework import (
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
Content,
|
||||
HandoffAgentUserRequest,
|
||||
HandoffBuilder,
|
||||
WorkflowAgent,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""Sample: Handoff Workflow as Agent with Human-in-the-Loop.
|
||||
|
||||
@@ -5,9 +5,9 @@ import asyncio
|
||||
from agent_framework import (
|
||||
ChatAgent,
|
||||
HostedCodeInterpreterTool,
|
||||
MagenticBuilder,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
|
||||
from agent_framework.orchestrations import MagenticBuilder
|
||||
|
||||
"""
|
||||
Sample: Build a Magentic orchestration and wrap it as an agent.
|
||||
@@ -62,7 +62,7 @@ async def main() -> None:
|
||||
max_reset_count=2,
|
||||
)
|
||||
# Enable intermediate outputs to observe the conversation as it unfolds
|
||||
# Intermediate outputs will be emitted as WorkflowOutputEvent events
|
||||
# Intermediate outputs will be emitted as WorkflowEvent with type "output" events
|
||||
.with_intermediate_outputs()
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import SequentialBuilder
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.orchestrations import SequentialBuilder
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""
|
||||
|
||||
@@ -33,7 +33,9 @@ Prerequisites:
|
||||
|
||||
|
||||
# Define tools that accept custom context via **kwargs
|
||||
# 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 and
|
||||
# samples/getting_started/tools/function_tool_with_approval_and_threads.py.
|
||||
@tool(approval_mode="never_require")
|
||||
def get_user_data(
|
||||
query: Annotated[str, Field(description="What user data to retrieve")],
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import AgentThread, ChatAgent, ChatMessageStore, SequentialBuilder
|
||||
from agent_framework import AgentThread, ChatAgent, ChatMessageStore
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework.orchestrations import SequentialBuilder
|
||||
|
||||
"""
|
||||
Sample: Workflow as Agent with Thread Conversation History and Checkpointing
|
||||
|
||||
+12
-8
@@ -1,9 +1,16 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, override
|
||||
from typing import Any
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override # type: ignore # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import override # type: ignore[import] # pragma: no cover
|
||||
|
||||
|
||||
# NOTE: the Azure client imports above are real dependencies. When running this
|
||||
# sample outside of Azure-enabled environments you may wish to swap in the
|
||||
@@ -15,13 +22,10 @@ from agent_framework import (
|
||||
ChatMessage,
|
||||
Executor,
|
||||
FileCheckpointStorage,
|
||||
RequestInfoEvent,
|
||||
Workflow,
|
||||
WorkflowBuilder,
|
||||
WorkflowCheckpoint,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
WorkflowStatusEvent,
|
||||
get_checkpoint_summary,
|
||||
handler,
|
||||
response_handler,
|
||||
@@ -53,7 +57,7 @@ Typical pause/resume flow
|
||||
3. Later, restart the script, select that checkpoint, and provide the stored
|
||||
human decision when prompted to pre-supply responses.
|
||||
Doing so applies the answer immediately on resume, so the system does **not**
|
||||
re-emit the same `RequestInfoEvent`.
|
||||
re-emit the same ``.
|
||||
"""
|
||||
|
||||
# Directory used for the sample's temporary checkpoint files. We isolate the
|
||||
@@ -259,11 +263,11 @@ async def run_interactive_session(
|
||||
raise ValueError("Either initial_message or checkpoint_id must be provided")
|
||||
|
||||
async for event in event_stream:
|
||||
if isinstance(event, WorkflowStatusEvent):
|
||||
if event.type == "status":
|
||||
print(event)
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
completed_output = event.data
|
||||
if isinstance(event, RequestInfoEvent):
|
||||
if event.type == "request_info":
|
||||
if isinstance(event.data, HumanApprovalRequest):
|
||||
requests[event.request_id] = event.data
|
||||
else:
|
||||
|
||||
@@ -24,21 +24,25 @@ Prerequisites:
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from random import random
|
||||
from typing import Any, override
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import (
|
||||
Executor,
|
||||
InMemoryCheckpointStorage,
|
||||
SuperStepCompletedEvent,
|
||||
WorkflowBuilder,
|
||||
WorkflowCheckpoint,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
handler,
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override # type: ignore # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import override # type: ignore[import] # pragma: no cover
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComputeTask:
|
||||
@@ -126,12 +130,12 @@ async def main():
|
||||
|
||||
output: str | None = None
|
||||
async for event in event_stream:
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
output = event.data
|
||||
break
|
||||
if isinstance(event, SuperStepCompletedEvent) and random() < 0.5:
|
||||
if event.type == "superstep_completed" and random() < 0.5:
|
||||
# Randomly simulate system interruptions
|
||||
# The `SuperStepCompletedEvent` ensures we only interrupt after
|
||||
# The type="superstep_completed" event ensures we only interrupt after
|
||||
# the current super-step is fully complete and checkpointed.
|
||||
# If we interrupt mid-step, the workflow may resume from an earlier point.
|
||||
print("\n** Simulating workflow interruption. Stopping execution. **")
|
||||
|
||||
+22
-21
@@ -12,15 +12,12 @@ from agent_framework import (
|
||||
ChatMessage,
|
||||
Content,
|
||||
FileCheckpointStorage,
|
||||
HandoffAgentUserRequest,
|
||||
HandoffBuilder,
|
||||
RequestInfoEvent,
|
||||
Workflow,
|
||||
WorkflowOutputEvent,
|
||||
WorkflowStatusEvent,
|
||||
WorkflowEvent,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""
|
||||
@@ -153,7 +150,7 @@ def _print_function_approval_request(request: Content, request_id: str) -> None:
|
||||
|
||||
|
||||
def _build_responses_for_requests(
|
||||
pending_requests: list[RequestInfoEvent],
|
||||
pending_requests: list[WorkflowEvent],
|
||||
*,
|
||||
user_response: str | None,
|
||||
approve_tools: bool | None,
|
||||
@@ -161,11 +158,15 @@ def _build_responses_for_requests(
|
||||
"""Create response payloads for each pending request."""
|
||||
responses: dict[str, object] = {}
|
||||
for request in pending_requests:
|
||||
if isinstance(request.data, HandoffAgentUserRequest):
|
||||
if isinstance(request.data, HandoffAgentUserRequest) and request.request_id:
|
||||
if user_response is None:
|
||||
raise ValueError("User response is required for HandoffAgentUserRequest")
|
||||
responses[request.request_id] = user_response
|
||||
elif isinstance(request.data, Content) and request.data.type == "function_approval_request":
|
||||
elif (
|
||||
isinstance(request.data, Content)
|
||||
and request.data.type == "function_approval_request"
|
||||
and request.request_id
|
||||
):
|
||||
if approve_tools is None:
|
||||
raise ValueError("Approval decision is required for function approval request")
|
||||
responses[request.request_id] = request.data.to_function_approval_response(approved=approve_tools)
|
||||
@@ -178,14 +179,14 @@ async def run_until_user_input_needed(
|
||||
workflow: Workflow,
|
||||
initial_message: str | None = None,
|
||||
checkpoint_id: str | None = None,
|
||||
) -> tuple[list[RequestInfoEvent], str | None]:
|
||||
) -> tuple[list[WorkflowEvent], str | None]:
|
||||
"""
|
||||
Run the workflow until it needs user input or approval, or completes.
|
||||
|
||||
Returns:
|
||||
Tuple of (pending_requests, checkpoint_id_to_use_for_resume)
|
||||
"""
|
||||
pending_requests: list[RequestInfoEvent] = []
|
||||
pending_requests: list[WorkflowEvent] = []
|
||||
latest_checkpoint_id: str | None = checkpoint_id
|
||||
|
||||
if initial_message:
|
||||
@@ -198,17 +199,17 @@ async def run_until_user_input_needed(
|
||||
raise ValueError("Must provide either initial_message or checkpoint_id")
|
||||
|
||||
async for event in event_stream:
|
||||
if isinstance(event, WorkflowStatusEvent):
|
||||
if event.type == "status":
|
||||
print(f"[Status] {event.state}")
|
||||
|
||||
elif isinstance(event, RequestInfoEvent):
|
||||
elif event.type == "request_info":
|
||||
pending_requests.append(event)
|
||||
if isinstance(event.data, HandoffAgentUserRequest):
|
||||
_print_handoff_request(event.data, event.request_id)
|
||||
elif isinstance(event.data, Content) and event.data.type == "function_approval_request":
|
||||
_print_function_approval_request(event.data, event.request_id)
|
||||
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
elif event.type == "output":
|
||||
print("\n[Workflow Completed]")
|
||||
if event.data:
|
||||
print(f"Final conversation length: {len(event.data)} messages")
|
||||
@@ -225,7 +226,7 @@ async def resume_with_responses(
|
||||
checkpoint_storage: FileCheckpointStorage,
|
||||
user_response: str | None = None,
|
||||
approve_tools: bool | None = None,
|
||||
) -> tuple[list[RequestInfoEvent], str | None]:
|
||||
) -> tuple[list[WorkflowEvent], str | None]:
|
||||
"""
|
||||
Two-step resume pattern (answers customer questions and tool approvals):
|
||||
|
||||
@@ -255,10 +256,10 @@ async def resume_with_responses(
|
||||
print(f"Step 1: Restoring checkpoint {latest_checkpoint.checkpoint_id}")
|
||||
|
||||
# Step 1: Restore the checkpoint to load pending requests into memory
|
||||
# The checkpoint restoration re-emits pending RequestInfoEvents
|
||||
restored_requests: list[RequestInfoEvent] = []
|
||||
# The checkpoint restoration re-emits pending request_info events
|
||||
restored_requests: list[WorkflowEvent] = []
|
||||
async for event in workflow.run(checkpoint_id=latest_checkpoint.checkpoint_id, stream=True): # type: ignore[attr-defined]
|
||||
if isinstance(event, RequestInfoEvent):
|
||||
if event.type == "request_info":
|
||||
restored_requests.append(event)
|
||||
if isinstance(event.data, HandoffAgentUserRequest):
|
||||
_print_handoff_request(event.data, event.request_id)
|
||||
@@ -275,13 +276,13 @@ async def resume_with_responses(
|
||||
)
|
||||
print(f"Step 2: Sending responses for {len(responses)} request(s)")
|
||||
|
||||
new_pending_requests: list[RequestInfoEvent] = []
|
||||
new_pending_requests: list[WorkflowEvent] = []
|
||||
|
||||
async for event in workflow.send_responses_streaming(responses):
|
||||
if isinstance(event, WorkflowStatusEvent):
|
||||
if event.type == "status":
|
||||
print(f"[Status] {event.state}")
|
||||
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
elif event.type == "output":
|
||||
print("\n[Workflow Output Event - Conversation Update]")
|
||||
if event.data and isinstance(event.data, list) and all(isinstance(msg, ChatMessage) for msg in event.data): # type: ignore
|
||||
# Now safe to cast event.data to list[ChatMessage]
|
||||
@@ -291,7 +292,7 @@ async def resume_with_responses(
|
||||
text = msg.text[:100] + "..." if len(msg.text) > 100 else msg.text
|
||||
print(f" {author}: {text}")
|
||||
|
||||
elif isinstance(event, RequestInfoEvent):
|
||||
elif event.type == "request_info":
|
||||
new_pending_requests.append(event)
|
||||
if isinstance(event.data, HandoffAgentUserRequest):
|
||||
_print_handoff_request(event.data, event.request_id)
|
||||
|
||||
@@ -3,29 +3,33 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import json
|
||||
import sys
|
||||
import uuid
|
||||
from dataclasses import dataclass, field, replace
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any, override
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import (
|
||||
Executor,
|
||||
FileCheckpointStorage,
|
||||
RequestInfoEvent,
|
||||
SubWorkflowRequestMessage,
|
||||
SubWorkflowResponseMessage,
|
||||
Workflow,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowEvent,
|
||||
WorkflowExecutor,
|
||||
WorkflowOutputEvent,
|
||||
WorkflowRunState,
|
||||
WorkflowStatusEvent,
|
||||
handler,
|
||||
response_handler,
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override # type: ignore # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import override # type: ignore[import] # pragma: no cover
|
||||
|
||||
CHECKPOINT_DIR = Path(__file__).with_suffix("").parent / "tmp" / "sub_workflow_checkpoints"
|
||||
|
||||
"""
|
||||
@@ -335,10 +339,10 @@ async def main() -> None:
|
||||
|
||||
request_id: str | None = None
|
||||
async for event in workflow.run("Contoso Gadget Launch", stream=True):
|
||||
if isinstance(event, RequestInfoEvent) and request_id is None:
|
||||
if event.type == "request_info" and request_id is None:
|
||||
request_id = event.request_id
|
||||
print(f"Captured review request id: {request_id}")
|
||||
if isinstance(event, WorkflowStatusEvent) and event.state is WorkflowRunState.IDLE_WITH_PENDING_REQUESTS:
|
||||
if event.type == "status" and event.state is WorkflowRunState.IDLE_WITH_PENDING_REQUESTS:
|
||||
break
|
||||
|
||||
if request_id is None:
|
||||
@@ -364,9 +368,9 @@ async def main() -> None:
|
||||
# Rebuild fresh instances to mimic a separate process resuming
|
||||
workflow2 = build_parent_workflow(storage)
|
||||
|
||||
request_info_event: RequestInfoEvent | None = None
|
||||
request_info_event: WorkflowEvent | None = None
|
||||
async for event in workflow2.run(checkpoint_id=resume_checkpoint.checkpoint_id, stream=True):
|
||||
if isinstance(event, RequestInfoEvent):
|
||||
if event.type == "request_info":
|
||||
request_info_event = event
|
||||
|
||||
if request_info_event is None:
|
||||
@@ -375,9 +379,9 @@ async def main() -> None:
|
||||
print("\n=== Stage 3: approve draft ==")
|
||||
|
||||
approval_response = "approve"
|
||||
output_event: WorkflowOutputEvent | None = None
|
||||
output_event: WorkflowEvent | None = None
|
||||
async for event in workflow2.send_responses_streaming({request_info_event.request_id: approval_response}):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
output_event = event
|
||||
|
||||
if output_event is None:
|
||||
|
||||
@@ -30,9 +30,9 @@ from agent_framework import (
|
||||
ChatAgent,
|
||||
ChatMessageStore,
|
||||
InMemoryCheckpointStorage,
|
||||
SequentialBuilder,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework.orchestrations import SequentialBuilder
|
||||
|
||||
|
||||
async def basic_checkpointing() -> None:
|
||||
@@ -157,7 +157,12 @@ async def streaming_with_checkpoints() -> None:
|
||||
print(f"\nCheckpoints created during stream: {len(checkpoints)}")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run all checkpoint examples."""
|
||||
await basic_checkpointing()
|
||||
await checkpointing_with_thread()
|
||||
await streaming_with_checkpoints()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(basic_checkpointing())
|
||||
asyncio.run(checkpointing_with_thread())
|
||||
asyncio.run(streaming_with_checkpoints())
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -6,12 +6,11 @@ from typing import Annotated, Any
|
||||
|
||||
from agent_framework import (
|
||||
ChatMessage,
|
||||
SequentialBuilder,
|
||||
WorkflowExecutor,
|
||||
WorkflowOutputEvent,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework.orchestrations import SequentialBuilder
|
||||
|
||||
"""
|
||||
Sample: Sub-Workflow kwargs Propagation
|
||||
@@ -32,7 +31,9 @@ Prerequisites:
|
||||
|
||||
|
||||
# Define tools that access custom context via **kwargs
|
||||
# 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 and
|
||||
# samples/getting_started/tools/function_tool_with_approval_and_threads.py.
|
||||
@tool(approval_mode="never_require")
|
||||
def get_authenticated_data(
|
||||
resource: Annotated[str, "The resource to fetch"],
|
||||
@@ -129,7 +130,7 @@ async def main() -> None:
|
||||
user_token=user_token,
|
||||
service_config=service_config,
|
||||
):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
output_data = event.data
|
||||
if isinstance(output_data, list):
|
||||
for item in output_data: # type: ignore
|
||||
@@ -140,6 +141,50 @@ async def main() -> None:
|
||||
print("Sample Complete - kwargs successfully flowed through sub-workflow!")
|
||||
print("=" * 70)
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
======================================================================
|
||||
Sub-Workflow kwargs Propagation Demo
|
||||
======================================================================
|
||||
|
||||
Context being passed to parent workflow:
|
||||
user_token: {
|
||||
"user_name": "alice@contoso.com",
|
||||
"access_level": "admin",
|
||||
"session_id": "sess_12345"
|
||||
}
|
||||
service_config: {
|
||||
"services": {
|
||||
"users": "https://api.example.com/v1/users",
|
||||
"orders": "https://api.example.com/v1/orders",
|
||||
"inventory": "https://api.example.com/v1/inventory"
|
||||
},
|
||||
"timeout": 30
|
||||
}
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Workflow Execution (kwargs flow: parent -> sub-workflow -> agent -> tool):
|
||||
----------------------------------------------------------------------
|
||||
|
||||
[get_authenticated_data] kwargs keys: ['user_token', 'service_config']
|
||||
[get_authenticated_data] User: alice@contoso.com, Access: admin
|
||||
|
||||
[call_configured_service] kwargs keys: ['user_token', 'service_config']
|
||||
[call_configured_service] Available services: ['users', 'orders', 'inventory']
|
||||
|
||||
[Final Answer]: Please fetch my profile data and then call the users service.
|
||||
|
||||
[Final Answer]: - Your profile data has been fetched.
|
||||
- The users service has been called.
|
||||
|
||||
Would you like details from either the profile data or the users service response?
|
||||
|
||||
======================================================================
|
||||
Sample Complete - kwargs successfully flowed through sub-workflow!
|
||||
======================================================================
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
+6
-6
@@ -3,16 +3,16 @@
|
||||
import asyncio
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
from typing import Any, Literal
|
||||
|
||||
from agent_framework import (
|
||||
Executor,
|
||||
RequestInfoEvent,
|
||||
SubWorkflowRequestMessage,
|
||||
SubWorkflowResponseMessage,
|
||||
Workflow,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowEvent,
|
||||
WorkflowExecutor,
|
||||
handler,
|
||||
response_handler,
|
||||
@@ -192,7 +192,7 @@ class ResourceAllocator(Executor):
|
||||
super().__init__(id)
|
||||
self._cache: dict[str, int] = {"cpu": 10, "memory": 50, "disk": 100}
|
||||
# Record pending requests to match responses
|
||||
self._pending_requests: dict[str, RequestInfoEvent] = {}
|
||||
self._pending_requests: dict[str, WorkflowEvent[Any]] = {}
|
||||
|
||||
async def _handle_resource_request(self, request: ResourceRequest) -> ResourceResponse | None:
|
||||
"""Allocates resources based on request and available cache."""
|
||||
@@ -207,7 +207,7 @@ class ResourceAllocator(Executor):
|
||||
self, request: SubWorkflowRequestMessage, ctx: WorkflowContext[SubWorkflowResponseMessage]
|
||||
) -> None:
|
||||
"""Handles requests from sub-workflows."""
|
||||
source_event: RequestInfoEvent = request.source_event
|
||||
source_event: WorkflowEvent[Any] = request.source_event
|
||||
if not isinstance(source_event.data, ResourceRequest):
|
||||
return
|
||||
|
||||
@@ -246,14 +246,14 @@ class PolicyEngine(Executor):
|
||||
"disk": 1000, # Liberal disk policy
|
||||
}
|
||||
# Record pending requests to match responses
|
||||
self._pending_requests: dict[str, RequestInfoEvent] = {}
|
||||
self._pending_requests: dict[str, WorkflowEvent[Any]] = {}
|
||||
|
||||
@handler
|
||||
async def handle_subworkflow_request(
|
||||
self, request: SubWorkflowRequestMessage, ctx: WorkflowContext[SubWorkflowResponseMessage]
|
||||
) -> None:
|
||||
"""Handles requests from sub-workflows."""
|
||||
source_event: RequestInfoEvent = request.source_event
|
||||
source_event: WorkflowEvent[Any] = request.source_event
|
||||
if not isinstance(source_event.data, PolicyRequest):
|
||||
return
|
||||
|
||||
|
||||
+1
-2
@@ -11,7 +11,6 @@ from agent_framework import (
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowExecutor,
|
||||
WorkflowOutputEvent,
|
||||
handler,
|
||||
response_handler,
|
||||
)
|
||||
@@ -303,7 +302,7 @@ async def main() -> None:
|
||||
for email in test_emails:
|
||||
print(f"\n🚀 Processing email to '{email.recipient}'")
|
||||
async for event in workflow.run(email, stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
print(f"🎉 Final result for '{email.recipient}': {'Delivered' if event.data else 'Blocked'}")
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ from agent_framework import (
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
executor,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
@@ -279,7 +278,7 @@ async def main() -> None:
|
||||
async for event in workflow.run(email, stream=True):
|
||||
if isinstance(event, DatabaseEvent):
|
||||
print(f"{event}")
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
elif event.type == "output":
|
||||
print(f"Workflow output: {event.data}")
|
||||
|
||||
"""
|
||||
|
||||
@@ -7,7 +7,6 @@ from agent_framework import (
|
||||
Executor,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
handler,
|
||||
)
|
||||
from typing_extensions import Never
|
||||
@@ -77,7 +76,7 @@ async def main() -> None:
|
||||
outputs: list[str] = []
|
||||
async for event in workflow.run("hello world", stream=True):
|
||||
print(f"Event: {event}")
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
outputs.append(cast(str, event.data))
|
||||
|
||||
if outputs:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import WorkflowBuilder, WorkflowContext, WorkflowOutputEvent, executor
|
||||
from agent_framework import WorkflowBuilder, WorkflowContext, executor
|
||||
from typing_extensions import Never
|
||||
|
||||
"""
|
||||
@@ -14,7 +14,8 @@ The second reverses the text and yields the workflow output. Events are printed
|
||||
Purpose:
|
||||
Show how to declare executors with the @executor decorator, connect them with WorkflowBuilder,
|
||||
pass intermediate values using ctx.send_message, and yield final output using ctx.yield_output().
|
||||
Demonstrate how streaming exposes ExecutorInvokedEvent and ExecutorCompletedEvent for observability.
|
||||
Demonstrate how streaming exposes executor_invoked events (type='executor_invoked') and
|
||||
executor_completed events (type='executor_completed') for observability.
|
||||
|
||||
Prerequisites:
|
||||
- No external services required.
|
||||
@@ -67,17 +68,17 @@ async def main():
|
||||
async for event in workflow.run("hello world", stream=True):
|
||||
# You will see executor invoke and completion events as the workflow progresses.
|
||||
print(f"Event: {event}")
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
print(f"Workflow completed with result: {event.data}")
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
Event: ExecutorInvokedEvent(executor_id=upper_case_executor)
|
||||
Event: ExecutorCompletedEvent(executor_id=upper_case_executor)
|
||||
Event: ExecutorInvokedEvent(executor_id=reverse_text_executor)
|
||||
Event: ExecutorCompletedEvent(executor_id=reverse_text_executor)
|
||||
Event: WorkflowOutputEvent(data='DLROW OLLEH', executor_id=reverse_text_executor)
|
||||
Event: executor_invoked event (type='executor_invoked', executor_id=upper_case_executor)
|
||||
Event: executor_completed event (type='executor_completed', executor_id=upper_case_executor)
|
||||
Event: executor_invoked event (type='executor_invoked', executor_id=reverse_text_executor)
|
||||
Event: executor_completed event (type='executor_completed', executor_id=reverse_text_executor)
|
||||
Event: output event (type='output', data='DLROW OLLEH', executor_id=reverse_text_executor)
|
||||
Workflow completed with result: DLROW OLLEH
|
||||
"""
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from agent_framework import (
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
Executor,
|
||||
ExecutorCompletedEvent,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
handler,
|
||||
@@ -143,7 +142,7 @@ async def main():
|
||||
# Step 2: Run the workflow and print the events.
|
||||
iterations = 0
|
||||
async for event in workflow.run(NumberSignal.INIT, stream=True):
|
||||
if isinstance(event, ExecutorCompletedEvent) and event.executor_id == "guess_number":
|
||||
if event.type == "executor_completed" and event.executor_id == "guess_number":
|
||||
iterations += 1
|
||||
print(f"Event: {event}")
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import logging
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from agent_framework import RequestInfoEvent, WorkflowOutputEvent
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.declarative import (
|
||||
AgentExternalInputRequest,
|
||||
@@ -259,7 +258,7 @@ async def main() -> None:
|
||||
stream = workflow.run(user_input, stream=True)
|
||||
|
||||
async for event in stream:
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
data = event.data
|
||||
source_id = getattr(event, "source_executor_id", "")
|
||||
|
||||
@@ -286,7 +285,7 @@ async def main() -> None:
|
||||
else:
|
||||
accumulated_response += str(data)
|
||||
|
||||
elif isinstance(event, RequestInfoEvent) and isinstance(event.data, AgentExternalInputRequest):
|
||||
elif event.type == "request_info" and isinstance(event.data, AgentExternalInputRequest):
|
||||
request = event.data
|
||||
|
||||
# The agent_response from the request contains the structured response
|
||||
|
||||
@@ -24,7 +24,6 @@ Usage:
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from agent_framework import WorkflowOutputEvent
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.declarative import WorkflowFactory
|
||||
from azure.identity import AzureCliCredential
|
||||
@@ -193,7 +192,7 @@ async def main() -> None:
|
||||
task = "What is the weather like in Seattle and how does it compare to the average for this time of year?"
|
||||
|
||||
async for event in workflow.run(task, stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
print(f"{event.data}", end="", flush=True)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
@@ -10,7 +10,7 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any
|
||||
|
||||
from agent_framework import FileCheckpointStorage, RequestInfoEvent, WorkflowOutputEvent, tool
|
||||
from agent_framework import FileCheckpointStorage, tool
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework_declarative import ExternalInputRequest, ExternalInputResponse, WorkflowFactory
|
||||
from azure.identity import AzureCliCredential
|
||||
@@ -98,12 +98,12 @@ async def main():
|
||||
first_response = True
|
||||
|
||||
async for event in stream:
|
||||
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, str):
|
||||
if event.type == "output" and isinstance(event.data, str):
|
||||
if first_response:
|
||||
print("MenuAgent: ", end="")
|
||||
first_response = False
|
||||
print(event.data, end="", flush=True)
|
||||
elif isinstance(event, RequestInfoEvent) and isinstance(event.data, ExternalInputRequest):
|
||||
elif event.type == "request_info" and isinstance(event.data, ExternalInputRequest):
|
||||
pending_request_id = event.request_id
|
||||
|
||||
print()
|
||||
|
||||
@@ -15,7 +15,7 @@ In a production scenario, you would integrate with a real UI or chat interface.
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from agent_framework import Workflow, WorkflowOutputEvent
|
||||
from agent_framework import Workflow
|
||||
from agent_framework.declarative import ExternalInputRequest, WorkflowFactory
|
||||
from agent_framework_declarative._workflows._handlers import TextOutputEvent
|
||||
|
||||
@@ -27,7 +27,7 @@ async def run_with_streaming(workflow: Workflow) -> None:
|
||||
|
||||
async for event in workflow.run({}, stream=True):
|
||||
# WorkflowOutputEvent wraps the actual output data
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
data = event.data
|
||||
if isinstance(data, TextOutputEvent):
|
||||
print(f"[Bot]: {data.text}")
|
||||
|
||||
@@ -15,7 +15,6 @@ Demonstrates sequential multi-agent pipeline:
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from agent_framework import WorkflowOutputEvent
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.declarative import WorkflowFactory
|
||||
from azure.identity import AzureCliCredential
|
||||
@@ -85,7 +84,7 @@ async def main() -> None:
|
||||
product = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours."
|
||||
|
||||
async for event in workflow.run(product, stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
print(f"{event.data}", end="", flush=True)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
@@ -22,7 +22,6 @@ Prerequisites:
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from agent_framework import WorkflowOutputEvent
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.declarative import WorkflowFactory
|
||||
from azure.identity import AzureCliCredential
|
||||
@@ -82,7 +81,7 @@ async def main() -> None:
|
||||
print("=" * 50)
|
||||
|
||||
async for event in workflow.run("How would you compute the value of PI?", stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
print(f"{event.data}", flush=True, end="")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
|
||||
@@ -11,12 +11,9 @@ from agent_framework import (
|
||||
AgentResponseUpdate,
|
||||
ChatMessage,
|
||||
Executor,
|
||||
RequestInfoEvent,
|
||||
Role,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
handler,
|
||||
response_handler,
|
||||
)
|
||||
@@ -30,13 +27,13 @@ Sample: AzureOpenAI Chat Agents in workflow with human feedback
|
||||
Pipeline layout:
|
||||
writer_agent -> Coordinator -> writer_agent -> Coordinator -> final_editor_agent -> Coordinator -> output
|
||||
|
||||
The writer agent drafts marketing copy. A custom executor emits a RequestInfoEvent so a human can comment,
|
||||
then relays the human guidance back into the conversation before the final editor agent produces the polished
|
||||
output.
|
||||
The writer agent drafts marketing copy. A custom executor emits a request_info event (type='request_info') so a
|
||||
human can comment, then relays the human guidance back into the conversation before the final editor agent
|
||||
produces the polished output.
|
||||
|
||||
Demonstrates:
|
||||
- Capturing agent responses in a custom executor.
|
||||
- Emitting RequestInfoEvent to request human input.
|
||||
- Emitting request_info events (type='request_info') to request human input.
|
||||
- Handling human feedback and routing it to the appropriate agents.
|
||||
|
||||
Prerequisites:
|
||||
@@ -103,8 +100,7 @@ class Coordinator(Executor):
|
||||
# Human approved the draft as-is; forward it unchanged.
|
||||
await ctx.send_message(
|
||||
AgentExecutorRequest(
|
||||
messages=original_request.conversation
|
||||
+ [ChatMessage(Role.USER, text="The draft is approved as-is.")],
|
||||
messages=original_request.conversation + [ChatMessage("user", text="The draft is approved as-is.")],
|
||||
should_respond=True,
|
||||
),
|
||||
target_id=self.final_editor_name,
|
||||
@@ -119,7 +115,7 @@ class Coordinator(Executor):
|
||||
"Rewrite the draft from the previous assistant message into a polished final version. "
|
||||
"Keep the response under 120 words and reflect any requested tone adjustments."
|
||||
)
|
||||
conversation.append(ChatMessage(Role.USER, text=instruction))
|
||||
conversation.append(ChatMessage("user", text=instruction))
|
||||
await ctx.send_message(
|
||||
AgentExecutorRequest(messages=conversation, should_respond=True), target_id=self.writer_name
|
||||
)
|
||||
@@ -132,9 +128,9 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
|
||||
|
||||
requests: list[tuple[str, DraftFeedbackRequest]] = []
|
||||
async for event in stream:
|
||||
if isinstance(event, RequestInfoEvent) and isinstance(event.data, DraftFeedbackRequest):
|
||||
if event.type == "request_info" and isinstance(event.data, DraftFeedbackRequest):
|
||||
requests.append((event.request_id, event.data))
|
||||
elif isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
|
||||
elif event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
# This workflow should only produce AgentResponseUpdate as outputs.
|
||||
# Streaming updates from an agent will be consecutive, because no two agents run simultaneously
|
||||
# in this workflow. So we can use last_author to format output nicely.
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ Demonstrate:
|
||||
Prerequisites:
|
||||
- Azure AI Agent Service configured, along with the required environment variables.
|
||||
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
|
||||
- Basic familiarity with WorkflowBuilder, edges, events, RequestInfoEvent, and streaming runs.
|
||||
- Basic familiarity with WorkflowBuilder, edges, events, request_info events (type='request_info'), and streaming runs.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
+3
-6
@@ -26,12 +26,10 @@ from collections.abc import AsyncIterable
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import (
|
||||
AgentExecutorResponse,
|
||||
ChatMessage,
|
||||
RequestInfoEvent,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
)
|
||||
from agent_framework._workflows._agent_executor import AgentExecutorResponse
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.orchestrations import AgentRequestInfoResponse, ConcurrentBuilder
|
||||
from azure.identity import AzureCliCredential
|
||||
@@ -97,11 +95,10 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
|
||||
|
||||
requests: dict[str, AgentExecutorResponse] = {}
|
||||
async for event in stream:
|
||||
if isinstance(event, RequestInfoEvent) and isinstance(event.data, AgentExecutorResponse):
|
||||
# Display agent output for review and potential modification
|
||||
if event.type == "request_info" and isinstance(event.data, AgentExecutorResponse):
|
||||
requests[event.request_id] = event.data
|
||||
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
# The output of the workflow comes from the aggregator and it's a single string
|
||||
print("\n" + "=" * 60)
|
||||
print("ANALYSIS COMPLETE")
|
||||
|
||||
+2
-4
@@ -29,9 +29,7 @@ from typing import cast
|
||||
from agent_framework import (
|
||||
AgentExecutorResponse,
|
||||
ChatMessage,
|
||||
RequestInfoEvent,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.orchestrations import AgentRequestInfoResponse, GroupChatBuilder
|
||||
@@ -43,10 +41,10 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
|
||||
|
||||
requests: dict[str, AgentExecutorResponse] = {}
|
||||
async for event in stream:
|
||||
if isinstance(event, RequestInfoEvent) and isinstance(event.data, AgentExecutorResponse):
|
||||
if event.type == "request_info" and isinstance(event.data, AgentExecutorResponse):
|
||||
requests[event.request_id] = event.data
|
||||
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
# The output of the workflow comes from the orchestrator and it's a list of messages
|
||||
print("\n" + "=" * 60)
|
||||
print("DISCUSSION COMPLETE")
|
||||
|
||||
+9
-9
@@ -10,11 +10,9 @@ from agent_framework import (
|
||||
AgentResponseUpdate,
|
||||
ChatMessage,
|
||||
Executor,
|
||||
RequestInfoEvent,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
handler,
|
||||
response_handler,
|
||||
)
|
||||
@@ -46,7 +44,7 @@ Prerequisites:
|
||||
|
||||
# How human-in-the-loop is achieved via `request_info` and `send_responses_streaming`:
|
||||
# - An executor (TurnManager) calls `ctx.request_info` with a payload (HumanFeedbackRequest).
|
||||
# - The workflow run pauses and emits a RequestInfoEvent with the payload and the request_id.
|
||||
# - The workflow run pauses and emits a with the payload and the request_id.
|
||||
# - The application captures the event, prompts the user, and collects replies.
|
||||
# - The application calls `send_responses_streaming` with a map of request_ids to replies.
|
||||
# - The workflow resumes, and the response is delivered to the executor method decorated with @response_handler.
|
||||
@@ -132,11 +130,13 @@ class TurnManager(Executor):
|
||||
return
|
||||
|
||||
# Provide feedback to the agent to try again.
|
||||
# We keep the agent's output strictly JSON to ensure stable parsing on the next turn.
|
||||
user_msg = ChatMessage(
|
||||
"user",
|
||||
text=(f'Feedback: {reply}. Return ONLY a JSON object matching the schema {{"guess": <int 1..10>}}.'),
|
||||
# response_format=GuessOutput on the agent ensures JSON output, so we just need to guide the logic.
|
||||
last_guess = original_request.prompt.split(": ")[1].split(".")[0]
|
||||
feedback_text = (
|
||||
f"Feedback: {reply}. Your last guess was {last_guess}. "
|
||||
f"Use this feedback to adjust and make your next guess (1-10)."
|
||||
)
|
||||
user_msg = ChatMessage("user", text=feedback_text)
|
||||
await ctx.send_message(AgentExecutorRequest(messages=[user_msg], should_respond=True))
|
||||
|
||||
|
||||
@@ -147,9 +147,9 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
|
||||
|
||||
requests: list[tuple[str, HumanFeedbackRequest]] = []
|
||||
async for event in stream:
|
||||
if isinstance(event, RequestInfoEvent) and isinstance(event.data, HumanFeedbackRequest):
|
||||
if event.type == "request_info" and isinstance(event.data, HumanFeedbackRequest):
|
||||
requests.append((event.request_id, event.data))
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
elif event.type == "output":
|
||||
if isinstance(event.data, AgentResponseUpdate):
|
||||
update = event.data
|
||||
response_id = update.response_id
|
||||
|
||||
+3
-5
@@ -13,7 +13,7 @@ using the standard request_info pattern for consistency.
|
||||
|
||||
Demonstrate:
|
||||
- Configuring request info with `.with_request_info()`
|
||||
- Handling RequestInfoEvent with AgentInputRequest data
|
||||
- Handling with AgentInputRequest data
|
||||
- Injecting responses back into the workflow via send_responses_streaming
|
||||
|
||||
Prerequisites:
|
||||
@@ -28,9 +28,7 @@ from typing import cast
|
||||
from agent_framework import (
|
||||
AgentExecutorResponse,
|
||||
ChatMessage,
|
||||
RequestInfoEvent,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.orchestrations import AgentRequestInfoResponse, SequentialBuilder
|
||||
@@ -42,10 +40,10 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
|
||||
|
||||
requests: dict[str, AgentExecutorResponse] = {}
|
||||
async for event in stream:
|
||||
if isinstance(event, RequestInfoEvent) and isinstance(event.data, AgentExecutorResponse):
|
||||
if event.type == "request_info" and isinstance(event.data, AgentExecutorResponse):
|
||||
requests[event.request_id] = event.data
|
||||
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
elif event.type == "output":
|
||||
# The output of the sequential workflow is a list of ChatMessages
|
||||
print("\n" + "=" * 60)
|
||||
print("WORKFLOW COMPLETE")
|
||||
|
||||
@@ -5,11 +5,8 @@ from typing import Any, cast
|
||||
|
||||
from agent_framework import (
|
||||
Executor,
|
||||
ExecutorCompletedEvent,
|
||||
ExecutorInvokedEvent,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
handler,
|
||||
)
|
||||
from typing_extensions import Never
|
||||
@@ -21,8 +18,8 @@ This sample demonstrates how to observe executor input and output data without m
|
||||
executor code. This is useful for debugging, logging, or building monitoring tools.
|
||||
|
||||
What this example shows:
|
||||
- ExecutorInvokedEvent.data contains the input message received by the executor
|
||||
- ExecutorCompletedEvent.data contains the messages sent via ctx.send_message()
|
||||
- executor_invoked events (type='executor_invoked') contain the input message in event.data
|
||||
- executor_completed events (type='executor_completed') contain the messages sent via ctx.send_message() in event.data
|
||||
- How to generically observe all executor I/O through workflow streaming events
|
||||
|
||||
This approach allows you to enable_instrumentation any workflow for observability without
|
||||
@@ -92,18 +89,18 @@ async def main() -> None:
|
||||
print("Running workflow with executor I/O observation...\n")
|
||||
|
||||
async for event in workflow.run("hello world", stream=True):
|
||||
if isinstance(event, ExecutorInvokedEvent):
|
||||
if event.type == "executor_invoked":
|
||||
# The input message received by the executor is in event.data
|
||||
print(f"[INVOKED] {event.executor_id}")
|
||||
print(f" Input: {format_io_data(event.data)}")
|
||||
|
||||
elif isinstance(event, ExecutorCompletedEvent):
|
||||
elif event.type == "executor_completed":
|
||||
# Messages sent via ctx.send_message() are in event.data
|
||||
print(f"[COMPLETED] {event.executor_id}")
|
||||
if event.data:
|
||||
print(f" Output: {format_io_data(event.data)}")
|
||||
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
elif event.type == "output":
|
||||
print(f"[WORKFLOW OUTPUT] {format_io_data(event.data)}")
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import cast
|
||||
|
||||
from agent_framework import (
|
||||
AgentRunUpdateEvent,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
MagenticBuilder,
|
||||
MagenticPlanReviewRequest,
|
||||
RequestInfoEvent,
|
||||
WorkflowOutputEvent,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
|
||||
"""
|
||||
Sample: Magentic Orchestration with Human Plan Review
|
||||
|
||||
This sample demonstrates how humans can review and provide feedback on plans
|
||||
generated by the Magentic workflow orchestrator. When plan review is enabled,
|
||||
the workflow requests human approval or revision before executing each plan.
|
||||
|
||||
Key concepts:
|
||||
- with_plan_review(): Enables human review of generated plans
|
||||
- MagenticPlanReviewRequest: The event type for plan review requests
|
||||
- Human can choose to: approve the plan or provide revision feedback
|
||||
|
||||
Plan review options:
|
||||
- approve(): Accept the proposed plan and continue execution
|
||||
- revise(feedback): Provide textual feedback to modify the plan
|
||||
|
||||
Prerequisites:
|
||||
- OpenAI credentials configured for `OpenAIChatClient`.
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
researcher_agent = ChatAgent(
|
||||
name="ResearcherAgent",
|
||||
description="Specialist in research and information gathering",
|
||||
instructions="You are a Researcher. You find information and gather facts.",
|
||||
chat_client=OpenAIChatClient(model_id="gpt-4o"),
|
||||
)
|
||||
|
||||
analyst_agent = ChatAgent(
|
||||
name="AnalystAgent",
|
||||
description="Data analyst who processes and summarizes research findings",
|
||||
instructions="You are an Analyst. You analyze findings and create summaries.",
|
||||
chat_client=OpenAIChatClient(model_id="gpt-4o"),
|
||||
)
|
||||
|
||||
manager_agent = ChatAgent(
|
||||
name="MagenticManager",
|
||||
description="Orchestrator that coordinates the workflow",
|
||||
instructions="You coordinate a team to complete tasks efficiently.",
|
||||
chat_client=OpenAIChatClient(model_id="gpt-4o"),
|
||||
)
|
||||
|
||||
print("\nBuilding Magentic Workflow with Human Plan Review...")
|
||||
|
||||
workflow = (
|
||||
MagenticBuilder()
|
||||
.participants([researcher_agent, analyst_agent])
|
||||
.with_manager(
|
||||
agent=manager_agent,
|
||||
max_round_count=10,
|
||||
max_stall_count=1,
|
||||
max_reset_count=2,
|
||||
)
|
||||
.with_plan_review() # Request human input for plan review
|
||||
.build()
|
||||
)
|
||||
|
||||
task = "Research sustainable aviation fuel technology and summarize the findings."
|
||||
|
||||
print(f"\nTask: {task}")
|
||||
print("\nStarting workflow execution...")
|
||||
print("=" * 60)
|
||||
|
||||
pending_request: RequestInfoEvent | None = None
|
||||
pending_responses: dict[str, object] | None = None
|
||||
output_event: WorkflowOutputEvent | None = None
|
||||
|
||||
while not output_event:
|
||||
if pending_responses is not None:
|
||||
stream = workflow.send_responses_streaming(pending_responses)
|
||||
else:
|
||||
stream = workflow.run(task, stream=True)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
import asyncio
|
||||
import random
|
||||
|
||||
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, WorkflowOutputEvent, handler
|
||||
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
|
||||
from typing_extensions import Never
|
||||
|
||||
"""
|
||||
@@ -87,7 +87,7 @@ async def main() -> None:
|
||||
# 2) Run the workflow
|
||||
output: list[int | float] | None = None
|
||||
async for event in workflow.run([random.randint(1, 100) for _ in range(10)], stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
output = event.data
|
||||
|
||||
if output is not None:
|
||||
|
||||
@@ -3,18 +3,14 @@
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
|
||||
from agent_framework import ( # Core chat primitives to build LLM requests
|
||||
from agent_framework import (
|
||||
AgentExecutorRequest, # The message bundle sent to an AgentExecutor
|
||||
AgentExecutorResponse, # The structured result returned by an AgentExecutor
|
||||
ChatAgent, # Tracing event for agent execution steps
|
||||
ChatMessage, # Chat message structure
|
||||
Executor, # Base class for custom Python executors
|
||||
ExecutorCompletedEvent,
|
||||
ExecutorInvokedEvent,
|
||||
Role, # Enum of chat roles (user, assistant, system)
|
||||
WorkflowBuilder, # Fluent builder for wiring the workflow graph
|
||||
WorkflowContext, # Per run context and event bus
|
||||
WorkflowOutputEvent, # Event emitted when workflow yields output
|
||||
handler, # Decorator to mark an Executor method as invokable
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
@@ -45,7 +41,7 @@ class DispatchToExperts(Executor):
|
||||
@handler
|
||||
async def dispatch(self, prompt: str, ctx: WorkflowContext[AgentExecutorRequest]) -> None:
|
||||
# Wrap the incoming prompt as a user message for each expert and request a response.
|
||||
initial_message = ChatMessage(Role.USER, text=prompt)
|
||||
initial_message = ChatMessage("user", text=prompt)
|
||||
await ctx.send_message(AgentExecutorRequest(messages=[initial_message], should_respond=True))
|
||||
|
||||
|
||||
@@ -143,12 +139,12 @@ async def main() -> None:
|
||||
async for event in workflow.run(
|
||||
"We are launching a new budget-friendly electric bike for urban commuters.", stream=True
|
||||
):
|
||||
if isinstance(event, ExecutorInvokedEvent):
|
||||
if event.type == "executor_invoked":
|
||||
# Show when executors are invoked and completed for lightweight observability.
|
||||
print(f"{event.executor_id} invoked")
|
||||
elif isinstance(event, ExecutorCompletedEvent):
|
||||
elif event.type == "executor_completed":
|
||||
print(f"{event.executor_id} completed")
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
elif event.type == "output":
|
||||
print("===== Final Aggregated Output =====")
|
||||
print(event.data)
|
||||
|
||||
|
||||
+2
-3
@@ -10,8 +10,7 @@ import aiofiles
|
||||
from agent_framework import (
|
||||
Executor, # Base class for custom workflow steps
|
||||
WorkflowBuilder, # Fluent builder for executors and edges
|
||||
WorkflowContext, # Per run context with workflow state and messaging
|
||||
WorkflowOutputEvent, # Event emitted when workflow yields output
|
||||
WorkflowContext, # Per run context with shared state and messaging
|
||||
WorkflowViz, # Utility to visualize a workflow graph
|
||||
handler, # Decorator to expose an Executor method as a step
|
||||
)
|
||||
@@ -332,7 +331,7 @@ async def main():
|
||||
# Step 4: Run the workflow with the raw text as input.
|
||||
async for event in workflow.run(raw_text, stream=True):
|
||||
print(f"Event: {event}")
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
print(f"Final Output: {event.data}")
|
||||
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Annotated, Any
|
||||
from typing import Annotated, Any, cast
|
||||
|
||||
from agent_framework import ChatMessage, WorkflowOutputEvent, tool
|
||||
from agent_framework import ChatMessage, tool
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework.orchestrations import SequentialBuilder
|
||||
from pydantic import Field
|
||||
@@ -27,7 +27,9 @@ Prerequisites:
|
||||
|
||||
|
||||
# Define tools that accept custom context via **kwargs
|
||||
# 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
|
||||
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
|
||||
@tool(approval_mode="never_require")
|
||||
def get_user_data(
|
||||
query: Annotated[str, Field(description="What user data to retrieve")],
|
||||
@@ -118,8 +120,8 @@ async def main() -> None:
|
||||
additional_function_arguments={"custom_data": custom_data, "user_token": user_token},
|
||||
stream=True,
|
||||
):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
output_data = event.data
|
||||
if event.type == "output":
|
||||
output_data = cast(list[ChatMessage], event.data)
|
||||
if isinstance(output_data, list):
|
||||
for item in output_data:
|
||||
if isinstance(item, ChatMessage) and item.text:
|
||||
|
||||
+7
-9
@@ -6,14 +6,12 @@ from typing import Annotated
|
||||
|
||||
from agent_framework import (
|
||||
ChatMessage,
|
||||
ConcurrentBuilder,
|
||||
Content,
|
||||
RequestInfoEvent,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework.orchestrations import ConcurrentBuilder
|
||||
|
||||
"""
|
||||
Sample: Concurrent Workflow with Tool Approval Requests
|
||||
@@ -36,7 +34,7 @@ agents may independently trigger approval requests.
|
||||
|
||||
Demonstrate:
|
||||
- Handling multiple approval requests from different agents in concurrent workflows.
|
||||
- Handling RequestInfoEvent during concurrent agent execution.
|
||||
- Handling during concurrent agent execution.
|
||||
- Understanding that approval pauses only the agent that triggered it, not all agents.
|
||||
|
||||
Prerequisites:
|
||||
@@ -89,12 +87,12 @@ def get_portfolio_balance() -> str:
|
||||
return "Portfolio: $50,000 invested, $10,000 cash available. Holdings: AAPL, GOOGL, MSFT."
|
||||
|
||||
|
||||
def _print_output(event: WorkflowOutputEvent) -> None:
|
||||
def _print_output(event: WorkflowEvent) -> None:
|
||||
if not event.data:
|
||||
raise ValueError("WorkflowOutputEvent has no data")
|
||||
raise ValueError("WorkflowEvent has no data")
|
||||
|
||||
if not isinstance(event.data, list) and not all(isinstance(msg, ChatMessage) for msg in event.data):
|
||||
raise ValueError("WorkflowOutputEvent data is not a list of ChatMessage")
|
||||
raise ValueError("WorkflowEvent data is not a list of ChatMessage")
|
||||
|
||||
messages: list[ChatMessage] = event.data # type: ignore
|
||||
|
||||
@@ -109,10 +107,10 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
|
||||
"""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):
|
||||
if event.type == "request_info" 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):
|
||||
elif event.type == "output":
|
||||
_print_output(event)
|
||||
|
||||
responses: dict[str, Content] = {}
|
||||
|
||||
+5
-8
@@ -7,14 +7,11 @@ from typing import Annotated, cast
|
||||
from agent_framework import (
|
||||
ChatMessage,
|
||||
Content,
|
||||
GroupChatBuilder,
|
||||
GroupChatState,
|
||||
RequestInfoEvent,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework.orchestrations import GroupChatBuilder, GroupChatState
|
||||
|
||||
"""
|
||||
Sample: Group Chat Workflow with Tool Approval Requests
|
||||
@@ -36,7 +33,7 @@ different agents have different levels of tool access.
|
||||
|
||||
Demonstrate:
|
||||
- Using set_select_speakers_func with agents that have approval-required tools.
|
||||
- Handling RequestInfoEvent in group chat scenarios.
|
||||
- Handling request_info events (type='request_info') in group chat scenarios.
|
||||
- Multi-round group chat with tool approval interruption and resumption.
|
||||
|
||||
Prerequisites:
|
||||
@@ -99,16 +96,16 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
|
||||
"""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):
|
||||
if event.type == "request_info" 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):
|
||||
elif event.type == "output":
|
||||
# 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
|
||||
speaker = msg.author_name or msg.role
|
||||
print(f"[{speaker}]: {msg.text}")
|
||||
|
||||
responses: dict[str, Content] = {}
|
||||
|
||||
+8
-8
@@ -7,13 +7,11 @@ from typing import Annotated, cast
|
||||
from agent_framework import (
|
||||
ChatMessage,
|
||||
Content,
|
||||
RequestInfoEvent,
|
||||
SequentialBuilder,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework.orchestrations import SequentialBuilder
|
||||
|
||||
"""
|
||||
Sample: Sequential Workflow with Tool Approval Requests
|
||||
@@ -26,7 +24,7 @@ This sample works as follows:
|
||||
1. A SequentialBuilder workflow is created with a single agent that has tools requiring approval.
|
||||
2. The agent receives a user task and determines it needs to call a sensitive tool.
|
||||
3. The tool call triggers a function_approval_request Content, pausing the workflow.
|
||||
4. The sample simulates human approval by responding to the RequestInfoEvent.
|
||||
4. The sample simulates human approval by responding to the .
|
||||
5. Once approved, the tool executes and the agent completes its response.
|
||||
6. The workflow outputs the final conversation with all messages.
|
||||
|
||||
@@ -36,7 +34,7 @@ requiring any additional builder configuration.
|
||||
|
||||
Demonstrate:
|
||||
- Using @tool(approval_mode="always_require") for sensitive operations.
|
||||
- Handling RequestInfoEvent with function_approval_request Content in sequential workflows.
|
||||
- Handling with function_approval_request Content in sequential workflows.
|
||||
- Resuming workflow execution after approval via send_responses_streaming.
|
||||
|
||||
Prerequisites:
|
||||
@@ -55,7 +53,9 @@ def execute_database_query(
|
||||
return f"Query executed successfully. Results: 3 rows affected by '{query}'"
|
||||
|
||||
|
||||
# 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 and
|
||||
# samples/getting_started/tools/function_tool_with_approval_and_threads.py.
|
||||
@tool(approval_mode="never_require")
|
||||
def get_database_schema() -> str:
|
||||
"""Get the current database schema. Does not require approval."""
|
||||
@@ -71,10 +71,10 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
|
||||
"""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):
|
||||
if event.type == "request_info" 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):
|
||||
elif event.type == "output":
|
||||
# The output of the workflow comes from the orchestrator and it's a list of messages
|
||||
print("\n" + "=" * 60)
|
||||
print("Workflow summary:")
|
||||
|
||||
@@ -6,7 +6,7 @@ import asyncio
|
||||
from collections.abc import Sequence
|
||||
from typing import cast
|
||||
|
||||
from agent_framework import ChatMessage, ConcurrentBuilder, WorkflowOutputEvent
|
||||
from agent_framework import ChatMessage, ConcurrentBuilderWorkflowEvent
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from semantic_kernel.agents import Agent, ChatCompletionAgent, ConcurrentOrchestration
|
||||
@@ -91,7 +91,7 @@ async def run_agent_framework_example(prompt: str) -> Sequence[list[ChatMessage]
|
||||
|
||||
outputs: list[list[ChatMessage]] = []
|
||||
async for event in workflow.run(prompt, stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
outputs.append(cast(list[ChatMessage], event.data))
|
||||
|
||||
return outputs
|
||||
|
||||
@@ -7,7 +7,7 @@ import sys
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, cast
|
||||
|
||||
from agent_framework import ChatAgent, ChatMessage, GroupChatBuilder, WorkflowOutputEvent
|
||||
from agent_framework import ChatAgent, ChatMessage, GroupChatBuilderWorkflowEvent
|
||||
from agent_framework.azure import AzureOpenAIChatClient, AzureOpenAIResponsesClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from semantic_kernel.agents import Agent, ChatCompletionAgent, GroupChatOrchestration
|
||||
@@ -240,7 +240,7 @@ async def run_agent_framework_example(task: str) -> str:
|
||||
|
||||
final_response = ""
|
||||
async for event in workflow.run(task, stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
data = event.data
|
||||
if isinstance(data, list) and len(data) > 0:
|
||||
# Get the final message from the conversation
|
||||
|
||||
@@ -8,12 +8,9 @@ from typing import cast
|
||||
|
||||
from agent_framework import (
|
||||
ChatMessage,
|
||||
HandoffBuilder,
|
||||
HandoffUserInputRequest,
|
||||
RequestInfoEvent,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
)
|
||||
from agent_framework.orchestrations import HandoffBuilder, HandoffUserInputRequest
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from semantic_kernel.agents import Agent, ChatCompletionAgent, HandoffOrchestration, OrchestrationHandoffs
|
||||
@@ -214,17 +211,17 @@ async def _drain_events(stream: AsyncIterable[WorkflowEvent]) -> list[WorkflowEv
|
||||
return [event async for event in stream]
|
||||
|
||||
|
||||
def _collect_handoff_requests(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
|
||||
requests: list[RequestInfoEvent] = []
|
||||
def _collect_handoff_requests(events: list[WorkflowEvent]) -> list[WorkflowEvent]:
|
||||
requests: list[WorkflowEvent] = []
|
||||
for event in events:
|
||||
if isinstance(event, RequestInfoEvent) and isinstance(event.data, HandoffUserInputRequest):
|
||||
if event.type == "request_info" and isinstance(event.data, HandoffUserInputRequest):
|
||||
requests.append(event)
|
||||
return requests
|
||||
|
||||
|
||||
def _extract_final_conversation(events: list[WorkflowEvent]) -> list[ChatMessage]:
|
||||
for event in events:
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
data = cast(list[ChatMessage], event.data)
|
||||
return data
|
||||
return []
|
||||
|
||||
@@ -6,7 +6,7 @@ import asyncio
|
||||
from collections.abc import Sequence
|
||||
from typing import cast
|
||||
|
||||
from agent_framework import ChatAgent, HostedCodeInterpreterTool, MagenticBuilder, WorkflowOutputEvent
|
||||
from agent_framework import ChatAgent, HostedCodeInterpreterTool, MagenticBuilderWorkflowEvent
|
||||
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
|
||||
from semantic_kernel.agents import (
|
||||
Agent,
|
||||
@@ -148,7 +148,7 @@ async def run_agent_framework_example(prompt: str) -> str | None:
|
||||
|
||||
final_text: str | None = None
|
||||
async for event in workflow.run(prompt, stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
final_text = cast(str, event.data)
|
||||
|
||||
return final_text
|
||||
|
||||
@@ -6,8 +6,9 @@ import asyncio
|
||||
from collections.abc import Sequence
|
||||
from typing import cast
|
||||
|
||||
from agent_framework import ChatMessage, SequentialBuilder, WorkflowOutputEvent
|
||||
from agent_framework import ChatMessage
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.orchestrations import SequentialBuilder
|
||||
from azure.identity import AzureCliCredential
|
||||
from semantic_kernel.agents import Agent, ChatCompletionAgent, SequentialOrchestration
|
||||
from semantic_kernel.agents.runtime import InProcessRuntime
|
||||
@@ -77,7 +78,7 @@ async def run_agent_framework_example(prompt: str) -> list[ChatMessage]:
|
||||
|
||||
conversation_outputs: list[list[ChatMessage]] = []
|
||||
async for event in workflow.run(prompt, stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
conversation_outputs.append(cast(list[ChatMessage], event.data))
|
||||
|
||||
return conversation_outputs[-1] if conversation_outputs else []
|
||||
|
||||
@@ -11,7 +11,7 @@ from typing import TYPE_CHECKING, ClassVar, cast
|
||||
######################################################################
|
||||
# region Agent Framework imports
|
||||
######################################################################
|
||||
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, WorkflowOutputEvent, handler
|
||||
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
######################################################################
|
||||
@@ -232,7 +232,7 @@ async def run_agent_framework_workflow_example() -> str | None:
|
||||
|
||||
final_text: str | None = None
|
||||
async for event in workflow.run(CommonEvents.START_PROCESS, stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
final_text = cast(str, event.data)
|
||||
|
||||
return final_text
|
||||
|
||||
@@ -17,7 +17,7 @@ from agent_framework import (
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowExecutor,
|
||||
WorkflowOutputEvent,
|
||||
|
||||
handler,
|
||||
)
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -257,7 +257,7 @@ async def run_agent_framework_nested_workflow(initial_message: str) -> Sequence[
|
||||
|
||||
results: list[str] = []
|
||||
async for event in outer_workflow.run(initial_message, stream=True):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
if event.type == "output":
|
||||
results.append(cast(str, event.data))
|
||||
|
||||
return results
|
||||
|
||||
Reference in New Issue
Block a user