1569 lines
66 KiB
Plaintext
1569 lines
66 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "d5acee06",
|
|
"metadata": {},
|
|
"source": [
|
|
"# Human-in-the-Loop Workflow with Microsoft Agent Framework\n",
|
|
"\n",
|
|
"## 🎯 Learning Objectives\n",
|
|
"\n",
|
|
"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.\n",
|
|
"\n",
|
|
"## 🔄 What is Human-in-the-Loop?\n",
|
|
"\n",
|
|
"**Human-in-the-loop (HITL)** is a design pattern where AI agents pause execution to request human input before continuing. This is essential for:\n",
|
|
"\n",
|
|
"- ✅ **Critical decisions** - Get human approval before taking important actions\n",
|
|
"- ✅ **Ambiguous situations** - Let humans clarify when AI is uncertain\n",
|
|
"- ✅ **User preferences** - Ask users to choose between multiple options\n",
|
|
"- ✅ **Compliance & safety** - Ensure human oversight for regulated operations\n",
|
|
"- ✅ **Interactive experiences** - Build conversational agents that respond to user input\n",
|
|
"\n",
|
|
"## 🏗️ How It Works in Microsoft Agent Framework\n",
|
|
"\n",
|
|
"The framework provides three key components for HITL:\n",
|
|
"\n",
|
|
"1. **`RequestInfoExecutor`** - A special executor that pauses the workflow and emits a `RequestInfoEvent`\n",
|
|
"2. **`RequestInfoMessage`** - Base class for typed request payloads sent to humans\n",
|
|
"3. **`RequestResponse`** - Correlates human responses with original requests using `request_id`\n",
|
|
"\n",
|
|
"**Workflow Pattern:**\n",
|
|
"```\n",
|
|
"Agent detects need for input\n",
|
|
" ↓\n",
|
|
"Sends message to RequestInfoExecutor\n",
|
|
" ↓\n",
|
|
"Workflow pauses & emits RequestInfoEvent\n",
|
|
" ↓\n",
|
|
"Application collects human input (console, UI, etc.)\n",
|
|
" ↓\n",
|
|
"Application sends RequestResponse via send_responses_streaming()\n",
|
|
" ↓\n",
|
|
"Workflow resumes with human input\n",
|
|
"```\n",
|
|
"\n",
|
|
"## 🏨 Our Example: Hotel Booking with User Confirmation\n",
|
|
"\n",
|
|
"We'll build upon the conditional workflow by adding human confirmation **before** suggesting alternative destinations:\n",
|
|
"\n",
|
|
"1. User requests a destination (e.g., \"Paris\")\n",
|
|
"2. `availability_agent` checks if rooms are available\n",
|
|
"3. **If no rooms** → `confirmation_agent` asks \"Would you like to see alternatives?\"\n",
|
|
"4. Workflow **pauses** using `RequestInfoExecutor`\n",
|
|
"5. **Human responds** \"yes\" or \"no\" via console input\n",
|
|
"6. `decision_manager` routes based on response:\n",
|
|
" - **Yes** → Show alternative destinations\n",
|
|
" - **No** → Cancel booking request\n",
|
|
"7. Display final result\n",
|
|
"\n",
|
|
"This demonstrates how to give users control over the agent's suggestions!\n",
|
|
"\n",
|
|
"---\n",
|
|
"\n",
|
|
"Let's get started! 🚀"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "f0012efd",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Step 1: Import Required Libraries\n",
|
|
"\n",
|
|
"We import the standard Agent Framework components plus **human-in-the-loop specific classes**:\n",
|
|
"- `RequestInfoExecutor` - Executor that pauses workflow for human input\n",
|
|
"- `RequestInfoEvent` - Event emitted when human input is requested\n",
|
|
"- `RequestInfoMessage` - Base class for typed request payloads\n",
|
|
"- `RequestResponse` - Correlates human responses with requests\n",
|
|
"- `WorkflowOutputEvent` - Event for detecting workflow outputs"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 21,
|
|
"id": "2bb201f4",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"✅ All imports successful!\n",
|
|
"🔄 Human-in-the-loop components loaded: RequestInfoExecutor, RequestInfoEvent, RequestResponse\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"import asyncio\n",
|
|
"import json\n",
|
|
"import os\n",
|
|
"from dataclasses import dataclass\n",
|
|
"from typing import Annotated, Any, Never\n",
|
|
"\n",
|
|
"from agent_framework import (\n",
|
|
" AgentExecutor,\n",
|
|
" AgentExecutorRequest,\n",
|
|
" AgentExecutorResponse,\n",
|
|
" ChatMessage,\n",
|
|
" Executor,\n",
|
|
" RequestInfoEvent, # NEW: Event when human input is requested\n",
|
|
" RequestInfoExecutor, # NEW: Executor that gathers human input\n",
|
|
" RequestInfoMessage, # NEW: Base class for request payloads\n",
|
|
" RequestResponse, # NEW: Correlates response with request\n",
|
|
" Role,\n",
|
|
" WorkflowBuilder,\n",
|
|
" WorkflowContext,\n",
|
|
" WorkflowOutputEvent, # NEW: Event for workflow outputs\n",
|
|
" WorkflowRunState, # NEW: Enum of workflow run states\n",
|
|
" WorkflowStatusEvent, # NEW: Event for run state changes\n",
|
|
" ai_function,\n",
|
|
" executor,\n",
|
|
" handler, # NEW: Decorator for executor methods\n",
|
|
")\n",
|
|
"\n",
|
|
"# 🤖 GitHub Models or OpenAI client integration\n",
|
|
"from agent_framework.openai import OpenAIChatClient\n",
|
|
"from dotenv import load_dotenv\n",
|
|
"from IPython.display import HTML, display\n",
|
|
"from pydantic import BaseModel\n",
|
|
"\n",
|
|
"print(\"✅ All imports successful!\")\n",
|
|
"print(\"🔄 Human-in-the-loop components loaded: RequestInfoExecutor, RequestInfoEvent, RequestResponse\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "e95b62b7",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Step 2: Define Pydantic Models for Structured Outputs\n",
|
|
"\n",
|
|
"These models define the **schema** that agents will return. We keep all models from the conditional workflow and add:\n",
|
|
"\n",
|
|
"**New for Human-in-the-Loop:**\n",
|
|
"- `HumanFeedbackRequest` - Subclass of `RequestInfoMessage` that defines the request payload sent to humans\n",
|
|
" - Contains `prompt` (question to ask) and `destination` (context about the unavailable city)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 22,
|
|
"id": "b423a7b8",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"✅ Pydantic models defined:\n",
|
|
" - BookingCheckResult (availability check)\n",
|
|
" - AlternativeResult (alternative suggestion)\n",
|
|
" - BookingConfirmation (booking confirmation)\n",
|
|
" - ConfirmationQuestion (agent response format) 🆕\n",
|
|
" - HumanFeedbackRequest (RequestInfoMessage for HITL) 🆕\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"# Existing models from conditional workflow\n",
|
|
"class BookingCheckResult(BaseModel):\n",
|
|
" \"\"\"Result from checking hotel availability at a destination.\"\"\"\n",
|
|
" destination: str\n",
|
|
" has_availability: bool\n",
|
|
" message: str\n",
|
|
"\n",
|
|
"\n",
|
|
"class AlternativeResult(BaseModel):\n",
|
|
" \"\"\"Suggested alternative destination when no rooms available.\"\"\"\n",
|
|
" alternative_destination: str\n",
|
|
" reason: str\n",
|
|
"\n",
|
|
"\n",
|
|
"class BookingConfirmation(BaseModel):\n",
|
|
" \"\"\"Booking suggestion when rooms are available.\"\"\"\n",
|
|
" destination: str\n",
|
|
" action: str\n",
|
|
" message: str\n",
|
|
"\n",
|
|
"\n",
|
|
"# NEW: Pydantic model for agent's response format\n",
|
|
"class ConfirmationQuestion(BaseModel):\n",
|
|
" \"\"\"\n",
|
|
" Pydantic model used by confirmation_agent's response_format.\n",
|
|
" This is what the agent will output as JSON.\n",
|
|
" \"\"\"\n",
|
|
" question: str # The question to ask the user\n",
|
|
" destination: str # The unavailable destination for context\n",
|
|
"\n",
|
|
"\n",
|
|
"# NEW: Dataclass for RequestInfoExecutor\n",
|
|
"@dataclass\n",
|
|
"class HumanFeedbackRequest(RequestInfoMessage):\n",
|
|
" \"\"\"\n",
|
|
" Request sent to RequestInfoExecutor asking if user wants alternatives.\n",
|
|
" \n",
|
|
" MUST be a dataclass subclassing RequestInfoMessage for type compatibility.\n",
|
|
" This is what gets sent to the RequestInfoExecutor.\n",
|
|
" \"\"\"\n",
|
|
" prompt: str = \"\" # The question to ask the user\n",
|
|
" destination: str = \"\" # The unavailable destination for context\n",
|
|
"\n",
|
|
"\n",
|
|
"print(\"✅ Pydantic models defined:\")\n",
|
|
"print(\" - BookingCheckResult (availability check)\")\n",
|
|
"print(\" - AlternativeResult (alternative suggestion)\")\n",
|
|
"print(\" - BookingConfirmation (booking confirmation)\")\n",
|
|
"print(\" - ConfirmationQuestion (agent response format) 🆕\")\n",
|
|
"print(\" - HumanFeedbackRequest (RequestInfoMessage for HITL) 🆕\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "128574c9",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Step 3: Create the Hotel Booking Tool\n",
|
|
"\n",
|
|
"Same tool from the conditional workflow - checks if rooms are available in the destination."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 23,
|
|
"id": "743314fa",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"✅ hotel_booking tool created with @ai_function decorator\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"@ai_function(description=\"Check hotel room availability for a destination city\")\n",
|
|
"def hotel_booking(destination: Annotated[str, \"The destination city to check for hotel rooms\"]) -> str:\n",
|
|
" \"\"\"\n",
|
|
" Simulates checking hotel room availability.\n",
|
|
" \n",
|
|
" Returns JSON string with availability status.\n",
|
|
" \"\"\"\n",
|
|
" display(\n",
|
|
" HTML(f\"\"\"\n",
|
|
" <div style='padding: 15px; background: #e3f2fd; border-left: 4px solid #2196f3; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>🔍 Tool Invoked:</strong> hotel_booking(\"{destination}\")\n",
|
|
" </div>\n",
|
|
" \"\"\")\n",
|
|
" )\n",
|
|
"\n",
|
|
" # Simulate availability check\n",
|
|
" cities_with_rooms = [\"stockholm\", \"seattle\", \"tokyo\", \"london\", \"amsterdam\"]\n",
|
|
" has_rooms = destination.lower() in cities_with_rooms\n",
|
|
"\n",
|
|
" result = {\"has_availability\": has_rooms, \"destination\": destination}\n",
|
|
"\n",
|
|
" return json.dumps(result)\n",
|
|
"\n",
|
|
"\n",
|
|
"print(\"✅ hotel_booking tool created with @ai_function decorator\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "65312a29",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Step 4: Define Condition Functions for Routing\n",
|
|
"\n",
|
|
"We need **four condition functions** for our human-in-the-loop workflow:\n",
|
|
"\n",
|
|
"**From conditional workflow:**\n",
|
|
"1. `has_availability_condition` - Routes when hotels ARE available\n",
|
|
"2. `no_availability_condition` - Routes when hotels are NOT available\n",
|
|
"\n",
|
|
"**New for human-in-the-loop:**\n",
|
|
"3. `user_wants_alternatives_condition` - Routes when user says \"yes\" to alternatives\n",
|
|
"4. `user_declines_alternatives_condition` - Routes when user says \"no\" to alternatives"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 24,
|
|
"id": "0f4468d3",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"✅ Condition functions defined:\n",
|
|
" - has_availability_condition (routes when rooms exist)\n",
|
|
" - no_availability_condition (routes when no rooms)\n",
|
|
" - user_wants_alternatives_condition (routes when user says yes) 🆕\n",
|
|
" - user_declines_alternatives_condition (routes when user says no) 🆕\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"# Existing condition functions from conditional workflow\n",
|
|
"def has_availability_condition(message: Any) -> bool:\n",
|
|
" \"\"\"Condition for routing when hotels ARE available.\"\"\"\n",
|
|
" if not isinstance(message, AgentExecutorResponse):\n",
|
|
" return True\n",
|
|
"\n",
|
|
" try:\n",
|
|
" result = BookingCheckResult.model_validate_json(message.agent_run_response.text)\n",
|
|
" display(\n",
|
|
" HTML(f\"\"\"\n",
|
|
" <div style='padding: 12px; background: #c8e6c9; border-left: 4px solid #4caf50; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>✅ Condition Check:</strong> has_availability = <strong>{result.has_availability}</strong> for {result.destination}\n",
|
|
" </div>\n",
|
|
" \"\"\")\n",
|
|
" )\n",
|
|
" return result.has_availability\n",
|
|
" except Exception as e:\n",
|
|
" display(HTML(f\"\"\"<div style='padding: 12px; background: #ffcdd2; border-left: 4px solid #f44336; border-radius: 4px; margin: 10px 0;'><strong>⚠️ Error:</strong> {str(e)}</div>\"\"\"))\n",
|
|
" return False\n",
|
|
"\n",
|
|
"\n",
|
|
"def no_availability_condition(message: Any) -> bool:\n",
|
|
" \"\"\"Condition for routing when hotels are NOT available.\"\"\"\n",
|
|
" if not isinstance(message, AgentExecutorResponse):\n",
|
|
" return False\n",
|
|
"\n",
|
|
" try:\n",
|
|
" result = BookingCheckResult.model_validate_json(message.agent_run_response.text)\n",
|
|
" display(\n",
|
|
" HTML(f\"\"\"\n",
|
|
" <div style='padding: 12px; background: #ffecb3; border-left: 4px solid #ff9800; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>❌ Condition Check:</strong> no_availability for {result.destination}\n",
|
|
" </div>\n",
|
|
" \"\"\")\n",
|
|
" )\n",
|
|
" return not result.has_availability\n",
|
|
" except Exception as e:\n",
|
|
" return False\n",
|
|
"\n",
|
|
"\n",
|
|
"# NEW: Condition functions for human-in-the-loop routing\n",
|
|
"def user_wants_alternatives_condition(message: Any) -> bool:\n",
|
|
" \"\"\"\n",
|
|
" Condition for routing when user WANTS to see alternatives.\n",
|
|
" \n",
|
|
" Checks the AgentExecutorRequest sent by decision_manager.\n",
|
|
" \"\"\"\n",
|
|
" # Check if it's an AgentExecutorRequest (what decision_manager sends)\n",
|
|
" if isinstance(message, AgentExecutorRequest):\n",
|
|
" # Check the message text to determine user's choice\n",
|
|
" if message.messages and len(message.messages) > 0:\n",
|
|
" msg_text = message.messages[0].text.lower()\n",
|
|
" wants_alternatives = \"wants to see alternative\" in msg_text or \"want to see alternative\" in msg_text\n",
|
|
" \n",
|
|
" display(\n",
|
|
" HTML(f\"\"\"\n",
|
|
" <div style='padding: 12px; background: #e1f5fe; border-left: 4px solid #0288d1; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>🔍 User Decision:</strong> User wants alternatives = <strong>{wants_alternatives}</strong>\n",
|
|
" </div>\n",
|
|
" \"\"\")\n",
|
|
" )\n",
|
|
" \n",
|
|
" return wants_alternatives\n",
|
|
" \n",
|
|
" return False\n",
|
|
"def user_declines_alternatives_condition(message: Any) -> bool:\n",
|
|
" \"\"\"\n",
|
|
" Condition for routing when user DECLINES alternatives.\n",
|
|
" \n",
|
|
" Checks the AgentExecutorRequest sent by decision_manager.\n",
|
|
" \"\"\"\n",
|
|
" # Check if it's an AgentExecutorRequest (what decision_manager sends)\n",
|
|
" if isinstance(message, AgentExecutorRequest):\n",
|
|
" # Check the message text to determine user's choice\n",
|
|
" if message.messages and len(message.messages) > 0:\n",
|
|
" msg_text = message.messages[0].text.lower()\n",
|
|
" declined = \"declined\" in msg_text or \"has declined\" in msg_text\n",
|
|
" \n",
|
|
" display(\n",
|
|
" HTML(f\"\"\"\n",
|
|
" <div style='padding: 12px; background: #fce4ec; border-left: 4px solid #c2185b; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>🚫 User Decision:</strong> User declined alternatives = <strong>{declined}</strong>\n",
|
|
" </div>\n",
|
|
" \"\"\")\n",
|
|
" )\n",
|
|
" \n",
|
|
" return declined\n",
|
|
" \n",
|
|
" return False\n",
|
|
"print(\"✅ Condition functions defined:\")\n",
|
|
"print(\" - has_availability_condition (routes when rooms exist)\")\n",
|
|
"print(\" - no_availability_condition (routes when no rooms)\")\n",
|
|
"print(\" - user_wants_alternatives_condition (routes when user says yes) 🆕\")\n",
|
|
"print(\" - user_declines_alternatives_condition (routes when user says no) 🆕\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "00dd74b8",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Step 5: Create the Decision Manager Executor\n",
|
|
"\n",
|
|
"This is the **core of the human-in-the-loop pattern**! The `DecisionManager` is a custom `Executor` that:\n",
|
|
"\n",
|
|
"1. **Receives human feedback** via `RequestResponse` objects\n",
|
|
"2. **Processes the user's decision** (yes/no)\n",
|
|
"3. **Routes the workflow** by sending messages to appropriate agents\n",
|
|
"\n",
|
|
"Key features:\n",
|
|
"- Uses `@handler` decorator to expose methods as workflow steps\n",
|
|
"- Receives `RequestResponse[HumanFeedbackRequest, str]` containing both the original request and user's answer\n",
|
|
"- Yields simple \"yes\" or \"no\" messages that trigger our condition functions"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 25,
|
|
"id": "1281a719",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"✅ DecisionManager executor created with @handler method for human feedback\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"class DecisionManager(Executor):\n",
|
|
" \"\"\"\n",
|
|
" Coordinates workflow routing based on human feedback.\n",
|
|
" \n",
|
|
" This executor receives RequestResponse objects from the RequestInfoExecutor\n",
|
|
" and makes routing decisions by sending simple messages that trigger\n",
|
|
" condition functions.\n",
|
|
" \"\"\"\n",
|
|
"\n",
|
|
" def __init__(self, id: str | None = None):\n",
|
|
" super().__init__(id=id or \"decision_manager\")\n",
|
|
"\n",
|
|
" @handler\n",
|
|
" async def on_human_feedback(\n",
|
|
" self,\n",
|
|
" feedback: RequestResponse[HumanFeedbackRequest, str],\n",
|
|
" ctx: WorkflowContext[AgentExecutorRequest],\n",
|
|
" ) -> None:\n",
|
|
" \"\"\"\n",
|
|
" Process human feedback and let the workflow route based on conditions.\n",
|
|
" \n",
|
|
" The RequestResponse contains:\n",
|
|
" - feedback.data: The user's string reply (e.g., \"yes\" or \"no\")\n",
|
|
" - feedback.original_request: The HumanFeedbackRequest with context\n",
|
|
" \n",
|
|
" This handler just displays feedback and passes the RequestResponse through.\n",
|
|
" The routing is done by condition functions on the edges.\n",
|
|
" \"\"\"\n",
|
|
" user_reply = (feedback.data or \"\").strip().lower()\n",
|
|
" destination = getattr(feedback.original_request, \"destination\", \"unknown\")\n",
|
|
"\n",
|
|
" display(\n",
|
|
" HTML(f\"\"\"\n",
|
|
" <div style='padding: 15px; background: #f3e5f5; border-left: 4px solid #9c27b0; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>🎯 Decision Manager:</strong> Processing user reply: <strong>\"{user_reply}\"</strong> for {destination}\n",
|
|
" </div>\n",
|
|
" \"\"\")\n",
|
|
" )\n",
|
|
"\n",
|
|
" if user_reply == \"yes\":\n",
|
|
" display(\n",
|
|
" HTML(\"\"\"\n",
|
|
" <div style='padding: 12px; background: #c8e6c9; border-left: 4px solid #4caf50; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>➡️ Routing:</strong> User wants alternatives → Will route to alternative_agent\n",
|
|
" </div>\n",
|
|
" \"\"\")\n",
|
|
" )\n",
|
|
" # Create and send a message for the alternative_agent\n",
|
|
" user_msg = ChatMessage(\n",
|
|
" Role.USER,\n",
|
|
" text=f\"The user wants to see alternative destinations near {destination}. Please suggest one.\",\n",
|
|
" )\n",
|
|
" await ctx.send_message(AgentExecutorRequest(messages=[user_msg], should_respond=True))\n",
|
|
" \n",
|
|
" elif user_reply == \"no\":\n",
|
|
" display(\n",
|
|
" HTML(\"\"\"\n",
|
|
" <div style='padding: 12px; background: #ffcdd2; border-left: 4px solid #f44336; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>🚫 Routing:</strong> User declined alternatives → Will route to cancellation_agent\n",
|
|
" </div>\n",
|
|
" \"\"\")\n",
|
|
" )\n",
|
|
" # Create and send a message for the cancellation_agent\n",
|
|
" user_msg = ChatMessage(\n",
|
|
" Role.USER,\n",
|
|
" text=\"The user has declined to see alternatives. Please acknowledge their decision.\",\n",
|
|
" )\n",
|
|
" await ctx.send_message(AgentExecutorRequest(messages=[user_msg], should_respond=True))\n",
|
|
" \n",
|
|
" else:\n",
|
|
" # Handle unexpected input - treat as decline\n",
|
|
" display(\n",
|
|
" HTML(f\"\"\"\n",
|
|
" <div style='padding: 12px; background: #fff3e0; border-left: 4px solid #ff9800; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>⚠️ Warning:</strong> Unexpected input \"{user_reply}\" - treating as decline\n",
|
|
" </div>\n",
|
|
" \"\"\")\n",
|
|
" )\n",
|
|
" user_msg = ChatMessage(\n",
|
|
" Role.USER,\n",
|
|
" text=\"The user has declined to see alternatives. Please acknowledge their decision.\",\n",
|
|
" )\n",
|
|
" await ctx.send_message(AgentExecutorRequest(messages=[user_msg], should_respond=True))\n",
|
|
"\n",
|
|
"\n",
|
|
"print(\"✅ DecisionManager executor created with @handler method for human feedback\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "0e3cf777",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Step 6: Create Custom Display Executor\n",
|
|
"\n",
|
|
"Same display executor from conditional workflow - yields final results as workflow output."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 26,
|
|
"id": "3b54aceb",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"✅ prepare_human_request executor created with @executor decorator\n",
|
|
"✅ display_result executor created with @executor decorator\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"@executor(id=\"prepare_human_request\")\n",
|
|
"async def prepare_human_request(\n",
|
|
" response: AgentExecutorResponse, \n",
|
|
" ctx: WorkflowContext[HumanFeedbackRequest]\n",
|
|
") -> None:\n",
|
|
" \"\"\"\n",
|
|
" Transform agent response into HumanFeedbackRequest for RequestInfoExecutor.\n",
|
|
" \n",
|
|
" This executor bridges the type gap between:\n",
|
|
" - confirmation_agent outputs AgentExecutorResponse with ConfirmationQuestion JSON\n",
|
|
" - request_info_executor expects HumanFeedbackRequest (RequestInfoMessage dataclass)\n",
|
|
" \"\"\"\n",
|
|
" display(\n",
|
|
" HTML(\"\"\"\n",
|
|
" <div style='padding: 12px; background: #e1f5fe; border-left: 4px solid #0288d1; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>🔄 Transform:</strong> Converting ConfirmationQuestion to HumanFeedbackRequest\n",
|
|
" </div>\n",
|
|
" \"\"\")\n",
|
|
" )\n",
|
|
" \n",
|
|
" # Parse the agent's Pydantic output (ConfirmationQuestion)\n",
|
|
" confirmation = ConfirmationQuestion.model_validate_json(response.agent_run_response.text)\n",
|
|
" \n",
|
|
" # Convert to HumanFeedbackRequest dataclass for RequestInfoExecutor\n",
|
|
" feedback_request = HumanFeedbackRequest(\n",
|
|
" prompt=confirmation.question,\n",
|
|
" destination=confirmation.destination\n",
|
|
" )\n",
|
|
" \n",
|
|
" # Send the properly typed RequestInfoMessage to the RequestInfoExecutor\n",
|
|
" await ctx.send_message(feedback_request)\n",
|
|
"\n",
|
|
"\n",
|
|
"@executor(id=\"display_result\")\n",
|
|
"async def display_result(response: AgentExecutorResponse, ctx: WorkflowContext[Never, str]) -> None:\n",
|
|
" \"\"\"\n",
|
|
" Display the final result as workflow output.\n",
|
|
" \n",
|
|
" This executor receives the final agent response and yields it as the workflow output.\n",
|
|
" \"\"\"\n",
|
|
" display(\n",
|
|
" HTML(\"\"\"\n",
|
|
" <div style='padding: 15px; background: #f3e5f5; border-left: 4px solid #9c27b0; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>📤 Display Executor:</strong> Yielding workflow output\n",
|
|
" </div>\n",
|
|
" \"\"\")\n",
|
|
" )\n",
|
|
"\n",
|
|
" await ctx.yield_output(response.agent_run_response.text)\n",
|
|
"\n",
|
|
"\n",
|
|
"print(\"✅ prepare_human_request executor created with @executor decorator\")\n",
|
|
"print(\"✅ display_result executor created with @executor decorator\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "a19ed583",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Step 7: Load Environment Variables\n",
|
|
"\n",
|
|
"Configure the LLM client (GitHub Models, Azure OpenAI, or OpenAI)."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "d4a07f6e",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"✅ Chat client configured with GitHub Models\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"# Load environment variables\n",
|
|
"load_dotenv()\n",
|
|
"\n",
|
|
"from azure.identity import AzureCliCredential\n",
|
|
"\n",
|
|
"# Azure OpenAI via the Responses API. Sign in with `az login` for keyless Entra ID auth.\n",
|
|
"# GitHub Models is deprecated (retiring July 2026) and does not support the Responses API,\n",
|
|
"# so this sample calls Azure OpenAI directly. OpenAIChatClient uses the Responses API.\n",
|
|
"chat_client = OpenAIChatClient(\n",
|
|
" azure_endpoint=os.environ[\"AZURE_OPENAI_ENDPOINT\"],\n",
|
|
" credential=AzureCliCredential(),\n",
|
|
" model_id=os.environ.get(\"AZURE_OPENAI_DEPLOYMENT\", \"gpt-4o-mini\"),\n",
|
|
")\n",
|
|
"\n",
|
|
"print(\"✅ Chat client configured with Azure OpenAI (Responses API)\")\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "7aa31e3d",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Step 8: Create AI Agents and Executors\n",
|
|
"\n",
|
|
"We create **six workflow components**:\n",
|
|
"\n",
|
|
"**Agents (wrapped in AgentExecutor):**\n",
|
|
"1. **availability_agent** - Checks hotel availability using the tool\n",
|
|
"2. **confirmation_agent** - 🆕 Prepares the human confirmation request\n",
|
|
"3. **alternative_agent** - Suggests alternative cities (when user says yes)\n",
|
|
"4. **booking_agent** - Encourages booking (when rooms available)\n",
|
|
"5. **cancellation_agent** - 🆕 Handles cancellation message (when user says no)\n",
|
|
"\n",
|
|
"**Special Executors:**\n",
|
|
"6. **request_info_executor** - 🆕 `RequestInfoExecutor` that pauses workflow for human input\n",
|
|
"7. **decision_manager** - 🆕 Custom executor that routes based on human response (already defined above)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "216da316",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/html": [
|
|
"\n",
|
|
" <div style='padding: 15px; background: #e3f2fd; border-left: 4px solid #2196f3; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>✅ Created Workflow Components:</strong>\n",
|
|
" <ul style='margin: 10px 0 0 0;'>\n",
|
|
" <li><strong>availability_agent</strong> - Checks availability with hotel_booking tool</li>\n",
|
|
" <li><strong>confirmation_agent</strong> 🆕 - Prepares human confirmation request</li>\n",
|
|
" <li><strong>alternative_agent</strong> - Suggests alternative cities</li>\n",
|
|
" <li><strong>booking_agent</strong> - Encourages booking</li>\n",
|
|
" <li><strong>cancellation_agent</strong> 🆕 - Handles user declining alternatives</li>\n",
|
|
" <li><strong>request_info_executor</strong> 🆕 - Pauses workflow for human input</li>\n",
|
|
" <li><strong>decision_manager</strong> 🆕 - Routes based on human response</li>\n",
|
|
" </ul>\n",
|
|
" </div>\n"
|
|
],
|
|
"text/plain": [
|
|
"<IPython.core.display.HTML object>"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
}
|
|
],
|
|
"source": [
|
|
"# Agent 1: Check availability with tool (same as conditional workflow)\n",
|
|
"availability_agent = AgentExecutor(\n",
|
|
" chat_client.as_agent(\n",
|
|
" instructions=(\n",
|
|
" \"You are a hotel booking assistant that checks room availability. \"\n",
|
|
" \"Use the hotel_booking tool to check if rooms are available at the destination. \"\n",
|
|
" \"Return JSON with fields: destination (string), has_availability (bool), and message (string). \"\n",
|
|
" \"The message should summarize the availability status.\"\n",
|
|
" ),\n",
|
|
" tools=[hotel_booking],\n",
|
|
" response_format=BookingCheckResult,\n",
|
|
" ),\n",
|
|
" id=\"availability_agent\",\n",
|
|
")\n",
|
|
"\n",
|
|
"# Agent 2: NEW - Prepare human confirmation request\n",
|
|
"confirmation_agent = AgentExecutor(\n",
|
|
" chat_client.as_agent(\n",
|
|
" instructions=(\n",
|
|
" \"You are a helpful assistant. The user's requested destination has no available hotel rooms. \"\n",
|
|
" \"Create a polite message asking if they would like to see alternative destinations nearby. \"\n",
|
|
" \"Return a JSON with: destination (the unavailable city), and question (a friendly yes/no question). \"\n",
|
|
" \"Keep the question concise and friendly.\"\n",
|
|
" ),\n",
|
|
" response_format=ConfirmationQuestion, # Use Pydantic model for agent output\n",
|
|
" ),\n",
|
|
" id=\"confirmation_agent\",\n",
|
|
")\n",
|
|
"\n",
|
|
"# Agent 3: Suggest alternative (when user says yes)\n",
|
|
"alternative_agent = AgentExecutor(\n",
|
|
" chat_client.as_agent(\n",
|
|
" instructions=(\n",
|
|
" \"You are a helpful travel assistant. When a user cannot find hotels in their requested city, \"\n",
|
|
" \"suggest an alternative nearby city that has availability. \"\n",
|
|
" \"Return JSON with fields: alternative_destination (string) and reason (string). \"\n",
|
|
" \"Make your suggestion sound appealing and helpful.\"\n",
|
|
" ),\n",
|
|
" response_format=AlternativeResult,\n",
|
|
" ),\n",
|
|
" id=\"alternative_agent\",\n",
|
|
")\n",
|
|
"\n",
|
|
"# Agent 4: Suggest booking (when rooms available)\n",
|
|
"booking_agent = AgentExecutor(\n",
|
|
" chat_client.as_agent(\n",
|
|
" instructions=(\n",
|
|
" \"You are a booking assistant. The user has found available hotel rooms. \"\n",
|
|
" \"Encourage them to book by highlighting the destination's appeal. \"\n",
|
|
" \"Return JSON with fields: destination (string), action (string), and message (string). \"\n",
|
|
" \"The action should be 'book_now' and message should be encouraging.\"\n",
|
|
" ),\n",
|
|
" response_format=BookingConfirmation,\n",
|
|
" ),\n",
|
|
" id=\"booking_agent\",\n",
|
|
")\n",
|
|
"\n",
|
|
"# Agent 5: NEW - Handle cancellation when user declines alternatives\n",
|
|
"class CancellationMessage(BaseModel):\n",
|
|
" \"\"\"Message when user declines alternatives.\"\"\"\n",
|
|
" status: str\n",
|
|
" message: str\n",
|
|
"\n",
|
|
"cancellation_agent = AgentExecutor(\n",
|
|
" chat_client.as_agent(\n",
|
|
" instructions=(\n",
|
|
" \"You are a helpful assistant. The user has declined to see alternative hotel destinations. \"\n",
|
|
" \"Create a polite cancellation message. \"\n",
|
|
" \"Return JSON with: status (should be 'cancelled'), and message (a friendly acknowledgment). \"\n",
|
|
" \"Keep the message brief and understanding.\"\n",
|
|
" ),\n",
|
|
" response_format=CancellationMessage,\n",
|
|
" ),\n",
|
|
" id=\"cancellation_agent\",\n",
|
|
")\n",
|
|
"\n",
|
|
"# NEW: RequestInfoExecutor - pauses workflow to gather human input\n",
|
|
"request_info_executor = RequestInfoExecutor(id=\"request_info\")\n",
|
|
"\n",
|
|
"# NEW: DecisionManager instance - routes based on human feedback\n",
|
|
"decision_manager = DecisionManager(id=\"decision_manager\")\n",
|
|
"\n",
|
|
"display(\n",
|
|
" HTML(\"\"\"\n",
|
|
" <div style='padding: 15px; background: #e3f2fd; border-left: 4px solid #2196f3; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>✅ Created Workflow Components:</strong>\n",
|
|
" <ul style='margin: 10px 0 0 0;'>\n",
|
|
" <li><strong>availability_agent</strong> - Checks availability with hotel_booking tool</li>\n",
|
|
" <li><strong>confirmation_agent</strong> 🆕 - Prepares human confirmation request</li>\n",
|
|
" <li><strong>alternative_agent</strong> - Suggests alternative cities</li>\n",
|
|
" <li><strong>booking_agent</strong> - Encourages booking</li>\n",
|
|
" <li><strong>cancellation_agent</strong> 🆕 - Handles user declining alternatives</li>\n",
|
|
" <li><strong>request_info_executor</strong> 🆕 - Pauses workflow for human input</li>\n",
|
|
" <li><strong>decision_manager</strong> 🆕 - Routes based on human response</li>\n",
|
|
" </ul>\n",
|
|
" </div>\n",
|
|
"\"\"\")\n",
|
|
")\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "8704fd1c",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Step 9: Build the Workflow with Human-in-the-Loop\n",
|
|
"\n",
|
|
"Now we construct the workflow graph with **conditional routing** including the human-in-the-loop path:\n",
|
|
"\n",
|
|
"**Workflow Structure:**\n",
|
|
"```\n",
|
|
"availability_agent (START)\n",
|
|
" ↓\n",
|
|
" Evaluate conditions\n",
|
|
" ↙ ↘\n",
|
|
"[no_availability] [has_availability]\n",
|
|
" ↓ ↓\n",
|
|
"confirmation_agent booking_agent\n",
|
|
" ↓ ↓\n",
|
|
"prepare_human_request display_result\n",
|
|
" ↓\n",
|
|
"request_info_executor (PAUSE)\n",
|
|
" ↓\n",
|
|
"decision_manager\n",
|
|
" ↙ ↘\n",
|
|
"[yes] [no]\n",
|
|
" ↓ ↓\n",
|
|
"alternative cancellation\n",
|
|
" ↓ ↓\n",
|
|
"display_result\n",
|
|
"```\n",
|
|
"\n",
|
|
"**Key Edges:**\n",
|
|
"- `availability_agent → confirmation_agent` (when no rooms)\n",
|
|
"- `confirmation_agent → prepare_human_request` (transform type)\n",
|
|
"- `prepare_human_request → request_info_executor` (pause for human)\n",
|
|
"- `request_info_executor → decision_manager` (always - provides RequestResponse)\n",
|
|
"- `decision_manager → alternative_agent` (when user says \"yes\")\n",
|
|
"- `decision_manager → cancellation_agent` (when user says \"no\")\n",
|
|
"- `availability_agent → booking_agent` (when rooms available)\n",
|
|
"- All paths end at `display_result`"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 29,
|
|
"id": "74b9102f",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/html": [
|
|
"\n",
|
|
" <div style='padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 8px; margin: 10px 0;'>\n",
|
|
" <h3 style='margin: 0 0 15px 0;'>✅ Workflow Built Successfully!</h3>\n",
|
|
" <p style='margin: 0; line-height: 1.6;'>\n",
|
|
" <strong>Human-in-the-Loop Routing:</strong><br>\n",
|
|
" • If <strong>NO availability</strong> → confirmation_agent → prepare_human_request → request_info_executor → <strong>PAUSE FOR HUMAN</strong> → decision_manager<br>\n",
|
|
" • If user says <strong>YES</strong> → alternative_agent → display_result<br>\n",
|
|
" • If user says <strong>NO</strong> → cancellation_agent → display_result<br>\n",
|
|
" • If <strong>availability</strong> → booking_agent → display_result (no human input needed)\n",
|
|
" </p>\n",
|
|
" </div>\n"
|
|
],
|
|
"text/plain": [
|
|
"<IPython.core.display.HTML object>"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
}
|
|
],
|
|
"source": [
|
|
"# Build the workflow with human-in-the-loop routing\n",
|
|
"workflow = (\n",
|
|
" WorkflowBuilder()\n",
|
|
" .set_start_executor(availability_agent)\n",
|
|
" \n",
|
|
" # NO AVAILABILITY PATH (with human-in-the-loop)\n",
|
|
" .add_edge(availability_agent, confirmation_agent, condition=no_availability_condition)\n",
|
|
" .add_edge(confirmation_agent, prepare_human_request) # Transform to HumanFeedbackRequest\n",
|
|
" .add_edge(prepare_human_request, request_info_executor) # Send to RequestInfoExecutor\n",
|
|
" .add_edge(request_info_executor, decision_manager) # Always goes to decision manager\n",
|
|
" \n",
|
|
" # Decision manager routes based on user response\n",
|
|
" .add_edge(decision_manager, alternative_agent, condition=user_wants_alternatives_condition)\n",
|
|
" .add_edge(decision_manager, cancellation_agent, condition=user_declines_alternatives_condition)\n",
|
|
" .add_edge(alternative_agent, display_result)\n",
|
|
" .add_edge(cancellation_agent, display_result)\n",
|
|
" \n",
|
|
" # HAS AVAILABILITY PATH (no human input needed)\n",
|
|
" .add_edge(availability_agent, booking_agent, condition=has_availability_condition)\n",
|
|
" .add_edge(booking_agent, display_result)\n",
|
|
" \n",
|
|
" .build()\n",
|
|
")\n",
|
|
"\n",
|
|
"display(\n",
|
|
" HTML(\"\"\"\n",
|
|
" <div style='padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 8px; margin: 10px 0;'>\n",
|
|
" <h3 style='margin: 0 0 15px 0;'>✅ Workflow Built Successfully!</h3>\n",
|
|
" <p style='margin: 0; line-height: 1.6;'>\n",
|
|
" <strong>Human-in-the-Loop Routing:</strong><br>\n",
|
|
" • If <strong>NO availability</strong> → confirmation_agent → prepare_human_request → request_info_executor → <strong>PAUSE FOR HUMAN</strong> → decision_manager<br>\n",
|
|
" • If user says <strong>YES</strong> → alternative_agent → display_result<br>\n",
|
|
" • If user says <strong>NO</strong> → cancellation_agent → display_result<br>\n",
|
|
" • If <strong>availability</strong> → booking_agent → display_result (no human input needed)\n",
|
|
" </p>\n",
|
|
" </div>\n",
|
|
"\"\"\")\n",
|
|
")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "82f28d21",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Step 10: Run Test Case 1 - City WITHOUT Availability (Paris with Human Confirmation)\n",
|
|
"\n",
|
|
"This test demonstrates the **full human-in-the-loop cycle**:\n",
|
|
"\n",
|
|
"1. Request hotel in Paris\n",
|
|
"2. availability_agent checks → No rooms\n",
|
|
"3. confirmation_agent creates human-facing question\n",
|
|
"4. request_info_executor **pauses workflow** and emits `RequestInfoEvent`\n",
|
|
"5. **Application detects event and prompts user in console**\n",
|
|
"6. User types \"yes\" or \"no\"\n",
|
|
"7. Application sends response via `send_responses_streaming()`\n",
|
|
"8. decision_manager routes based on response\n",
|
|
"9. Final result displayed\n",
|
|
"\n",
|
|
"**Key Pattern:**\n",
|
|
"- Use `workflow.run_stream()` for first iteration\n",
|
|
"- Use `workflow.send_responses_streaming(pending_responses)` for subsequent iterations\n",
|
|
"- Listen for `RequestInfoEvent` to detect when human input is needed\n",
|
|
"- Listen for `WorkflowOutputEvent` to capture final results"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "d89e37c8",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/html": [
|
|
"\n",
|
|
" <div style='padding: 20px; background: #fff3e0; border-left: 4px solid #ff9800; border-radius: 8px; margin: 20px 0;'>\n",
|
|
" <h3 style='margin: 0 0 10px 0; color: #e65100;'>🧪 TEST CASE 1: Paris (No Availability - Human-in-the-Loop)</h3>\n",
|
|
" <p style='margin: 0;'>Expected workflow path: availability_agent → confirmation_agent → request_info_executor → <strong>PAUSE</strong> → decision_manager → (depends on user input)</p>\n",
|
|
" </div>\n"
|
|
],
|
|
"text/plain": [
|
|
"<IPython.core.display.HTML object>"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"\n",
|
|
"🔄 Starting human-in-the-loop workflow...\n",
|
|
"============================================================\n",
|
|
"\n",
|
|
"🚀 Starting workflow with request: 'I want to book a hotel in Paris'\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"text/html": [
|
|
"\n",
|
|
" <div style='padding: 15px; background: #e3f2fd; border-left: 4px solid #2196f3; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>🔍 Tool Invoked:</strong> hotel_booking(\"Paris\")\n",
|
|
" </div>\n",
|
|
" "
|
|
],
|
|
"text/plain": [
|
|
"<IPython.core.display.HTML object>"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"data": {
|
|
"text/html": [
|
|
"\n",
|
|
" <div style='padding: 12px; background: #ffecb3; border-left: 4px solid #ff9800; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>❌ Condition Check:</strong> no_availability for Paris\n",
|
|
" </div>\n",
|
|
" "
|
|
],
|
|
"text/plain": [
|
|
"<IPython.core.display.HTML object>"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"data": {
|
|
"text/html": [
|
|
"\n",
|
|
" <div style='padding: 12px; background: #c8e6c9; border-left: 4px solid #4caf50; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>✅ Condition Check:</strong> has_availability = <strong>False</strong> for Paris\n",
|
|
" </div>\n",
|
|
" "
|
|
],
|
|
"text/plain": [
|
|
"<IPython.core.display.HTML object>"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"data": {
|
|
"text/html": [
|
|
"\n",
|
|
" <div style='padding: 12px; background: #e1f5fe; border-left: 4px solid #0288d1; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>🔄 Transform:</strong> Converting ConfirmationQuestion to HumanFeedbackRequest\n",
|
|
" </div>\n",
|
|
" "
|
|
],
|
|
"text/plain": [
|
|
"<IPython.core.display.HTML object>"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"\n",
|
|
"⏸️ WORKFLOW PAUSED - Human input requested!\n",
|
|
" Request ID: 032c8fce-b9d1-400e-ba8d-afd2248e2926\n",
|
|
" Destination: Paris\n",
|
|
"\n",
|
|
"============================================================\n",
|
|
"💬 QUESTION FOR YOU:\n",
|
|
" Unfortunately, there are no rooms available in Paris. Would you like to explore nearby alternative destinations?\n",
|
|
"============================================================\n",
|
|
"\n",
|
|
"📝 You answered: yes\n",
|
|
"\n",
|
|
"📤 Sending human responses: {'032c8fce-b9d1-400e-ba8d-afd2248e2926': 'yes'}\n",
|
|
"\n",
|
|
"🚀 Starting workflow with request: 'I want to book a hotel in Paris'\n",
|
|
"\n",
|
|
"📝 You answered: yes\n",
|
|
"\n",
|
|
"📤 Sending human responses: {'032c8fce-b9d1-400e-ba8d-afd2248e2926': 'yes'}\n",
|
|
"\n",
|
|
"🚀 Starting workflow with request: 'I want to book a hotel in Paris'\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"text/html": [
|
|
"\n",
|
|
" <div style='padding: 15px; background: #e3f2fd; border-left: 4px solid #2196f3; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>🔍 Tool Invoked:</strong> hotel_booking(\"Paris\")\n",
|
|
" </div>\n",
|
|
" "
|
|
],
|
|
"text/plain": [
|
|
"<IPython.core.display.HTML object>"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"data": {
|
|
"text/html": [
|
|
"\n",
|
|
" <div style='padding: 12px; background: #ffecb3; border-left: 4px solid #ff9800; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>❌ Condition Check:</strong> no_availability for Paris\n",
|
|
" </div>\n",
|
|
" "
|
|
],
|
|
"text/plain": [
|
|
"<IPython.core.display.HTML object>"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"data": {
|
|
"text/html": [
|
|
"\n",
|
|
" <div style='padding: 12px; background: #c8e6c9; border-left: 4px solid #4caf50; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>✅ Condition Check:</strong> has_availability = <strong>False</strong> for Paris\n",
|
|
" </div>\n",
|
|
" "
|
|
],
|
|
"text/plain": [
|
|
"<IPython.core.display.HTML object>"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"data": {
|
|
"text/html": [
|
|
"\n",
|
|
" <div style='padding: 12px; background: #e1f5fe; border-left: 4px solid #0288d1; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>🔄 Transform:</strong> Converting ConfirmationQuestion to HumanFeedbackRequest\n",
|
|
" </div>\n",
|
|
" "
|
|
],
|
|
"text/plain": [
|
|
"<IPython.core.display.HTML object>"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"\n",
|
|
"⏸️ WORKFLOW PAUSED - Human input requested!\n",
|
|
" Request ID: cf48dad0-ee5e-4f60-8806-341a7a292bd4\n",
|
|
" Destination: Paris\n",
|
|
"\n",
|
|
"============================================================\n",
|
|
"💬 QUESTION FOR YOU:\n",
|
|
" I'm sorry to inform you that there are no available hotel rooms in Paris. Would you like me to suggest nearby alternative destinations?\n",
|
|
"============================================================\n",
|
|
"\n",
|
|
"📝 You answered: \n",
|
|
"\n",
|
|
"📤 Sending human responses: {'cf48dad0-ee5e-4f60-8806-341a7a292bd4': ''}\n",
|
|
"\n",
|
|
"🚀 Starting workflow with request: 'I want to book a hotel in Paris'\n",
|
|
"\n",
|
|
"📝 You answered: \n",
|
|
"\n",
|
|
"📤 Sending human responses: {'cf48dad0-ee5e-4f60-8806-341a7a292bd4': ''}\n",
|
|
"\n",
|
|
"🚀 Starting workflow with request: 'I want to book a hotel in Paris'\n"
|
|
]
|
|
},
|
|
{
|
|
"data": {
|
|
"text/html": [
|
|
"\n",
|
|
" <div style='padding: 15px; background: #e3f2fd; border-left: 4px solid #2196f3; border-radius: 4px; margin: 10px 0;'>\n",
|
|
" <strong>🔍 Tool Invoked:</strong> hotel_booking(\"Paris\")\n",
|
|
" </div>\n",
|
|
" "
|
|
],
|
|
"text/plain": [
|
|
"<IPython.core.display.HTML object>"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
}
|
|
],
|
|
"source": [
|
|
"display(\n",
|
|
" HTML(\"\"\"\n",
|
|
" <div style='padding: 20px; background: #fff3e0; border-left: 4px solid #ff9800; border-radius: 8px; margin: 20px 0;'>\n",
|
|
" <h3 style='margin: 0 0 10px 0; color: #e65100;'>🧪 TEST CASE 1: Paris (No Availability - Human-in-the-Loop)</h3>\n",
|
|
" <p style='margin: 0;'>Expected workflow path: availability_agent → confirmation_agent → request_info_executor → <strong>PAUSE</strong> → decision_manager → (depends on user input)</p>\n",
|
|
" </div>\n",
|
|
"\"\"\")\n",
|
|
")\n",
|
|
"\n",
|
|
"# Create request for Paris\n",
|
|
"request_paris = AgentExecutorRequest(\n",
|
|
" messages=[ChatMessage(Role.USER, text=\"I want to book a hotel in Paris\")], \n",
|
|
" should_respond=True\n",
|
|
")\n",
|
|
"\n",
|
|
"# Human-in-the-loop execution pattern\n",
|
|
"pending_responses: dict[str, str] | None = None\n",
|
|
"completed = False\n",
|
|
"workflow_output: str | None = None\n",
|
|
"\n",
|
|
"print(\"\\n🔄 Starting human-in-the-loop workflow...\")\n",
|
|
"print(\"=\" * 60)\n",
|
|
"\n",
|
|
"while not completed:\n",
|
|
" # First iteration uses run_stream with the request\n",
|
|
" # Subsequent iterations use send_responses_streaming with collected human responses\n",
|
|
" if pending_responses:\n",
|
|
" print(f\"\\n📤 Sending human responses: {pending_responses}\")\n",
|
|
" stream = workflow.send_responses_streaming(pending_responses)\n",
|
|
" pending_responses = None # Clear immediately after sending\n",
|
|
" else:\n",
|
|
" print(f\"\\n🚀 Starting workflow with request: 'I want to book a hotel in Paris'\")\n",
|
|
" stream = workflow.run_stream(request_paris)\n",
|
|
" \n",
|
|
" # Collect all events from this iteration\n",
|
|
" events = [event async for event in stream]\n",
|
|
" \n",
|
|
" # Process events\n",
|
|
" requests: list[tuple[str, str]] = [] # (request_id, prompt)\n",
|
|
" \n",
|
|
" for event in events:\n",
|
|
" # Check for human input requests\n",
|
|
" if isinstance(event, RequestInfoEvent) and isinstance(event.data, HumanFeedbackRequest):\n",
|
|
" print(f\"\\n⏸️ WORKFLOW PAUSED - Human input requested!\")\n",
|
|
" print(f\" Request ID: {event.request_id}\")\n",
|
|
" print(f\" Destination: {event.data.destination}\")\n",
|
|
" requests.append((event.request_id, event.data.prompt))\n",
|
|
" \n",
|
|
" # Check for workflow outputs\n",
|
|
" elif isinstance(event, WorkflowOutputEvent):\n",
|
|
" workflow_output = str(event.data)\n",
|
|
" completed = True\n",
|
|
" print(f\"\\n✅ Workflow completed with output!\")\n",
|
|
" \n",
|
|
" # If we have human requests, prompt the user\n",
|
|
" if requests and not completed:\n",
|
|
" responses: dict[str, str] = {}\n",
|
|
" for req_id, prompt in requests:\n",
|
|
" print(f\"\\n{'='*60}\")\n",
|
|
" print(f\"💬 QUESTION FOR YOU:\")\n",
|
|
" print(f\" {prompt}\")\n",
|
|
" print(f\"{'='*60}\")\n",
|
|
" \n",
|
|
" # Get user input (in notebook, this will pause execution)\n",
|
|
" answer = input(\"👉 Enter 'yes' or 'no': \").strip().lower()\n",
|
|
" \n",
|
|
" print(f\"\\n📝 You answered: {answer}\")\n",
|
|
" responses[req_id] = answer\n",
|
|
" \n",
|
|
" pending_responses = responses\n",
|
|
"\n",
|
|
"print(f\"\\n{'='*60}\")\n",
|
|
"print(f\"🏆 FINAL WORKFLOW OUTPUT:\")\n",
|
|
"print(f\"{'='*60}\")\n",
|
|
"\n",
|
|
"# Display final result\n",
|
|
"if workflow_output:\n",
|
|
" # Try to parse as JSON for pretty display\n",
|
|
" try:\n",
|
|
" result_data = json.loads(workflow_output)\n",
|
|
" if \"alternative_destination\" in result_data:\n",
|
|
" result_obj = AlternativeResult.model_validate_json(workflow_output)\n",
|
|
" display(\n",
|
|
" HTML(f\"\"\"\n",
|
|
" <div style='padding: 25px; background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); border-radius: 12px; box-shadow: 0 4px 12px rgba(255,165,0,0.3); margin: 20px 0;'>\n",
|
|
" <h3 style='margin: 0 0 15px 0; color: #333;'>🏆 WORKFLOW RESULT</h3>\n",
|
|
" <div style='background: white; padding: 20px; border-radius: 8px;'>\n",
|
|
" <p style='margin: 0 0 10px 0; font-size: 16px;'><strong>Status:</strong> ❌ No rooms in Paris</p>\n",
|
|
" <p style='margin: 0 0 10px 0; font-size: 16px;'><strong>User Decision:</strong> ✅ Accepted alternatives</p>\n",
|
|
" <p style='margin: 0 0 10px 0; font-size: 16px;'><strong>Alternative Suggestion:</strong> 🏨 {result_obj.alternative_destination}</p>\n",
|
|
" <p style='margin: 0; font-size: 14px; color: #666;'><strong>Reason:</strong> {result_obj.reason}</p>\n",
|
|
" </div>\n",
|
|
" </div>\n",
|
|
" \"\"\")\n",
|
|
" )\n",
|
|
" else:\n",
|
|
" # User declined\n",
|
|
" display(\n",
|
|
" HTML(f\"\"\"\n",
|
|
" <div style='padding: 25px; background: linear-gradient(135deg, #f44336 0%, #e91e63 100%); color: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(244,67,54,0.3); margin: 20px 0;'>\n",
|
|
" <h3 style='margin: 0 0 15px 0;'>🏆 WORKFLOW RESULT</h3>\n",
|
|
" <div style='background: white; color: #333; padding: 20px; border-radius: 8px;'>\n",
|
|
" <p style='margin: 0 0 10px 0; font-size: 16px;'><strong>Status:</strong> ❌ No rooms in Paris</p>\n",
|
|
" <p style='margin: 0 0 10px 0; font-size: 16px;'><strong>User Decision:</strong> 🚫 Declined alternatives</p>\n",
|
|
" <p style='margin: 0; font-size: 14px; color: #666;'><strong>Result:</strong> Booking request cancelled</p>\n",
|
|
" </div>\n",
|
|
" </div>\n",
|
|
" \"\"\")\n",
|
|
" )\n",
|
|
" except:\n",
|
|
" print(workflow_output)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "8409f8d1",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Step 11: Run Test Case 2 - City WITH Availability (Stockholm - No Human Input Needed)\n",
|
|
"\n",
|
|
"This test demonstrates the **direct path** when rooms are available:\n",
|
|
"\n",
|
|
"1. Request hotel in Stockholm\n",
|
|
"2. availability_agent checks → Rooms available ✅\n",
|
|
"3. booking_agent suggests booking\n",
|
|
"4. display_result shows confirmation\n",
|
|
"5. **No human input required!**\n",
|
|
"\n",
|
|
"The workflow bypasses the human-in-the-loop path entirely when rooms are available."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "f9971239",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"display(\n",
|
|
" HTML(\"\"\"\n",
|
|
" <div style='padding: 20px; background: #e8f5e9; border-left: 4px solid #4caf50; border-radius: 8px; margin: 20px 0;'>\n",
|
|
" <h3 style='margin: 0 0 10px 0; color: #1b5e20;'>🧪 TEST CASE 2: Stockholm (Has Availability - No Human Input)</h3>\n",
|
|
" <p style='margin: 0;'>Expected workflow path: availability_agent → booking_agent → display_result (direct, no pause)</p>\n",
|
|
" </div>\n",
|
|
"\"\"\")\n",
|
|
")\n",
|
|
"\n",
|
|
"# Create request for Stockholm\n",
|
|
"request_stockholm = AgentExecutorRequest(\n",
|
|
" messages=[ChatMessage(Role.USER, text=\"I want to book a hotel in Stockholm\")], \n",
|
|
" should_respond=True\n",
|
|
")\n",
|
|
"\n",
|
|
"# Run the workflow (should complete without human input)\n",
|
|
"events_stockholm = await workflow.run(request_stockholm)\n",
|
|
"outputs_stockholm = events_stockholm.get_outputs()\n",
|
|
"\n",
|
|
"# Display results\n",
|
|
"if outputs_stockholm:\n",
|
|
" result_stockholm = BookingConfirmation.model_validate_json(outputs_stockholm[0])\n",
|
|
"\n",
|
|
" display(\n",
|
|
" HTML(f\"\"\"\n",
|
|
" <div style='padding: 25px; background: linear-gradient(135deg, #4caf50 0%, #8bc34a 100%); color: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(76,175,80,0.3); margin: 20px 0;'>\n",
|
|
" <h3 style='margin: 0 0 15px 0;'>🏆 WORKFLOW RESULT (Stockholm - No Human Input)</h3>\n",
|
|
" <div style='background: white; color: #333; padding: 20px; border-radius: 8px;'>\n",
|
|
" <p style='margin: 0 0 10px 0; font-size: 16px;'><strong>Status:</strong> ✅ Rooms Available!</p>\n",
|
|
" <p style='margin: 0 0 10px 0; font-size: 16px;'><strong>Destination:</strong> 🏨 {result_stockholm.destination}</p>\n",
|
|
" <p style='margin: 0 0 10px 0; font-size: 16px;'><strong>Action:</strong> {result_stockholm.action}</p>\n",
|
|
" <p style='margin: 0 0 10px 0; font-size: 14px; color: #666;'><strong>Message:</strong> {result_stockholm.message}</p>\n",
|
|
" <p style='margin: 10px 0 0 0; font-size: 12px; color: #999; font-style: italic;'>Note: No human input was requested because rooms were available!</p>\n",
|
|
" </div>\n",
|
|
" </div>\n",
|
|
" \"\"\")\n",
|
|
" )"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "b40fa995",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Key Takeaways and Human-in-the-Loop Best Practices\n",
|
|
"\n",
|
|
"### ✅ What You've Learned:\n",
|
|
"\n",
|
|
"#### 1. **RequestInfoExecutor Pattern**\n",
|
|
"The human-in-the-loop pattern in Microsoft Agent Framework uses three key components:\n",
|
|
"- `RequestInfoExecutor` - Pauses workflow and emits events\n",
|
|
"- `RequestInfoMessage` - Base class for typed request payloads (subclass this!)\n",
|
|
"- `RequestResponse` - Correlates human responses with original requests\n",
|
|
"\n",
|
|
"**Critical Understanding:**\n",
|
|
"- `RequestInfoExecutor` does NOT collect input itself - it only pauses the workflow\n",
|
|
"- Your application code must listen for `RequestInfoEvent` and collect input\n",
|
|
"- You must call `send_responses_streaming()` with a dict mapping `request_id` to user's answer\n",
|
|
"\n",
|
|
"#### 2. **Streaming Execution Pattern**\n",
|
|
"```python\n",
|
|
"# First iteration\n",
|
|
"stream = workflow.run_stream(initial_request)\n",
|
|
"\n",
|
|
"# Subsequent iterations (after human input)\n",
|
|
"stream = workflow.send_responses_streaming(pending_responses)\n",
|
|
"\n",
|
|
"# Always process events\n",
|
|
"events = [event async for event in stream]\n",
|
|
"```\n",
|
|
"\n",
|
|
"#### 3. **Event-Driven Architecture**\n",
|
|
"Listen for specific events to control workflow:\n",
|
|
"- `RequestInfoEvent` - Human input is needed (workflow paused)\n",
|
|
"- `WorkflowOutputEvent` - Final result is available (workflow complete)\n",
|
|
"- `WorkflowStatusEvent` - State changes (IN_PROGRESS, IDLE_WITH_PENDING_REQUESTS, etc.)\n",
|
|
"\n",
|
|
"#### 4. **Custom Executors with @handler**\n",
|
|
"The `DecisionManager` demonstrates how to create executors that:\n",
|
|
"- Use `@handler` decorator to expose methods as workflow steps\n",
|
|
"- Receive typed messages (e.g., `RequestResponse[HumanFeedbackRequest, str]`)\n",
|
|
"- Route workflow by sending messages to other executors\n",
|
|
"- Access context via `WorkflowContext`\n",
|
|
"\n",
|
|
"#### 5. **Conditional Routing with Human Decisions**\n",
|
|
"You can create condition functions that evaluate human responses:\n",
|
|
"```python\n",
|
|
"def user_wants_alternatives_condition(message: Any) -> bool:\n",
|
|
" response_text = message.agent_run_response.text.lower()\n",
|
|
" return response_text == \"yes\"\n",
|
|
"```\n",
|
|
"\n",
|
|
"### 🎯 Real-World Applications:\n",
|
|
"\n",
|
|
"1. **Approval Workflows**\n",
|
|
" - Get manager approval before processing expense reports\n",
|
|
" - Require human review before sending automated emails\n",
|
|
" - Confirm high-value transactions before execution\n",
|
|
"\n",
|
|
"2. **Content Moderation**\n",
|
|
" - Flag questionable content for human review\n",
|
|
" - Ask moderators to make final decision on edge cases\n",
|
|
" - Escalate to humans when AI confidence is low\n",
|
|
"\n",
|
|
"3. **Customer Service**\n",
|
|
" - Let AI handle routine questions automatically\n",
|
|
" - Escalate complex issues to human agents\n",
|
|
" - Ask customer if they want to speak to a human\n",
|
|
"\n",
|
|
"4. **Data Processing**\n",
|
|
" - Ask humans to resolve ambiguous data entries\n",
|
|
" - Confirm AI interpretations of unclear documents\n",
|
|
" - Let users choose between multiple valid interpretations\n",
|
|
"\n",
|
|
"5. **Safety-Critical Systems**\n",
|
|
" - Require human confirmation before irreversible actions\n",
|
|
" - Get approval before accessing sensitive data\n",
|
|
" - Confirm decisions in regulated industries (healthcare, finance)\n",
|
|
"\n",
|
|
"6. **Interactive Agents**\n",
|
|
" - Build conversational bots that ask follow-up questions\n",
|
|
" - Create wizards that guide users through complex processes\n",
|
|
" - Design agents that collaborate with humans step-by-step\n",
|
|
"\n",
|
|
"### 🔄 Comparison: With vs Without Human-in-the-Loop\n",
|
|
"\n",
|
|
"| Feature | Conditional Workflow | Human-in-the-Loop Workflow |\n",
|
|
"|---------|---------------------|---------------------------|\n",
|
|
"| **Execution** | Single `workflow.run()` | Loop with `run_stream()` + `send_responses_streaming()` |\n",
|
|
"| **User Input** | None (fully automated) | Interactive prompts via `input()` or UI |\n",
|
|
"| **Components** | Agents + Executors | + RequestInfoExecutor + DecisionManager |\n",
|
|
"| **Events** | AgentExecutorResponse only | RequestInfoEvent, WorkflowOutputEvent, etc. |\n",
|
|
"| **Pausing** | No pausing | Workflow pauses at RequestInfoExecutor |\n",
|
|
"| **Human Control** | No human control | Humans make key decisions |\n",
|
|
"| **Use Case** | Automation | Collaboration & oversight |\n",
|
|
"\n",
|
|
"### 🚀 Advanced Patterns:\n",
|
|
"\n",
|
|
"#### Multiple Human Decision Points\n",
|
|
"You can have multiple `RequestInfoExecutor` nodes in the same workflow:\n",
|
|
"```python\n",
|
|
".add_edge(agent1, request_info_1) # First human decision\n",
|
|
".add_edge(decision_manager_1, agent2)\n",
|
|
".add_edge(agent2, request_info_2) # Second human decision\n",
|
|
".add_edge(decision_manager_2, final_agent)\n",
|
|
"```\n",
|
|
"\n",
|
|
"#### Timeout Handling\n",
|
|
"Implement timeouts for human responses:\n",
|
|
"```python\n",
|
|
"import asyncio\n",
|
|
"\n",
|
|
"try:\n",
|
|
" answer = await asyncio.wait_for(\n",
|
|
" asyncio.to_thread(input, \"Enter yes/no: \"),\n",
|
|
" timeout=60.0\n",
|
|
" )\n",
|
|
"except asyncio.TimeoutError:\n",
|
|
" answer = \"no\" # Default to safe option\n",
|
|
"```\n",
|
|
"\n",
|
|
"#### Rich UI Integration\n",
|
|
"Instead of `input()`, integrate with web UI, Slack, Teams, etc.:\n",
|
|
"```python\n",
|
|
"if isinstance(event, RequestInfoEvent):\n",
|
|
" # Send notification to user's preferred channel\n",
|
|
" await slack_client.send_message(\n",
|
|
" user_id=current_user,\n",
|
|
" text=event.data.prompt,\n",
|
|
" request_id=event.request_id\n",
|
|
" )\n",
|
|
"```\n",
|
|
"\n",
|
|
"#### Conditional Human-in-the-Loop\n",
|
|
"Only ask for human input in specific situations:\n",
|
|
"```python\n",
|
|
"def needs_human_approval_condition(message: Any) -> bool:\n",
|
|
" # Only route to human if confidence is low or value is high\n",
|
|
" if result.confidence < 0.7 or result.value > 10000:\n",
|
|
" return True\n",
|
|
" return False\n",
|
|
"```\n",
|
|
"\n",
|
|
"### ⚠️ Best Practices:\n",
|
|
"\n",
|
|
"1. **Always Subclass RequestInfoMessage**\n",
|
|
" - Provides type safety and validation\n",
|
|
" - Enables rich context for UI rendering\n",
|
|
" - Clarifies intent of each request type\n",
|
|
"\n",
|
|
"2. **Use Descriptive Prompts**\n",
|
|
" - Include context about what you're asking\n",
|
|
" - Explain consequences of each choice\n",
|
|
" - Keep questions simple and clear\n",
|
|
"\n",
|
|
"3. **Handle Unexpected Input**\n",
|
|
" - Validate user responses\n",
|
|
" - Provide defaults for invalid input\n",
|
|
" - Give clear error messages\n",
|
|
"\n",
|
|
"4. **Track Request IDs**\n",
|
|
" - Use the correlation between request_id and responses\n",
|
|
" - Don't try to manage state manually\n",
|
|
"\n",
|
|
"5. **Design for Non-Blocking**\n",
|
|
" - Don't block threads waiting for input\n",
|
|
" - Use async patterns throughout\n",
|
|
" - Support concurrent workflow instances\n",
|
|
"\n",
|
|
"### 📚 Related Concepts:\n",
|
|
"\n",
|
|
"- **Agent Middleware** - Intercept agent calls (previous notebook)\n",
|
|
"- **Workflow State Management** - Persist workflow state between runs\n",
|
|
"- **Multi-Agent Collaboration** - Combine human-in-the-loop with agent teams\n",
|
|
"- **Event-Driven Architectures** - Build reactive systems with events\n",
|
|
"\n",
|
|
"---\n",
|
|
"\n",
|
|
"### 🎓 Congratulations!\n",
|
|
"\n",
|
|
"You've mastered human-in-the-loop workflows with Microsoft Agent Framework! You now know how to:\n",
|
|
"- ✅ Pause workflows to gather human input\n",
|
|
"- ✅ Use RequestInfoExecutor and RequestInfoMessage\n",
|
|
"- ✅ Handle streaming execution with events\n",
|
|
"- ✅ Create custom executors with @handler\n",
|
|
"- ✅ Route workflows based on human decisions\n",
|
|
"- ✅ Build interactive AI agents that collaborate with humans\n",
|
|
"\n",
|
|
"**This is a critical pattern for building trustworthy, controllable AI systems!** 🚀"
|
|
]
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": ".venv (3.12.11)",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"language_info": {
|
|
"codemirror_mode": {
|
|
"name": "ipython",
|
|
"version": 3
|
|
},
|
|
"file_extension": ".py",
|
|
"mimetype": "text/x-python",
|
|
"name": "python",
|
|
"nbconvert_exporter": "python",
|
|
"pygments_lexer": "ipython3",
|
|
"version": "3.12.11"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 5
|
|
}
|