mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python [BREAKING]: support magentic agent tool call approvals and plan stalling HITL behavior (#2569)
* Provide way for HITL with magentic * support tool call approvals and hitl stall replan * human plan intervention sample * Clean up * Improve loging * updates
This commit is contained in:
committed by
GitHub
Unverified
parent
292a2078fa
commit
19ac6e57c0
@@ -105,6 +105,8 @@ For additional observability samples in Agent Framework, see the [observability
|
||||
| Handoff (Return-to-Previous) | [orchestration/handoff_return_to_previous.py](./orchestration/handoff_return_to_previous.py) | Return-to-previous routing: after user input, routes back to the previous specialist instead of coordinator using `.enable_return_to_previous()` |
|
||||
| Magentic Workflow (Multi-Agent) | [orchestration/magentic.py](./orchestration/magentic.py) | Orchestrate multiple agents with Magentic manager and streaming |
|
||||
| Magentic + Human Plan Review | [orchestration/magentic_human_plan_update.py](./orchestration/magentic_human_plan_update.py) | Human reviews/updates the plan before execution |
|
||||
| Magentic + Human Stall Intervention | [orchestration/magentic_human_replan.py](./orchestration/magentic_human_replan.py) | Human intervenes when workflow stalls with `with_human_input_on_stall()` |
|
||||
| Magentic + Agent Clarification | [orchestration/magentic_agent_clarification.py](./orchestration/magentic_agent_clarification.py) | Agents ask clarifying questions via `ask_user` tool with `@ai_function(approval_mode="always_require")` |
|
||||
| Magentic + Checkpoint Resume | [orchestration/magentic_checkpoint.py](./orchestration/magentic_checkpoint.py) | Resume Magentic orchestration from saved checkpoints |
|
||||
| Sequential Orchestration (Agents) | [orchestration/sequential_agents.py](./orchestration/sequential_agents.py) | Chain agents sequentially with shared conversation context |
|
||||
| Sequential Orchestration (Custom Executor) | [orchestration/sequential_custom_executors.py](./orchestration/sequential_custom_executors.py) | Mix agents with a summarizer that appends a compact summary |
|
||||
|
||||
@@ -48,13 +48,21 @@ async def main() -> None:
|
||||
tools=HostedCodeInterpreterTool(),
|
||||
)
|
||||
|
||||
# Create a manager agent for orchestration
|
||||
manager_agent = ChatAgent(
|
||||
name="MagenticManager",
|
||||
description="Orchestrator that coordinates the research and coding workflow",
|
||||
instructions="You coordinate a team to complete complex tasks efficiently.",
|
||||
chat_client=OpenAIChatClient(),
|
||||
)
|
||||
|
||||
print("\nBuilding Magentic Workflow...")
|
||||
|
||||
workflow = (
|
||||
MagenticBuilder()
|
||||
.participants(researcher=researcher_agent, coder=coder_agent)
|
||||
.with_standard_manager(
|
||||
chat_client=OpenAIChatClient(),
|
||||
agent=manager_agent,
|
||||
max_round_count=10,
|
||||
max_stall_count=3,
|
||||
max_reset_count=2,
|
||||
|
||||
@@ -65,6 +65,14 @@ async def main() -> None:
|
||||
tools=HostedCodeInterpreterTool(),
|
||||
)
|
||||
|
||||
# Create a manager agent for orchestration
|
||||
manager_agent = ChatAgent(
|
||||
name="MagenticManager",
|
||||
description="Orchestrator that coordinates the research and coding workflow",
|
||||
instructions="You coordinate a team to complete complex tasks efficiently.",
|
||||
chat_client=OpenAIChatClient(),
|
||||
)
|
||||
|
||||
print("\nBuilding Magentic Workflow...")
|
||||
|
||||
# State used by on_agent_stream callback
|
||||
@@ -75,7 +83,7 @@ async def main() -> None:
|
||||
MagenticBuilder()
|
||||
.participants(researcher=researcher_agent, coder=coder_agent)
|
||||
.with_standard_manager(
|
||||
chat_client=OpenAIChatClient(),
|
||||
agent=manager_agent,
|
||||
max_round_count=10,
|
||||
max_stall_count=3,
|
||||
max_reset_count=2,
|
||||
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Annotated, cast
|
||||
|
||||
from agent_framework import (
|
||||
MAGENTIC_EVENT_TYPE_AGENT_DELTA,
|
||||
MAGENTIC_EVENT_TYPE_ORCHESTRATOR,
|
||||
AgentRunUpdateEvent,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
MagenticBuilder,
|
||||
MagenticHumanInterventionDecision,
|
||||
MagenticHumanInterventionKind,
|
||||
MagenticHumanInterventionReply,
|
||||
MagenticHumanInterventionRequest,
|
||||
RequestInfoEvent,
|
||||
WorkflowOutputEvent,
|
||||
ai_function,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
|
||||
logging.basicConfig(level=logging.WARNING)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
"""
|
||||
Sample: Agent Clarification via Tool Calls in Magentic Workflows
|
||||
|
||||
This sample demonstrates how agents can ask clarifying questions to users during
|
||||
execution via the HITL (Human-in-the-Loop) mechanism.
|
||||
|
||||
Scenario: "Onboard Jessica Smith"
|
||||
- User provides an ambiguous task: "Onboard Jessica Smith"
|
||||
- The onboarding agent recognizes missing information and uses the ask_user tool
|
||||
- The ask_user call surfaces as a TOOL_APPROVAL request via RequestInfoEvent
|
||||
- User provides the answer (e.g., "Engineering, Software Engineer")
|
||||
- The answer is fed back to the agent as a FunctionResultContent
|
||||
- Agent continues execution with the clarified information
|
||||
|
||||
How it works:
|
||||
1. Agent has an `ask_user` tool decorated with `@ai_function(approval_mode="always_require")`
|
||||
2. When agent calls `ask_user`, it surfaces as a FunctionApprovalRequestContent
|
||||
3. MagenticAgentExecutor converts this to a MagenticHumanInterventionRequest(kind=TOOL_APPROVAL)
|
||||
4. User provides answer via MagenticHumanInterventionReply with response_text
|
||||
5. The response_text becomes the function result fed back to the agent
|
||||
6. Agent receives the result and continues processing
|
||||
|
||||
Prerequisites:
|
||||
- OpenAI credentials configured for `OpenAIChatClient`.
|
||||
"""
|
||||
|
||||
|
||||
@ai_function(approval_mode="always_require")
|
||||
def ask_user(question: Annotated[str, "The question to ask the user for clarification"]) -> str:
|
||||
"""Ask the user a clarifying question to gather missing information.
|
||||
|
||||
Use this tool when you need additional information from the user to complete
|
||||
your task effectively. The user's response will be returned so you can
|
||||
continue with your work.
|
||||
|
||||
Args:
|
||||
question: The question to ask the user
|
||||
|
||||
Returns:
|
||||
The user's response to the question
|
||||
"""
|
||||
# This function body is a placeholder - the actual interaction happens via HITL.
|
||||
# When the agent calls this tool:
|
||||
# 1. The tool call surfaces as a FunctionApprovalRequestContent
|
||||
# 2. MagenticAgentExecutor detects this and emits a HITL request
|
||||
# 3. The user provides their answer
|
||||
# 4. The answer is fed back as the function result
|
||||
return f"User was asked: {question}"
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# Create an onboarding agent that asks clarifying questions
|
||||
onboarding_agent = ChatAgent(
|
||||
name="OnboardingAgent",
|
||||
description="HR specialist who handles employee onboarding",
|
||||
instructions=(
|
||||
"You are an HR Onboarding Specialist. Your job is to onboard new employees.\n\n"
|
||||
"IMPORTANT: When given an onboarding request, you MUST gather the following "
|
||||
"information before proceeding:\n"
|
||||
"1. Department (e.g., Engineering, Sales, Marketing)\n"
|
||||
"2. Role/Title (e.g., Software Engineer, Account Executive)\n"
|
||||
"3. Start date (if not specified)\n"
|
||||
"4. Manager's name (if known)\n\n"
|
||||
"Use the ask_user tool to request ANY missing information. "
|
||||
"Do not proceed with onboarding until you have at least the department and role.\n\n"
|
||||
"Once you have the information, create an onboarding plan."
|
||||
),
|
||||
chat_client=OpenAIChatClient(model_id="gpt-4o"),
|
||||
tools=[ask_user], # Tool decorated with @ai_function(approval_mode="always_require")
|
||||
)
|
||||
|
||||
# Create a manager agent
|
||||
manager_agent = ChatAgent(
|
||||
name="MagenticManager",
|
||||
description="Orchestrator that coordinates the onboarding workflow",
|
||||
instructions="You coordinate a team to complete HR tasks efficiently.",
|
||||
chat_client=OpenAIChatClient(model_id="gpt-4o"),
|
||||
)
|
||||
|
||||
print("\nBuilding Magentic Workflow with Agent Clarification...")
|
||||
|
||||
workflow = (
|
||||
MagenticBuilder()
|
||||
.participants(onboarding=onboarding_agent)
|
||||
.with_standard_manager(
|
||||
agent=manager_agent,
|
||||
max_round_count=10,
|
||||
max_stall_count=3,
|
||||
max_reset_count=2,
|
||||
)
|
||||
.build()
|
||||
)
|
||||
|
||||
# Ambiguous task - agent should ask for clarification
|
||||
task = "Onboard Jessica Smith"
|
||||
|
||||
print(f"\nTask: {task}")
|
||||
print("(This is intentionally vague - the agent should ask for more details)")
|
||||
print("\nStarting workflow execution...")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
pending_request: RequestInfoEvent | None = None
|
||||
pending_responses: dict[str, object] | None = None
|
||||
completed = False
|
||||
workflow_output: str | None = None
|
||||
|
||||
last_stream_agent_id: str | None = None
|
||||
stream_line_open: bool = False
|
||||
|
||||
while not completed:
|
||||
if pending_responses is not None:
|
||||
stream = workflow.send_responses_streaming(pending_responses)
|
||||
else:
|
||||
stream = workflow.run_stream(task)
|
||||
|
||||
async for event in stream:
|
||||
if isinstance(event, AgentRunUpdateEvent):
|
||||
props = event.data.additional_properties if event.data else None
|
||||
event_type = props.get("magentic_event_type") if props else None
|
||||
|
||||
if event_type == MAGENTIC_EVENT_TYPE_ORCHESTRATOR:
|
||||
kind = props.get("orchestrator_message_kind", "") if props else ""
|
||||
text = event.data.text if event.data else ""
|
||||
if stream_line_open:
|
||||
print()
|
||||
stream_line_open = False
|
||||
print(f"\n[ORCHESTRATOR: {kind}]\n{text}\n{'-' * 40}")
|
||||
elif event_type == MAGENTIC_EVENT_TYPE_AGENT_DELTA:
|
||||
agent_id = props.get("agent_id", "unknown") if props else "unknown"
|
||||
if last_stream_agent_id != agent_id or not stream_line_open:
|
||||
if stream_line_open:
|
||||
print()
|
||||
print(f"\n[{agent_id}]: ", end="", flush=True)
|
||||
last_stream_agent_id = agent_id
|
||||
stream_line_open = True
|
||||
if event.data and event.data.text:
|
||||
print(event.data.text, end="", flush=True)
|
||||
|
||||
elif isinstance(event, RequestInfoEvent) and event.request_type is MagenticHumanInterventionRequest:
|
||||
if stream_line_open:
|
||||
print()
|
||||
stream_line_open = False
|
||||
pending_request = event
|
||||
req = cast(MagenticHumanInterventionRequest, event.data)
|
||||
|
||||
if req.kind == MagenticHumanInterventionKind.TOOL_APPROVAL:
|
||||
print("\n" + "=" * 60)
|
||||
print("AGENT ASKING FOR CLARIFICATION")
|
||||
print("=" * 60)
|
||||
print(f"\nAgent: {req.agent_id}")
|
||||
print(f"Question: {req.prompt}")
|
||||
if req.context:
|
||||
print(f"Details: {req.context}")
|
||||
print()
|
||||
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
if stream_line_open:
|
||||
print()
|
||||
stream_line_open = False
|
||||
workflow_output = event.data if event.data else None
|
||||
completed = True
|
||||
|
||||
if stream_line_open:
|
||||
print()
|
||||
stream_line_open = False
|
||||
pending_responses = None
|
||||
|
||||
if pending_request is not None:
|
||||
req = cast(MagenticHumanInterventionRequest, pending_request.data)
|
||||
|
||||
if req.kind == MagenticHumanInterventionKind.TOOL_APPROVAL:
|
||||
# Agent is asking for clarification
|
||||
print("Please provide your answer:")
|
||||
answer = input("> ").strip() # noqa: ASYNC250
|
||||
|
||||
if answer.lower() == "exit":
|
||||
print("Exiting workflow...")
|
||||
return
|
||||
|
||||
# Send the answer back - it will be fed to the agent as the function result
|
||||
reply = MagenticHumanInterventionReply(
|
||||
decision=MagenticHumanInterventionDecision.APPROVE,
|
||||
response_text=answer if answer else "No additional information provided.",
|
||||
)
|
||||
pending_responses = {pending_request.request_id: reply}
|
||||
pending_request = None
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("WORKFLOW COMPLETED")
|
||||
print("=" * 60)
|
||||
if workflow_output:
|
||||
messages = cast(list[ChatMessage], workflow_output)
|
||||
if messages:
|
||||
final_msg = messages[-1]
|
||||
print(f"\nFinal Result:\n{final_msg.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Workflow execution failed: {e}")
|
||||
logger.exception("Workflow exception", exc_info=e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -8,9 +8,10 @@ from agent_framework import (
|
||||
ChatAgent,
|
||||
FileCheckpointStorage,
|
||||
MagenticBuilder,
|
||||
MagenticPlanReviewDecision,
|
||||
MagenticPlanReviewReply,
|
||||
MagenticPlanReviewRequest,
|
||||
MagenticHumanInterventionDecision,
|
||||
MagenticHumanInterventionKind,
|
||||
MagenticHumanInterventionReply,
|
||||
MagenticHumanInterventionRequest,
|
||||
RequestInfoEvent,
|
||||
WorkflowCheckpoint,
|
||||
WorkflowOutputEvent,
|
||||
@@ -69,6 +70,14 @@ def build_workflow(checkpoint_storage: FileCheckpointStorage):
|
||||
chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()),
|
||||
)
|
||||
|
||||
# Create a manager agent for orchestration
|
||||
manager_agent = ChatAgent(
|
||||
name="MagenticManager",
|
||||
description="Orchestrator that coordinates the research and writing workflow",
|
||||
instructions="You coordinate a team to complete complex tasks efficiently.",
|
||||
chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()),
|
||||
)
|
||||
|
||||
# The builder wires in the Magentic orchestrator, sets the plan review path, and
|
||||
# stores the checkpoint backend so the runtime knows where to persist snapshots.
|
||||
return (
|
||||
@@ -76,7 +85,7 @@ def build_workflow(checkpoint_storage: FileCheckpointStorage):
|
||||
.participants(researcher=researcher, writer=writer)
|
||||
.with_plan_review()
|
||||
.with_standard_manager(
|
||||
chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()),
|
||||
agent=manager_agent,
|
||||
max_round_count=10,
|
||||
max_stall_count=3,
|
||||
)
|
||||
@@ -103,9 +112,12 @@ async def main() -> None:
|
||||
# the plan for human review.
|
||||
plan_review_request_id: str | None = None
|
||||
async for event in workflow.run_stream(TASK):
|
||||
if isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest:
|
||||
plan_review_request_id = event.request_id
|
||||
print(f"Captured plan review request: {plan_review_request_id}")
|
||||
if isinstance(event, RequestInfoEvent) and event.request_type is MagenticHumanInterventionRequest:
|
||||
request = event.data
|
||||
if isinstance(request, MagenticHumanInterventionRequest):
|
||||
if request.kind == MagenticHumanInterventionKind.PLAN_REVIEW:
|
||||
plan_review_request_id = event.request_id
|
||||
print(f"Captured plan review request: {plan_review_request_id}")
|
||||
|
||||
if isinstance(event, WorkflowStatusEvent) and event.state is WorkflowRunState.IDLE_WITH_PENDING_REQUESTS:
|
||||
break
|
||||
@@ -137,12 +149,12 @@ async def main() -> None:
|
||||
resumed_workflow = build_workflow(checkpoint_storage)
|
||||
|
||||
# Construct an approval reply to supply when the plan review request is re-emitted.
|
||||
approval = MagenticPlanReviewReply(decision=MagenticPlanReviewDecision.APPROVE)
|
||||
approval = MagenticHumanInterventionReply(decision=MagenticHumanInterventionDecision.APPROVE)
|
||||
|
||||
# Resume execution and capture the re-emitted plan review request.
|
||||
request_info_event: RequestInfoEvent | None = None
|
||||
async for event in resumed_workflow.run_stream(checkpoint_id=resume_checkpoint.checkpoint_id):
|
||||
if isinstance(event, RequestInfoEvent) and isinstance(event.data, MagenticPlanReviewRequest):
|
||||
if isinstance(event, RequestInfoEvent) and isinstance(event.data, MagenticHumanInterventionRequest):
|
||||
request_info_event = event
|
||||
|
||||
if request_info_event is None:
|
||||
|
||||
+55
-18
@@ -11,9 +11,10 @@ from agent_framework import (
|
||||
ChatAgent,
|
||||
HostedCodeInterpreterTool,
|
||||
MagenticBuilder,
|
||||
MagenticPlanReviewDecision,
|
||||
MagenticPlanReviewReply,
|
||||
MagenticPlanReviewRequest,
|
||||
MagenticHumanInterventionDecision,
|
||||
MagenticHumanInterventionKind,
|
||||
MagenticHumanInterventionReply,
|
||||
MagenticHumanInterventionRequest,
|
||||
RequestInfoEvent,
|
||||
WorkflowOutputEvent,
|
||||
)
|
||||
@@ -66,6 +67,14 @@ async def main() -> None:
|
||||
tools=HostedCodeInterpreterTool(),
|
||||
)
|
||||
|
||||
# Create a manager agent for the orchestration
|
||||
manager_agent = ChatAgent(
|
||||
name="MagenticManager",
|
||||
description="Orchestrator that coordinates the research and coding workflow",
|
||||
instructions="You coordinate a team to complete complex tasks efficiently.",
|
||||
chat_client=OpenAIChatClient(),
|
||||
)
|
||||
|
||||
# Callbacks
|
||||
def on_exception(exception: Exception) -> None:
|
||||
print(f"Exception occurred: {exception}")
|
||||
@@ -80,7 +89,7 @@ async def main() -> None:
|
||||
MagenticBuilder()
|
||||
.participants(researcher=researcher_agent, coder=coder_agent)
|
||||
.with_standard_manager(
|
||||
chat_client=OpenAIChatClient(),
|
||||
agent=manager_agent,
|
||||
max_round_count=10,
|
||||
max_stall_count=3,
|
||||
max_reset_count=2,
|
||||
@@ -103,7 +112,7 @@ async def main() -> None:
|
||||
|
||||
try:
|
||||
pending_request: RequestInfoEvent | None = None
|
||||
pending_responses: dict[str, MagenticPlanReviewReply] | None = None
|
||||
pending_responses: dict[str, MagenticHumanInterventionReply] | None = None
|
||||
completed = False
|
||||
workflow_output: str | None = None
|
||||
|
||||
@@ -134,11 +143,12 @@ async def main() -> None:
|
||||
stream_line_open = True
|
||||
if event.data and event.data.text:
|
||||
print(event.data.text, end="", flush=True)
|
||||
elif isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest:
|
||||
pending_request = event
|
||||
review_req = cast(MagenticPlanReviewRequest, event.data)
|
||||
if review_req.plan_text:
|
||||
print(f"\n=== PLAN REVIEW REQUEST ===\n{review_req.plan_text}\n")
|
||||
elif isinstance(event, RequestInfoEvent) and event.request_type is MagenticHumanInterventionRequest:
|
||||
request = cast(MagenticHumanInterventionRequest, event.data)
|
||||
if request.kind == MagenticHumanInterventionKind.PLAN_REVIEW:
|
||||
pending_request = event
|
||||
if request.plan_text:
|
||||
print(f"\n=== PLAN REVIEW REQUEST ===\n{request.plan_text}\n")
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
# Capture workflow output during streaming
|
||||
workflow_output = str(event.data) if event.data else None
|
||||
@@ -154,21 +164,48 @@ async def main() -> None:
|
||||
# Get human input for plan review decision
|
||||
print("Plan review options:")
|
||||
print("1. approve - Approve the plan as-is")
|
||||
print("2. revise - Request revision of the plan")
|
||||
print("3. exit - Exit the workflow")
|
||||
print("2. approve with comments - Approve with feedback for the manager")
|
||||
print("3. revise - Request revision with your feedback")
|
||||
print("4. edit - Directly edit the plan text")
|
||||
print("5. exit - Exit the workflow")
|
||||
|
||||
while True:
|
||||
choice = input("Enter your choice (approve/revise/exit): ").strip().lower() # noqa: ASYNC250
|
||||
choice = input("Enter your choice (1-5): ").strip().lower() # noqa: ASYNC250
|
||||
if choice in ["approve", "1"]:
|
||||
reply = MagenticPlanReviewReply(decision=MagenticPlanReviewDecision.APPROVE)
|
||||
reply = MagenticHumanInterventionReply(decision=MagenticHumanInterventionDecision.APPROVE)
|
||||
break
|
||||
if choice in ["revise", "2"]:
|
||||
reply = MagenticPlanReviewReply(decision=MagenticPlanReviewDecision.REVISE)
|
||||
if choice in ["approve with comments", "2"]:
|
||||
comments = input("Enter your comments for the manager: ").strip() # noqa: ASYNC250
|
||||
reply = MagenticHumanInterventionReply(
|
||||
decision=MagenticHumanInterventionDecision.APPROVE,
|
||||
comments=comments if comments else None,
|
||||
)
|
||||
break
|
||||
if choice in ["exit", "3"]:
|
||||
if choice in ["revise", "3"]:
|
||||
comments = input("Enter feedback for revising the plan: ").strip() # noqa: ASYNC250
|
||||
reply = MagenticHumanInterventionReply(
|
||||
decision=MagenticHumanInterventionDecision.REVISE,
|
||||
comments=comments if comments else None,
|
||||
)
|
||||
break
|
||||
if choice in ["edit", "4"]:
|
||||
print("Enter your edited plan (end with an empty line):")
|
||||
lines = []
|
||||
while True:
|
||||
line = input() # noqa: ASYNC250
|
||||
if line == "":
|
||||
break
|
||||
lines.append(line)
|
||||
edited_plan = "\n".join(lines)
|
||||
reply = MagenticHumanInterventionReply(
|
||||
decision=MagenticHumanInterventionDecision.REVISE,
|
||||
edited_plan_text=edited_plan if edited_plan else None,
|
||||
)
|
||||
break
|
||||
if choice in ["exit", "5"]:
|
||||
print("Exiting workflow...")
|
||||
return
|
||||
print("Invalid choice. Please enter 'approve', 'revise', or 'exit'.")
|
||||
print("Invalid choice. Please enter a number 1-5.")
|
||||
|
||||
pending_responses = {pending_request.request_id: reply}
|
||||
pending_request = None
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from agent_framework import (
|
||||
MAGENTIC_EVENT_TYPE_AGENT_DELTA,
|
||||
MAGENTIC_EVENT_TYPE_ORCHESTRATOR,
|
||||
AgentRunUpdateEvent,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
MagenticBuilder,
|
||||
MagenticHumanInterventionDecision,
|
||||
MagenticHumanInterventionKind,
|
||||
MagenticHumanInterventionReply,
|
||||
MagenticHumanInterventionRequest,
|
||||
RequestInfoEvent,
|
||||
WorkflowOutputEvent,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
"""
|
||||
Sample: Magentic Orchestration with Human Stall Intervention
|
||||
|
||||
This sample demonstrates how humans can intervene when a Magentic workflow stalls.
|
||||
When agents stop making progress, the workflow requests human input instead of
|
||||
automatically replanning.
|
||||
|
||||
Key concepts:
|
||||
- with_human_input_on_stall(): Enables human intervention when workflow detects stalls
|
||||
- MagenticHumanInterventionKind.STALL: The request kind for stall interventions
|
||||
- Human can choose to: continue, trigger replan, or provide guidance
|
||||
|
||||
Stall intervention options:
|
||||
- CONTINUE: Reset stall counter and continue with current plan
|
||||
- REPLAN: Trigger automatic replanning by the manager
|
||||
- GUIDANCE: Provide text guidance to help agents get back on track
|
||||
|
||||
Prerequisites:
|
||||
- OpenAI credentials configured for `OpenAIChatClient`.
|
||||
|
||||
NOTE: it is sometimes difficult to get the agents to actually stall depending on the task.
|
||||
"""
|
||||
|
||||
|
||||
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 Stall Intervention...")
|
||||
|
||||
workflow = (
|
||||
MagenticBuilder()
|
||||
.participants(researcher=researcher_agent, analyst=analyst_agent)
|
||||
.with_standard_manager(
|
||||
agent=manager_agent,
|
||||
max_round_count=10,
|
||||
max_stall_count=1, # Stall detection after 1 round without progress
|
||||
max_reset_count=2,
|
||||
)
|
||||
.with_human_input_on_stall() # Request human input when stalled (instead of auto-replan)
|
||||
.build()
|
||||
)
|
||||
|
||||
task = "Research sustainable aviation fuel technology and summarize the findings."
|
||||
|
||||
print(f"\nTask: {task}")
|
||||
print("\nStarting workflow execution...")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
pending_request: RequestInfoEvent | None = None
|
||||
pending_responses: dict[str, object] | None = None
|
||||
completed = False
|
||||
workflow_output: str | None = None
|
||||
|
||||
last_stream_agent_id: str | None = None
|
||||
stream_line_open: bool = False
|
||||
|
||||
while not completed:
|
||||
if pending_responses is not None:
|
||||
stream = workflow.send_responses_streaming(pending_responses)
|
||||
else:
|
||||
stream = workflow.run_stream(task)
|
||||
|
||||
async for event in stream:
|
||||
if isinstance(event, AgentRunUpdateEvent):
|
||||
props = event.data.additional_properties if event.data else None
|
||||
event_type = props.get("magentic_event_type") if props else None
|
||||
|
||||
if event_type == MAGENTIC_EVENT_TYPE_ORCHESTRATOR:
|
||||
kind = props.get("orchestrator_message_kind", "") if props else ""
|
||||
text = event.data.text if event.data else ""
|
||||
if stream_line_open:
|
||||
print()
|
||||
stream_line_open = False
|
||||
print(f"\n[ORCHESTRATOR: {kind}]\n{text}\n{'-' * 40}")
|
||||
elif event_type == MAGENTIC_EVENT_TYPE_AGENT_DELTA:
|
||||
agent_id = props.get("agent_id", "unknown") if props else "unknown"
|
||||
if last_stream_agent_id != agent_id or not stream_line_open:
|
||||
if stream_line_open:
|
||||
print()
|
||||
print(f"\n[{agent_id}]: ", end="", flush=True)
|
||||
last_stream_agent_id = agent_id
|
||||
stream_line_open = True
|
||||
if event.data and event.data.text:
|
||||
print(event.data.text, end="", flush=True)
|
||||
|
||||
elif isinstance(event, RequestInfoEvent) and event.request_type is MagenticHumanInterventionRequest:
|
||||
if stream_line_open:
|
||||
print()
|
||||
stream_line_open = False
|
||||
pending_request = event
|
||||
req = cast(MagenticHumanInterventionRequest, event.data)
|
||||
|
||||
if req.kind == MagenticHumanInterventionKind.STALL:
|
||||
print("\n" + "=" * 60)
|
||||
print("STALL INTERVENTION REQUESTED")
|
||||
print("=" * 60)
|
||||
print(f"\nWorkflow appears stalled after {req.stall_count} rounds")
|
||||
print(f"Reason: {req.stall_reason}")
|
||||
if req.last_agent:
|
||||
print(f"Last active agent: {req.last_agent}")
|
||||
if req.plan_text:
|
||||
print(f"\nCurrent plan:\n{req.plan_text}")
|
||||
print()
|
||||
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
if stream_line_open:
|
||||
print()
|
||||
stream_line_open = False
|
||||
workflow_output = event.data if event.data else None
|
||||
completed = True
|
||||
|
||||
if stream_line_open:
|
||||
print()
|
||||
stream_line_open = False
|
||||
pending_responses = None
|
||||
|
||||
# Handle stall intervention request
|
||||
if pending_request is not None:
|
||||
req = cast(MagenticHumanInterventionRequest, pending_request.data)
|
||||
reply: MagenticHumanInterventionReply | None = None
|
||||
|
||||
if req.kind == MagenticHumanInterventionKind.STALL:
|
||||
print("Stall intervention options:")
|
||||
print("1. continue - Continue with current plan (reset stall counter)")
|
||||
print("2. replan - Trigger automatic replanning")
|
||||
print("3. guidance - Provide guidance to help agents")
|
||||
print("4. exit - Exit the workflow")
|
||||
|
||||
while True:
|
||||
choice = input("Enter your choice (1-4): ").strip().lower() # noqa: ASYNC250
|
||||
if choice in ["continue", "1"]:
|
||||
reply = MagenticHumanInterventionReply(decision=MagenticHumanInterventionDecision.CONTINUE)
|
||||
break
|
||||
if choice in ["replan", "2"]:
|
||||
reply = MagenticHumanInterventionReply(decision=MagenticHumanInterventionDecision.REPLAN)
|
||||
break
|
||||
if choice in ["guidance", "3"]:
|
||||
guidance = input("Enter your guidance: ").strip() # noqa: ASYNC250
|
||||
reply = MagenticHumanInterventionReply(
|
||||
decision=MagenticHumanInterventionDecision.GUIDANCE,
|
||||
comments=guidance if guidance else None,
|
||||
)
|
||||
break
|
||||
if choice in ["exit", "4"]:
|
||||
print("Exiting workflow...")
|
||||
return
|
||||
print("Invalid choice. Please enter a number 1-4.")
|
||||
|
||||
if reply is not None:
|
||||
pending_responses = {pending_request.request_id: reply}
|
||||
pending_request = None
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("WORKFLOW COMPLETED")
|
||||
print("=" * 60)
|
||||
if workflow_output:
|
||||
messages = cast(list[ChatMessage], workflow_output)
|
||||
if messages:
|
||||
final_msg = messages[-1]
|
||||
print(f"\nFinal Result:\n{final_msg.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Workflow execution failed: {e}")
|
||||
logger.exception("Workflow exception", exc_info=e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user