chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,560 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "69f48405",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Travel Recommendations with Concurrent Orchestration\n",
|
||||
"\n",
|
||||
"This notebook demonstrates **concurrent orchestration** using the Microsoft Agent Framework. We'll build a travel recommendation system with three specialized agents that work in parallel to provide comprehensive travel insights.\n",
|
||||
"\n",
|
||||
"## What You'll Learn:\n",
|
||||
"1. **Concurrent Orchestration**: Running multiple agents in parallel (fan-out/fan-in pattern)\n",
|
||||
"2. **ConcurrentBuilder**: High-level API for building concurrent workflows\n",
|
||||
"3. **Travel Recommendations**: Three specialized agents working together\n",
|
||||
"4. **Default Aggregation**: Combining multiple agent responses\n",
|
||||
"5. **Performance Benefits**: Parallel execution vs sequential processing\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"## Three Specialized Agents:\n",
|
||||
"\n",
|
||||
"1. **Attractions Agent**: Tourist attractions, activities, landmarks\n",
|
||||
"2. **Dining Agent**: Local cuisine, restaurants, food experiences\n",
|
||||
"3. **History Agent**: Historical facts, cultural significance, context"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "1c8918b5",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import asyncio\n",
|
||||
"import json\n",
|
||||
"import os\n",
|
||||
"from typing import Any, cast\n",
|
||||
"\n",
|
||||
"from agent_framework import (\n",
|
||||
" Executor,\n",
|
||||
" Message,\n",
|
||||
" WorkflowBuilder,\n",
|
||||
" WorkflowContext,\n",
|
||||
" handler,\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": "2733c34d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 1: Define Pydantic Models for Structured Outputs\n",
|
||||
"\n",
|
||||
"These models define the schema that each specialized agent will return. This ensures consistent and parseable responses from all agents."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "0d144c4c",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 1: Define Pydantic Models for Structured Outputs\n",
|
||||
"\n",
|
||||
"These models define the schema that each specialized agent will return. This ensures consistent and parseable responses from all agents."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "e0749818",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"class AttractionsRecommendation(BaseModel):\n",
|
||||
" \"\"\"Tourist attractions and activities recommendations.\"\"\"\n",
|
||||
"\n",
|
||||
" destination: str\n",
|
||||
" top_attractions: list[str]\n",
|
||||
" activities: list[str]\n",
|
||||
" best_time_to_visit: str\n",
|
||||
" transportation_tips: str \n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class DiningRecommendation(BaseModel):\n",
|
||||
" \"\"\"Food and dining recommendations.\"\"\"\n",
|
||||
"\n",
|
||||
" destination: str\n",
|
||||
" local_cuisine: str\n",
|
||||
" must_try_dishes: list[str]\n",
|
||||
" recommended_restaurants: list[str]\n",
|
||||
" food_experiences: list[str]\n",
|
||||
" dining_etiquette: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class HistoryRecommendation(BaseModel):\n",
|
||||
" \"\"\"Historical and cultural information.\"\"\"\n",
|
||||
"\n",
|
||||
" destination: str\n",
|
||||
" historical_significance: str\n",
|
||||
" cultural_highlights: list[str]\n",
|
||||
" important_periods: list[str]\n",
|
||||
" cultural_experiences: list[str]\n",
|
||||
" interesting_facts: list[str]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "979521fb",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 2: Load Environment Variables and Configure the Foundry Provider\n",
|
||||
"\n",
|
||||
"Use `FoundryChatClient` with keyless `AzureCliCredential` authentication, matching the pattern used in lessons 01–13.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "c5179ca3",
|
||||
"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",
|
||||
"\n",
|
||||
"print(\"Microsoft Foundry provider configured successfully!\")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "8e4eb398",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 3: Create Three Specialized Travel Agents\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "6a9571c6",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Agent 1: Tourist Attractions Expert\n",
|
||||
"attractions_agent = provider.as_agent(\n",
|
||||
" name=\"attractions-agent\",\n",
|
||||
" instructions=(\n",
|
||||
" \"You are a tourism expert specializing in attractions and activities. \"\n",
|
||||
" \"When given a travel destination, provide comprehensive recommendations for \"\n",
|
||||
" \"tourist attractions, activities, best times to visit, and transportation tips. \"\n",
|
||||
" \"Focus on popular landmarks, unique experiences, and practical travel advice. \"\n",
|
||||
" \"Return structured JSON matching the AttractionsRecommendation schema.\"\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Agent 2: Food and Dining Expert\n",
|
||||
"dining_agent = provider.as_agent(\n",
|
||||
" name=\"dining-agent\",\n",
|
||||
" instructions=(\n",
|
||||
" \"You are a culinary expert specializing in local food and dining experiences. \"\n",
|
||||
" \"When given a travel destination, provide recommendations for local cuisine, \"\n",
|
||||
" \"must-try dishes, recommended restaurants, and unique food experiences. \"\n",
|
||||
" \"Include dining etiquette and cultural food customs. \"\n",
|
||||
" \"Return structured JSON matching the DiningRecommendation schema.\"\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Agent 3: History and Culture Expert\n",
|
||||
"history_agent = provider.as_agent(\n",
|
||||
" name=\"history-agent\",\n",
|
||||
" instructions=(\n",
|
||||
" \"You are a historian and cultural expert. \"\n",
|
||||
" \"When given a travel destination, provide historical context, cultural significance, \"\n",
|
||||
" \"important historical periods, cultural experiences, and interesting facts. \"\n",
|
||||
" \"Focus on helping travelers understand the cultural heritage and historical importance. \"\n",
|
||||
" \"Return structured JSON matching the HistoryRecommendation schema.\"\n",
|
||||
" ),\n",
|
||||
")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "2b22d245",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Step 4: Build the Concurrent Workflow\n",
|
||||
"\n",
|
||||
"`WorkflowBuilder` with a small dispatcher executor and `add_fan_out_edges`:\n",
|
||||
"1. **Dispatcher** broadcasts the same input to all three agents\n",
|
||||
"2. **Three agents** run in parallel\n",
|
||||
"3. **Output** collects each agent's response separately"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "4bad34b0",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# A passthrough executor that broadcasts the user input to every agent in parallel.\n",
|
||||
"class InputDispatcher(Executor):\n",
|
||||
" \"\"\"Forward the user input unchanged to all participating agents.\"\"\"\n",
|
||||
"\n",
|
||||
" @handler\n",
|
||||
" async def forward(self, text: str, ctx: WorkflowContext[str]) -> None:\n",
|
||||
" await ctx.send_message(text)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"dispatcher = InputDispatcher(id=\"dispatcher\")\n",
|
||||
"agents = [attractions_agent, dining_agent, history_agent]\n",
|
||||
"\n",
|
||||
"workflow = (\n",
|
||||
" WorkflowBuilder(\n",
|
||||
" start_executor=dispatcher,\n",
|
||||
" output_executors=agents,\n",
|
||||
" )\n",
|
||||
" .add_fan_out_edges(dispatcher, agents)\n",
|
||||
" .build()\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"display(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;'>Concurrent Workflow Built Successfully!</h3>\n",
|
||||
" <p style='margin: 0; line-height: 1.6;'>\n",
|
||||
" <strong>Architecture:</strong><br>\n",
|
||||
" • Input → <strong>Dispatcher</strong> (fan-out)<br>\n",
|
||||
" • <strong>3 Agents</strong> run in parallel (attractions, dining, history)<br>\n",
|
||||
" • Output → 3 AgentResponse objects, one per agent\n",
|
||||
" </p>\n",
|
||||
"</div>\n",
|
||||
"\"\"\"))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "e920f2f6",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 5: Test Case 1 - Tokyo Travel Recommendations\n",
|
||||
"\n",
|
||||
"Let's test our concurrent workflow with Tokyo as the destination. All three agents will work simultaneously to provide comprehensive travel recommendations."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "090ae929",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"async def display_travel_recommendations(destination: str):\n",
|
||||
" \"\"\"Run the concurrent workflow and display formatted results.\"\"\"\n",
|
||||
"\n",
|
||||
" display(HTML(f\"\"\"\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;'>Processing Travel Recommendations for {destination}</h3>\n",
|
||||
" <p style='margin: 0;'><strong>Status:</strong> Running 3 agents concurrently...</p>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
" # Run the workflow. With WorkflowBuilder(output_executors=[a1, a2, a3]),\n",
|
||||
" # outputs is a list of AgentResponse objects in the same order as output_executors.\n",
|
||||
" events = await workflow.run(f\"I want comprehensive travel recommendations for {destination}\")\n",
|
||||
" outputs = events.get_outputs()\n",
|
||||
"\n",
|
||||
" # Display results header\n",
|
||||
" display(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",
|
||||
" <h2 style='margin: 0 0 20px 0;'>Complete Travel Guide for {destination}</h2>\n",
|
||||
" <p style='margin: 0; font-size: 14px; opacity: 0.9;'>Generated by 3 concurrent agents</p>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
" sections = [\n",
|
||||
" (\"attractions-agent\", AttractionsRecommendation, display_attractions_section),\n",
|
||||
" (\"dining-agent\", DiningRecommendation, display_dining_section),\n",
|
||||
" (\"history-agent\", HistoryRecommendation, display_history_section),\n",
|
||||
" ]\n",
|
||||
"\n",
|
||||
" for i, (agent_name, schema, render) in enumerate(sections):\n",
|
||||
" if i >= len(outputs):\n",
|
||||
" continue\n",
|
||||
" text = outputs[i].text\n",
|
||||
" try:\n",
|
||||
" data = schema.model_validate_json(text)\n",
|
||||
" render(data)\n",
|
||||
" except Exception as e:\n",
|
||||
" display(HTML(f\"\"\"\n",
|
||||
" <div style='padding: 15px; background: #ffcdd2; border-left: 4px solid #f44336; border-radius: 4px; margin: 10px 0;'>\n",
|
||||
" <strong>Error parsing {agent_name} response:</strong> {str(e)}\n",
|
||||
" <details><summary>Raw response</summary>{text}</details>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def display_attractions_section(data: AttractionsRecommendation):\n",
|
||||
" \"\"\"Display attractions recommendations in a formatted section.\"\"\"\n",
|
||||
" attractions_list = \"\".join([f\"<li>{attraction}</li>\" for attraction in data.top_attractions])\n",
|
||||
" activities_list = \"\".join([f\"<li>{activity}</li>\" for activity in data.activities])\n",
|
||||
"\n",
|
||||
" display(HTML(f\"\"\"\n",
|
||||
" <div style='padding: 20px; background: #e3f2fd; border-radius: 8px; margin: 15px 0; border-left: 4px solid #2196f3;'>\n",
|
||||
" <h3 style='margin: 0 0 15px 0; color: #1976d2;'>🏛️ Tourist Attractions & Activities</h3>\n",
|
||||
" <div style='margin-bottom: 15px;'>\n",
|
||||
" <h4 style='margin: 0 0 8px 0; color: #333;'>Top Attractions:</h4>\n",
|
||||
" <ul style='margin: 0; padding-left: 20px; color: #555;'>{attractions_list}</ul>\n",
|
||||
" </div>\n",
|
||||
" <div style='margin-bottom: 15px;'>\n",
|
||||
" <h4 style='margin: 0 0 8px 0; color: #333;'>Recommended Activities:</h4>\n",
|
||||
" <ul style='margin: 0; padding-left: 20px; color: #555;'>{activities_list}</ul>\n",
|
||||
" </div>\n",
|
||||
" <div style='margin-bottom: 10px;'>\n",
|
||||
" <strong style='color: #333;'>Best Time to Visit:</strong> {data.best_time_to_visit}\n",
|
||||
" </div>\n",
|
||||
" <div>\n",
|
||||
" <strong style='color: #333;'>Transportation Tips:</strong> {data.transportation_tips}\n",
|
||||
" </div>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def display_dining_section(data: DiningRecommendation):\n",
|
||||
" \"\"\"Display dining recommendations in a formatted section.\"\"\"\n",
|
||||
" dishes_list = \"\".join(\n",
|
||||
" [f\"<li>{dish}</li>\" for dish in data.must_try_dishes])\n",
|
||||
" restaurants_list = \"\".join(\n",
|
||||
" [f\"<li>{restaurant}</li>\" for restaurant in data.recommended_restaurants])\n",
|
||||
" experiences_list = \"\".join(\n",
|
||||
" [f\"<li>{exp}</li>\" for exp in data.food_experiences])\n",
|
||||
"\n",
|
||||
" display(HTML(f\"\"\"\n",
|
||||
" <div style='padding: 20px; background: #fff3e0; border-radius: 8px; margin: 15px 0; border-left: 4px solid #ff9800;'>\n",
|
||||
" <h3 style='margin: 0 0 15px 0; color: #f57c00;'>🍜 Food & Dining Experiences</h3>\n",
|
||||
" <div style='margin-bottom: 15px;'>\n",
|
||||
" <strong style='color: #333;'>Local Cuisine:</strong> {data.local_cuisine}\n",
|
||||
" </div>\n",
|
||||
" <div style='margin-bottom: 15px;'>\n",
|
||||
" <h4 style='margin: 0 0 8px 0; color: #333;'>Must-Try Dishes:</h4>\n",
|
||||
" <ul style='margin: 0; padding-left: 20px; color: #555;'>{dishes_list}</ul>\n",
|
||||
" </div>\n",
|
||||
" <div style='margin-bottom: 15px;'>\n",
|
||||
" <h4 style='margin: 0 0 8px 0; color: #333;'>Recommended Restaurants:</h4>\n",
|
||||
" <ul style='margin: 0; padding-left: 20px; color: #555;'>{restaurants_list}</ul>\n",
|
||||
" </div>\n",
|
||||
" <div style='margin-bottom: 15px;'>\n",
|
||||
" <h4 style='margin: 0 0 8px 0; color: #333;'>Food Experiences:</h4>\n",
|
||||
" <ul style='margin: 0; padding-left: 20px; color: #555;'>{experiences_list}</ul>\n",
|
||||
" </div>\n",
|
||||
" <div>\n",
|
||||
" <strong style='color: #333;'>Dining Etiquette:</strong> {data.dining_etiquette}\n",
|
||||
" </div>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def display_history_section(data: HistoryRecommendation):\n",
|
||||
" \"\"\"Display history recommendations in a formatted section.\"\"\"\n",
|
||||
" highlights_list = \"\".join(\n",
|
||||
" [f\"<li>{highlight}</li>\" for highlight in data.cultural_highlights])\n",
|
||||
" periods_list = \"\".join(\n",
|
||||
" [f\"<li>{period}</li>\" for period in data.important_periods])\n",
|
||||
" experiences_list = \"\".join(\n",
|
||||
" [f\"<li>{exp}</li>\" for exp in data.cultural_experiences])\n",
|
||||
" facts_list = \"\".join(\n",
|
||||
" [f\"<li>{fact}</li>\" for fact in data.interesting_facts])\n",
|
||||
"\n",
|
||||
" display(HTML(f\"\"\"\n",
|
||||
" <div style='padding: 20px; background: #f3e5f5; border-radius: 8px; margin: 15px 0; border-left: 4px solid #9c27b0;'>\n",
|
||||
" <h3 style='margin: 0 0 15px 0; color: #7b1fa2;'>📚 History & Culture</h3>\n",
|
||||
" <div style='margin-bottom: 15px;'>\n",
|
||||
" <strong style='color: #333;'>Historical Significance:</strong> {data.historical_significance}\n",
|
||||
" </div>\n",
|
||||
" <div style='margin-bottom: 15px;'>\n",
|
||||
" <h4 style='margin: 0 0 8px 0; color: #333;'>Cultural Highlights:</h4>\n",
|
||||
" <ul style='margin: 0; padding-left: 20px; color: #555;'>{highlights_list}</ul>\n",
|
||||
" </div>\n",
|
||||
" <div style='margin-bottom: 15px;'>\n",
|
||||
" <h4 style='margin: 0 0 8px 0; color: #333;'>Important Historical Periods:</h4>\n",
|
||||
" <ul style='margin: 0; padding-left: 20px; color: #555;'>{periods_list}</ul>\n",
|
||||
" </div>\n",
|
||||
" <div style='margin-bottom: 15px;'>\n",
|
||||
" <h4 style='margin: 0 0 8px 0; color: #333;'>Cultural Experiences:</h4>\n",
|
||||
" <ul style='margin: 0; padding-left: 20px; color: #555;'>{experiences_list}</ul>\n",
|
||||
" </div>\n",
|
||||
" <div>\n",
|
||||
" <h4 style='margin: 0 0 8px 0; color: #333;'>Interesting Facts:</h4>\n",
|
||||
" <ul style='margin: 0; padding-left: 20px; color: #555;'>{facts_list}</ul>\n",
|
||||
" </div>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Test with Tokyo\n",
|
||||
"await display_travel_recommendations(\"Tokyo\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "454fa849",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Step 6: Test Case 2 - Paris Travel Recommendations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "8a23dbc4",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"await display_travel_recommendations(\"Paris\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "7b173674",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 7: Performance Analysis - Concurrent vs Sequential\n",
|
||||
"\n",
|
||||
"Let's measure the performance difference between concurrent and sequential execution to demonstrate the benefits of concurrent orchestration.\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "e0badd2e",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import time\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async def measure_concurrent_performance(destination: str):\n",
|
||||
" \"\"\"Measure concurrent execution time.\"\"\"\n",
|
||||
" start_time = time.time()\n",
|
||||
"\n",
|
||||
" events = await workflow.run(f\"I want travel recommendations for {destination}\")\n",
|
||||
" outputs = events.get_outputs()\n",
|
||||
"\n",
|
||||
" end_time = time.time()\n",
|
||||
" return end_time - start_time, len(outputs)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async def measure_sequential_performance(destination: str):\n",
|
||||
" \"\"\"Measure sequential execution time.\"\"\"\n",
|
||||
" # Build a sequential workflow that chains the same agents one after another.\n",
|
||||
" sequential_workflow = (\n",
|
||||
" WorkflowBuilder(\n",
|
||||
" start_executor=attractions_agent,\n",
|
||||
" output_executors=[attractions_agent, dining_agent, history_agent],\n",
|
||||
" )\n",
|
||||
" .add_chain([attractions_agent, dining_agent, history_agent])\n",
|
||||
" .build()\n",
|
||||
" )\n",
|
||||
" start_time = time.time()\n",
|
||||
"\n",
|
||||
" events = await sequential_workflow.run(f\"I want travel recommendations for {destination}\")\n",
|
||||
" outputs = events.get_outputs()\n",
|
||||
"\n",
|
||||
" end_time = time.time()\n",
|
||||
" return end_time - start_time, len(outputs)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async def performance_comparison():\n",
|
||||
" \"\"\"Compare concurrent vs sequential performance.\"\"\"\n",
|
||||
" test_destination = \"Barcelona\"\n",
|
||||
"\n",
|
||||
" display(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;'>Performance Comparison Test</h3>\n",
|
||||
" <p style='margin: 0;'>Testing with destination: <strong>Barcelona</strong></p>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
" # Test concurrent execution\n",
|
||||
" print(\"Running concurrent workflow...\")\n",
|
||||
" concurrent_time, concurrent_count = await measure_concurrent_performance(test_destination)\n",
|
||||
"\n",
|
||||
" # Test sequential execution\n",
|
||||
" print(\"Running sequential workflow...\")\n",
|
||||
" sequential_time, sequential_count = await measure_sequential_performance(test_destination)\n",
|
||||
"\n",
|
||||
" # Calculate performance improvement\n",
|
||||
" improvement = ((sequential_time - concurrent_time) / sequential_time) * 100\n",
|
||||
"\n",
|
||||
" display(HTML(f\"\"\"\n",
|
||||
" <div style='padding: 25px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 12px;\n",
|
||||
" box-shadow: 0 4px 12px rgba(102,126,234,0.4); margin: 20px 0;'>\n",
|
||||
" <h2 style='margin: 0 0 20px 0;'>Performance Results</h2>\n",
|
||||
" <div style='display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;'>\n",
|
||||
" <div style='background: rgba(255,255,255,0.1); padding: 15px; border-radius: 8px;'>\n",
|
||||
" <h4 style='margin: 0 0 10px 0;'>⚡ Concurrent Execution</h4>\n",
|
||||
" <p style='margin: 0; font-size: 24px; font-weight: bold;'>{concurrent_time:.2f}s</p>\n",
|
||||
" <p style='margin: 5px 0 0 0; font-size: 14px; opacity: 0.9;'>{concurrent_count} agent responses</p>\n",
|
||||
" </div>\n",
|
||||
" <div style='background: rgba(255,255,255,0.1); padding: 15px; border-radius: 8px;'>\n",
|
||||
" <h4 style='margin: 0 0 10px 0;'>🔄 Sequential Execution</h4>\n",
|
||||
" <p style='margin: 0; font-size: 24px; font-weight: bold;'>{sequential_time:.2f}s</p>\n",
|
||||
" <p style='margin: 5px 0 0 0; font-size: 14px; opacity: 0.9;'>{sequential_count} agent responses</p>\n",
|
||||
" </div>\n",
|
||||
" </div>\n",
|
||||
" <div style='background: rgba(255,255,255,0.15); padding: 15px; border-radius: 8px;'>\n",
|
||||
" <h4 style='margin: 0 0 10px 0;'>Performance Improvement</h4>\n",
|
||||
" <p style='margin: 0; font-size: 20px; font-weight: bold;'>{improvement:.1f}% faster</p>\n",
|
||||
" <p style='margin: 5px 0 0 0; font-size: 14px; opacity: 0.9;'>\n",
|
||||
" Saved {sequential_time - concurrent_time:.2f} seconds with concurrent execution\n",
|
||||
" </p>\n",
|
||||
" </div>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Run performance comparison\n",
|
||||
"await performance_comparison()"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "4b2cf5f5",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import asyncio\n",
|
||||
"import json\n",
|
||||
"import os\n",
|
||||
"from typing import Annotated, Any, Never\n",
|
||||
"\n",
|
||||
"from agent_framework import (\n",
|
||||
" AgentExecutor,\n",
|
||||
" AgentExecutorRequest,\n",
|
||||
" AgentExecutorResponse,\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": "001c224e",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 1: Define Pydantic Models for Structured Outputs\n",
|
||||
"\n",
|
||||
"These models define the **schema** that agents will return. Using `response_format` with Pydantic ensures:\n",
|
||||
"- ✅ Type-safe data extraction\n",
|
||||
"- ✅ Automatic validation\n",
|
||||
"- ✅ No parsing errors from free-text responses\n",
|
||||
"- ✅ Easy conditional routing based on fields"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "6c2ef582",
|
||||
"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",
|
||||
"\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)\")\n",
|
||||
"print(\" - AlternativeResult (alternative suggestion)\")\n",
|
||||
"print(\" - BookingConfirmation (booking confirmation)\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "48423ecc",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 2: Create the Hotel Booking Tool\n",
|
||||
"\n",
|
||||
"This tool is what the **availability_agent** will call to check if rooms are available. We use the `@ai_function` decorator to:\n",
|
||||
"- Convert a Python function into an AI-callable tool\n",
|
||||
"- Automatically generate JSON schema for the LLM\n",
|
||||
"- Handle parameter validation\n",
|
||||
"- Enable automatic invocation by agents\n",
|
||||
"\n",
|
||||
"For this demo:\n",
|
||||
"- **Stockholm, Seattle, Tokyo, London, Amsterdam** → Have rooms ✅\n",
|
||||
"- **All other cities** → No rooms ❌"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "aad7e7ec",
|
||||
"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": "134c54b0",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 3: Define Condition Functions for Routing\n",
|
||||
"\n",
|
||||
"These functions inspect the agent's response and determine which path to take in the workflow.\n",
|
||||
"\n",
|
||||
"**Key Pattern:**\n",
|
||||
"1. Check if the message is `AgentExecutorResponse`\n",
|
||||
"2. Parse the structured output (Pydantic model)\n",
|
||||
"3. Return `True` or `False` to control routing\n",
|
||||
"\n",
|
||||
"The workflow will evaluate these conditions on **edges** to decide which executor to invoke next."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "6960edd1",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def has_availability_condition(message: Any) -> bool:\n",
|
||||
" \"\"\"\n",
|
||||
" Condition for routing when hotels ARE available.\n",
|
||||
" \n",
|
||||
" Returns True if the destination has hotel rooms.\n",
|
||||
" \"\"\"\n",
|
||||
" if not isinstance(message, AgentExecutorResponse):\n",
|
||||
" return True # Default to True if unexpected type\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: #c8e6c9; border-left: 4px solid #4caf50; border-radius: 4px; margin: 10px 0;'>\n",
|
||||
" <strong>✅ Condition Check:</strong> has_availability = <strong>{result.has_availability}</strong> for {result.destination}\n",
|
||||
" </div>\n",
|
||||
" \"\"\")\n",
|
||||
" )\n",
|
||||
"\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",
|
||||
" \"\"\"\n",
|
||||
" Condition for routing when hotels are NOT available.\n",
|
||||
" \n",
|
||||
" Returns True if the destination has no hotel rooms.\n",
|
||||
" \"\"\"\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 as e:\n",
|
||||
" return False\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(\"✅ Condition functions defined:\")\n",
|
||||
"print(\" - has_availability_condition (routes when rooms exist)\")\n",
|
||||
"print(\" - no_availability_condition (routes when no rooms)\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "9dc783ba",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 4: Create Custom Display Executor\n",
|
||||
"\n",
|
||||
"Executors are workflow components that perform transformations or side effects. We use the `@executor` decorator to create a custom executor that displays the final result.\n",
|
||||
"\n",
|
||||
"**Key Concepts:**\n",
|
||||
"- `@executor(id=\"...\")` - Registers a function as a workflow executor\n",
|
||||
"- `WorkflowContext[Never, str]` - Type hints for input/output\n",
|
||||
"- `ctx.yield_output(...)` - Yields the final workflow result"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "c67f6b55",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"@executor(id=\"display_result\")\n",
|
||||
"async def display_result(response: AgentExecutorResponse, ctx: WorkflowContext[Never, str]) -> None:\n",
|
||||
" \"\"\"\n",
|
||||
" Display the final result as workflow output.\n",
|
||||
" \n",
|
||||
" This executor receives the final agent response and yields it as the workflow output.\n",
|
||||
" \"\"\"\n",
|
||||
" display(\n",
|
||||
" HTML(\"\"\"\n",
|
||||
" <div style='padding: 15px; background: #f3e5f5; border-left: 4px solid #9c27b0; border-radius: 4px; margin: 10px 0;'>\n",
|
||||
" <strong>📤 Display Executor:</strong> Yielding workflow output\n",
|
||||
" </div>\n",
|
||||
" \"\"\")\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" await ctx.yield_output(response.agent_run_response.text)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(\"✅ display_result executor created with @executor decorator\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "7de6eb90",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 5: Load Environment Variables\n",
|
||||
"\n",
|
||||
"Configure the LLM client. This example works with:\n",
|
||||
"- **GitHub Models** (Free tier with GitHub token)\n",
|
||||
"- **Azure OpenAI**\n",
|
||||
"- **OpenAI**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "1e8f0d88",
|
||||
"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": "3fc61fe7",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 6: Create AI Agents with Structured Outputs\n",
|
||||
"\n",
|
||||
"We create **three specialized agents**, each wrapped in an `AgentExecutor`:\n",
|
||||
"\n",
|
||||
"1. **availability_agent** - Checks hotel availability using the tool\n",
|
||||
"2. **alternative_agent** - Suggests alternative cities (when no rooms)\n",
|
||||
"3. **booking_agent** - Encourages booking (when rooms available)\n",
|
||||
"\n",
|
||||
"**Key Features:**\n",
|
||||
"- `tools=[hotel_booking]` - Provides the tool to the agent\n",
|
||||
"- `response_format=PydanticModel` - Forces structured JSON output\n",
|
||||
"- `AgentExecutor(..., id=\"...\")` - Wraps agent for workflow use"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "66466dda",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Agent 1: Check availability with tool\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), and message (string). \"\n",
|
||||
" \"The message should summarize the availability status.\"\n",
|
||||
" ),\n",
|
||||
" tools=[hotel_booking],\n",
|
||||
" default_options={\"response_format\": BookingCheckResult},\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",
|
||||
" \"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> - Checks availability with hotel_booking tool</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": "7879a0cb",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 7: Build the Workflow with Conditional Edges\n",
|
||||
"\n",
|
||||
"Now we use `WorkflowBuilder` to construct the graph with conditional routing:\n",
|
||||
"\n",
|
||||
"**Workflow Structure:**\n",
|
||||
"```\n",
|
||||
"availability_agent (START)\n",
|
||||
" ↓\n",
|
||||
" Evaluate conditions\n",
|
||||
" ↙ ↘\n",
|
||||
"[no_availability] [has_availability]\n",
|
||||
" ↓ ↓\n",
|
||||
"alternative_agent booking_agent\n",
|
||||
" ↓ ↓\n",
|
||||
" display_result ←───┘\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"**Key Methods:**\n",
|
||||
"- `.set_start_executor(...)` - Sets the entry point\n",
|
||||
"- `.add_edge(from, to, condition=...)` - Adds conditional edge\n",
|
||||
"- `.build()` - Finalizes the workflow"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "90bb29dd",
|
||||
"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\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:</strong><br>\n",
|
||||
" • If <strong>NO availability</strong> → alternative_agent → display_result<br>\n",
|
||||
" • If <strong>availability</strong> → booking_agent → display_result\n",
|
||||
" </p>\n",
|
||||
" </div>\n",
|
||||
"\"\"\")\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "0a3ad845",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 8: Run Test Case 1 - City WITHOUT Availability (Paris)\n",
|
||||
"\n",
|
||||
"Let's test the **no availability** path by requesting hotels in Paris (which has no rooms in our simulation)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "af1538cd",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"display(\n",
|
||||
" HTML(\"\"\"\n",
|
||||
" <div style='padding: 20px; background: #fff3e0; border-left: 4px solid #ff9800; border-radius: 8px; margin: 20px 0;'>\n",
|
||||
" <h3 style='margin: 0 0 10px 0; color: #e65100;'>🧪 TEST CASE 1: Paris (No Availability)</h3>\n",
|
||||
" <p style='margin: 0;'>Expected workflow path: availability_agent → alternative_agent → display_result</p>\n",
|
||||
" </div>\n",
|
||||
"\"\"\")\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Create request for Paris\n",
|
||||
"request_paris = AgentExecutorRequest(\n",
|
||||
" messages=[Message(role=\"user\", text=\"I want to book a hotel in Paris\")], should_respond=True\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Run the workflow\n",
|
||||
"events_paris = await workflow.run(request_paris)\n",
|
||||
"outputs_paris = events_paris.get_outputs()\n",
|
||||
"\n",
|
||||
"# Display results\n",
|
||||
"if outputs_paris:\n",
|
||||
" result_paris = AlternativeResult.model_validate_json(outputs_paris[0])\n",
|
||||
"\n",
|
||||
" display(\n",
|
||||
" HTML(f\"\"\"\n",
|
||||
" <div style='padding: 25px; background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); border-radius: 12px; box-shadow: 0 4px 12px rgba(255,165,0,0.3); margin: 20px 0;'>\n",
|
||||
" <h3 style='margin: 0 0 15px 0; color: #333;'>🏆 WORKFLOW RESULT (Paris)</h3>\n",
|
||||
" <div style='background: white; padding: 20px; border-radius: 8px;'>\n",
|
||||
" <p style='margin: 0 0 10px 0; font-size: 16px;'><strong>Status:</strong> ❌ No rooms in Paris</p>\n",
|
||||
" <p style='margin: 0 0 10px 0; font-size: 16px;'><strong>Alternative Suggestion:</strong> 🏨 {result_paris.alternative_destination}</p>\n",
|
||||
" <p style='margin: 0; font-size: 14px; color: #666;'><strong>Reason:</strong> {result_paris.reason}</p>\n",
|
||||
" </div>\n",
|
||||
" </div>\n",
|
||||
" \"\"\")\n",
|
||||
" )"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "408a3f60",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 9: Run Test Case 2 - City WITH Availability (Stockholm)\n",
|
||||
"\n",
|
||||
"Now let's test the **availability** path by requesting hotels in Stockholm (which has rooms in our simulation)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "e1471000",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"display(\n",
|
||||
" HTML(\"\"\"\n",
|
||||
" <div style='padding: 20px; background: #e8f5e9; border-left: 4px solid #4caf50; border-radius: 8px; margin: 20px 0;'>\n",
|
||||
" <h3 style='margin: 0 0 10px 0; color: #1b5e20;'>🧪 TEST CASE 2: Stockholm (Has Availability)</h3>\n",
|
||||
" <p style='margin: 0;'>Expected workflow path: availability_agent → booking_agent → display_result</p>\n",
|
||||
" </div>\n",
|
||||
"\"\"\")\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Create request for Stockholm\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 the 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; box-shadow: 0 4px 12px rgba(76,175,80,0.3); margin: 20px 0;'>\n",
|
||||
" <h3 style='margin: 0 0 15px 0;'>🏆 WORKFLOW RESULT (Stockholm)</h3>\n",
|
||||
" <div style='background: white; color: #333; padding: 20px; border-radius: 8px;'>\n",
|
||||
" <p style='margin: 0 0 10px 0; font-size: 16px;'><strong>Status:</strong> ✅ Rooms Available!</p>\n",
|
||||
" <p style='margin: 0 0 10px 0; font-size: 16px;'><strong>Destination:</strong> 🏨 {result_stockholm.destination}</p>\n",
|
||||
" <p style='margin: 0 0 10px 0; font-size: 16px;'><strong>Action:</strong> {result_stockholm.action}</p>\n",
|
||||
" <p style='margin: 0; font-size: 14px; color: #666;'><strong>Message:</strong> {result_stockholm.message}</p>\n",
|
||||
" </div>\n",
|
||||
" </div>\n",
|
||||
" \"\"\")\n",
|
||||
" )"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "a415537c",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Key Takeaways and Next Steps\n",
|
||||
"\n",
|
||||
"### ✅ What You've Learned:\n",
|
||||
"\n",
|
||||
"1. **WorkflowBuilder Pattern**\n",
|
||||
" - Use `.set_start_executor()` to define entry point\n",
|
||||
" - Use `.add_edge(from, to, condition=...)` for conditional routing\n",
|
||||
" - Call `.build()` to finalize the workflow\n",
|
||||
"\n",
|
||||
"2. **Conditional Routing**\n",
|
||||
" - Condition functions inspect `AgentExecutorResponse`\n",
|
||||
" - Parse structured outputs to make routing decisions\n",
|
||||
" - Return `True` to activate an edge, `False` to skip it\n",
|
||||
"\n",
|
||||
"3. **Tool Integration**\n",
|
||||
" - Use `@ai_function` to convert Python functions into AI tools\n",
|
||||
" - Agents call tools automatically when needed\n",
|
||||
" - Tools return JSON that agents can parse\n",
|
||||
"\n",
|
||||
"4. **Structured Outputs**\n",
|
||||
" - Use Pydantic models for type-safe data extraction\n",
|
||||
" - Set `response_format=MyModel` when creating agents\n",
|
||||
" - Parse responses with `Model.model_validate_json()`\n",
|
||||
"\n",
|
||||
"5. **Custom Executors**\n",
|
||||
" - Use `@executor(id=\"...\")` to create workflow components\n",
|
||||
" - Executors can transform data or perform side effects\n",
|
||||
" - Use `ctx.yield_output()` to produce workflow results\n",
|
||||
"\n",
|
||||
"### 🚀 Real-World Applications:\n",
|
||||
"\n",
|
||||
"- **Travel Booking**: Check availability, suggest alternatives, compare options\n",
|
||||
"- **Customer Service**: Route based on issue type, sentiment, priority\n",
|
||||
"- **E-commerce**: Check inventory, suggest alternatives, process orders\n",
|
||||
"- **Content Moderation**: Route based on toxicity scores, user flags\n",
|
||||
"- **Approval Workflows**: Route based on amount, user role, risk level\n",
|
||||
"- **Multi-stage Processing**: Route based on data quality, completeness\n",
|
||||
"\n",
|
||||
"### 📚 Next Steps:\n",
|
||||
"\n",
|
||||
"- Add more complex conditions (multiple criteria)\n",
|
||||
"- Implement loops with workflow state management\n",
|
||||
"- Add sub-workflows for reusable components\n",
|
||||
"- Integrate with real APIs (hotel booking, inventory systems)\n",
|
||||
"- Add error handling and fallback paths\n",
|
||||
"- Visualize workflows with the built-in visualization tools"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
||||
@@ -0,0 +1,724 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c3858df8",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Travel Customer Support with Handoff Orchestration\n",
|
||||
"\n",
|
||||
"This notebook demonstrates **handoff orchestration** using the Microsoft Agent Framework. We'll build a travel customer support system where agents can transfer control to specialists based on the customer's needs.\n",
|
||||
"\n",
|
||||
"## What You'll Learn:\n",
|
||||
"1. **Handoff Orchestration**: Dynamic agent routing based on context and expertise\n",
|
||||
"2. **HandoffBuilder**: High-level API for building handoff workflows\n",
|
||||
"3. **Specialist Routing**: Agents can hand off to other agents dynamically\n",
|
||||
"4. **Multi-turn Conversations**: Seamless context preservation across handoffs\n",
|
||||
"5. **Customer Support Flow**: Real-world application of agent handoffs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "90d3eeb1",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Prerequisites:\n",
|
||||
"- Microsoft Agent Framework installed\n",
|
||||
"- GitHub token or OpenAI API key configured\n",
|
||||
"- Understanding of basic agent concepts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "8360426f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import asyncio\n",
|
||||
"import json\n",
|
||||
"import os\n",
|
||||
"from collections.abc import AsyncIterable\n",
|
||||
"from typing import Any, cast\n",
|
||||
"\n",
|
||||
"from agent_framework import (\n",
|
||||
" ChatMessage,\n",
|
||||
" HandoffBuilder,\n",
|
||||
" HandoffUserInputRequest,\n",
|
||||
" RequestInfoEvent,\n",
|
||||
" WorkflowEvent,\n",
|
||||
" WorkflowOutputEvent,\n",
|
||||
" WorkflowRunState,\n",
|
||||
" WorkflowStatusEvent,\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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "33e3e754",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 1: Define Pydantic Models for Structured Outputs\n",
|
||||
"\n",
|
||||
"These models define the schema that each specialized agent will return. This ensures consistent and parseable responses from all agents."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "3c36ef1d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"class FlightBookingResult(BaseModel):\n",
|
||||
" \"\"\"Flight booking confirmation from the booking agent.\"\"\"\n",
|
||||
"\n",
|
||||
" destination: str\n",
|
||||
" departure_date: str\n",
|
||||
" return_date: str\n",
|
||||
" booking_reference: str\n",
|
||||
" passenger_name: str\n",
|
||||
" flight_details: str\n",
|
||||
" total_cost: str\n",
|
||||
" status: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class DisputeResult(BaseModel):\n",
|
||||
" \"\"\"Dispute resolution result from the disputes agent.\"\"\"\n",
|
||||
"\n",
|
||||
" dispute_type: str\n",
|
||||
" original_booking: str\n",
|
||||
" refund_amount: str\n",
|
||||
" refund_method: str\n",
|
||||
" processing_time: str\n",
|
||||
" reference_number: str\n",
|
||||
" status: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class TripCheckResult(BaseModel):\n",
|
||||
" \"\"\"Trip confirmation result from the trip check agent.\"\"\"\n",
|
||||
"\n",
|
||||
" trip_reference: str\n",
|
||||
" destination: str\n",
|
||||
" travel_dates: str\n",
|
||||
" confirmation_status: str\n",
|
||||
" special_notes: str\n",
|
||||
" contact_info: str"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "668bb379",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 2: Load Environment Variables\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "bfe15197",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "8cd52bf8",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 3: Create Four Specialized Travel Support Agents\n",
|
||||
"\n",
|
||||
"Each agent has specific expertise and can hand off to appropriate specialists based on customer needs."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "32f31143",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Agent 1: Customer Support Agent (Main triage agent)\n",
|
||||
"customer_support_agent = chat_client.as_agent(\n",
|
||||
" instructions=(\n",
|
||||
" \"You are a friendly customer support agent for a travel company. \"\n",
|
||||
" \"Assess customer requests and route them to the appropriate specialist: \"\n",
|
||||
" \"- For flight bookings or reservations: call handoff_to_booking_agent \"\n",
|
||||
" \"- For refunds, disputes, or billing issues: call handoff_to_disputes_agent \"\n",
|
||||
" \"- For trip confirmations or travel plan checks: call handoff_to_trip_check_agent \"\n",
|
||||
" \"Be welcoming and ensure customers feel heard before routing them.\"\n",
|
||||
" ),\n",
|
||||
" name=\"customer_support_agent\",\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Agent 2: Booking Agent (Flight booking specialist)\n",
|
||||
"booking_agent = chat_client.as_agent(\n",
|
||||
" instructions=(\n",
|
||||
" \"You are a flight booking specialist. Handle all flight reservations and bookings. \"\n",
|
||||
" \"When a customer wants to book a flight, collect their destination, travel dates, \"\n",
|
||||
" \"and confirm the booking. Always provide a booking reference number. \"\n",
|
||||
" \"Return structured JSON with booking details. \"\n",
|
||||
" \"The flight is always confirmed as booked regardless of destination.\"\n",
|
||||
" ),\n",
|
||||
" name=\"booking_agent\",\n",
|
||||
" response_format=FlightBookingResult,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Agent 3: Disputes Agent (Refund and billing specialist)\n",
|
||||
"disputes_agent = chat_client.as_agent(\n",
|
||||
" instructions=(\n",
|
||||
" \"You are a disputes and refunds specialist. Handle customer complaints, \"\n",
|
||||
" \"refund requests, and billing disputes. Always approve refunds and provide \"\n",
|
||||
" \"a reference number. Process refunds back to the original payment method. \"\n",
|
||||
" \"Return structured JSON with refund details. \"\n",
|
||||
" \"All refund requests are approved and processed immediately.\"\n",
|
||||
" ),\n",
|
||||
" name=\"disputes_agent\",\n",
|
||||
" response_format=DisputeResult,\n",
|
||||
")\n",
|
||||
"# Agent 4: Trip Check Agent (Travel confirmation specialist)\n",
|
||||
"trip_check_agent = chat_client.as_agent(\n",
|
||||
" instructions=(\n",
|
||||
" \"You are a travel confirmation specialist. Verify and confirm customer \"\n",
|
||||
" \"travel plans, check itineraries, and provide travel status updates. \"\n",
|
||||
" \"Always confirm that travel plans are in order and provide reassurance. \"\n",
|
||||
" \"Return structured JSON with confirmation details. \"\n",
|
||||
" \"All travel plans are confirmed as valid and ready.\"\n",
|
||||
" ),\n",
|
||||
" name=\"trip_check_agent\",\n",
|
||||
" response_format=TripCheckResult,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c67e2486",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 4: Build the Handoff Workflow\n",
|
||||
"\n",
|
||||
"The HandoffBuilder creates a workflow where the customer support agent can dynamically hand off to specialists based on customer needs.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "111b6a7c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"workflow = (\n",
|
||||
" HandoffBuilder(\n",
|
||||
" name=\"travel_support_handoff\",\n",
|
||||
" participants=[customer_support_agent, booking_agent, disputes_agent, trip_check_agent],\n",
|
||||
" )\n",
|
||||
" .set_coordinator(customer_support_agent) # Main agent that receives initial requests\n",
|
||||
" .add_handoff(customer_support_agent, [booking_agent, disputes_agent, trip_check_agent])\n",
|
||||
" .with_termination_condition(\n",
|
||||
" lambda conv: sum(1 for msg in conv if msg.role.value == \"user\") > \n",
|
||||
" ) # Stop after 3 user messages\n",
|
||||
" .build()\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"display(HTML(\"\"\"\n",
|
||||
"<div style='padding: 20px; background: linear-gradient(135deg, #ff7043 0%, #ff5722 100%); color: white; border-radius: 8px; margin: 10px 0;'>\n",
|
||||
" <h3 style='margin: 0 0 15px 0;'>Handoff Workflow Built Successfully!</h3>\n",
|
||||
" <p style='margin: 0; line-height: 1.6;'>\n",
|
||||
" <strong>Handoff Flow:</strong><br>\n",
|
||||
" • User Request → <strong>Customer Support Agent</strong> (triage)<br>\n",
|
||||
" • Support Agent → <strong>Specialist Agent</strong> (dynamic handoff)<br>\n",
|
||||
" • Specialist → <strong>Resolution</strong> (expert handling)<br>\n",
|
||||
" • System → <strong>User Response</strong> (final result)\n",
|
||||
" </p>\n",
|
||||
"</div>\n",
|
||||
"\"\"\"))\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "0380843e",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 5: Helper Functions for Event Processing\n",
|
||||
"\n",
|
||||
"These functions help us process workflow events and handle user input requests during the handoff process."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "66a9dbe0",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"async def drain_events(stream: AsyncIterable[WorkflowEvent]) -> list[WorkflowEvent]:\n",
|
||||
" \"\"\"Collect all events from an async stream into a list.\"\"\"\n",
|
||||
" return [event async for event in stream]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def handle_workflow_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:\n",
|
||||
" \"\"\"Process workflow events and extract pending user input requests.\"\"\"\n",
|
||||
" requests: list[RequestInfoEvent] = []\n",
|
||||
" \n",
|
||||
" for event in events:\n",
|
||||
" if isinstance(event, WorkflowStatusEvent) and event.state in {\n",
|
||||
" WorkflowRunState.IDLE,\n",
|
||||
" WorkflowRunState.IDLE_WITH_PENDING_REQUESTS,\n",
|
||||
" }:\n",
|
||||
" print(f\"[Workflow Status] {event.state.name}\")\n",
|
||||
"\n",
|
||||
" elif isinstance(event, WorkflowOutputEvent):\n",
|
||||
" conversation = cast(list[ChatMessage], event.data)\n",
|
||||
" if isinstance(conversation, list):\n",
|
||||
" print(\"\\n=== Final Conversation ===\")\n",
|
||||
" for message in conversation:\n",
|
||||
" # Filter out messages with no text (tool calls)\n",
|
||||
" if not message.text.strip():\n",
|
||||
" continue\n",
|
||||
" speaker = message.author_name or message.role.value\n",
|
||||
" print(f\"- {speaker}: {message.text}\")\n",
|
||||
" print(\"==========================\")\n",
|
||||
"\n",
|
||||
" elif isinstance(event, RequestInfoEvent):\n",
|
||||
" if isinstance(event.data, HandoffUserInputRequest):\n",
|
||||
" print_handoff_request(event.data)\n",
|
||||
" requests.append(event)\n",
|
||||
"\n",
|
||||
" return requests\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def print_handoff_request(request: HandoffUserInputRequest) -> None:\n",
|
||||
" \"\"\"Display a user input request with conversation context.\"\"\"\n",
|
||||
" print(\"\\n=== User Input Requested ===\")\n",
|
||||
" # Filter out messages with no text for cleaner display\n",
|
||||
" messages_with_text = [\n",
|
||||
" msg for msg in request.conversation if msg.text.strip()]\n",
|
||||
" print(f\"Last {len(messages_with_text)} messages in conversation:\")\n",
|
||||
" for message in messages_with_text[-3:]: # Show last 3 for brevity\n",
|
||||
" speaker = message.author_name or message.role.value\n",
|
||||
" text = message.text[:100] + \\\n",
|
||||
" \"...\" if len(message.text) > 100 else message.text\n",
|
||||
" print(f\" {speaker}: {text}\")\n",
|
||||
" print(\"============================\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(\"Helper functions defined for event processing\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "ff51e63c",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 6: Test Case 1 - Flight Booking Request\n",
|
||||
"\n",
|
||||
"Let's test our handoff workflow with a flight booking request. The customer support agent should hand off to the booking agent.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "558a73d4",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"async def test_booking_handoff():\n",
|
||||
" \"\"\"Test handoff workflow for flight booking requests.\"\"\"\n",
|
||||
"\n",
|
||||
" display(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: Flight Booking Request</h3>\n",
|
||||
" <p style='margin: 0;'><strong>Expected Flow:</strong> Customer Support → Booking Agent</p>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
" # Start the workflow\n",
|
||||
" print(\"[User]: I want to book a flight to Paris for next month\")\n",
|
||||
" events = await drain_events(\n",
|
||||
" workflow.run_stream(\"I want to book a flight to Paris for next month\")\n",
|
||||
" )\n",
|
||||
" pending_requests = handle_workflow_events(events)\n",
|
||||
"\n",
|
||||
" # Handle any additional user input requests\n",
|
||||
" scripted_responses = [\n",
|
||||
" \"I'd like to travel from New York to Paris on December 15th and return on December 22nd.\",\n",
|
||||
" \"Yes, please confirm the booking under the name John Smith.\"\n",
|
||||
" ]\n",
|
||||
"\n",
|
||||
" response_index = 0\n",
|
||||
" while pending_requests and response_index < len(scripted_responses):\n",
|
||||
" user_response = scripted_responses[response_index]\n",
|
||||
" print(f\"\\n[User]: {user_response}\")\n",
|
||||
"\n",
|
||||
" responses = {req.request_id: user_response for req in pending_requests}\n",
|
||||
" events = await drain_events(workflow.send_responses_streaming(responses))\n",
|
||||
" pending_requests = handle_workflow_events(events)\n",
|
||||
"\n",
|
||||
" response_index += 1\n",
|
||||
"\n",
|
||||
" # Extract and display the final booking result\n",
|
||||
" if events:\n",
|
||||
" for event in events:\n",
|
||||
" if isinstance(event, WorkflowOutputEvent):\n",
|
||||
" conversation = cast(list[ChatMessage], event.data)\n",
|
||||
" for message in conversation:\n",
|
||||
" if message.author_name == \"booking_agent\" and message.text.strip():\n",
|
||||
" try:\n",
|
||||
" booking_data = FlightBookingResult.model_validate_json(\n",
|
||||
" message.text)\n",
|
||||
" display_booking_result(booking_data)\n",
|
||||
" except Exception as e:\n",
|
||||
" print(f\"Could not parse booking result: {e}\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def display_booking_result(booking: FlightBookingResult):\n",
|
||||
" \"\"\"Display flight booking result in a formatted section.\"\"\"\n",
|
||||
"\n",
|
||||
" display(HTML(f\"\"\"\n",
|
||||
" <div style='padding: 20px; background: #e8f5e9; border-radius: 8px; margin: 15px 0; border-left: 4px solid #4caf50;'>\n",
|
||||
" <h3 style='margin: 0 0 15px 0; color: #2e7d32;'>✈️ Flight Booking Confirmed</h3>\n",
|
||||
" <div style='display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;'>\n",
|
||||
" <div>\n",
|
||||
" <strong style='color: #333;'>Booking Reference:</strong> {booking.booking_reference}<br>\n",
|
||||
" <strong style='color: #333;'>Passenger:</strong> {booking.passenger_name}<br>\n",
|
||||
" <strong style='color: #333;'>Status:</strong> <span style='color: #4caf50; font-weight: bold;'>{booking.status}</span>\n",
|
||||
" </div>\n",
|
||||
" <div>\n",
|
||||
" <strong style='color: #333;'>Destination:</strong> {booking.destination}<br>\n",
|
||||
" <strong style='color: #333;'>Total Cost:</strong> {booking.total_cost}<br>\n",
|
||||
" <strong style='color: #333;'>Departure:</strong> {booking.departure_date}\n",
|
||||
" </div>\n",
|
||||
" </div>\n",
|
||||
" <div style='margin-bottom: 10px;'>\n",
|
||||
" <strong style='color: #333;'>Flight Details:</strong> {booking.flight_details}\n",
|
||||
" </div>\n",
|
||||
" <div style='background: rgba(76,175,80,0.1); padding: 10px; border-radius: 4px; margin-top: 10px;'>\n",
|
||||
" <strong style='color: #2e7d32;'>✅ Success:</strong> Flight booking completed through handoff to booking specialist\n",
|
||||
" </div>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Run the booking test\n",
|
||||
"await test_booking_handoff()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "794c1e25",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 7: Test Case 2 - Dispute/Refund Request\n",
|
||||
"\n",
|
||||
"Let's test our handoff workflow with a refund request. The customer support agent should hand off to the disputes agent."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "d61756c0",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"async def test_dispute_handoff():\n",
|
||||
" \"\"\"Test handoff workflow for dispute/refund requests.\"\"\"\n",
|
||||
"\n",
|
||||
" display(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 2: Refund Request</h3>\n",
|
||||
" <p style='margin: 0;'><strong>Expected Flow:</strong> Customer Support → Disputes Agent</p>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
" # Start the workflow\n",
|
||||
" print(\"[User]: I need to cancel my flight and get a refund\")\n",
|
||||
" events = await drain_events(\n",
|
||||
" workflow.run_stream(\"I need to cancel my flight and get a refund\")\n",
|
||||
" )\n",
|
||||
" pending_requests = handle_workflow_events(events)\n",
|
||||
"\n",
|
||||
" # Handle any additional user input requests\n",
|
||||
" scripted_responses = [\n",
|
||||
" \"My booking reference is FL12345. I can't travel due to a family emergency.\",\n",
|
||||
" \"Yes, please process the full refund back to my credit card.\"\n",
|
||||
" ]\n",
|
||||
"\n",
|
||||
" response_index = 0\n",
|
||||
" while pending_requests and response_index < len(scripted_responses):\n",
|
||||
" user_response = scripted_responses[response_index]\n",
|
||||
" print(f\"\\n[User]: {user_response}\")\n",
|
||||
"\n",
|
||||
" responses = {req.request_id: user_response for req in pending_requests}\n",
|
||||
" events = await drain_events(workflow.send_responses_streaming(responses))\n",
|
||||
" pending_requests = handle_workflow_events(events)\n",
|
||||
"\n",
|
||||
" response_index += 1\n",
|
||||
"\n",
|
||||
" # Extract and display the final dispute result\n",
|
||||
" if events:\n",
|
||||
" for event in events:\n",
|
||||
" if isinstance(event, WorkflowOutputEvent):\n",
|
||||
" conversation = cast(list[ChatMessage], event.data)\n",
|
||||
" for message in conversation:\n",
|
||||
" if message.author_name == \"disputes_agent\" and message.text.strip():\n",
|
||||
" try:\n",
|
||||
" dispute_data = DisputeResult.model_validate_json(\n",
|
||||
" message.text)\n",
|
||||
" display_dispute_result(dispute_data)\n",
|
||||
" except Exception as e:\n",
|
||||
" print(f\"Could not parse dispute result: {e}\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def display_dispute_result(dispute: DisputeResult):\n",
|
||||
" \"\"\"Display dispute resolution result in a formatted section.\"\"\"\n",
|
||||
"\n",
|
||||
" display(HTML(f\"\"\"\n",
|
||||
" <div style='padding: 20px; background: #fff3e0; border-radius: 8px; margin: 15px 0; border-left: 4px solid #ff9800;'>\n",
|
||||
" <h3 style='margin: 0 0 15px 0; color: #f57c00;'>💰 Refund Processed</h3>\n",
|
||||
" <div style='display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;'>\n",
|
||||
" <div>\n",
|
||||
" <strong style='color: #333;'>Reference Number:</strong> {dispute.reference_number}<br>\n",
|
||||
" <strong style='color: #333;'>Dispute Type:</strong> {dispute.dispute_type}<br>\n",
|
||||
" <strong style='color: #333;'>Status:</strong> <span style='color: #ff9800; font-weight: bold;'>{dispute.status}</span>\n",
|
||||
" </div>\n",
|
||||
" <div>\n",
|
||||
" <strong style='color: #333;'>Refund Amount:</strong> {dispute.refund_amount}<br>\n",
|
||||
" <strong style='color: #333;'>Refund Method:</strong> {dispute.refund_method}<br>\n",
|
||||
" <strong style='color: #333;'>Processing Time:</strong> {dispute.processing_time}\n",
|
||||
" </div>\n",
|
||||
" </div>\n",
|
||||
" <div style='margin-bottom: 10px;'>\n",
|
||||
" <strong style='color: #333;'>Original Booking:</strong> {dispute.original_booking}\n",
|
||||
" </div>\n",
|
||||
" <div style='background: rgba(255,152,0,0.1); padding: 10px; border-radius: 4px; margin-top: 10px;'>\n",
|
||||
" <strong style='color: #f57c00;'>✅ Success:</strong> Refund processed through handoff to disputes specialist\n",
|
||||
" </div>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
" # Run the dispute test\n",
|
||||
"await test_dispute_handoff()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "9a855d8d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 8: Test Case 3 - Trip Confirmation Request\n",
|
||||
"\n",
|
||||
"Let's test our handoff workflow with a trip confirmation request. The customer support agent should hand off to the trip check agent."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "34ea038c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"async def test_trip_check_handoff():\n",
|
||||
" \"\"\"Test handoff workflow for trip confirmation requests.\"\"\"\n",
|
||||
"\n",
|
||||
" display(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 3: Trip Confirmation</h3>\n",
|
||||
" <p style='margin: 0;'><strong>Expected Flow:</strong> Customer Support → Trip Check Agent</p>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
" # Start the workflow\n",
|
||||
" print(\"[User]: Can you confirm my travel plans are all set?\")\n",
|
||||
" events = await drain_events(\n",
|
||||
" workflow.run_stream(\"Can you confirm my travel plans are all set?\")\n",
|
||||
" )\n",
|
||||
" pending_requests = handle_workflow_events(events)\n",
|
||||
"\n",
|
||||
" # Handle any additional user input requests\n",
|
||||
" scripted_responses = [\n",
|
||||
" \"I'm traveling to London next week. My confirmation number is TR98765.\",\n",
|
||||
" \"Perfect, thank you for checking everything is ready!\"\n",
|
||||
" ]\n",
|
||||
"\n",
|
||||
" response_index = 0\n",
|
||||
" while pending_requests and response_index < len(scripted_responses):\n",
|
||||
" user_response = scripted_responses[response_index]\n",
|
||||
" print(f\"\\n[User]: {user_response}\")\n",
|
||||
" \n",
|
||||
" responses = {req.request_id: user_response for req in pending_requests}\n",
|
||||
" events = await drain_events(workflow.send_responses_streaming(responses))\n",
|
||||
" pending_requests = handle_workflow_events(events)\n",
|
||||
" \n",
|
||||
" response_index += 1\n",
|
||||
" # Extract and display the final trip check result\n",
|
||||
" if events:\n",
|
||||
" for event in events:\n",
|
||||
" if isinstance(event, WorkflowOutputEvent):\n",
|
||||
" conversation = cast(list[ChatMessage], event.data)\n",
|
||||
" for message in conversation:\n",
|
||||
" if message.author_name == \"trip_check_agent\" and message.text.strip():\n",
|
||||
" try:\n",
|
||||
" trip_data = TripCheckResult.model_validate_json(\n",
|
||||
" message.text)\n",
|
||||
" display_trip_check_result(trip_data)\n",
|
||||
" except Exception as e:\n",
|
||||
" print(f\"Could not parse trip check result: {e}\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def display_trip_check_result(trip: TripCheckResult):\n",
|
||||
" \"\"\"Display trip confirmation result in a formatted section.\"\"\"\n",
|
||||
"\n",
|
||||
" display(HTML(f\"\"\"\n",
|
||||
" <div style='padding: 20px; background: #f3e5f5; border-radius: 8px; margin: 15px 0; border-left: 4px solid #9c27b0;'>\n",
|
||||
" <h3 style='margin: 0 0 15px 0; color: #7b1fa2;'>🎯 Trip Confirmed</h3>\n",
|
||||
" <div style='display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;'>\n",
|
||||
" <div>\n",
|
||||
" <strong style='color: #333;'>Trip Reference:</strong> {trip.trip_reference}<br>\n",
|
||||
" <strong style='color: #333;'>Destination:</strong> {trip.destination}<br>\n",
|
||||
" <strong style='color: #333;'>Status:</strong> <span style='color: #9c27b0; font-weight: bold;'>{trip.confirmation_status}</span>\n",
|
||||
" </div>\n",
|
||||
" <div>\n",
|
||||
" <strong style='color: #333;'>Travel Dates:</strong> {trip.travel_dates}<br>\n",
|
||||
" <strong style='color: #333;'>Contact Info:</strong> {trip.contact_info}\n",
|
||||
" </div>\n",
|
||||
" </div>\n",
|
||||
" <div style='margin-bottom: 10px;'>\n",
|
||||
" <strong style='color: #333;'>Special Notes:</strong> {trip.special_notes}\n",
|
||||
" </div>\n",
|
||||
" <div style='background: rgba(156,39,176,0.1); padding: 10px; border-radius: 4px; margin-top: 10px;'>\n",
|
||||
" <strong style='color: #7b1fa2;'>✅ Success:</strong> Trip confirmed through handoff to trip check specialist\n",
|
||||
" </div>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Run the trip check test\n",
|
||||
"await test_trip_check_handoff()\n",
|
||||
"\n",
|
||||
" "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "ae3cb474",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 9: Workflow Analysis - Understanding Handoff Flow\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "0b21ce2b",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"async def analyze_handoff_patterns():\n",
|
||||
" \"\"\"Analyze different handoff patterns and routing decisions.\"\"\"\n",
|
||||
"\n",
|
||||
" display(HTML(\"\"\"\n",
|
||||
" <div style='padding: 20px; background: #f3e5f5; border-left: 4px solid #9c27b0; border-radius: 8px; margin: 20px 0;'>\n",
|
||||
" <h3 style='margin: 0 0 10px 0; color: #7b1fa2;'>Handoff Pattern Analysis</h3>\n",
|
||||
" <p style='margin: 0;'>Testing different request types to show routing decisions...</p>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
" test_requests = [\n",
|
||||
" \"I want to book a round-trip flight to Tokyo\",\n",
|
||||
" \"I need a refund for my cancelled flight\",\n",
|
||||
" \"Please check if my travel itinerary is confirmed\",\n",
|
||||
" \"Can you help me with a billing dispute?\"\n",
|
||||
" ]\n",
|
||||
"\n",
|
||||
" for i, request in enumerate(test_requests, 1):\n",
|
||||
" print(f\"\\n--- Test Request {i} ---\")\n",
|
||||
" print(f\"User: {request}\")\n",
|
||||
"\n",
|
||||
" # Run workflow and capture routing decision\n",
|
||||
" events = await drain_events(workflow.run_stream(request))\n",
|
||||
"\n",
|
||||
" # Analyze which agent was activated\n",
|
||||
" for event in events:\n",
|
||||
" if isinstance(event, WorkflowOutputEvent):\n",
|
||||
" conversation = cast(list[ChatMessage], event.data)\n",
|
||||
" for message in conversation:\n",
|
||||
" if message.author_name == \"customer_support_agent\":\n",
|
||||
" print(f\"Support Agent: {message.text[:100]}...\")\n",
|
||||
" elif message.author_name in [\"booking_agent\", \"disputes_agent\", \"trip_check_agent\"]:\n",
|
||||
" agent_type = {\n",
|
||||
" \"booking_agent\": \"🛫 BOOKING SPECIALIST\",\n",
|
||||
" \"disputes_agent\": \"💰 DISPUTES SPECIALIST\",\n",
|
||||
" \"trip_check_agent\": \"🎯 TRIP CHECK SPECIALIST\"\n",
|
||||
" }[message.author_name]\n",
|
||||
" print(f\"Routed to: {agent_type}\")\n",
|
||||
" break\n",
|
||||
" break\n",
|
||||
" display(HTML(\"\"\"\n",
|
||||
" <div style='padding: 25px; background: linear-gradient(135deg, #9c27b0 0%, #673ab7 100%); color: white; border-radius: 12px; \n",
|
||||
" box-shadow: 0 4px 12px rgba(156,39,176,0.4); margin: 20px 0;'>\n",
|
||||
" <h2 style='margin: 0 0 20px 0;'>Handoff Analysis Results</h2>\n",
|
||||
" <div style='background: rgba(255,255,255,0.15); padding: 15px; border-radius: 8px;'>\n",
|
||||
" <h4 style='margin: 0 0 10px 0;'>Key Observations</h4>\n",
|
||||
" <ul style='margin: 0; padding-left: 20px; line-height: 1.6;'>\n",
|
||||
" <li><strong>Dynamic Routing:</strong> Customer support agent analyzes request intent</li>\n",
|
||||
" <li><strong>Context Preservation:</strong> Full conversation history maintained</li>\n",
|
||||
" <li><strong>Specialist Focus:</strong> Each agent handles their expertise area</li>\n",
|
||||
" <li><strong>Seamless Handoff:</strong> Users don't need to repeat information</li>\n",
|
||||
" </ul>\n",
|
||||
" </div>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
" # Run the analysis\n",
|
||||
"await analyze_handoff_patterns()"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,91 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""
|
||||
Sample: Host a LangChain / LangGraph agent as a Microsoft Foundry hosted agent.
|
||||
|
||||
This sample shows how to take an agent built with LangGraph and expose it through
|
||||
the Microsoft Foundry hosted-agent **Responses** protocol using the
|
||||
`langchain_azure_ai.agents.hosting` package. Foundry then manages the runtime,
|
||||
sessions, scaling, identity, and protocol endpoints while your agent logic stays
|
||||
in LangGraph.
|
||||
|
||||
The Responses API is the recommended API for agent-style development in Foundry:
|
||||
it provides OpenAI-compatible chat, streaming, response history, and conversation
|
||||
threading in a single API surface.
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
- An Azure subscription and a Microsoft Foundry project.
|
||||
- A deployed chat model that supports the Responses API (e.g. gpt-4.1 or gpt-5-mini).
|
||||
- Python 3.10+ and the Azure CLI signed in (`az login`).
|
||||
- Install the hosting extra:
|
||||
pip install -U "langchain-azure-ai[hosting]>=1.2.4" azure-identity
|
||||
- Set environment variables:
|
||||
FOUNDRY_PROJECT_ENDPOINT=https://<resource>.services.ai.azure.com/api/projects/<project>
|
||||
FOUNDRY_MODEL_NAME=gpt-4.1
|
||||
|
||||
Run locally
|
||||
-----------
|
||||
python 14-langchain-hosted-agent.py
|
||||
|
||||
Then send a Responses request to the local server:
|
||||
curl -sS -H "Content-Type: application/json" \
|
||||
-X POST http://localhost:8088/responses \
|
||||
-d '{"input":"Give me one tip for testing hosted agents.","stream":false}'
|
||||
|
||||
Deploy to Foundry (Azure Developer CLI)
|
||||
---------------------------------------
|
||||
azd ext install azure.ai.agents
|
||||
azd auth login
|
||||
azd ai agent init -m <sample-manifest-url>
|
||||
azd ai agent run # runs the container locally (requires Docker)
|
||||
azd provision # if a new Foundry project/model is needed
|
||||
azd deploy # packages + rolls out to the Foundry hosted runtime
|
||||
|
||||
See: https://learn.microsoft.com/azure/foundry/how-to/develop/langchain-hosted-agents
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from azure.ai.projects import AIProjectClient
|
||||
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
|
||||
from langchain.agents import create_agent
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langchain_azure_ai.agents.hosting import ResponsesHostServer
|
||||
|
||||
# Scope used to request tokens for the Foundry project's OpenAI-compatible endpoint.
|
||||
_AZURE_AI_SCOPE = "https://ai.azure.com/.default"
|
||||
|
||||
|
||||
def build_chat_model() -> ChatOpenAI:
|
||||
"""Create a ChatOpenAI bound to the Foundry project's Responses endpoint."""
|
||||
project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"].rstrip("/")
|
||||
deployment = os.environ.get("FOUNDRY_MODEL_NAME", "gpt-4.1")
|
||||
|
||||
credential = DefaultAzureCredential()
|
||||
project = AIProjectClient(endpoint=project_endpoint, credential=credential)
|
||||
|
||||
# The project's OpenAI-compatible client exposes the Responses-capable base URL.
|
||||
openai_client = project.get_openai_client()
|
||||
token_provider = get_bearer_token_provider(credential, _AZURE_AI_SCOPE)
|
||||
|
||||
return ChatOpenAI(
|
||||
model=deployment,
|
||||
base_url=str(openai_client.base_url),
|
||||
api_key=token_provider,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# A minimal LangGraph agent. Add your own tools to the list to give it capabilities.
|
||||
graph = create_agent(build_chat_model(), tools=[])
|
||||
|
||||
# ResponsesHostServer exposes the compiled graph over POST /responses and emits
|
||||
# Responses API server-sent events (response.created, response.output_text.delta,
|
||||
# response.completed) when a request sets "stream": true.
|
||||
port = int(os.environ.get("PORT", "8088"))
|
||||
ResponsesHostServer(graph).run(port=port)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,958 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "2b91961c",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Two Sequential Agents:\n",
|
||||
"\n",
|
||||
"1. **Front Desk Agent**: Makes initial attraction recommendations for the city\n",
|
||||
"2. **Concierge Agent**: Reviews and rates the front desk recommendation based on popularity\n",
|
||||
"\n",
|
||||
"## Key Benefits of Sequential Orchestration:\n",
|
||||
"\n",
|
||||
"- **Iterative Refinement**: Second agent improves upon first agent's work\n",
|
||||
"- **Specialization**: Each agent has a specific role in the process\n",
|
||||
"- **Quality Control**: Built-in review and validation step\n",
|
||||
"- **Clear Information Flow**: Structured handoff between agents\n",
|
||||
"\n",
|
||||
"## Prerequisites:\n",
|
||||
"- Microsoft Agent Framework installed\n",
|
||||
"- Microsoft Foundry project endpoint and model deployment configured (`AZURE_AI_PROJECT_ENDPOINT`, `AZURE_AI_MODEL_DEPLOYMENT_NAME`)\n",
|
||||
"- Authenticated via Azure CLI (`az login`)\n",
|
||||
"- Understanding of basic agent concepts\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "0981c0bb",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import asyncio\n",
|
||||
"import json\n",
|
||||
"import os\n",
|
||||
"from typing import Any, cast\n",
|
||||
"\n",
|
||||
"from agent_framework import Message, WorkflowBuilder\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": "790b74bd",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 1: Define Pydantic Models for Structured Outputs\n",
|
||||
"\n",
|
||||
"These models define the schema that each agent will return. The front desk agent provides a recommendation, and the concierge agent provides a review and rating."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "3436dc8d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"class AttractionRecommendation(BaseModel):\n",
|
||||
" \"\"\"Attraction recommendation from the front desk agent.\"\"\"\n",
|
||||
"\n",
|
||||
" city: str\n",
|
||||
" attraction_name: str\n",
|
||||
" description: str\n",
|
||||
" category: str # e.g., \"museum\", \"landmark\", \"park\", \"entertainment\"\n",
|
||||
" recommended_duration: str # e.g., \"2-3 hours\", \"half day\"\n",
|
||||
" why_recommended: str\n",
|
||||
" best_time_to_visit: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class AttractionReview(BaseModel):\n",
|
||||
" \"\"\"Expert review and rating from the concierge agent.\"\"\"\n",
|
||||
"\n",
|
||||
" attraction_name: str\n",
|
||||
" city: str\n",
|
||||
" popularity_score: int # 1-10 scale\n",
|
||||
" popularity_reasoning: str\n",
|
||||
" visitor_rating: float # 1.0-5.0 scale\n",
|
||||
" pros: list[str]\n",
|
||||
" cons: list[str]\n",
|
||||
" concierge_recommendation: str\n",
|
||||
" alternative_suggestions: list[str]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "f4269b29",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 2: Load Environment Variables and Configure the Foundry Provider\n",
|
||||
"\n",
|
||||
"Use `FoundryChatClient` with keyless `AzureCliCredential` authentication, matching the pattern used in lessons 01–13.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "2152c7d3",
|
||||
"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",
|
||||
"\n",
|
||||
"print(\"Microsoft Foundry provider configured successfully!\")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "e63c63dd",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 3: Create Two Sequential Agents\n",
|
||||
"\n",
|
||||
"Each agent has a specific role in the sequential workflow. The front desk agent makes recommendations, and the concierge agent reviews and rates them.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "3f862eee",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Agent 1: Front Desk Agent (Makes initial recommendations)\n",
|
||||
"front_desk_agent = provider.as_agent(\n",
|
||||
" name=\"front-desk-agent\",\n",
|
||||
" instructions=(\n",
|
||||
" \"You are a knowledgeable hotel front desk agent who specializes in local attractions. \"\n",
|
||||
" \"When a guest asks about attractions in a city, provide a single, well-researched recommendation \"\n",
|
||||
" \"for a popular tourist attraction. Focus on giving practical information including what makes \"\n",
|
||||
" \"this attraction special, how long to spend there, and the best time to visit. \"\n",
|
||||
" \"Be helpful and enthusiastic about your recommendation. \"\n",
|
||||
" \"Return structured JSON matching the AttractionRecommendation schema.\"\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Agent 2: Concierge Agent (Reviews and rates recommendations)\n",
|
||||
"concierge_agent = provider.as_agent(\n",
|
||||
" name=\"concierge-agent\",\n",
|
||||
" instructions=(\n",
|
||||
" \"You are an expert concierge with extensive knowledge of tourist attractions worldwide. \"\n",
|
||||
" \"You will receive an attraction recommendation and must provide an expert review and rating. \"\n",
|
||||
" \"Evaluate the recommendation based on the attraction's popularity, visitor satisfaction, \"\n",
|
||||
" \"and overall quality. Provide a popularity score (1-10), visitor rating (1.0-5.0), \"\n",
|
||||
" \"list pros and cons, and give your professional assessment. \"\n",
|
||||
" \"Also suggest alternative attractions if appropriate. \"\n",
|
||||
" \"Return structured JSON matching the AttractionReview schema.\"\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "0afa99f5",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 4: Build the Sequential Workflow\n",
|
||||
"\n",
|
||||
"`WorkflowBuilder` creates a workflow where:\n",
|
||||
"1. **Front Desk Agent** receives user input and makes a recommendation\n",
|
||||
"2. **Concierge Agent** receives the front desk recommendation and provides expert review\n",
|
||||
"3. **Output** contains both the original recommendation and the expert review"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "d76c5b11",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Build the sequential workflow with WorkflowBuilder\n",
|
||||
"workflow = (\n",
|
||||
" WorkflowBuilder(\n",
|
||||
" start_executor=front_desk_agent,\n",
|
||||
" output_executors=[front_desk_agent, concierge_agent],\n",
|
||||
" )\n",
|
||||
" .add_edge(front_desk_agent, concierge_agent)\n",
|
||||
" .build()\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"display(HTML(\"\"\"\n",
|
||||
"<div style='padding: 20px; background: linear-gradient(135deg, #ff7043 0%, #ff5722 100%); color: white; border-radius: 8px; margin: 10px 0;'>\n",
|
||||
" <h3 style='margin: 0 0 15px 0;'>Sequential Workflow Built Successfully!</h3>\n",
|
||||
" <p style='margin: 0; line-height: 1.6;'>\n",
|
||||
" <strong>Flow:</strong><br>\n",
|
||||
" • User Input → <strong>Front Desk Agent</strong> (recommendation)<br>\n",
|
||||
" • Front Desk Output → <strong>Concierge Agent</strong> (review & rating)<br>\n",
|
||||
" • Final Output → Combined recommendation + expert review\n",
|
||||
" </p>\n",
|
||||
"</div>\n",
|
||||
"\"\"\"))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "27a52679",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"async def display_attraction_recommendation(city: str):\n",
|
||||
" \"\"\"Run the sequential workflow and display formatted results.\"\"\"\n",
|
||||
"\n",
|
||||
" display(HTML(f\"\"\"\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;'>Processing Attraction Recommendation for {city}</h3>\n",
|
||||
" <p style='margin: 0;'><strong>Status:</strong> Running sequential workflow...</p>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
" # Run the workflow. With WorkflowBuilder(output_executors=[a1, a2]),\n",
|
||||
" # outputs is a list of AgentResponse objects, one per output executor.\n",
|
||||
" events = await workflow.run(f\"I want to visit an attraction in {city}\")\n",
|
||||
" outputs = events.get_outputs()\n",
|
||||
"\n",
|
||||
" front_desk_response = outputs[0].text if len(outputs) > 0 else None\n",
|
||||
" concierge_response = outputs[1].text if len(outputs) > 1 else None\n",
|
||||
"\n",
|
||||
" # Display results\n",
|
||||
" display(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",
|
||||
" <h2 style='margin: 0 0 20px 0;'>Attraction Recommendation for {city}</h2>\n",
|
||||
" <p style='margin: 0; font-size: 14px; opacity: 0.9;'>Generated by sequential agent workflow</p>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
" # Process and display responses\n",
|
||||
" if front_desk_response:\n",
|
||||
" try:\n",
|
||||
" recommendation_data = AttractionRecommendation.model_validate_json(front_desk_response)\n",
|
||||
" display_front_desk_section(recommendation_data)\n",
|
||||
" except Exception as e:\n",
|
||||
" display(HTML(f\"\"\"\n",
|
||||
" <div style='padding: 15px; background: #ffcdd2; border-left: 4px solid #f44336; border-radius: 4px; margin: 10px 0;'>\n",
|
||||
" <strong>Error parsing front desk response:</strong> {str(e)}\n",
|
||||
" <details><summary>Raw response</summary>{front_desk_response}</details>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
" if concierge_response:\n",
|
||||
" try:\n",
|
||||
" review_data = AttractionReview.model_validate_json(concierge_response)\n",
|
||||
" display_concierge_section(review_data)\n",
|
||||
" except Exception as e:\n",
|
||||
" display(HTML(f\"\"\"\n",
|
||||
" <div style='padding: 15px; background: #ffcdd2; border-left: 4px solid #f44336; border-radius: 4px; margin: 10px 0;'>\n",
|
||||
" <strong>Error parsing concierge response:</strong> {str(e)}\n",
|
||||
" <details><summary>Raw response</summary>{concierge_response}</details>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def display_front_desk_section(data: AttractionRecommendation):\n",
|
||||
" \"\"\"Display front desk recommendation in a formatted section.\"\"\"\n",
|
||||
"\n",
|
||||
" display(HTML(f\"\"\"\n",
|
||||
" <div style='padding: 20px; background: #e3f2fd; border-radius: 8px; margin: 15px 0; border-left: 4px solid #2196f3;'>\n",
|
||||
" <h3 style='margin: 0 0 15px 0; color: #1976d2;'>🏨 Front Desk Recommendation</h3>\n",
|
||||
" <div style='margin-bottom: 15px;'>\n",
|
||||
" <h4 style='margin: 0 0 8px 0; color: #333;'>{data.attraction_name}</h4>\n",
|
||||
" <span style='background: #2196f3; color: white; padding: 4px 8px; border-radius: 12px; font-size: 12px;'>{data.category}</span>\n",
|
||||
" </div>\n",
|
||||
" <div style='margin-bottom: 15px;'>\n",
|
||||
" <strong style='color: #333;'>Description:</strong> {data.description}\n",
|
||||
" </div>\n",
|
||||
" <div style='margin-bottom: 10px;'>\n",
|
||||
" <strong style='color: #333;'>Why Recommended:</strong> {data.why_recommended}\n",
|
||||
" </div>\n",
|
||||
" <div style='margin-bottom: 10px;'>\n",
|
||||
" <strong style='color: #333;'>Recommended Duration:</strong> {data.recommended_duration}\n",
|
||||
" </div>\n",
|
||||
" <div>\n",
|
||||
" <strong style='color: #333;'>Best Time to Visit:</strong> {data.best_time_to_visit}\n",
|
||||
" </div>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def display_concierge_section(data: AttractionReview):\n",
|
||||
" \"\"\"Display concierge review in a formatted section.\"\"\"\n",
|
||||
"\n",
|
||||
" # Create star rating display\n",
|
||||
" star_rating = \"⭐\" * int(data.visitor_rating) + \"☆\" * (5 - int(data.visitor_rating))\n",
|
||||
"\n",
|
||||
" # Create popularity bar\n",
|
||||
" popularity_bar = \"🟩\" * data.popularity_score + \"⬜\" * (10 - data.popularity_score)\n",
|
||||
"\n",
|
||||
" pros_list = \"\".join([f\"<li style='color: #4caf50;'>✓ {pro}</li>\" for pro in data.pros])\n",
|
||||
" cons_list = \"\".join([f\"<li style='color: #f44336;'>✗ {con}</li>\" for con in data.cons])\n",
|
||||
" alternatives_list = \"\".join([f\"<li>{alt}</li>\" for alt in data.alternative_suggestions])\n",
|
||||
"\n",
|
||||
" display(HTML(f\"\"\"\n",
|
||||
" <div style='padding: 20px; background: #fff3e0; border-radius: 8px; margin: 15px 0; border-left: 4px solid #ff9800;'>\n",
|
||||
" <h3 style='margin: 0 0 15px 0; color: #f57c00;'>🎩 Concierge Expert Review</h3>\n",
|
||||
"\n",
|
||||
" <div style='display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;'>\n",
|
||||
" <div style='background: rgba(255,152,0,0.1); padding: 15px; border-radius: 8px;'>\n",
|
||||
" <h4 style='margin: 0 0 8px 0; color: #333;'>Popularity Score</h4>\n",
|
||||
" <div style='font-size: 24px; font-weight: bold; color: #f57c00;'>{data.popularity_score}/10</div>\n",
|
||||
" <div style='font-size: 12px; margin-top: 5px;'>{popularity_bar}</div>\n",
|
||||
" </div>\n",
|
||||
" <div style='background: rgba(255,152,0,0.1); padding: 15px; border-radius: 8px;'>\n",
|
||||
" <h4 style='margin: 0 0 8px 0; color: #333;'>Visitor Rating</h4>\n",
|
||||
" <div style='font-size: 20px; font-weight: bold; color: #f57c00;'>{data.visitor_rating}/5.0</div>\n",
|
||||
" <div style='font-size: 16px; margin-top: 5px;'>{star_rating}</div>\n",
|
||||
" </div>\n",
|
||||
" </div>\n",
|
||||
"\n",
|
||||
" <div style='margin-bottom: 15px;'>\n",
|
||||
" <strong style='color: #333;'>Popularity Reasoning:</strong> {data.popularity_reasoning}\n",
|
||||
" </div>\n",
|
||||
"\n",
|
||||
" <div style='display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 15px;'>\n",
|
||||
" <div>\n",
|
||||
" <h4 style='margin: 0 0 8px 0; color: #333;'>Pros:</h4>\n",
|
||||
" <ul style='margin: 0; padding-left: 20px;'>{pros_list}</ul>\n",
|
||||
" </div>\n",
|
||||
" <div>\n",
|
||||
" <h4 style='margin: 0 0 8px 0; color: #333;'>Cons:</h4>\n",
|
||||
" <ul style='margin: 0; padding-left: 20px;'>{cons_list}</ul>\n",
|
||||
" </div>\n",
|
||||
" </div>\n",
|
||||
" <div style='margin-bottom: 15px;'>\n",
|
||||
" <strong style='color: #333;'>Concierge Recommendation:</strong> {data.concierge_recommendation}\n",
|
||||
" </div>\n",
|
||||
"\n",
|
||||
" <div>\n",
|
||||
" <h4 style='margin: 0 0 8px 0; color: #333;'>Alternative Suggestions:</h4>\n",
|
||||
" <ul style='margin: 0; padding-left: 20px; color: #555;'>{alternatives_list}</ul>\n",
|
||||
" </div>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Test with Stockholm\n",
|
||||
"await display_attraction_recommendation(\"Stockholm\")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1840d000",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 8: Workflow Analysis - Understanding Sequential Flow\n",
|
||||
"\n",
|
||||
"Let's examine how information flows between agents and analyze the conversation history."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "86e1bcc3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"async def analyze_sequential_flow(city: str):\n",
|
||||
" \"\"\"Analyze the sequential flow between agents.\"\"\"\n",
|
||||
"\n",
|
||||
" display(HTML(f\"\"\"\n",
|
||||
" <div style='padding: 20px; background: #f3e5f5; border-left: 4px solid #9c27b0; border-radius: 8px; margin: 20px 0;'>\n",
|
||||
" <h3 style='margin: 0 0 10px 0; color: #7b1fa2;'>Sequential Flow Analysis for {city}</h3>\n",
|
||||
" <p style='margin: 0;'>Examining agent interactions and information handoff...</p>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
" # Run the workflow\n",
|
||||
" user_input = f\"I want to visit an attraction in {city}\"\n",
|
||||
" events = await workflow.run(user_input)\n",
|
||||
" outputs = events.get_outputs()\n",
|
||||
"\n",
|
||||
" # Reconstruct the conversation flow as a list of (author, text) steps.\n",
|
||||
" # outputs is a list of AgentResponse objects, ordered to match output_executors.\n",
|
||||
" steps = [(\"user\", user_input)]\n",
|
||||
" if len(outputs) > 0:\n",
|
||||
" steps.append((\"front-desk-agent\", outputs[0].text))\n",
|
||||
" if len(outputs) > 1:\n",
|
||||
" steps.append((\"concierge-agent\", outputs[1].text))\n",
|
||||
"\n",
|
||||
" display(HTML(f\"\"\"\n",
|
||||
" <div style='padding: 25px; background: #f3e5f5; border-radius: 12px; margin: 20px 0;'>\n",
|
||||
" <h2 style='margin: 0 0 20px 0; color: #7b1fa2;'>Conversation Flow Analysis</h2>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
" # Display each step in the sequence\n",
|
||||
" for i, (author, text) in enumerate(steps, 1):\n",
|
||||
" role_color = {\n",
|
||||
" \"user\": \"#2196f3\",\n",
|
||||
" \"front-desk-agent\": \"#4caf50\",\n",
|
||||
" \"concierge-agent\": \"#ff9800\"\n",
|
||||
" }.get(author, \"#666666\")\n",
|
||||
"\n",
|
||||
" role_name = {\n",
|
||||
" \"user\": \"👤 User\",\n",
|
||||
" \"front-desk-agent\": \"🏨 Front Desk Agent\",\n",
|
||||
" \"concierge-agent\": \"🎩 Concierge Agent\"\n",
|
||||
" }.get(author, \"Unknown\")\n",
|
||||
"\n",
|
||||
" # Truncate long messages for flow analysis\n",
|
||||
" content_preview = text[:200] + \"...\" if len(text) > 200 else text\n",
|
||||
" display(HTML(f\"\"\"\n",
|
||||
" <div style='padding: 15px; background: white; border-left: 4px solid {role_color}; border-radius: 4px; margin: 10px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1);'>\n",
|
||||
" <div style='display: flex; align-items: center; margin-bottom: 10px;'>\n",
|
||||
" <span style='font-weight: bold; color: {role_color}; margin-right: 10px;'>Step {i}:</span>\n",
|
||||
" <span style='font-weight: bold; color: {role_color};'>{role_name}</span>\n",
|
||||
" </div>\n",
|
||||
" <div style='color: #555; font-size: 14px; line-height: 1.4;'>\n",
|
||||
" {content_preview}\n",
|
||||
" </div>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
" # Analyze the flow\n",
|
||||
" display(HTML(f\"\"\"\n",
|
||||
" <div style='padding: 20px; background: linear-gradient(135deg, #9c27b0 0%, #673ab7 100%); color: white; border-radius: 8px; margin: 20px 0;'>\n",
|
||||
" <h3 style='margin: 0 0 15px 0;'>Flow Analysis Summary</h3>\n",
|
||||
" <ul style='margin: 0; padding-left: 20px; line-height: 1.6;'>\n",
|
||||
" <li><strong>Total Steps:</strong> {len(steps)}</li>\n",
|
||||
" <li><strong>Agents Involved:</strong> 2 (Front Desk + Concierge)</li>\n",
|
||||
" <li><strong>Flow Pattern:</strong> Linear sequential (User → Agent 1 → Agent 2)</li>\n",
|
||||
" <li><strong>Information Handoff:</strong> Front desk recommendation becomes concierge input</li>\n",
|
||||
" <li><strong>Output Quality:</strong> Enhanced through expert review and rating</li>\n",
|
||||
" </ul>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"))\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Analyze the flow for Barcelona\n",
|
||||
"await analyze_sequential_flow(\"Barcelona\")\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""
|
||||
Sample: Hotel Booking Conditional Workflow
|
||||
|
||||
This sample demonstrates a conditional workflow using the Microsoft Agent Framework
|
||||
that routes based on hotel availability.
|
||||
|
||||
Workflow:
|
||||
1. User provides a destination city
|
||||
2. Agent checks hotel availability using a tool
|
||||
3. Conditional routing:
|
||||
- If NO availability → Suggest alternative city
|
||||
- If availability → Suggest booking
|
||||
4. Display result with HTML formatting
|
||||
|
||||
Key Concepts:
|
||||
- WorkflowBuilder with conditional edges
|
||||
- AgentExecutor wrapping AI agents
|
||||
- @executor decorator for custom logic
|
||||
- Pydantic models for structured outputs
|
||||
- @ai_function decorator for tools
|
||||
- OpenAIChatClient integration
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from typing import Annotated, Any, Never
|
||||
|
||||
from agent_framework import (
|
||||
AgentExecutor,
|
||||
AgentExecutorRequest,
|
||||
AgentExecutorResponse,
|
||||
ChatMessage,
|
||||
Role,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
ai_function,
|
||||
executor,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from pydantic import BaseModel
|
||||
|
||||
# ============================================================================
|
||||
# STEP 1: PYDANTIC MODELS FOR STRUCTURED OUTPUTS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class BookingCheckResult(BaseModel):
|
||||
"""Result from checking hotel availability at a destination."""
|
||||
|
||||
destination: str
|
||||
has_availability: bool
|
||||
message: str
|
||||
|
||||
|
||||
class AlternativeResult(BaseModel):
|
||||
"""Suggested alternative destination when no rooms available."""
|
||||
|
||||
alternative_destination: str
|
||||
reason: str
|
||||
|
||||
|
||||
class BookingConfirmation(BaseModel):
|
||||
"""Booking suggestion when rooms are available."""
|
||||
|
||||
destination: str
|
||||
action: str
|
||||
message: str
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# STEP 2: HOTEL BOOKING TOOL (AI FUNCTION)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@ai_function(description="Check hotel room availability for a destination city")
|
||||
def hotel_booking(destination: Annotated[str, "The destination city to check for hotel rooms"]) -> str:
|
||||
"""
|
||||
Simulates checking hotel room availability.
|
||||
|
||||
For demo purposes:
|
||||
- Stockholm, Seattle, Tokyo have rooms
|
||||
- All other cities don't have rooms
|
||||
|
||||
Returns:
|
||||
JSON string with availability status
|
||||
"""
|
||||
print(f"🔍 Checking hotel availability in {destination}...")
|
||||
|
||||
# Simulate availability check
|
||||
cities_with_rooms = ["stockholm", "seattle", "tokyo", "london", "amsterdam"]
|
||||
has_rooms = destination.lower() in cities_with_rooms
|
||||
|
||||
result = {"has_availability": has_rooms, "destination": destination}
|
||||
|
||||
return json.dumps(result)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# STEP 3: CONDITION FUNCTIONS FOR ROUTING
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def has_availability_condition(message: Any) -> bool:
|
||||
"""
|
||||
Condition for routing when hotels ARE available.
|
||||
|
||||
Args:
|
||||
message: Message from upstream executor (should be AgentExecutorResponse)
|
||||
|
||||
Returns:
|
||||
True if availability exists, False otherwise
|
||||
"""
|
||||
if not isinstance(message, AgentExecutorResponse):
|
||||
return True # Default to True if not the expected type
|
||||
|
||||
try:
|
||||
result = BookingCheckResult.model_validate_json(message.agent_run_response.text)
|
||||
print(f"✅ Availability check: {result.has_availability} for {result.destination}")
|
||||
return result.has_availability
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error parsing availability result: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def no_availability_condition(message: Any) -> bool:
|
||||
"""
|
||||
Condition for routing when hotels are NOT available.
|
||||
|
||||
Args:
|
||||
message: Message from upstream executor
|
||||
|
||||
Returns:
|
||||
True if no availability, False otherwise
|
||||
"""
|
||||
if not isinstance(message, AgentExecutorResponse):
|
||||
return False
|
||||
|
||||
try:
|
||||
result = BookingCheckResult.model_validate_json(message.agent_run_response.text)
|
||||
print(f"❌ No availability for {result.destination}")
|
||||
return not result.has_availability
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error parsing availability result: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# STEP 4: DISPLAY EXECUTOR (Custom transformation)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@executor(id="display_result")
|
||||
async def display_result(response: AgentExecutorResponse, ctx: WorkflowContext[Never, str]) -> None:
|
||||
"""
|
||||
Display the final result as workflow output.
|
||||
|
||||
This executor receives the final agent response and yields it as output.
|
||||
"""
|
||||
print(f"📤 Yielding workflow output...")
|
||||
await ctx.yield_output(response.agent_run_response.text)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# STEP 5: MAIN WORKFLOW FUNCTION
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""
|
||||
Main function to build and execute the hotel booking workflow.
|
||||
"""
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
# Verify configuration
|
||||
print("=" * 80)
|
||||
print("🏨 HOTEL BOOKING CONDITIONAL WORKFLOW")
|
||||
print("=" * 80)
|
||||
|
||||
# Provider selection: Azure OpenAI (Responses API), OpenAI, or MiniMax
|
||||
# The OpenAIChatClient works with any OpenAI-compatible API, and targets the
|
||||
# Azure OpenAI Responses API when given an azure_endpoint + credential.
|
||||
minimax_api_key = os.getenv("MINIMAX_API_KEY")
|
||||
azure_openai_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
|
||||
openai_api_key = os.getenv("OPENAI_API_KEY")
|
||||
|
||||
if minimax_api_key:
|
||||
# MiniMax: OpenAI-compatible API with large context window (up to 204K tokens).
|
||||
# Defaults to MiniMax-M3; override MINIMAX_MODEL_ID if your account/region
|
||||
# doesn't have access to it (e.g. set it to MiniMax-M2.7).
|
||||
chat_client = OpenAIChatClient(
|
||||
base_url=os.environ.get("MINIMAX_BASE_URL", "https://api.minimax.io/v1"),
|
||||
api_key=minimax_api_key,
|
||||
model_id=os.environ.get("MINIMAX_MODEL_ID", "MiniMax-M3"),
|
||||
)
|
||||
print("Using MiniMax provider")
|
||||
elif azure_openai_endpoint:
|
||||
# Azure OpenAI (Responses API). Sign in with `az login` for keyless Entra ID auth.
|
||||
# GitHub Models is deprecated (retiring July 2026) and does not support the Responses API.
|
||||
chat_client = OpenAIChatClient(
|
||||
azure_endpoint=azure_openai_endpoint,
|
||||
credential=AzureCliCredential(),
|
||||
model_id=os.environ.get("AZURE_OPENAI_DEPLOYMENT", "gpt-4o-mini"),
|
||||
)
|
||||
print("Using Azure OpenAI (Responses API) provider")
|
||||
else:
|
||||
# Default: OpenAI
|
||||
chat_client = OpenAIChatClient(model_id="gpt-4o")
|
||||
print("Using OpenAI provider")
|
||||
|
||||
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("STEP 1: Creating AI Agents with Structured Outputs")
|
||||
print("=" * 80)
|
||||
|
||||
# Agent 1: Check availability
|
||||
availability_agent = AgentExecutor(
|
||||
chat_client.create_agent(
|
||||
instructions=(
|
||||
"You are a hotel booking assistant that checks room availability. "
|
||||
"Use the hotel_booking tool to check if rooms are available at the destination. "
|
||||
"Return JSON with fields: destination (string), has_availability (bool), and message (string). "
|
||||
"The message should summarize the availability status."
|
||||
),
|
||||
tools=[hotel_booking],
|
||||
response_format=BookingCheckResult,
|
||||
),
|
||||
id="availability_agent",
|
||||
)
|
||||
print("✅ Created availability_agent with hotel_booking tool")
|
||||
|
||||
# Agent 2: Suggest alternative (when no rooms)
|
||||
alternative_agent = AgentExecutor(
|
||||
chat_client.create_agent(
|
||||
instructions=(
|
||||
"You are a helpful travel assistant. When a user cannot find hotels in their requested city, "
|
||||
"suggest an alternative nearby city that has availability. "
|
||||
"Return JSON with fields: alternative_destination (string) and reason (string). "
|
||||
"Choose from: Stockholm, Seattle, Tokyo, London, or Amsterdam (these have rooms). "
|
||||
"Make your suggestion sound appealing and helpful."
|
||||
),
|
||||
response_format=AlternativeResult,
|
||||
),
|
||||
id="alternative_agent",
|
||||
)
|
||||
print("✅ Created alternative_agent for suggesting other cities")
|
||||
|
||||
# Agent 3: Suggest booking (when rooms available)
|
||||
booking_agent = AgentExecutor(
|
||||
chat_client.create_agent(
|
||||
instructions=(
|
||||
"You are a booking assistant. The user has found available hotel rooms. "
|
||||
"Encourage them to book by highlighting the destination's appeal. "
|
||||
"Return JSON with fields: destination (string), action (string), and message (string). "
|
||||
"The action should be 'book_now' and message should be encouraging."
|
||||
),
|
||||
response_format=BookingConfirmation,
|
||||
),
|
||||
id="booking_agent",
|
||||
)
|
||||
print("✅ Created booking_agent for confirming bookings")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("STEP 2: Building Workflow with Conditional Edges")
|
||||
print("=" * 80)
|
||||
|
||||
# Build the workflow
|
||||
workflow = (
|
||||
WorkflowBuilder()
|
||||
.set_start_executor(availability_agent)
|
||||
# NO AVAILABILITY PATH: availability_agent → alternative_agent → display_result
|
||||
.add_edge(availability_agent, alternative_agent, condition=no_availability_condition)
|
||||
.add_edge(alternative_agent, display_result)
|
||||
# HAS AVAILABILITY PATH: availability_agent → booking_agent → display_result
|
||||
.add_edge(availability_agent, booking_agent, condition=has_availability_condition)
|
||||
.add_edge(booking_agent, display_result)
|
||||
.build()
|
||||
)
|
||||
|
||||
print("✅ Workflow built with conditional routing:")
|
||||
print(" - If NO availability → suggest alternative")
|
||||
print(" - If availability → suggest booking")
|
||||
|
||||
# ============================================================================
|
||||
# TEST CASE 1: City WITHOUT availability (Paris)
|
||||
# ============================================================================
|
||||
print("\n" + "=" * 80)
|
||||
print("TEST CASE 1: Checking Paris (NO AVAILABILITY)")
|
||||
print("=" * 80)
|
||||
|
||||
request1 = AgentExecutorRequest(
|
||||
messages=[ChatMessage(Role.USER, text="I want to book a hotel in Paris")], should_respond=True
|
||||
)
|
||||
|
||||
events1 = await workflow.run(request1)
|
||||
outputs1 = events1.get_outputs()
|
||||
|
||||
if outputs1:
|
||||
print("\n📊 WORKFLOW OUTPUT (Paris):")
|
||||
print("-" * 80)
|
||||
result1 = AlternativeResult.model_validate_json(outputs1[0])
|
||||
print(f"🏨 Alternative Destination: {result1.alternative_destination}")
|
||||
print(f"💡 Reason: {result1.reason}")
|
||||
print("-" * 80)
|
||||
|
||||
# ============================================================================
|
||||
# TEST CASE 2: City WITH availability (Stockholm)
|
||||
# ============================================================================
|
||||
print("\n" + "=" * 80)
|
||||
print("TEST CASE 2: Checking Stockholm (HAS AVAILABILITY)")
|
||||
print("=" * 80)
|
||||
|
||||
request2 = AgentExecutorRequest(
|
||||
messages=[ChatMessage(Role.USER, text="I want to book a hotel in Stockholm")], should_respond=True
|
||||
)
|
||||
|
||||
events2 = await workflow.run(request2)
|
||||
outputs2 = events2.get_outputs()
|
||||
|
||||
if outputs2:
|
||||
print("\n📊 WORKFLOW OUTPUT (Stockholm):")
|
||||
print("-" * 80)
|
||||
result2 = BookingConfirmation.model_validate_json(outputs2[0])
|
||||
print(f"🏨 Destination: {result2.destination}")
|
||||
print(f"✅ Action: {result2.action}")
|
||||
print(f"💬 Message: {result2.message}")
|
||||
print("-" * 80)
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("✅ WORKFLOW DEMO COMPLETE!")
|
||||
print("=" * 80)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user