{ "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",
" 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",
" 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",
"
Expected workflow path: availability_agent β confirmation_agent β request_info_executor β PAUSE β decision_manager β (depends on user input)
\n", "Expected workflow path: availability_agent β confirmation_agent β request_info_executor β PAUSE β decision_manager β (depends on user input)
\n", "Status: β No rooms in Paris
\n", "User Decision: β Accepted alternatives
\n", "Alternative Suggestion: π¨ {result_obj.alternative_destination}
\n", "Reason: {result_obj.reason}
\n", "Status: β No rooms in Paris
\n", "User Decision: π« Declined alternatives
\n", "Result: Booking request cancelled
\n", "Expected workflow path: availability_agent β booking_agent β display_result (direct, no pause)
\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", "