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

Sequential Workflow Built Successfully!

\n", "

\n", " Flow:
\n", " • User Input → Front Desk Agent (recommendation)
\n", " • Front Desk Output → Concierge Agent (review & rating)
\n", " • Final Output → Combined recommendation + expert review\n", "

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

Processing Attraction Recommendation for {city}

\n", "

Status: Running sequential workflow...

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

Attraction Recommendation for {city}

\n", "

Generated by sequential agent workflow

\n", "
\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", "
\n", " Error parsing front desk response: {str(e)}\n", "
Raw response{front_desk_response}
\n", "
\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", "
\n", " Error parsing concierge response: {str(e)}\n", "
Raw response{concierge_response}
\n", "
\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", "
\n", "

🏨 Front Desk Recommendation

\n", "
\n", "

{data.attraction_name}

\n", " {data.category}\n", "
\n", "
\n", " Description: {data.description}\n", "
\n", "
\n", " Why Recommended: {data.why_recommended}\n", "
\n", "
\n", " Recommended Duration: {data.recommended_duration}\n", "
\n", "
\n", " Best Time to Visit: {data.best_time_to_visit}\n", "
\n", "
\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\"
  • ✓ {pro}
  • \" for pro in data.pros])\n", " cons_list = \"\".join([f\"
  • ✗ {con}
  • \" for con in data.cons])\n", " alternatives_list = \"\".join([f\"
  • {alt}
  • \" for alt in data.alternative_suggestions])\n", "\n", " display(HTML(f\"\"\"\n", "
    \n", "

    🎩 Concierge Expert Review

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

    Popularity Score

    \n", "
    {data.popularity_score}/10
    \n", "
    {popularity_bar}
    \n", "
    \n", "
    \n", "

    Visitor Rating

    \n", "
    {data.visitor_rating}/5.0
    \n", "
    {star_rating}
    \n", "
    \n", "
    \n", "\n", "
    \n", " Popularity Reasoning: {data.popularity_reasoning}\n", "
    \n", "\n", "
    \n", "
    \n", "

    Pros:

    \n", "
      {pros_list}
    \n", "
    \n", "
    \n", "

    Cons:

    \n", "
      {cons_list}
    \n", "
    \n", "
    \n", "
    \n", " Concierge Recommendation: {data.concierge_recommendation}\n", "
    \n", "\n", "
    \n", "

    Alternative Suggestions:

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

    Sequential Flow Analysis for {city}

    \n", "

    Examining agent interactions and information handoff...

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

    Conversation Flow Analysis

    \n", "
    \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", "
    \n", "
    \n", " Step {i}:\n", " {role_name}\n", "
    \n", "
    \n", " {content_preview}\n", "
    \n", "
    \n", " \"\"\"))\n", "\n", " # Analyze the flow\n", " display(HTML(f\"\"\"\n", "
    \n", "

    Flow Analysis Summary

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