959 lines
38 KiB
Plaintext
959 lines
38 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "263ac938",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Hotel Booking with Priority Member Middleware\n",
|
||
"\n",
|
||
"This notebook demonstrates **function-based middleware** using the Microsoft Agent Framework. We build upon the conditional workflow example by adding a middleware layer that gives priority members special privileges.\n",
|
||
"\n",
|
||
"## What You'll Learn:\n",
|
||
"1. **Function-Based Middleware**: Intercept and modify function results\n",
|
||
"2. **Context Access**: Read and modify `context.result` after execution\n",
|
||
"3. **Business Logic Implementation**: Priority member benefits\n",
|
||
"4. **Result Override**: Change function outcomes based on user status\n",
|
||
"5. **Same Workflow, Different Outcomes**: Middleware-driven behavior changes\n",
|
||
"\n",
|
||
"## Workflow Architecture with Middleware:\n",
|
||
"\n",
|
||
"```\n",
|
||
"User Input: \"I want to book a hotel in Paris\"\n",
|
||
" ↓\n",
|
||
" [availability_agent]\n",
|
||
" - Calls hotel_booking tool\n",
|
||
" - 🌟 priority_check middleware intercepts\n",
|
||
" - Checks user membership status\n",
|
||
" - IF priority + no rooms → Override to available!\n",
|
||
" - Returns BookingCheckResult\n",
|
||
" ↓\n",
|
||
" Conditional Routing\n",
|
||
" / \\\n",
|
||
" [has_availability] [no_availability]\n",
|
||
" ↓ ↓\n",
|
||
" [booking_agent] [alternative_agent]\n",
|
||
" (Priority override!) (Regular users)\n",
|
||
" ↓ ↓\n",
|
||
" [display_result executor]\n",
|
||
"```\n",
|
||
"\n",
|
||
"## Key Difference from Conditional Workflow:\n",
|
||
"\n",
|
||
"**Without Middleware** (14-conditional-workflow.ipynb):\n",
|
||
"- Paris has no rooms → Route to alternative_agent\n",
|
||
"\n",
|
||
"**With Middleware** (this notebook):\n",
|
||
"- Regular user + Paris → No rooms → Route to alternative_agent\n",
|
||
"- Priority user + Paris → 🌟 Middleware overrides! → Available → Route to booking_agent\n",
|
||
"\n",
|
||
"## Prerequisites:\n",
|
||
"- Microsoft Agent Framework installed\n",
|
||
"- Understanding of conditional workflows (see 14-conditional-workflow.ipynb)\n",
|
||
"- GitHub token or OpenAI API key\n",
|
||
"- Basic understanding of middleware patterns"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "ec6cafec",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"import asyncio\n",
|
||
"import json\n",
|
||
"import os\n",
|
||
"from collections.abc import Awaitable, Callable\n",
|
||
"from typing import Annotated, Any, Never\n",
|
||
"\n",
|
||
"from agent_framework import (\n",
|
||
" AgentExecutor,\n",
|
||
" AgentExecutorRequest,\n",
|
||
" AgentExecutorResponse,\n",
|
||
" FunctionInvocationContext,\n",
|
||
" Message,\n",
|
||
" WorkflowBuilder,\n",
|
||
" WorkflowContext,\n",
|
||
" executor,\n",
|
||
" tool,\n",
|
||
")\n",
|
||
"from agent_framework.foundry import FoundryChatClient\n",
|
||
"from azure.identity import AzureCliCredential\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"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "4d67d9a1",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Step 1: Define Pydantic Models for Structured Outputs\n",
|
||
"\n",
|
||
"These models define the **schema** that agents will return. We've added a `priority_override` field to track when middleware modifies the availability result."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "9973ac54",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class BookingCheckResult(BaseModel):\n",
|
||
" \"\"\"Result from checking hotel availability at a destination.\"\"\"\n",
|
||
"\n",
|
||
" destination: str\n",
|
||
" has_availability: bool\n",
|
||
" message: str\n",
|
||
" # Tracks if middleware overrode the result. The Azure structured-output\n",
|
||
" # contract requires every property to be in the JSON schema's `required`\n",
|
||
" # array, so we cannot give this a default value the way the original\n",
|
||
" # notebook did.\n",
|
||
" priority_override: bool\n",
|
||
"\n",
|
||
"\n",
|
||
"class AlternativeResult(BaseModel):\n",
|
||
" \"\"\"Suggested alternative destination when no rooms available.\"\"\"\n",
|
||
"\n",
|
||
" alternative_destination: str\n",
|
||
" reason: str\n",
|
||
"\n",
|
||
"\n",
|
||
"class BookingConfirmation(BaseModel):\n",
|
||
" \"\"\"Booking suggestion when rooms are available.\"\"\"\n",
|
||
"\n",
|
||
" destination: str\n",
|
||
" action: str\n",
|
||
" message: str\n",
|
||
"\n",
|
||
"\n",
|
||
"print(\"✅ Pydantic models defined:\")\n",
|
||
"print(\" - BookingCheckResult (availability check with priority_override)\")\n",
|
||
"print(\" - AlternativeResult (alternative suggestion)\")\n",
|
||
"print(\" - BookingConfirmation (booking confirmation)\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "8e3d0853",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Step 2: Define Priority Members Database\n",
|
||
"\n",
|
||
"For this demo, we'll simulate a priority membership database. In production, this would query a real database or API.\n",
|
||
"\n",
|
||
"**Priority Members:**\n",
|
||
"- `alice@example.com` - VIP member\n",
|
||
"- `bob@example.com` - Premium member \n",
|
||
"- `priority_user` - Test account"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "c6fc0feb",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# Simulated priority members database\n",
|
||
"PRIORITY_MEMBERS = {\n",
|
||
" \"alice@example.com\",\n",
|
||
" \"bob@example.com\",\n",
|
||
" \"priority_user\",\n",
|
||
"}\n",
|
||
"\n",
|
||
"# Global variable to track current user (in real app, use proper session management)\n",
|
||
"current_user_id = \"regular_user\" # Default: regular user\n",
|
||
"\n",
|
||
"\n",
|
||
"def set_user(user_id: str):\n",
|
||
" \"\"\"Set the current user for the session.\"\"\"\n",
|
||
" global current_user_id\n",
|
||
" current_user_id = user_id\n",
|
||
" is_priority = user_id in PRIORITY_MEMBERS\n",
|
||
" status = \"🌟 PRIORITY MEMBER\" if is_priority else \"👤 Regular User\"\n",
|
||
"\n",
|
||
" display(\n",
|
||
" HTML(f\"\"\"\n",
|
||
" <div style='padding: 15px; background: {\"linear-gradient(135deg, #FFD700 0%, #FFA500 100%)\" if is_priority else \"#e3f2fd\"}; \n",
|
||
" border-left: 4px solid {\"#FF6B35\" if is_priority else \"#2196f3\"}; border-radius: 4px; margin: 10px 0;'>\n",
|
||
" <strong>👤 Current User Set:</strong> {user_id}<br>\n",
|
||
" <strong>Status:</strong> {status}\n",
|
||
" </div>\n",
|
||
" \"\"\")\n",
|
||
" )\n",
|
||
"\n",
|
||
"\n",
|
||
"print(\"✅ Priority members database created\")\n",
|
||
"print(f\" Priority members: {len(PRIORITY_MEMBERS)} users\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "1b503f57",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Step 3: Create the Hotel Booking Tool\n",
|
||
"\n",
|
||
"Same as the conditional workflow, but now it will be intercepted by middleware!"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "1e17f7f2",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"@tool(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 @tool decorator\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "88ffd637",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Step 4: 🌟 Create Priority Check Middleware (THE KEY FEATURE!)\n",
|
||
"\n",
|
||
"This is the **core functionality** of this notebook. The middleware:\n",
|
||
"\n",
|
||
"1. **Intercepts** the hotel_booking function call\n",
|
||
"2. **Executes** the function normally by calling `next(context)`\n",
|
||
"3. **Inspects** the result in `context.result`\n",
|
||
"4. **Overrides** the result if user is priority and no rooms available\n",
|
||
"5. **Returns** the modified result back to the agent\n",
|
||
"\n",
|
||
"**Key Pattern:**\n",
|
||
"```python\n",
|
||
"async def my_middleware(context, next):\n",
|
||
" await next(context) # Execute function\n",
|
||
" # Now context.result contains the function's output\n",
|
||
" if some_condition:\n",
|
||
" context.result = new_value # Override!\n",
|
||
"```"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "ba160f08",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"async def priority_check_middleware(\n",
|
||
" context: FunctionInvocationContext,\n",
|
||
" next: Callable[[FunctionInvocationContext], Awaitable[None]],\n",
|
||
") -> None:\n",
|
||
" \"\"\"\n",
|
||
" Function middleware that overrides hotel_booking results for priority members.\n",
|
||
" \n",
|
||
" Workflow:\n",
|
||
" 1. Let the function execute normally\n",
|
||
" 2. Check if user is a priority member\n",
|
||
" 3. If priority + no availability → Override to make rooms available!\n",
|
||
" 4. Agent will then route to booking path instead of alternative path\n",
|
||
" \"\"\"\n",
|
||
" function_name = context.function.name\n",
|
||
"\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>🔄 Middleware:</strong> Intercepting {function_name}...\n",
|
||
" </div>\n",
|
||
" \"\"\")\n",
|
||
" )\n",
|
||
"\n",
|
||
" # Execute the original function\n",
|
||
" await next(context)\n",
|
||
"\n",
|
||
" # Now inspect and potentially modify the result\n",
|
||
" if context.result and function_name == \"hotel_booking\":\n",
|
||
" result_data = json.loads(context.result)\n",
|
||
" destination = result_data.get(\"destination\", \"\")\n",
|
||
" has_availability = result_data.get(\"has_availability\", False)\n",
|
||
"\n",
|
||
" # Check if user is priority member\n",
|
||
" is_priority = current_user_id in PRIORITY_MEMBERS\n",
|
||
"\n",
|
||
" # Override logic: Priority member + no availability → Make available!\n",
|
||
" if is_priority and not has_availability:\n",
|
||
" display(\n",
|
||
" HTML(f\"\"\"\n",
|
||
" <div style='padding: 20px; background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); \n",
|
||
" border-radius: 8px; margin: 10px 0; box-shadow: 0 4px 12px rgba(255,165,0,0.4);'>\n",
|
||
" <h3 style='margin: 0 0 10px 0; color: #333;'>🌟 PRIORITY OVERRIDE ACTIVATED! 🌟</h3>\n",
|
||
" <p style='margin: 0; color: #555; line-height: 1.6;'>\n",
|
||
" <strong>User:</strong> {current_user_id}<br>\n",
|
||
" <strong>Status:</strong> VIP Priority Member<br>\n",
|
||
" <strong>Action:</strong> Overriding \"No Availability\" for {destination}<br>\n",
|
||
" <strong>Result:</strong> ✅ Rooms now available for priority booking!\n",
|
||
" </p>\n",
|
||
" </div>\n",
|
||
" \"\"\")\n",
|
||
" )\n",
|
||
"\n",
|
||
" # Override the result!\n",
|
||
" result_data[\"has_availability\"] = True\n",
|
||
" result_data[\"priority_override\"] = True\n",
|
||
" context.result = json.dumps(result_data)\n",
|
||
"\n",
|
||
" elif not has_availability:\n",
|
||
" display(\n",
|
||
" HTML(f\"\"\"\n",
|
||
" <div style='padding: 12px; background: #ffebee; border-left: 4px solid #f44336; border-radius: 4px; margin: 10px 0;'>\n",
|
||
" <strong>ℹ️ Middleware:</strong> No priority override (user: {current_user_id})\n",
|
||
" </div>\n",
|
||
" \"\"\")\n",
|
||
" )\n",
|
||
"\n",
|
||
"\n",
|
||
"print(\"✅ priority_check_middleware created\")\n",
|
||
"print(\" - Intercepts hotel_booking function\")\n",
|
||
"print(\" - Overrides availability for priority members\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "630b6c2b",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Step 5: Define Condition Functions for Routing\n",
|
||
"\n",
|
||
"Same condition functions as the conditional workflow - they inspect the structured output to determine routing."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "c82b0633",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def has_availability_condition(message: Any) -> bool:\n",
|
||
" \"\"\"Condition for routing when hotels ARE available (including priority overrides!).\"\"\"\n",
|
||
" if not isinstance(message, AgentExecutorResponse):\n",
|
||
" return True\n",
|
||
"\n",
|
||
" try:\n",
|
||
" result = BookingCheckResult.model_validate_json(message.agent_run_response.text)\n",
|
||
"\n",
|
||
" # Check if this was a priority override\n",
|
||
" override_indicator = \" 🌟\" if result.priority_override else \"\"\n",
|
||
"\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}{override_indicator}\n",
|
||
" </div>\n",
|
||
" \"\"\")\n",
|
||
" )\n",
|
||
"\n",
|
||
" return result.has_availability\n",
|
||
" except Exception as e:\n",
|
||
" display(\n",
|
||
" HTML(f\"\"\"\n",
|
||
" <div style='padding: 12px; background: #ffcdd2; border-left: 4px solid #f44336; border-radius: 4px; margin: 10px 0;'>\n",
|
||
" <strong>⚠️ Error:</strong> {str(e)}\n",
|
||
" </div>\n",
|
||
" \"\"\")\n",
|
||
" )\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",
|
||
"\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",
|
||
"\n",
|
||
" return not result.has_availability\n",
|
||
" except Exception:\n",
|
||
" return False\n",
|
||
"\n",
|
||
"\n",
|
||
"print(\"✅ Condition functions defined\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "2189c529",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Step 6: Create Custom Display Executor\n",
|
||
"\n",
|
||
"Same executor as before - displays the final workflow output."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "a4cc88da",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"@executor(id=\"display_result\")\n",
|
||
"async def display_result(response: AgentExecutorResponse, ctx: WorkflowContext[Never, str]) -> None:\n",
|
||
" \"\"\"Display the final result as workflow output.\"\"\"\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(\"✅ display_result executor created\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "18b1857c",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Step 7: Load Environment Variables\n",
|
||
"\n",
|
||
"Configure the LLM client (GitHub Models or OpenAI)."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "5e6584c2",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# Load environment variables\n",
|
||
"load_dotenv()\n",
|
||
"\n",
|
||
"# Configure the Microsoft Foundry provider with keyless authentication\n",
|
||
"provider = FoundryChatClient(\n",
|
||
" project_endpoint=os.environ[\"AZURE_AI_PROJECT_ENDPOINT\"],\n",
|
||
" model=os.environ[\"AZURE_AI_MODEL_DEPLOYMENT_NAME\"],\n",
|
||
" credential=AzureCliCredential(),\n",
|
||
")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "e4993e63",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Step 8: Create AI Agents with Middleware\n",
|
||
"\n",
|
||
"**KEY DIFFERENCE:** When creating the availability_agent, we pass the `middleware` parameter!\n",
|
||
"\n",
|
||
"This is how we inject the priority_check_middleware into the agent's function invocation pipeline."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "8218790d",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# Agent 1: Check availability with tool + middleware\n",
|
||
"availability_agent = AgentExecutor(\n",
|
||
" provider.as_agent(\n",
|
||
" name=\"availability-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), message (string), \"\n",
|
||
" \"and priority_override (bool, true if priority member got special access). \"\n",
|
||
" \"The message should summarize the availability status and mention if priority override occurred.\"\n",
|
||
" ),\n",
|
||
" tools=[hotel_booking],\n",
|
||
" default_options={\"response_format\": BookingCheckResult},\n",
|
||
" middleware=[priority_check_middleware], # 🌟 MIDDLEWARE INJECTION!\n",
|
||
" ),\n",
|
||
" id=\"availability_agent\",\n",
|
||
")\n",
|
||
"\n",
|
||
"# Agent 2: Suggest alternative (when no rooms)\n",
|
||
"alternative_agent = AgentExecutor(\n",
|
||
" provider.as_agent(\n",
|
||
" name=\"alternative-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",
|
||
" default_options={\"response_format\": AlternativeResult},\n",
|
||
" ),\n",
|
||
" id=\"alternative_agent\",\n",
|
||
")\n",
|
||
"\n",
|
||
"# Agent 3: Suggest booking (when rooms available)\n",
|
||
"booking_agent = AgentExecutor(\n",
|
||
" provider.as_agent(\n",
|
||
" name=\"booking-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",
|
||
" \"If priority_override is true in the input, mention that they received priority member access. \"\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",
|
||
" default_options={\"response_format\": BookingConfirmation},\n",
|
||
" ),\n",
|
||
" id=\"booking_agent\",\n",
|
||
")\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 3 Agents:</strong>\n",
|
||
" <ul style='margin: 10px 0 0 0;'>\n",
|
||
" <li><strong>availability_agent</strong> - WITH priority_check_middleware 🌟</li>\n",
|
||
" <li><strong>alternative_agent</strong> - Suggests alternative cities</li>\n",
|
||
" <li><strong>booking_agent</strong> - Encourages booking</li>\n",
|
||
" </ul>\n",
|
||
" </div>\n",
|
||
"\"\"\")\n",
|
||
")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "c6c2ac90",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Step 9: Build the Workflow\n",
|
||
"\n",
|
||
"Same workflow structure as before - conditional routing based on availability."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "9cc9b853",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# Build the workflow with conditional routing\n",
|
||
"workflow = (\n",
|
||
" WorkflowBuilder(\n",
|
||
" start_executor=availability_agent,\n",
|
||
" output_executors=[display_result],\n",
|
||
" )\n",
|
||
" # NO AVAILABILITY PATH\n",
|
||
" .add_edge(availability_agent, alternative_agent, condition=no_availability_condition)\n",
|
||
" .add_edge(alternative_agent, display_result)\n",
|
||
" # HAS AVAILABILITY PATH (can be triggered by middleware override!)\n",
|
||
" .add_edge(availability_agent, booking_agent, condition=has_availability_condition)\n",
|
||
" .add_edge(booking_agent, display_result)\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>Conditional Routing (Middleware-Aware):</strong><br>\n",
|
||
" • If <strong>NO availability</strong> → alternative_agent → display_result<br>\n",
|
||
" • If <strong>availability</strong> (or 🌟 <strong>priority override</strong>) → booking_agent → display_result\n",
|
||
" </p>\n",
|
||
" </div>\n",
|
||
"\"\"\")\n",
|
||
")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "56c53031",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Step 10: Test Case 1 - Regular User in Paris (No Override)\n",
|
||
"\n",
|
||
"A regular user tries to book Paris → No rooms → Routes to alternative_agent"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "e8cf4e21",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# Set as regular user\n",
|
||
"set_user(\"regular_user\")\n",
|
||
"\n",
|
||
"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: Regular User + Paris</h3>\n",
|
||
" <p style='margin: 0;'><strong>Expected:</strong> No rooms → No middleware override → Alternative suggestion</p>\n",
|
||
" </div>\n",
|
||
"\"\"\")\n",
|
||
")\n",
|
||
"\n",
|
||
"# Create request\n",
|
||
"request_regular = AgentExecutorRequest(\n",
|
||
" messages=[Message(role=\"user\", text=\"I want to book a hotel in Paris\")], should_respond=True\n",
|
||
")\n",
|
||
"\n",
|
||
"# Run workflow\n",
|
||
"events_regular = await workflow.run(request_regular)\n",
|
||
"outputs_regular = events_regular.get_outputs()\n",
|
||
"\n",
|
||
"# Display results\n",
|
||
"if outputs_regular:\n",
|
||
" result_regular = AlternativeResult.model_validate_json(outputs_regular[0])\n",
|
||
"\n",
|
||
" display(\n",
|
||
" HTML(f\"\"\"\n",
|
||
" <div style='padding: 25px; background: #fff; border: 2px solid #ff9800; border-radius: 12px; margin: 20px 0;'>\n",
|
||
" <h3 style='margin: 0 0 15px 0; color: #e65100;'>📊 RESULT (Regular User)</h3>\n",
|
||
" <div style='background: #fff3e0; padding: 20px; border-radius: 8px;'>\n",
|
||
" <p style='margin: 0 0 10px 0;'><strong>Status:</strong> ❌ No rooms in Paris</p>\n",
|
||
" <p style='margin: 0 0 10px 0;'><strong>Middleware:</strong> No priority override (regular user)</p>\n",
|
||
" <p style='margin: 0 0 10px 0;'><strong>Alternative:</strong> 🏨 {result_regular.alternative_destination}</p>\n",
|
||
" <p style='margin: 0;'><strong>Reason:</strong> {result_regular.reason}</p>\n",
|
||
" </div>\n",
|
||
" </div>\n",
|
||
" \"\"\")\n",
|
||
" )"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "cea981b0",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Step 11: Test Case 2 - 🌟 Priority User in Paris (WITH Override!)\n",
|
||
"\n",
|
||
"A priority member tries to book Paris → No rooms initially → 🌟 Middleware overrides! → Routes to booking_agent\n",
|
||
"\n",
|
||
"**This is the key demonstration of middleware power!**"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "d390f5ac",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# Set as priority user\n",
|
||
"set_user(\"priority_user\")\n",
|
||
"\n",
|
||
"display(\n",
|
||
" HTML(\"\"\"\n",
|
||
" <div style='padding: 20px; background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); border-radius: 8px; margin: 20px 0;'>\n",
|
||
" <h3 style='margin: 0 0 10px 0; color: #333;'>🧪 TEST CASE 2: 🌟 Priority User + Paris</h3>\n",
|
||
" <p style='margin: 0; color: #555;'><strong>Expected:</strong> No rooms → 🌟 MIDDLEWARE OVERRIDE → Rooms available → Booking suggestion!</p>\n",
|
||
" </div>\n",
|
||
"\"\"\")\n",
|
||
")\n",
|
||
"\n",
|
||
"# Create request\n",
|
||
"request_priority = AgentExecutorRequest(\n",
|
||
" messages=[Message(role=\"user\", text=\"I want to book a hotel in Paris\")], should_respond=True\n",
|
||
")\n",
|
||
"\n",
|
||
"# Run workflow\n",
|
||
"events_priority = await workflow.run(request_priority)\n",
|
||
"outputs_priority = events_priority.get_outputs()\n",
|
||
"\n",
|
||
"# Display results\n",
|
||
"if outputs_priority:\n",
|
||
" result_priority = BookingConfirmation.model_validate_json(outputs_priority[0])\n",
|
||
"\n",
|
||
" display(\n",
|
||
" HTML(f\"\"\"\n",
|
||
" <div style='padding: 25px; background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); border-radius: 12px;\n",
|
||
" box-shadow: 0 8px 16px rgba(255,165,0,0.4); margin: 20px 0;'>\n",
|
||
" <h3 style='margin: 0 0 15px 0; color: #333;'>🏆 RESULT (Priority Member) 🌟</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> ✅ Rooms Available (Priority Override!)</p>\n",
|
||
" <p style='margin: 0 0 10px 0; font-size: 16px;'><strong>Middleware:</strong> 🌟 OVERRIDE ACTIVATED!</p>\n",
|
||
" <p style='margin: 0 0 10px 0; font-size: 16px;'><strong>Destination:</strong> 🏨 {result_priority.destination}</p>\n",
|
||
" <p style='margin: 0 0 10px 0; font-size: 16px;'><strong>Action:</strong> {result_priority.action}</p>\n",
|
||
" <p style='margin: 0; font-size: 14px; color: #666;'><strong>Message:</strong> {result_priority.message}</p>\n",
|
||
" <div style='margin-top: 15px; padding: 15px; background: #fff3cd; border-radius: 6px; border-left: 4px solid #FF6B35;'>\n",
|
||
" <strong>💡 What Just Happened:</strong><br>\n",
|
||
" 1. hotel_booking tool returned \"no availability\"<br>\n",
|
||
" 2. priority_check_middleware intercepted the result<br>\n",
|
||
" 3. Middleware checked user status: priority_user ✅<br>\n",
|
||
" 4. Middleware OVERRODE the result to \"available\"<br>\n",
|
||
" 5. Workflow routed to booking_agent instead of alternative_agent!\n",
|
||
" </div>\n",
|
||
" </div>\n",
|
||
" </div>\n",
|
||
" \"\"\")\n",
|
||
" )"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "baab4b1a",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Step 12: Test Case 3 - Priority User in Stockholm (Already Available)\n",
|
||
"\n",
|
||
"Priority user tries Stockholm → Rooms available → No override needed → Routes to booking_agent\n",
|
||
"\n",
|
||
"This shows that middleware only acts when needed!"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "316a0d31",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# Priority user is still set from previous test\n",
|
||
"\n",
|
||
"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 3: Priority User + Stockholm</h3>\n",
|
||
" <p style='margin: 0;'><strong>Expected:</strong> Rooms available → No override needed → Booking suggestion</p>\n",
|
||
" </div>\n",
|
||
"\"\"\")\n",
|
||
")\n",
|
||
"\n",
|
||
"# Create request\n",
|
||
"request_stockholm = AgentExecutorRequest(\n",
|
||
" messages=[Message(role=\"user\", text=\"I want to book a hotel in Stockholm\")], should_respond=True\n",
|
||
")\n",
|
||
"\n",
|
||
"# Run workflow\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;\n",
|
||
" box-shadow: 0 4px 12px rgba(76,175,80,0.3); margin: 20px 0;'>\n",
|
||
" <h3 style='margin: 0 0 15px 0;'>🏆 RESULT (Priority User - No Override Needed)</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 (Natural)</p>\n",
|
||
" <p style='margin: 0 0 10px 0; font-size: 16px;'><strong>Middleware:</strong> No override needed</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; font-size: 14px; color: #666;'><strong>Message:</strong> {result_stockholm.message}</p>\n",
|
||
" <div style='margin-top: 15px; padding: 15px; background: #e8f5e9; border-radius: 6px; border-left: 4px solid #4caf50;'>\n",
|
||
" <strong>💡 Middleware Behavior:</strong><br>\n",
|
||
" • hotel_booking returned \"available\" naturally<br>\n",
|
||
" • Middleware saw available = true → No override needed<br>\n",
|
||
" • Workflow proceeded normally to booking_agent\n",
|
||
" </div>\n",
|
||
" </div>\n",
|
||
" </div>\n",
|
||
" \"\"\")\n",
|
||
" )"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "e498ee16",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Key Takeaways and Middleware Concepts\n",
|
||
"\n",
|
||
"### ✅ What You've Learned:\n",
|
||
"\n",
|
||
"#### **1. Function-Based Middleware Pattern**\n",
|
||
"\n",
|
||
"Middleware intercepts function calls using a simple async function:\n",
|
||
"\n",
|
||
"```python\n",
|
||
"async def my_middleware(\n",
|
||
" context: FunctionInvocationContext,\n",
|
||
" next: Callable,\n",
|
||
") -> None:\n",
|
||
" # Before function execution\n",
|
||
" print(\"Intercepting...\")\n",
|
||
" \n",
|
||
" # Execute the function\n",
|
||
" await next(context)\n",
|
||
" \n",
|
||
" # After function execution - inspect result\n",
|
||
" if context.result:\n",
|
||
" # Modify result if needed\n",
|
||
" context.result = modified_value\n",
|
||
"```\n",
|
||
"\n",
|
||
"#### **2. Context Access and Result Override**\n",
|
||
"\n",
|
||
"- `context.function` - Access the function being called\n",
|
||
"- `context.arguments` - Read function arguments\n",
|
||
"- `context.kwargs` - Access additional parameters\n",
|
||
"- `await next(context)` - Execute the function\n",
|
||
"- `context.result` - Read/modify the function's output\n",
|
||
"\n",
|
||
"#### **3. Business Logic Implementation**\n",
|
||
"\n",
|
||
"Our middleware implements priority member benefits:\n",
|
||
"- **Regular users**: No modifications, standard workflow\n",
|
||
"- **Priority users**: Override \"no availability\" → \"available\"\n",
|
||
"- **Conditional logic**: Only overrides when needed\n",
|
||
"\n",
|
||
"#### **4. Same Workflow, Different Outcomes**\n",
|
||
"\n",
|
||
"The power of middleware:\n",
|
||
"- ✅ No changes to the workflow structure\n",
|
||
"- ✅ No changes to the tool function\n",
|
||
"- ✅ No changes to conditional routing logic\n",
|
||
"- ✅ Just middleware → Different behavior!\n",
|
||
"\n",
|
||
"### 🚀 Real-World Applications:\n",
|
||
"\n",
|
||
"1. **VIP/Premium Features**\n",
|
||
" - Override rate limits for premium users\n",
|
||
" - Provide priority access to resources\n",
|
||
" - Unlock premium features dynamically\n",
|
||
"\n",
|
||
"2. **A/B Testing**\n",
|
||
" - Route users to different implementations\n",
|
||
" - Test new features with specific users\n",
|
||
" - Gradual feature rollouts\n",
|
||
"\n",
|
||
"3. **Security & Compliance**\n",
|
||
" - Audit function calls\n",
|
||
" - Block sensitive operations\n",
|
||
" - Enforce business rules\n",
|
||
"\n",
|
||
"4. **Performance Optimization**\n",
|
||
" - Cache results for specific users\n",
|
||
" - Skip expensive operations when possible\n",
|
||
" - Dynamic resource allocation\n",
|
||
"\n",
|
||
"5. **Error Handling & Retry**\n",
|
||
" - Catch and handle errors gracefully\n",
|
||
" - Implement retry logic\n",
|
||
" - Fallback to alternative implementations\n",
|
||
"\n",
|
||
"6. **Logging & Monitoring**\n",
|
||
" - Track function execution times\n",
|
||
" - Log parameters and results\n",
|
||
" - Monitor usage patterns\n",
|
||
"\n",
|
||
"### 🔑 Key Differences from Decorators:\n",
|
||
"\n",
|
||
"| Feature | Decorator | Middleware |\n",
|
||
"|---------|-----------|------------|\n",
|
||
"| **Scope** | Single function | All functions in agent |\n",
|
||
"| **Flexibility** | Fixed at definition | Dynamic at runtime |\n",
|
||
"| **Context** | Limited | Full agent context |\n",
|
||
"| **Composition** | Multiple decorators | Middleware pipeline |\n",
|
||
"| **Agent-Aware** | No | Yes (access to agent state) |\n",
|
||
"\n",
|
||
"### 📚 When to Use Middleware:\n",
|
||
"\n",
|
||
"✅ **Use middleware when:**\n",
|
||
"- You need to modify behavior based on user/session state\n",
|
||
"- You want to apply logic to multiple functions\n",
|
||
"- You need access to agent-level context\n",
|
||
"- You're implementing cross-cutting concerns (logging, auth, etc.)\n",
|
||
"\n",
|
||
"❌ **Don't use middleware when:**\n",
|
||
"- Simple input validation (use Pydantic)\n",
|
||
"- Function-specific logic (keep in function)\n",
|
||
"- One-time modifications (just change the function)\n",
|
||
"\n",
|
||
"### 🎓 Advanced Patterns:\n",
|
||
"\n",
|
||
"```python\n",
|
||
"# Multiple middleware (execution order matters!)\n",
|
||
"middleware=[\n",
|
||
" logging_middleware, # Logs first\n",
|
||
" auth_middleware, # Then checks auth\n",
|
||
" cache_middleware, # Then checks cache\n",
|
||
" rate_limit_middleware, # Then rate limits\n",
|
||
" priority_check_middleware # Finally priority check\n",
|
||
"]\n",
|
||
"\n",
|
||
"# Conditional middleware execution\n",
|
||
"async def conditional_middleware(context, next):\n",
|
||
" if should_execute(context):\n",
|
||
" await next(context)\n",
|
||
" # Modify result\n",
|
||
" else:\n",
|
||
" # Skip execution entirely\n",
|
||
" context.result = cached_value\n",
|
||
"```\n",
|
||
"\n",
|
||
"### 🔗 Related Concepts:\n",
|
||
"\n",
|
||
"- **Agent Middleware**: Intercepts agent.run() calls\n",
|
||
"- **Function Middleware**: Intercepts tool function calls (what we used!)\n",
|
||
"- **Middleware Pipeline**: Chain of middleware executing in order\n",
|
||
"- **Context Propagation**: Pass state through middleware chain"
|
||
]
|
||
}
|
||
],
|
||
"metadata": {
|
||
"kernelspec": {
|
||
"display_name": ".venv (3.12.12)",
|
||
"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.12"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 5
|
||
}
|