{ "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", "
\n", " πŸ” Tool Invoked: hotel_booking(\"{destination}\")\n", "
\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", "
\n", " βœ… Condition Check: has_availability = {result.has_availability} for {result.destination}\n", "
\n", " \"\"\")\n", " )\n", " return result.has_availability\n", " except Exception as e:\n", " display(HTML(f\"\"\"
⚠️ Error: {str(e)}
\"\"\"))\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", "
\n", " ❌ Condition Check: no_availability for {result.destination}\n", "
\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", "
\n", " πŸ” User Decision: User wants alternatives = {wants_alternatives}\n", "
\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", "
\n", " 🚫 User Decision: User declined alternatives = {declined}\n", "
\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", "
\n", " 🎯 Decision Manager: Processing user reply: \"{user_reply}\" for {destination}\n", "
\n", " \"\"\")\n", " )\n", "\n", " if user_reply == \"yes\":\n", " display(\n", " HTML(\"\"\"\n", "
\n", " ➑️ Routing: User wants alternatives β†’ Will route to alternative_agent\n", "
\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", "
\n", " 🚫 Routing: User declined alternatives β†’ Will route to cancellation_agent\n", "
\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", "
\n", " ⚠️ Warning: Unexpected input \"{user_reply}\" - treating as decline\n", "
\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", "
\n", " πŸ”„ Transform: Converting ConfirmationQuestion to HumanFeedbackRequest\n", "
\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", "
\n", " πŸ“€ Display Executor: Yielding workflow output\n", "
\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", "
\n", " βœ… Created Workflow Components:\n", " \n", "
\n" ], "text/plain": [ "" ] }, "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", "
\n", " βœ… Created Workflow Components:\n", " \n", "
\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", "
\n", "

βœ… Workflow Built Successfully!

\n", "

\n", " Human-in-the-Loop Routing:
\n", " β€’ If NO availability β†’ confirmation_agent β†’ prepare_human_request β†’ request_info_executor β†’ PAUSE FOR HUMAN β†’ decision_manager
\n", "   β€’ If user says YES β†’ alternative_agent β†’ display_result
\n", "   β€’ If user says NO β†’ cancellation_agent β†’ display_result
\n", " β€’ If availability β†’ booking_agent β†’ display_result (no human input needed)\n", "

\n", "
\n" ], "text/plain": [ "" ] }, "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", "
\n", "

βœ… Workflow Built Successfully!

\n", "

\n", " Human-in-the-Loop Routing:
\n", " β€’ If NO availability β†’ confirmation_agent β†’ prepare_human_request β†’ request_info_executor β†’ PAUSE FOR HUMAN β†’ decision_manager
\n", "   β€’ If user says YES β†’ alternative_agent β†’ display_result
\n", "   β€’ If user says NO β†’ cancellation_agent β†’ display_result
\n", " β€’ If availability β†’ booking_agent β†’ display_result (no human input needed)\n", "

\n", "
\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", "
\n", "

πŸ§ͺ TEST CASE 1: Paris (No Availability - Human-in-the-Loop)

\n", "

Expected workflow path: availability_agent β†’ confirmation_agent β†’ request_info_executor β†’ PAUSE β†’ decision_manager β†’ (depends on user input)

\n", "
\n" ], "text/plain": [ "" ] }, "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", "
\n", " πŸ” Tool Invoked: hotel_booking(\"Paris\")\n", "
\n", " " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "
\n", " ❌ Condition Check: no_availability for Paris\n", "
\n", " " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "
\n", " βœ… Condition Check: has_availability = False for Paris\n", "
\n", " " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "
\n", " πŸ”„ Transform: Converting ConfirmationQuestion to HumanFeedbackRequest\n", "
\n", " " ], "text/plain": [ "" ] }, "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", "
\n", " πŸ” Tool Invoked: hotel_booking(\"Paris\")\n", "
\n", " " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "
\n", " ❌ Condition Check: no_availability for Paris\n", "
\n", " " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "
\n", " βœ… Condition Check: has_availability = False for Paris\n", "
\n", " " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "
\n", " πŸ”„ Transform: Converting ConfirmationQuestion to HumanFeedbackRequest\n", "
\n", " " ], "text/plain": [ "" ] }, "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", "
\n", " πŸ” Tool Invoked: hotel_booking(\"Paris\")\n", "
\n", " " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "display(\n", " HTML(\"\"\"\n", "
\n", "

πŸ§ͺ TEST CASE 1: Paris (No Availability - Human-in-the-Loop)

\n", "

Expected workflow path: availability_agent β†’ confirmation_agent β†’ request_info_executor β†’ PAUSE β†’ decision_manager β†’ (depends on user input)

\n", "
\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", "
\n", "

πŸ† WORKFLOW RESULT

\n", "
\n", "

Status: ❌ No rooms in Paris

\n", "

User Decision: βœ… Accepted alternatives

\n", "

Alternative Suggestion: 🏨 {result_obj.alternative_destination}

\n", "

Reason: {result_obj.reason}

\n", "
\n", "
\n", " \"\"\")\n", " )\n", " else:\n", " # User declined\n", " display(\n", " HTML(f\"\"\"\n", "
\n", "

πŸ† WORKFLOW RESULT

\n", "
\n", "

Status: ❌ No rooms in Paris

\n", "

User Decision: 🚫 Declined alternatives

\n", "

Result: Booking request cancelled

\n", "
\n", "
\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", "
\n", "

πŸ§ͺ TEST CASE 2: Stockholm (Has Availability - No Human Input)

\n", "

Expected workflow path: availability_agent β†’ booking_agent β†’ display_result (direct, no pause)

\n", "
\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", "
\n", "

πŸ† WORKFLOW RESULT (Stockholm - No Human Input)

\n", "
\n", "

Status: βœ… Rooms Available!

\n", "

Destination: 🏨 {result_stockholm.destination}

\n", "

Action: {result_stockholm.action}

\n", "

Message: {result_stockholm.message}

\n", "

Note: No human input was requested because rooms were available!

\n", "
\n", "
\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 }