mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Workflow run state and structured error events, sample updates, tests (#725)
* Add workflow event types to surface workflow state, status, and failures. * Address PR feedback * Updates to use new workflow run state enums
This commit is contained in:
committed by
GitHub
Unverified
parent
e7cd03b32e
commit
68b76e6726
@@ -104,11 +104,14 @@ async def main():
|
||||
# provides the WorkflowCompletedEvent emitted by the terminal node.
|
||||
events = await workflow.run("hello world")
|
||||
print(events.get_completed_event())
|
||||
# Summarize the final run state (e.g., COMPLETED)
|
||||
print("Final state:", events.get_final_state())
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
WorkflowCompletedEvent(data=DLROW OLLEH)
|
||||
Final state: WorkflowRunState.COMPLETED
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -59,6 +59,8 @@ async def main():
|
||||
print(f"{event.executor_id}: {event.data}")
|
||||
|
||||
print(f"{'=' * 60}\n{events.get_completed_event()}")
|
||||
# Summarize the final run state (e.g., COMPLETED)
|
||||
print("Final state:", events.get_final_state())
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
@@ -4,7 +4,17 @@ import asyncio
|
||||
|
||||
from agent_framework import ChatAgent, ChatMessage
|
||||
from agent_framework.azure import AzureChatClient
|
||||
from agent_framework.workflow import Executor, WorkflowBuilder, WorkflowCompletedEvent, WorkflowContext, handler
|
||||
from agent_framework.workflow import (
|
||||
Executor,
|
||||
ExecutorFailedEvent,
|
||||
WorkflowBuilder,
|
||||
WorkflowCompletedEvent,
|
||||
WorkflowContext,
|
||||
WorkflowFailedEvent,
|
||||
WorkflowRunState,
|
||||
WorkflowStatusEvent,
|
||||
handler,
|
||||
)
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""
|
||||
@@ -107,20 +117,41 @@ async def main():
|
||||
workflow = WorkflowBuilder().set_start_executor(writer).add_edge(writer, reviewer).build()
|
||||
|
||||
# Run the workflow with the user's initial message and stream events as they occur.
|
||||
# Events include executor invoke and completion, as well as the terminal WorkflowCompletedEvent.
|
||||
# In addition to executor events and WorkflowCompletedEvent, this also surfaces run-state and errors.
|
||||
async for event in workflow.run_stream(
|
||||
ChatMessage(role="user", text="Create a slogan for a new electric SUV that is affordable and fun to drive.")
|
||||
):
|
||||
print(event)
|
||||
if isinstance(event, WorkflowStatusEvent):
|
||||
if event.state == WorkflowRunState.IN_PROGRESS:
|
||||
print("State: IN_PROGRESS")
|
||||
elif event.state == WorkflowRunState.COMPLETED:
|
||||
print("State: COMPLETED")
|
||||
elif event.state == WorkflowRunState.IN_PROGRESS_PENDING_REQUESTS:
|
||||
print("State: IN_PROGRESS_PENDING_REQUESTS (requests in flight)")
|
||||
elif event.state == WorkflowRunState.IDLE:
|
||||
print("State: IDLE (no active work)")
|
||||
elif event.state == WorkflowRunState.IDLE_WITH_PENDING_REQUESTS:
|
||||
print("State: IDLE_WITH_PENDING_REQUESTS (prompt user or UI now)")
|
||||
else:
|
||||
print(f"State: {event.state}")
|
||||
elif isinstance(event, ExecutorFailedEvent):
|
||||
print(f"Executor failed: {event.executor_id} {event.details.error_type}: {event.details.message}")
|
||||
elif isinstance(event, WorkflowFailedEvent):
|
||||
details = event.details
|
||||
print(f"Workflow failed: {details.error_type}: {details.message}")
|
||||
else:
|
||||
print(event)
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
State: IN_PROGRESS
|
||||
ExecutorInvokeEvent(executor_id=writer)
|
||||
ExecutorCompletedEvent(executor_id=writer)
|
||||
ExecutorInvokeEvent(executor_id=reviewer)
|
||||
WorkflowCompletedEvent(data=Drive the Future. Affordable Adventure, Electrified.)
|
||||
ExecutorCompletedEvent(executor_id=reviewer)
|
||||
State: COMPLETED
|
||||
"""
|
||||
|
||||
|
||||
|
||||
+36
-11
@@ -3,12 +3,13 @@
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
|
||||
from agent_framework import AgentProtocol, ChatMessage, Role
|
||||
from agent_framework import ChatMessage, Role
|
||||
from agent_framework.azure import AzureChatClient
|
||||
from agent_framework.workflow import (
|
||||
AgentExecutor, # Wraps an agent so it can run inside a workflow
|
||||
AgentExecutor, # Executor that runs the agent
|
||||
AgentExecutorRequest, # Message bundle sent to an AgentExecutor
|
||||
AgentExecutorResponse, # Result returned by an AgentExecutor
|
||||
Executor,
|
||||
RequestInfoEvent, # Event emitted when human input is requested
|
||||
RequestInfoExecutor, # Special executor that collects human input out of band
|
||||
RequestInfoMessage, # Base class for request payloads sent to RequestInfoExecutor
|
||||
@@ -16,6 +17,8 @@ from agent_framework.workflow import (
|
||||
WorkflowBuilder, # Fluent builder for assembling the graph
|
||||
WorkflowCompletedEvent, # Terminal event used to finish the workflow
|
||||
WorkflowContext, # Per run context and event bus
|
||||
WorkflowRunState, # Enum of workflow run states
|
||||
WorkflowStatusEvent, # Event emitted on run state changes
|
||||
handler, # Decorator to expose an Executor method as a step
|
||||
)
|
||||
from azure.identity import AzureCliCredential
|
||||
@@ -73,7 +76,7 @@ class GuessOutput(BaseModel):
|
||||
guess: int
|
||||
|
||||
|
||||
class TurnManager(AgentExecutor):
|
||||
class TurnManager(Executor):
|
||||
"""Coordinates turns between the agent and the human.
|
||||
|
||||
Responsibilities:
|
||||
@@ -82,8 +85,8 @@ class TurnManager(AgentExecutor):
|
||||
- After each human reply, either finish the game or prompt the agent again with feedback.
|
||||
"""
|
||||
|
||||
def __init__(self, agent: AgentProtocol, id: str | None = None):
|
||||
super().__init__(agent, id=id)
|
||||
def __init__(self, id: str | None = None):
|
||||
super().__init__(id=id)
|
||||
|
||||
@handler
|
||||
async def start(self, _: str, ctx: WorkflowContext[AgentExecutorRequest]) -> None:
|
||||
@@ -166,9 +169,10 @@ async def main() -> None:
|
||||
response_format=GuessOutput,
|
||||
)
|
||||
|
||||
# Build a simple loop: TurnManager <-> RequestInfoExecutor.
|
||||
# TurnManager runs the agent, asks the human, processes feedback, and either finishes or repeats.
|
||||
turn_manager = TurnManager(agent=agent, id="turn_manager")
|
||||
# Build a simple loop: TurnManager <-> AgentExecutor <-> RequestInfoExecutor.
|
||||
# TurnManager coordinates, AgentExecutor runs the model, RequestInfoExecutor gathers human replies.
|
||||
turn_manager = TurnManager(id="turn_manager")
|
||||
agent_exec = AgentExecutor(agent=agent, id="agent")
|
||||
|
||||
# Naming note:
|
||||
# This variable is currently named hitl for historical reasons. The name can feel ambiguous or magical.
|
||||
@@ -179,9 +183,10 @@ async def main() -> None:
|
||||
top_builder = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(turn_manager)
|
||||
.add_edge(turn_manager, turn_manager) # TurnManager executes its own agent step
|
||||
.add_edge(turn_manager, agent_exec) # Ask agent to make/adjust a guess
|
||||
.add_edge(agent_exec, turn_manager) # Agent's response comes back to coordinator
|
||||
.add_edge(turn_manager, hitl) # Ask human for guidance
|
||||
.add_edge(hitl, turn_manager) # Feed human guidance back to the agent turn manager
|
||||
.add_edge(hitl, turn_manager) # Feed human guidance back to coordinator
|
||||
)
|
||||
|
||||
# Build the workflow (no checkpointing in this minimal sample).
|
||||
@@ -206,6 +211,10 @@ async def main() -> None:
|
||||
stream = (
|
||||
workflow.send_responses_streaming(pending_responses) if pending_responses else workflow.run_stream("start")
|
||||
)
|
||||
# Collect events for this turn. Among these you may see WorkflowStatusEvent
|
||||
# with state IDLE_WITH_PENDING_REQUESTS when the workflow pauses for
|
||||
# human input, preceded by IN_PROGRESS_PENDING_REQUESTS as requests are
|
||||
# emitted.
|
||||
events = [event async for event in stream]
|
||||
pending_responses = None
|
||||
|
||||
@@ -219,6 +228,22 @@ async def main() -> None:
|
||||
requests.append((event.request_id, event.data.prompt))
|
||||
# Other events are ignored for brevity.
|
||||
|
||||
# Detect run state transitions for a better developer experience.
|
||||
pending_status = any(
|
||||
isinstance(e, WorkflowStatusEvent)
|
||||
and e.state == WorkflowRunState.IN_PROGRESS_PENDING_REQUESTS
|
||||
for e in events
|
||||
)
|
||||
idle_with_requests = any(
|
||||
isinstance(e, WorkflowStatusEvent)
|
||||
and e.state == WorkflowRunState.IDLE_WITH_PENDING_REQUESTS
|
||||
for e in events
|
||||
)
|
||||
if pending_status:
|
||||
print("State: IN_PROGRESS_PENDING_REQUESTS (requests outstanding)")
|
||||
if idle_with_requests:
|
||||
print("State: IDLE_WITH_PENDING_REQUESTS (awaiting human input)")
|
||||
|
||||
# If we have any human requests, prompt the user and prepare responses.
|
||||
if requests and not completed:
|
||||
responses: dict[str, str] = {}
|
||||
@@ -227,7 +252,7 @@ async def main() -> None:
|
||||
print(f"HITL> {prompt}")
|
||||
# Instructional print already appears above. The input line below is the user entry point.
|
||||
# If desired, you can add more guidance here, but keep it concise.
|
||||
answer = input("Enter higher/lower/correct/exit: ").lower()
|
||||
answer = input("Enter higher/lower/correct/exit: ").lower() # noqa: ASYNC250
|
||||
if answer == "exit":
|
||||
print("Exiting...")
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user