14 Human Loop
Human-in-the-Loop Workflow with Microsoft Agent Framework
π― Learning Objectives
In this notebook, you'll learn how to implement human-in-the-loop workflows using Microsoft Agent Framework's RequestInfoExecutor. This powerful pattern allows you to pause AI workflows to gather human input, making your agents interactive and giving humans control over critical decisions.
π What is Human-in-the-Loop?
Human-in-the-loop (HITL) is a design pattern where AI agents pause execution to request human input before continuing. This is essential for:
- β Critical decisions - Get human approval before taking important actions
- β Ambiguous situations - Let humans clarify when AI is uncertain
- β User preferences - Ask users to choose between multiple options
- β Compliance & safety - Ensure human oversight for regulated operations
- β Interactive experiences - Build conversational agents that respond to user input
ποΈ How It Works in Microsoft Agent Framework
The framework provides three key components for HITL:
RequestInfoExecutor- A special executor that pauses the workflow and emits aRequestInfoEventRequestInfoMessage- Base class for typed request payloads sent to humansRequestResponse- Correlates human responses with original requests usingrequest_id
Workflow Pattern:
Agent detects need for input
β
Sends message to RequestInfoExecutor
β
Workflow pauses & emits RequestInfoEvent
β
Application collects human input (console, UI, etc.)
β
Application sends RequestResponse via send_responses_streaming()
β
Workflow resumes with human input
π¨ Our Example: Hotel Booking with User Confirmation
We'll build upon the conditional workflow by adding human confirmation before suggesting alternative destinations:
- User requests a destination (e.g., "Paris")
availability_agentchecks if rooms are available- If no rooms β
confirmation_agentasks "Would you like to see alternatives?" - Workflow pauses using
RequestInfoExecutor - Human responds "yes" or "no" via console input
decision_managerroutes based on response:- Yes β Show alternative destinations
- No β Cancel booking request
- Display final result
This demonstrates how to give users control over the agent's suggestions!
Let's get started! π
Step 1: Import Required Libraries
We import the standard Agent Framework components plus human-in-the-loop specific classes:
RequestInfoExecutor- Executor that pauses workflow for human inputRequestInfoEvent- Event emitted when human input is requestedRequestInfoMessage- Base class for typed request payloadsRequestResponse- Correlates human responses with requestsWorkflowOutputEvent- Event for detecting workflow outputs
β All imports successful! π Human-in-the-loop components loaded: RequestInfoExecutor, RequestInfoEvent, RequestResponse
Step 2: Define Pydantic Models for Structured Outputs
These models define the schema that agents will return. We keep all models from the conditional workflow and add:
New for Human-in-the-Loop:
HumanFeedbackRequest- Subclass ofRequestInfoMessagethat defines the request payload sent to humans- Contains
prompt(question to ask) anddestination(context about the unavailable city)
- Contains
β Pydantic models defined: - BookingCheckResult (availability check) - AlternativeResult (alternative suggestion) - BookingConfirmation (booking confirmation) - ConfirmationQuestion (agent response format) π - HumanFeedbackRequest (RequestInfoMessage for HITL) π
Step 3: Create the Hotel Booking Tool
Same tool from the conditional workflow - checks if rooms are available in the destination.
β hotel_booking tool created with @ai_function decorator
Step 4: Define Condition Functions for Routing
We need four condition functions for our human-in-the-loop workflow:
From conditional workflow:
has_availability_condition- Routes when hotels ARE availableno_availability_condition- Routes when hotels are NOT available
New for human-in-the-loop:
3. user_wants_alternatives_condition - Routes when user says "yes" to alternatives
4. user_declines_alternatives_condition - Routes when user says "no" to alternatives
β Condition functions defined: - has_availability_condition (routes when rooms exist) - no_availability_condition (routes when no rooms) - user_wants_alternatives_condition (routes when user says yes) π - user_declines_alternatives_condition (routes when user says no) π
Step 5: Create the Decision Manager Executor
This is the core of the human-in-the-loop pattern! The DecisionManager is a custom Executor that:
- Receives human feedback via
RequestResponseobjects - Processes the user's decision (yes/no)
- Routes the workflow by sending messages to appropriate agents
Key features:
- Uses
@handlerdecorator to expose methods as workflow steps - Receives
RequestResponse[HumanFeedbackRequest, str]containing both the original request and user's answer - Yields simple "yes" or "no" messages that trigger our condition functions
β DecisionManager executor created with @handler method for human feedback
Step 6: Create Custom Display Executor
Same display executor from conditional workflow - yields final results as workflow output.
β prepare_human_request executor created with @executor decorator β display_result executor created with @executor decorator
Step 7: Load Environment Variables
Configure the LLM client (GitHub Models, Azure OpenAI, or OpenAI).
β Chat client configured with GitHub Models
Step 8: Create AI Agents and Executors
We create six workflow components:
Agents (wrapped in AgentExecutor):
- availability_agent - Checks hotel availability using the tool
- confirmation_agent - π Prepares the human confirmation request
- alternative_agent - Suggests alternative cities (when user says yes)
- booking_agent - Encourages booking (when rooms available)
- cancellation_agent - π Handles cancellation message (when user says no)
Special Executors:
6. request_info_executor - π RequestInfoExecutor that pauses workflow for human input
7. decision_manager - π Custom executor that routes based on human response (already defined above)
Step 9: Build the Workflow with Human-in-the-Loop
Now we construct the workflow graph with conditional routing including the human-in-the-loop path:
Workflow Structure:
availability_agent (START)
β
Evaluate conditions
β β
[no_availability] [has_availability]
β β
confirmation_agent booking_agent
β β
prepare_human_request display_result
β
request_info_executor (PAUSE)
β
decision_manager
β β
[yes] [no]
β β
alternative cancellation
β β
display_result
Key Edges:
availability_agent β confirmation_agent(when no rooms)confirmation_agent β prepare_human_request(transform type)prepare_human_request β request_info_executor(pause for human)request_info_executor β decision_manager(always - provides RequestResponse)decision_manager β alternative_agent(when user says "yes")decision_manager β cancellation_agent(when user says "no")availability_agent β booking_agent(when rooms available)- All paths end at
display_result
Step 10: Run Test Case 1 - City WITHOUT Availability (Paris with Human Confirmation)
This test demonstrates the full human-in-the-loop cycle:
- Request hotel in Paris
- availability_agent checks β No rooms
- confirmation_agent creates human-facing question
- request_info_executor pauses workflow and emits
RequestInfoEvent - Application detects event and prompts user in console
- User types "yes" or "no"
- Application sends response via
send_responses_streaming() - decision_manager routes based on response
- Final result displayed
Key Pattern:
- Use
workflow.run_stream()for first iteration - Use
workflow.send_responses_streaming(pending_responses)for subsequent iterations - Listen for
RequestInfoEventto detect when human input is needed - Listen for
WorkflowOutputEventto capture final results
π Starting human-in-the-loop workflow... ============================================================ π Starting workflow with request: 'I want to book a hotel in Paris'
βΈοΈ WORKFLOW PAUSED - Human input requested!
Request ID: 032c8fce-b9d1-400e-ba8d-afd2248e2926
Destination: Paris
============================================================
π¬ QUESTION FOR YOU:
Unfortunately, there are no rooms available in Paris. Would you like to explore nearby alternative destinations?
============================================================
π You answered: yes
π€ Sending human responses: {'032c8fce-b9d1-400e-ba8d-afd2248e2926': 'yes'}
π Starting workflow with request: 'I want to book a hotel in Paris'
π You answered: yes
π€ Sending human responses: {'032c8fce-b9d1-400e-ba8d-afd2248e2926': 'yes'}
π Starting workflow with request: 'I want to book a hotel in Paris'
βΈοΈ WORKFLOW PAUSED - Human input requested!
Request ID: cf48dad0-ee5e-4f60-8806-341a7a292bd4
Destination: Paris
============================================================
π¬ QUESTION FOR YOU:
I'm sorry to inform you that there are no available hotel rooms in Paris. Would you like me to suggest nearby alternative destinations?
============================================================
π You answered:
π€ Sending human responses: {'cf48dad0-ee5e-4f60-8806-341a7a292bd4': ''}
π Starting workflow with request: 'I want to book a hotel in Paris'
π You answered:
π€ Sending human responses: {'cf48dad0-ee5e-4f60-8806-341a7a292bd4': ''}
π Starting workflow with request: 'I want to book a hotel in Paris'
Step 11: Run Test Case 2 - City WITH Availability (Stockholm - No Human Input Needed)
This test demonstrates the direct path when rooms are available:
- Request hotel in Stockholm
- availability_agent checks β Rooms available β
- booking_agent suggests booking
- display_result shows confirmation
- No human input required!
The workflow bypasses the human-in-the-loop path entirely when rooms are available.
Key Takeaways and Human-in-the-Loop Best Practices
β What You've Learned:
1. RequestInfoExecutor Pattern
The human-in-the-loop pattern in Microsoft Agent Framework uses three key components:
RequestInfoExecutor- Pauses workflow and emits eventsRequestInfoMessage- Base class for typed request payloads (subclass this!)RequestResponse- Correlates human responses with original requests
Critical Understanding:
RequestInfoExecutordoes NOT collect input itself - it only pauses the workflow- Your application code must listen for
RequestInfoEventand collect input - You must call
send_responses_streaming()with a dict mappingrequest_idto user's answer
2. Streaming Execution Pattern
# First iteration
stream = workflow.run_stream(initial_request)
# Subsequent iterations (after human input)
stream = workflow.send_responses_streaming(pending_responses)
# Always process events
events = [event async for event in stream]
3. Event-Driven Architecture
Listen for specific events to control workflow:
RequestInfoEvent- Human input is needed (workflow paused)WorkflowOutputEvent- Final result is available (workflow complete)WorkflowStatusEvent- State changes (IN_PROGRESS, IDLE_WITH_PENDING_REQUESTS, etc.)
4. Custom Executors with @handler
The DecisionManager demonstrates how to create executors that:
- Use
@handlerdecorator to expose methods as workflow steps - Receive typed messages (e.g.,
RequestResponse[HumanFeedbackRequest, str]) - Route workflow by sending messages to other executors
- Access context via
WorkflowContext
5. Conditional Routing with Human Decisions
You can create condition functions that evaluate human responses:
def user_wants_alternatives_condition(message: Any) -> bool:
response_text = message.agent_run_response.text.lower()
return response_text == "yes"
π― Real-World Applications:
-
Approval Workflows
- Get manager approval before processing expense reports
- Require human review before sending automated emails
- Confirm high-value transactions before execution
-
Content Moderation
- Flag questionable content for human review
- Ask moderators to make final decision on edge cases
- Escalate to humans when AI confidence is low
-
Customer Service
- Let AI handle routine questions automatically
- Escalate complex issues to human agents
- Ask customer if they want to speak to a human
-
Data Processing
- Ask humans to resolve ambiguous data entries
- Confirm AI interpretations of unclear documents
- Let users choose between multiple valid interpretations
-
Safety-Critical Systems
- Require human confirmation before irreversible actions
- Get approval before accessing sensitive data
- Confirm decisions in regulated industries (healthcare, finance)
-
Interactive Agents
- Build conversational bots that ask follow-up questions
- Create wizards that guide users through complex processes
- Design agents that collaborate with humans step-by-step
π Comparison: With vs Without Human-in-the-Loop
| Feature | Conditional Workflow | Human-in-the-Loop Workflow |
|---|---|---|
| Execution | Single workflow.run() | Loop with run_stream() + send_responses_streaming() |
| User Input | None (fully automated) | Interactive prompts via input() or UI |
| Components | Agents + Executors | + RequestInfoExecutor + DecisionManager |
| Events | AgentExecutorResponse only | RequestInfoEvent, WorkflowOutputEvent, etc. |
| Pausing | No pausing | Workflow pauses at RequestInfoExecutor |
| Human Control | No human control | Humans make key decisions |
| Use Case | Automation | Collaboration & oversight |
π Advanced Patterns:
Multiple Human Decision Points
You can have multiple RequestInfoExecutor nodes in the same workflow:
.add_edge(agent1, request_info_1) # First human decision
.add_edge(decision_manager_1, agent2)
.add_edge(agent2, request_info_2) # Second human decision
.add_edge(decision_manager_2, final_agent)
Timeout Handling
Implement timeouts for human responses:
import asyncio
try:
answer = await asyncio.wait_for(
asyncio.to_thread(input, "Enter yes/no: "),
timeout=60.0
)
except asyncio.TimeoutError:
answer = "no" # Default to safe option
Rich UI Integration
Instead of input(), integrate with web UI, Slack, Teams, etc.:
if isinstance(event, RequestInfoEvent):
# Send notification to user's preferred channel
await slack_client.send_message(
user_id=current_user,
text=event.data.prompt,
request_id=event.request_id
)
Conditional Human-in-the-Loop
Only ask for human input in specific situations:
def needs_human_approval_condition(message: Any) -> bool:
# Only route to human if confidence is low or value is high
if result.confidence < 0.7 or result.value > 10000:
return True
return False
β οΈ Best Practices:
-
Always Subclass RequestInfoMessage
- Provides type safety and validation
- Enables rich context for UI rendering
- Clarifies intent of each request type
-
Use Descriptive Prompts
- Include context about what you're asking
- Explain consequences of each choice
- Keep questions simple and clear
-
Handle Unexpected Input
- Validate user responses
- Provide defaults for invalid input
- Give clear error messages
-
Track Request IDs
- Use the correlation between request_id and responses
- Don't try to manage state manually
-
Design for Non-Blocking
- Don't block threads waiting for input
- Use async patterns throughout
- Support concurrent workflow instances
π Related Concepts:
- Agent Middleware - Intercept agent calls (previous notebook)
- Workflow State Management - Persist workflow state between runs
- Multi-Agent Collaboration - Combine human-in-the-loop with agent teams
- Event-Driven Architectures - Build reactive systems with events
π Congratulations!
You've mastered human-in-the-loop workflows with Microsoft Agent Framework! You now know how to:
- β Pause workflows to gather human input
- β Use RequestInfoExecutor and RequestInfoMessage
- β Handle streaming execution with events
- β Create custom executors with @handler
- β Route workflows based on human decisions
- β Build interactive AI agents that collaborate with humans
This is a critical pattern for building trustworthy, controllable AI systems! π