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:
Evan Mattson
2025-12-04 14:13:27 +09:00
committed by GitHub
Unverified
parent 292a2078fa
commit 19ac6e57c0
11 changed files with 1152 additions and 207 deletions
@@ -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,
@@ -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:
@@ -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())