Files
2026-07-13 12:59:43 +08:00

561 lines
23 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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 0113.\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
}