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

Concurrent Workflow Built Successfully!

\n", "

\n", " Architecture:
\n", " • Input → Dispatcher (fan-out)
\n", " • 3 Agents run in parallel (attractions, dining, history)
\n", " • Output → 3 AgentResponse objects, one per agent\n", "

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

Processing Travel Recommendations for {destination}

\n", "

Status: Running 3 agents concurrently...

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

Complete Travel Guide for {destination}

\n", "

Generated by 3 concurrent agents

\n", "
\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", "
\n", " Error parsing {agent_name} response: {str(e)}\n", "
Raw response{text}
\n", "
\n", " \"\"\"))\n", "\n", "\n", "def display_attractions_section(data: AttractionsRecommendation):\n", " \"\"\"Display attractions recommendations in a formatted section.\"\"\"\n", " attractions_list = \"\".join([f\"
  • {attraction}
  • \" for attraction in data.top_attractions])\n", " activities_list = \"\".join([f\"
  • {activity}
  • \" for activity in data.activities])\n", "\n", " display(HTML(f\"\"\"\n", "
    \n", "

    🏛️ Tourist Attractions & Activities

    \n", "
    \n", "

    Top Attractions:

    \n", " \n", "
    \n", "
    \n", "

    Recommended Activities:

    \n", " \n", "
    \n", "
    \n", " Best Time to Visit: {data.best_time_to_visit}\n", "
    \n", "
    \n", " Transportation Tips: {data.transportation_tips}\n", "
    \n", "
    \n", " \"\"\"))\n", "\n", "\n", "def display_dining_section(data: DiningRecommendation):\n", " \"\"\"Display dining recommendations in a formatted section.\"\"\"\n", " dishes_list = \"\".join(\n", " [f\"
  • {dish}
  • \" for dish in data.must_try_dishes])\n", " restaurants_list = \"\".join(\n", " [f\"
  • {restaurant}
  • \" for restaurant in data.recommended_restaurants])\n", " experiences_list = \"\".join(\n", " [f\"
  • {exp}
  • \" for exp in data.food_experiences])\n", "\n", " display(HTML(f\"\"\"\n", "
    \n", "

    🍜 Food & Dining Experiences

    \n", "
    \n", " Local Cuisine: {data.local_cuisine}\n", "
    \n", "
    \n", "

    Must-Try Dishes:

    \n", " \n", "
    \n", "
    \n", "

    Recommended Restaurants:

    \n", " \n", "
    \n", "
    \n", "

    Food Experiences:

    \n", " \n", "
    \n", "
    \n", " Dining Etiquette: {data.dining_etiquette}\n", "
    \n", "
    \n", " \"\"\"))\n", "\n", "\n", "def display_history_section(data: HistoryRecommendation):\n", " \"\"\"Display history recommendations in a formatted section.\"\"\"\n", " highlights_list = \"\".join(\n", " [f\"
  • {highlight}
  • \" for highlight in data.cultural_highlights])\n", " periods_list = \"\".join(\n", " [f\"
  • {period}
  • \" for period in data.important_periods])\n", " experiences_list = \"\".join(\n", " [f\"
  • {exp}
  • \" for exp in data.cultural_experiences])\n", " facts_list = \"\".join(\n", " [f\"
  • {fact}
  • \" for fact in data.interesting_facts])\n", "\n", " display(HTML(f\"\"\"\n", "
    \n", "

    📚 History & Culture

    \n", "
    \n", " Historical Significance: {data.historical_significance}\n", "
    \n", "
    \n", "

    Cultural Highlights:

    \n", " \n", "
    \n", "
    \n", "

    Important Historical Periods:

    \n", " \n", "
    \n", "
    \n", "

    Cultural Experiences:

    \n", " \n", "
    \n", "
    \n", "

    Interesting Facts:

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

    Performance Comparison Test

    \n", "

    Testing with destination: Barcelona

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

    Performance Results

    \n", "
    \n", "
    \n", "

    ⚡ Concurrent Execution

    \n", "

    {concurrent_time:.2f}s

    \n", "

    {concurrent_count} agent responses

    \n", "
    \n", "
    \n", "

    🔄 Sequential Execution

    \n", "

    {sequential_time:.2f}s

    \n", "

    {sequential_count} agent responses

    \n", "
    \n", "
    \n", "
    \n", "

    Performance Improvement

    \n", "

    {improvement:.1f}% faster

    \n", "

    \n", " Saved {sequential_time - concurrent_time:.2f} seconds with concurrent execution\n", "

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