chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
FOUNDRY_PROJECT_ENDPOINT="<your-project-endpoint>"
|
||||
FOUNDRY_MODEL_WORKFLOW="<your-model-deployment>"
|
||||
FOUNDRY_MODEL_EVAL="<your-model-deployment>"
|
||||
@@ -0,0 +1,30 @@
|
||||
# Multi-Agent Travel Planning Workflow Evaluation
|
||||
|
||||
This sample demonstrates evaluating a multi-agent workflow using Azure AI's built-in evaluators. The workflow processes travel planning requests through seven specialized agents in a fan-out/fan-in pattern: travel request handler, hotel/flight/activity search agents, booking aggregator, booking confirmation, and payment processing.
|
||||
|
||||
## Evaluation Metrics
|
||||
|
||||
The evaluation uses four Azure AI built-in evaluators:
|
||||
|
||||
- **Relevance** - How well responses address the user query
|
||||
- **Groundedness** - Whether responses are grounded in available context
|
||||
- **Tool Call Accuracy** - Correct tool selection and parameter usage
|
||||
- **Tool Output Utilization** - Effective use of tool outputs in responses
|
||||
|
||||
## Setup
|
||||
|
||||
Create a `.env` file with configuration as in the `.env.example` file in this folder.
|
||||
|
||||
## Running the Evaluation
|
||||
|
||||
Execute the complete workflow and evaluation:
|
||||
|
||||
```bash
|
||||
python run_evaluation.py
|
||||
```
|
||||
|
||||
The script will:
|
||||
1. Execute the multi-agent travel planning workflow
|
||||
2. Display response summary for each agent
|
||||
3. Create and run evaluation on hotel, flight, and activity search agents
|
||||
4. Monitor progress and display the evaluation report URL
|
||||
@@ -0,0 +1,749 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import tool
|
||||
from pydantic import Field
|
||||
|
||||
# --- Travel Planning Tools ---
|
||||
# Note: These are mock tools for demonstration purposes. They return simulated data
|
||||
# and do not make real API calls or bookings.
|
||||
|
||||
|
||||
# Mock hotel search tool
|
||||
@tool(name="search_hotels", description="Search for available hotels based on location and dates.")
|
||||
def search_hotels(
|
||||
location: Annotated[str, Field(description="City or region to search for hotels.")],
|
||||
check_in: Annotated[str, Field(description="Check-in date (e.g., 'December 15, 2025').")],
|
||||
check_out: Annotated[str, Field(description="Check-out date (e.g., 'December 18, 2025').")],
|
||||
guests: Annotated[int, Field(description="Number of guests.")] = 2,
|
||||
) -> str:
|
||||
"""Search for available hotels based on location and dates.
|
||||
|
||||
Returns:
|
||||
JSON string containing search results with hotel details including name, rating,
|
||||
price, distance to landmarks, amenities, and availability.
|
||||
"""
|
||||
# Specific mock data for Paris December 15-18, 2025
|
||||
if "paris" in location.lower():
|
||||
mock_hotels = [
|
||||
{
|
||||
"name": "Hotel Eiffel Trocadéro",
|
||||
"rating": 4.6,
|
||||
"price_per_night": "$185",
|
||||
"total_price": "$555 for 3 nights",
|
||||
"distance_to_eiffel_tower": "0.3 miles",
|
||||
"amenities": ["WiFi", "Breakfast", "Eiffel Tower View", "Concierge"],
|
||||
"availability": "Available",
|
||||
"address": "35 Rue Benjamin Franklin, 16th arr., Paris",
|
||||
},
|
||||
{
|
||||
"name": "Mercure Paris Centre Tour Eiffel",
|
||||
"rating": 4.4,
|
||||
"price_per_night": "$220",
|
||||
"total_price": "$660 for 3 nights",
|
||||
"distance_to_eiffel_tower": "0.5 miles",
|
||||
"amenities": ["WiFi", "Restaurant", "Bar", "Gym", "Air Conditioning"],
|
||||
"availability": "Available",
|
||||
"address": "20 Rue Jean Rey, 15th arr., Paris",
|
||||
},
|
||||
{
|
||||
"name": "Pullman Paris Tour Eiffel",
|
||||
"rating": 4.7,
|
||||
"price_per_night": "$280",
|
||||
"total_price": "$840 for 3 nights",
|
||||
"distance_to_eiffel_tower": "0.2 miles",
|
||||
"amenities": ["WiFi", "Spa", "Gym", "Restaurant", "Rooftop Bar", "Concierge"],
|
||||
"availability": "Limited",
|
||||
"address": "18 Avenue de Suffren, 15th arr., Paris",
|
||||
},
|
||||
]
|
||||
else:
|
||||
mock_hotels = [
|
||||
{
|
||||
"name": "Grand Plaza Hotel",
|
||||
"rating": 4.5,
|
||||
"price_per_night": "$150",
|
||||
"amenities": ["WiFi", "Pool", "Gym", "Restaurant"],
|
||||
"availability": "Available",
|
||||
}
|
||||
]
|
||||
|
||||
return json.dumps({
|
||||
"location": location,
|
||||
"check_in": check_in,
|
||||
"check_out": check_out,
|
||||
"guests": guests,
|
||||
"hotels_found": len(mock_hotels),
|
||||
"hotels": mock_hotels,
|
||||
"note": "Hotel search results matching your query",
|
||||
})
|
||||
|
||||
|
||||
# Mock hotel details tool
|
||||
@tool(name="get_hotel_details", description="Get detailed information about a specific hotel.")
|
||||
def get_hotel_details(
|
||||
hotel_name: Annotated[str, Field(description="Name of the hotel to get details for.")],
|
||||
) -> str:
|
||||
"""Get detailed information about a specific hotel.
|
||||
|
||||
Returns:
|
||||
JSON string containing detailed hotel information including description,
|
||||
check-in/out times, cancellation policy, reviews, and nearby attractions.
|
||||
"""
|
||||
hotel_details = {
|
||||
"Hotel Eiffel Trocadéro": {
|
||||
"description": "Charming boutique hotel with stunning Eiffel Tower views from select rooms. Perfect for couples and families.",
|
||||
"check_in_time": "3:00 PM",
|
||||
"check_out_time": "11:00 AM",
|
||||
"cancellation_policy": "Free cancellation up to 24 hours before check-in",
|
||||
"reviews": {
|
||||
"total": 1247,
|
||||
"recent_comments": [
|
||||
"Amazing location! Walked to Eiffel Tower in 5 minutes.",
|
||||
"Staff was incredibly helpful with restaurant recommendations.",
|
||||
"Rooms are cozy and clean with great views.",
|
||||
],
|
||||
},
|
||||
"nearby_attractions": ["Eiffel Tower (0.3 mi)", "Trocadéro Gardens (0.2 mi)", "Seine River (0.4 mi)"],
|
||||
},
|
||||
"Mercure Paris Centre Tour Eiffel": {
|
||||
"description": "Modern hotel with contemporary rooms and excellent dining options. Close to metro stations.",
|
||||
"check_in_time": "2:00 PM",
|
||||
"check_out_time": "12:00 PM",
|
||||
"cancellation_policy": "Free cancellation up to 48 hours before check-in",
|
||||
"reviews": {
|
||||
"total": 2156,
|
||||
"recent_comments": [
|
||||
"Great value for money, clean and comfortable.",
|
||||
"Restaurant had excellent French cuisine.",
|
||||
"Easy access to public transportation.",
|
||||
],
|
||||
},
|
||||
"nearby_attractions": ["Eiffel Tower (0.5 mi)", "Champ de Mars (0.4 mi)", "Les Invalides (0.8 mi)"],
|
||||
},
|
||||
"Pullman Paris Tour Eiffel": {
|
||||
"description": "Luxury hotel offering panoramic views, upscale amenities, and exceptional service. Ideal for a premium experience.",
|
||||
"check_in_time": "3:00 PM",
|
||||
"check_out_time": "12:00 PM",
|
||||
"cancellation_policy": "Free cancellation up to 72 hours before check-in",
|
||||
"reviews": {
|
||||
"total": 3421,
|
||||
"recent_comments": [
|
||||
"Rooftop bar has the best Eiffel Tower views in Paris!",
|
||||
"Luxurious rooms with every amenity you could want.",
|
||||
"Worth the price for the location and service.",
|
||||
],
|
||||
},
|
||||
"nearby_attractions": ["Eiffel Tower (0.2 mi)", "Seine River Cruise Dock (0.3 mi)", "Trocadéro (0.5 mi)"],
|
||||
},
|
||||
}
|
||||
|
||||
details = hotel_details.get(
|
||||
hotel_name,
|
||||
{
|
||||
"name": hotel_name,
|
||||
"description": "Comfortable hotel with modern amenities",
|
||||
"check_in_time": "3:00 PM",
|
||||
"check_out_time": "11:00 AM",
|
||||
"cancellation_policy": "Standard cancellation policy applies",
|
||||
"reviews": {"total": 0, "recent_comments": []},
|
||||
"nearby_attractions": [],
|
||||
},
|
||||
)
|
||||
|
||||
return json.dumps({"hotel_name": hotel_name, "details": details})
|
||||
|
||||
|
||||
# Mock flight search tool
|
||||
@tool(name="search_flights", description="Search for available flights between two locations.")
|
||||
def search_flights(
|
||||
origin: Annotated[str, Field(description="Departure airport or city (e.g., 'JFK' or 'New York').")],
|
||||
destination: Annotated[str, Field(description="Arrival airport or city (e.g., 'CDG' or 'Paris').")],
|
||||
departure_date: Annotated[str, Field(description="Departure date (e.g., 'December 15, 2025').")],
|
||||
return_date: Annotated[str | None, Field(description="Return date (e.g., 'December 18, 2025').")] = None,
|
||||
passengers: Annotated[int, Field(description="Number of passengers.")] = 1,
|
||||
) -> str:
|
||||
"""Search for available flights between two locations.
|
||||
|
||||
Returns:
|
||||
JSON string containing flight search results with details including flight numbers,
|
||||
airlines, departure/arrival times, prices, durations, and baggage allowances.
|
||||
"""
|
||||
# Specific mock data for JFK to Paris December 15-18, 2025
|
||||
if "jfk" in origin.lower() or "new york" in origin.lower():
|
||||
if "paris" in destination.lower() or "cdg" in destination.lower():
|
||||
mock_flights = [
|
||||
{
|
||||
"outbound": {
|
||||
"flight_number": "AF007",
|
||||
"airline": "Air France",
|
||||
"departure": "December 15, 2025 at 6:30 PM",
|
||||
"arrival": "December 16, 2025 at 8:15 AM",
|
||||
"duration": "7h 45m",
|
||||
"aircraft": "Boeing 777-300ER",
|
||||
"class": "Economy",
|
||||
"price": "$520",
|
||||
},
|
||||
"return": {
|
||||
"flight_number": "AF008",
|
||||
"airline": "Air France",
|
||||
"departure": "December 18, 2025 at 11:00 AM",
|
||||
"arrival": "December 18, 2025 at 2:15 PM",
|
||||
"duration": "8h 15m",
|
||||
"aircraft": "Airbus A350-900",
|
||||
"class": "Economy",
|
||||
"price": "Included",
|
||||
},
|
||||
"total_price": "$520",
|
||||
"stops": "Nonstop",
|
||||
"baggage": "1 checked bag included",
|
||||
},
|
||||
{
|
||||
"outbound": {
|
||||
"flight_number": "DL264",
|
||||
"airline": "Delta",
|
||||
"departure": "December 15, 2025 at 10:15 PM",
|
||||
"arrival": "December 16, 2025 at 12:05 PM",
|
||||
"duration": "7h 50m",
|
||||
"aircraft": "Airbus A330-900neo",
|
||||
"class": "Economy",
|
||||
"price": "$485",
|
||||
},
|
||||
"return": {
|
||||
"flight_number": "DL265",
|
||||
"airline": "Delta",
|
||||
"departure": "December 18, 2025 at 1:45 PM",
|
||||
"arrival": "December 18, 2025 at 5:00 PM",
|
||||
"duration": "8h 15m",
|
||||
"aircraft": "Airbus A330-900neo",
|
||||
"class": "Economy",
|
||||
"price": "Included",
|
||||
},
|
||||
"total_price": "$485",
|
||||
"stops": "Nonstop",
|
||||
"baggage": "1 checked bag included",
|
||||
},
|
||||
{
|
||||
"outbound": {
|
||||
"flight_number": "UA57",
|
||||
"airline": "United Airlines",
|
||||
"departure": "December 15, 2025 at 5:00 PM",
|
||||
"arrival": "December 16, 2025 at 6:50 AM",
|
||||
"duration": "7h 50m",
|
||||
"aircraft": "Boeing 767-400ER",
|
||||
"class": "Economy",
|
||||
"price": "$560",
|
||||
},
|
||||
"return": {
|
||||
"flight_number": "UA58",
|
||||
"airline": "United Airlines",
|
||||
"departure": "December 18, 2025 at 9:30 AM",
|
||||
"arrival": "December 18, 2025 at 12:45 PM",
|
||||
"duration": "8h 15m",
|
||||
"aircraft": "Boeing 787-10",
|
||||
"class": "Economy",
|
||||
"price": "Included",
|
||||
},
|
||||
"total_price": "$560",
|
||||
"stops": "Nonstop",
|
||||
"baggage": "1 checked bag included",
|
||||
},
|
||||
]
|
||||
else:
|
||||
mock_flights = [
|
||||
{"flight_number": "XX123", "airline": "Generic Air", "price": "$400", "note": "Generic route"}
|
||||
]
|
||||
else:
|
||||
mock_flights = [
|
||||
{
|
||||
"outbound": {
|
||||
"flight_number": "AA123",
|
||||
"airline": "Generic Airlines",
|
||||
"departure": f"{departure_date} at 9:00 AM",
|
||||
"arrival": f"{departure_date} at 2:30 PM",
|
||||
"duration": "5h 30m",
|
||||
"class": "Economy",
|
||||
"price": "$350",
|
||||
},
|
||||
"total_price": "$350",
|
||||
"stops": "Nonstop",
|
||||
}
|
||||
]
|
||||
|
||||
return json.dumps({
|
||||
"origin": origin,
|
||||
"destination": destination,
|
||||
"departure_date": departure_date,
|
||||
"return_date": return_date,
|
||||
"passengers": passengers,
|
||||
"flights_found": len(mock_flights),
|
||||
"flights": mock_flights,
|
||||
"note": "Flight search results for JFK to Paris CDG",
|
||||
})
|
||||
|
||||
|
||||
# Mock flight details tool
|
||||
@tool(name="get_flight_details", description="Get detailed information about a specific flight.")
|
||||
def get_flight_details(
|
||||
flight_number: Annotated[str, Field(description="Flight number (e.g., 'AF007' or 'DL264').")],
|
||||
) -> str:
|
||||
"""Get detailed information about a specific flight.
|
||||
|
||||
Returns:
|
||||
JSON string containing detailed flight information including airline, aircraft type,
|
||||
departure/arrival airports and times, gates, terminals, duration, and amenities.
|
||||
"""
|
||||
mock_details = {
|
||||
"flight_number": flight_number,
|
||||
"airline": "Sky Airways",
|
||||
"aircraft": "Boeing 737-800",
|
||||
"departure": {
|
||||
"airport": "JFK International Airport",
|
||||
"terminal": "Terminal 4",
|
||||
"gate": "B23",
|
||||
"time": "08:00 AM",
|
||||
},
|
||||
"arrival": {
|
||||
"airport": "Charles de Gaulle Airport",
|
||||
"terminal": "Terminal 2E",
|
||||
"gate": "K15",
|
||||
"time": "11:30 AM local time",
|
||||
},
|
||||
"duration": "3h 30m",
|
||||
"baggage_allowance": {"carry_on": "1 bag (10kg)", "checked": "1 bag (23kg)"},
|
||||
"amenities": ["WiFi", "In-flight entertainment", "Meals included"],
|
||||
}
|
||||
|
||||
return json.dumps({"flight_details": mock_details})
|
||||
|
||||
|
||||
# Mock activity search tool
|
||||
@tool(name="search_activities", description="Search for available activities and attractions at a destination.")
|
||||
def search_activities(
|
||||
location: Annotated[str, Field(description="City or region to search for activities.")],
|
||||
date: Annotated[str | None, Field(description="Date for the activity (e.g., 'December 16, 2025').")] = None,
|
||||
category: Annotated[
|
||||
str | None, Field(description="Activity category (e.g., 'Sightseeing', 'Culture', 'Culinary').")
|
||||
] = None,
|
||||
) -> str:
|
||||
"""Search for available activities and attractions at a destination.
|
||||
|
||||
Returns:
|
||||
JSON string containing activity search results with details including name, category,
|
||||
duration, price, rating, description, availability, and booking requirements.
|
||||
"""
|
||||
# Specific mock data for Paris activities
|
||||
if "paris" in location.lower():
|
||||
all_activities = [
|
||||
{
|
||||
"name": "Eiffel Tower Summit Access",
|
||||
"category": "Sightseeing",
|
||||
"duration": "2-3 hours",
|
||||
"price": "$35",
|
||||
"rating": 4.8,
|
||||
"description": "Skip-the-line access to all three levels including the summit. Best views of Paris!",
|
||||
"availability": "Daily 9:30 AM - 11:00 PM",
|
||||
"best_time": "Early morning or sunset",
|
||||
"booking_required": True,
|
||||
},
|
||||
{
|
||||
"name": "Louvre Museum Guided Tour",
|
||||
"category": "Sightseeing",
|
||||
"duration": "3 hours",
|
||||
"price": "$55",
|
||||
"rating": 4.7,
|
||||
"description": "Expert-guided tour covering masterpieces including Mona Lisa and Venus de Milo.",
|
||||
"availability": "Daily except Tuesdays, 9:00 AM entry",
|
||||
"best_time": "Morning entry recommended",
|
||||
"booking_required": True,
|
||||
},
|
||||
{
|
||||
"name": "Seine River Cruise",
|
||||
"category": "Sightseeing",
|
||||
"duration": "1 hour",
|
||||
"price": "$18",
|
||||
"rating": 4.6,
|
||||
"description": "Scenic cruise past Notre-Dame, Eiffel Tower, and historic bridges.",
|
||||
"availability": "Every 30 minutes, 10:00 AM - 10:00 PM",
|
||||
"best_time": "Evening for illuminated monuments",
|
||||
"booking_required": False,
|
||||
},
|
||||
{
|
||||
"name": "Musée d'Orsay Visit",
|
||||
"category": "Culture",
|
||||
"duration": "2-3 hours",
|
||||
"price": "$16",
|
||||
"rating": 4.7,
|
||||
"description": "Impressionist masterpieces in a stunning Beaux-Arts railway station.",
|
||||
"availability": "Tuesday-Sunday 9:30 AM - 6:00 PM",
|
||||
"best_time": "Weekday mornings",
|
||||
"booking_required": True,
|
||||
},
|
||||
{
|
||||
"name": "Versailles Palace Day Trip",
|
||||
"category": "Culture",
|
||||
"duration": "5-6 hours",
|
||||
"price": "$75",
|
||||
"rating": 4.9,
|
||||
"description": "Explore the opulent palace and stunning gardens of Louis XIV (includes transport).",
|
||||
"availability": "Daily except Mondays, 8:00 AM departure",
|
||||
"best_time": "Full day trip",
|
||||
"booking_required": True,
|
||||
},
|
||||
{
|
||||
"name": "Montmartre Walking Tour",
|
||||
"category": "Culture",
|
||||
"duration": "2.5 hours",
|
||||
"price": "$25",
|
||||
"rating": 4.6,
|
||||
"description": "Discover the artistic heart of Paris, including Sacré-Cœur and artists' square.",
|
||||
"availability": "Daily at 10:00 AM and 2:00 PM",
|
||||
"best_time": "Morning or late afternoon",
|
||||
"booking_required": False,
|
||||
},
|
||||
{
|
||||
"name": "French Cooking Class",
|
||||
"category": "Culinary",
|
||||
"duration": "3 hours",
|
||||
"price": "$120",
|
||||
"rating": 4.9,
|
||||
"description": "Learn to make classic French dishes like coq au vin and crème brûlée, then enjoy your creations.",
|
||||
"availability": "Tuesday-Saturday, 10:00 AM and 6:00 PM sessions",
|
||||
"best_time": "Morning or evening sessions",
|
||||
"booking_required": True,
|
||||
},
|
||||
{
|
||||
"name": "Wine & Cheese Tasting",
|
||||
"category": "Culinary",
|
||||
"duration": "1.5 hours",
|
||||
"price": "$65",
|
||||
"rating": 4.7,
|
||||
"description": "Sample French wines and artisanal cheeses with expert sommelier guidance.",
|
||||
"availability": "Daily at 5:00 PM and 7:30 PM",
|
||||
"best_time": "Evening sessions",
|
||||
"booking_required": True,
|
||||
},
|
||||
{
|
||||
"name": "Food Market Tour",
|
||||
"category": "Culinary",
|
||||
"duration": "2 hours",
|
||||
"price": "$45",
|
||||
"rating": 4.6,
|
||||
"description": "Explore authentic Parisian markets and taste local specialties like cheeses, pastries, and charcuterie.",
|
||||
"availability": "Tuesday, Thursday, Saturday mornings",
|
||||
"best_time": "Morning (markets are freshest)",
|
||||
"booking_required": False,
|
||||
},
|
||||
]
|
||||
|
||||
activities = [act for act in all_activities if act["category"] == category] if category else all_activities
|
||||
else:
|
||||
activities = [
|
||||
{
|
||||
"name": "City Walking Tour",
|
||||
"category": "Sightseeing",
|
||||
"duration": "3 hours",
|
||||
"price": "$45",
|
||||
"rating": 4.7,
|
||||
"description": "Explore the historic downtown area with an expert guide",
|
||||
"availability": "Daily at 10:00 AM and 2:00 PM",
|
||||
}
|
||||
]
|
||||
|
||||
return json.dumps({
|
||||
"location": location,
|
||||
"date": date,
|
||||
"category": category,
|
||||
"activities_found": len(activities),
|
||||
"activities": activities,
|
||||
"note": "Activity search results for Paris with sightseeing, culture, and culinary options",
|
||||
})
|
||||
|
||||
|
||||
# Mock activity details tool
|
||||
@tool(name="get_activity_details", description="Get detailed information about a specific activity.")
|
||||
def get_activity_details(
|
||||
activity_name: Annotated[str, Field(description="Name of the activity to get details for.")],
|
||||
) -> str:
|
||||
"""Get detailed information about a specific activity.
|
||||
|
||||
Returns:
|
||||
JSON string containing detailed activity information including description, duration,
|
||||
price, included items, meeting point, what to bring, cancellation policy, and reviews.
|
||||
"""
|
||||
# Paris-specific activity details
|
||||
activity_details_map = {
|
||||
"Eiffel Tower Summit Access": {
|
||||
"name": "Eiffel Tower Summit Access",
|
||||
"description": "Skip-the-line access to all three levels of the Eiffel Tower, including the summit. Enjoy panoramic views of Paris from 276 meters high.",
|
||||
"duration": "2-3 hours (self-guided)",
|
||||
"price": "$35 per person",
|
||||
"included": ["Skip-the-line ticket", "Access to all 3 levels", "Summit access", "Audio guide app"],
|
||||
"meeting_point": "Eiffel Tower South Pillar entrance, look for priority access line",
|
||||
"what_to_bring": ["Photo ID", "Comfortable shoes", "Camera", "Light jacket (summit can be windy)"],
|
||||
"cancellation_policy": "Free cancellation up to 24 hours in advance",
|
||||
"languages": ["English", "French", "Spanish", "German", "Italian"],
|
||||
"max_group_size": "No limit",
|
||||
"rating": 4.8,
|
||||
"reviews_count": 15234,
|
||||
},
|
||||
"Louvre Museum Guided Tour": {
|
||||
"name": "Louvre Museum Guided Tour",
|
||||
"description": "Expert-guided tour of the world's largest art museum, focusing on must-see masterpieces including Mona Lisa, Venus de Milo, and Winged Victory.",
|
||||
"duration": "3 hours",
|
||||
"price": "$55 per person",
|
||||
"included": [
|
||||
"Skip-the-line entry",
|
||||
"Expert art historian guide",
|
||||
"Headsets for groups over 6",
|
||||
"Museum highlights map",
|
||||
],
|
||||
"meeting_point": "Glass Pyramid main entrance, look for guide with 'Louvre Tours' sign",
|
||||
"what_to_bring": ["Photo ID", "Comfortable shoes", "Camera (no flash)", "Water bottle"],
|
||||
"cancellation_policy": "Free cancellation up to 48 hours in advance",
|
||||
"languages": ["English", "French", "Spanish"],
|
||||
"max_group_size": 20,
|
||||
"rating": 4.7,
|
||||
"reviews_count": 8921,
|
||||
},
|
||||
"French Cooking Class": {
|
||||
"name": "French Cooking Class",
|
||||
"description": "Hands-on cooking experience where you'll learn to prepare classic French dishes like coq au vin, ratatouille, and crème brûlée under expert chef guidance.",
|
||||
"duration": "3 hours",
|
||||
"price": "$120 per person",
|
||||
"included": [
|
||||
"All ingredients",
|
||||
"Chef instruction",
|
||||
"Apron and recipe booklet",
|
||||
"Wine pairing",
|
||||
"Lunch/dinner of your creations",
|
||||
],
|
||||
"meeting_point": "Le Chef Cooking Studio, 15 Rue du Bac, 7th arrondissement",
|
||||
"what_to_bring": ["Appetite", "Camera for food photos"],
|
||||
"cancellation_policy": "Free cancellation up to 72 hours in advance",
|
||||
"languages": ["English", "French"],
|
||||
"max_group_size": 12,
|
||||
"rating": 4.9,
|
||||
"reviews_count": 2341,
|
||||
},
|
||||
}
|
||||
|
||||
details = activity_details_map.get(
|
||||
activity_name,
|
||||
{
|
||||
"name": activity_name,
|
||||
"description": "An immersive experience that showcases the best of local culture and attractions.",
|
||||
"duration": "3 hours",
|
||||
"price": "$45 per person",
|
||||
"included": ["Professional guide", "Entry fees"],
|
||||
"meeting_point": "Central meeting location",
|
||||
"what_to_bring": ["Comfortable shoes", "Camera"],
|
||||
"cancellation_policy": "Free cancellation up to 24 hours in advance",
|
||||
"languages": ["English"],
|
||||
"max_group_size": 15,
|
||||
"rating": 4.5,
|
||||
"reviews_count": 100,
|
||||
},
|
||||
)
|
||||
|
||||
return json.dumps({"activity_details": details})
|
||||
|
||||
|
||||
# Mock booking confirmation tool
|
||||
@tool(name="confirm_booking", description="Confirm a booking reservation.")
|
||||
def confirm_booking(
|
||||
booking_type: Annotated[str, Field(description="Type of booking (e.g., 'hotel', 'flight', 'activity').")],
|
||||
booking_id: Annotated[str, Field(description="Unique booking identifier.")],
|
||||
customer_info: Annotated[dict, Field(description="Customer information including name and email.")],
|
||||
) -> str:
|
||||
"""Confirm a booking reservation.
|
||||
|
||||
Returns:
|
||||
JSON string containing confirmation details including confirmation number,
|
||||
booking status, customer information, and next steps.
|
||||
"""
|
||||
confirmation_number = f"CONF-{booking_type.upper()}-{booking_id}"
|
||||
|
||||
confirmation_data = {
|
||||
"confirmation_number": confirmation_number,
|
||||
"booking_type": booking_type,
|
||||
"status": "Confirmed",
|
||||
"customer_name": customer_info.get("name", "Guest"),
|
||||
"email": customer_info.get("email", "guest@example.com"),
|
||||
"confirmation_sent": True,
|
||||
"next_steps": [
|
||||
"Check your email for booking details",
|
||||
"Arrive 30 minutes before scheduled time",
|
||||
"Bring confirmation number and valid ID",
|
||||
],
|
||||
}
|
||||
|
||||
return json.dumps({"confirmation": confirmation_data})
|
||||
|
||||
|
||||
# Mock hotel availability check tool
|
||||
@tool(name="check_hotel_availability", description="Check availability for hotel rooms.")
|
||||
def check_hotel_availability(
|
||||
hotel_name: Annotated[str, Field(description="Name of the hotel to check availability for.")],
|
||||
check_in: Annotated[str, Field(description="Check-in date (e.g., 'December 15, 2025').")],
|
||||
check_out: Annotated[str, Field(description="Check-out date (e.g., 'December 18, 2025').")],
|
||||
rooms: Annotated[int, Field(description="Number of rooms needed.")] = 1,
|
||||
) -> str:
|
||||
"""Check availability for hotel rooms.
|
||||
|
||||
Sample Date format: "December 15, 2025"
|
||||
|
||||
Returns:
|
||||
JSON string containing availability status, available rooms count, price per night,
|
||||
and last checked timestamp.
|
||||
"""
|
||||
availability_status = "Available"
|
||||
|
||||
availability_data = {
|
||||
"service_type": "hotel",
|
||||
"hotel_name": hotel_name,
|
||||
"check_in": check_in,
|
||||
"check_out": check_out,
|
||||
"rooms_requested": rooms,
|
||||
"status": availability_status,
|
||||
"available_rooms": 8,
|
||||
"price_per_night": "$185",
|
||||
"last_checked": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
return json.dumps({"availability": availability_data})
|
||||
|
||||
|
||||
# Mock flight availability check tool
|
||||
@tool(name="check_flight_availability", description="Check availability for flight seats.")
|
||||
def check_flight_availability(
|
||||
flight_number: Annotated[str, Field(description="Flight number to check availability for.")],
|
||||
date: Annotated[str, Field(description="Flight date (e.g., 'December 15, 2025').")],
|
||||
passengers: Annotated[int, Field(description="Number of passengers.")] = 1,
|
||||
) -> str:
|
||||
"""Check availability for flight seats.
|
||||
|
||||
Sample Date format: "December 15, 2025"
|
||||
|
||||
Returns:
|
||||
JSON string containing availability status, available seats count, price per passenger,
|
||||
and last checked timestamp.
|
||||
"""
|
||||
availability_status = "Available"
|
||||
|
||||
availability_data = {
|
||||
"service_type": "flight",
|
||||
"flight_number": flight_number,
|
||||
"date": date,
|
||||
"passengers_requested": passengers,
|
||||
"status": availability_status,
|
||||
"available_seats": 45,
|
||||
"price_per_passenger": "$520",
|
||||
"last_checked": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
return json.dumps({"availability": availability_data})
|
||||
|
||||
|
||||
# Mock activity availability check tool
|
||||
@tool(name="check_activity_availability", description="Check availability for activity bookings.")
|
||||
def check_activity_availability(
|
||||
activity_name: Annotated[str, Field(description="Name of the activity to check availability for.")],
|
||||
date: Annotated[str, Field(description="Activity date (e.g., 'December 16, 2025').")],
|
||||
participants: Annotated[int, Field(description="Number of participants.")] = 1,
|
||||
) -> str:
|
||||
"""Check availability for activity bookings.
|
||||
|
||||
Sample Date format: "December 16, 2025"
|
||||
|
||||
Returns:
|
||||
JSON string containing availability status, available spots count, price per person,
|
||||
and last checked timestamp.
|
||||
"""
|
||||
availability_status = "Available"
|
||||
|
||||
availability_data = {
|
||||
"service_type": "activity",
|
||||
"activity_name": activity_name,
|
||||
"date": date,
|
||||
"participants_requested": participants,
|
||||
"status": availability_status,
|
||||
"available_spots": 15,
|
||||
"price_per_person": "$45",
|
||||
"last_checked": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
return json.dumps({"availability": availability_data})
|
||||
|
||||
|
||||
# Mock payment processing tool
|
||||
@tool(name="process_payment", description="Process payment for a booking.")
|
||||
def process_payment(
|
||||
amount: Annotated[float, Field(description="Payment amount.")],
|
||||
currency: Annotated[str, Field(description="Currency code (e.g., 'USD', 'EUR').")],
|
||||
payment_method: Annotated[dict, Field(description="Payment method details (type, card info).")],
|
||||
booking_reference: Annotated[str, Field(description="Booking reference number for the payment.")],
|
||||
) -> str:
|
||||
"""Process payment for a booking.
|
||||
|
||||
Returns:
|
||||
JSON string containing payment result with transaction ID, status, amount, currency,
|
||||
payment method details, and receipt URL.
|
||||
"""
|
||||
transaction_id = f"TXN-{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
||||
|
||||
payment_result = {
|
||||
"transaction_id": transaction_id,
|
||||
"amount": amount,
|
||||
"currency": currency,
|
||||
"status": "Success",
|
||||
"payment_method": payment_method.get("type", "Credit Card"),
|
||||
"last_4_digits": payment_method.get("last_4", "****"),
|
||||
"booking_reference": booking_reference,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"receipt_url": f"https://payments.travelagency.com/receipt/{transaction_id}",
|
||||
}
|
||||
|
||||
return json.dumps({"payment_result": payment_result})
|
||||
|
||||
|
||||
# Mock payment validation tool
|
||||
@tool(name="validate_payment_method", description="Validate a payment method before processing.")
|
||||
def validate_payment_method(
|
||||
payment_method: Annotated[dict, Field(description="Payment method to validate (type, number, expiry, cvv).")],
|
||||
) -> str:
|
||||
"""Validate payment method details.
|
||||
|
||||
Returns:
|
||||
JSON string containing validation result with is_valid flag, payment method type,
|
||||
validation messages, supported currencies, and processing fee information.
|
||||
"""
|
||||
method_type = payment_method.get("type", "credit_card")
|
||||
|
||||
# Validation logic
|
||||
is_valid = True
|
||||
validation_messages = []
|
||||
|
||||
if method_type == "credit_card":
|
||||
if not payment_method.get("number"):
|
||||
is_valid = False
|
||||
validation_messages.append("Card number is required")
|
||||
if not payment_method.get("expiry"):
|
||||
is_valid = False
|
||||
validation_messages.append("Expiry date is required")
|
||||
if not payment_method.get("cvv"):
|
||||
is_valid = False
|
||||
validation_messages.append("CVV is required")
|
||||
|
||||
validation_result = {
|
||||
"is_valid": is_valid,
|
||||
"payment_method_type": method_type,
|
||||
"validation_messages": validation_messages if not is_valid else ["Payment method is valid"],
|
||||
"supported_currencies": ["USD", "EUR", "GBP", "JPY"],
|
||||
"processing_fee": "2.5%",
|
||||
}
|
||||
|
||||
return json.dumps({"validation_result": validation_result})
|
||||
@@ -0,0 +1,398 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
# type: ignore
|
||||
"""
|
||||
Multi-Agent Travel Planning Workflow Evaluation with Multiple Response Tracking
|
||||
|
||||
This sample demonstrates a multi-agent travel planning workflow using the Azure AI Client that:
|
||||
1. Processes travel queries through 7 specialized agents
|
||||
2. Tracks MULTIPLE response and conversation IDs per agent for evaluation
|
||||
3. Uses the new Prompt Agents API (V2)
|
||||
4. Captures complete interaction sequences including multiple invocations
|
||||
5. Aggregates findings through a travel planning coordinator
|
||||
|
||||
WORKFLOW STRUCTURE (7 agents):
|
||||
- Travel Agent Executor → Hotel Search, Flight Search, Activity Search (fan-out)
|
||||
- Hotel Search Executor → Booking Information Aggregation Executor
|
||||
- Flight Search Executor → Booking Information Aggregation Executor
|
||||
- Booking Information Aggregation Executor → Booking Confirmation Executor
|
||||
- Booking Confirmation Executor → Booking Payment Executor
|
||||
- Booking Information Aggregation, Booking Payment, Activity Search → Travel Planning Coordinator (ResearchLead) for final aggregation (fan-in)
|
||||
|
||||
Agents:
|
||||
1. Travel Agent - Main coordinator (no tools to avoid thread conflicts)
|
||||
2. Hotel Search - Searches hotels with tools
|
||||
3. Flight Search - Searches flights with tools
|
||||
4. Activity Search - Searches activities with tools
|
||||
5. Booking Information Aggregation - Aggregates hotel & flight booking info
|
||||
6. Booking Confirmation - Confirms bookings with tools
|
||||
7. Booking Payment - Processes payments with tools
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from collections import defaultdict
|
||||
|
||||
from _tools import (
|
||||
check_flight_availability,
|
||||
check_hotel_availability,
|
||||
confirm_booking,
|
||||
get_flight_details,
|
||||
get_hotel_details,
|
||||
process_payment,
|
||||
search_activities,
|
||||
search_flights,
|
||||
# Travel planning tools
|
||||
search_hotels,
|
||||
validate_payment_method,
|
||||
)
|
||||
from agent_framework import (
|
||||
Agent,
|
||||
AgentExecutorResponse,
|
||||
AgentResponseUpdate,
|
||||
Executor,
|
||||
Message,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowEvent,
|
||||
executor,
|
||||
handler,
|
||||
)
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.identity.aio import DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
from typing_extensions import Never
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
@executor(id="start_executor")
|
||||
async def start_executor(input: str, ctx: WorkflowContext[list[Message]]) -> None:
|
||||
"""Initiates the workflow by sending the user query to all specialized agents."""
|
||||
await ctx.send_message([Message("user", [input])])
|
||||
|
||||
|
||||
class ResearchLead(Executor):
|
||||
"""Aggregates and summarizes travel planning findings from all specialized agents."""
|
||||
|
||||
def __init__(self, client: FoundryChatClient, id: str = "travel-planning-coordinator"):
|
||||
# Use default_options to persist conversation history for evaluation.
|
||||
self.agent = Agent(
|
||||
client=client,
|
||||
id="travel-planning-coordinator",
|
||||
instructions=(
|
||||
"You are the final coordinator. You will receive responses from multiple agents: "
|
||||
"booking-info-aggregation-agent (hotel/flight options), booking-payment-agent (payment confirmation), "
|
||||
"and activity-search-agent (activities). "
|
||||
"Review each agent's response, then create a comprehensive travel itinerary organized by: "
|
||||
"1. Flights 2. Hotels 3. Activities 4. Booking confirmations 5. Payment details. "
|
||||
"Clearly indicate which information came from which agent. Do not use tools."
|
||||
),
|
||||
name="travel-planning-coordinator",
|
||||
)
|
||||
super().__init__(id=id)
|
||||
|
||||
@handler
|
||||
async def fan_in_handle(self, responses: list[AgentExecutorResponse], ctx: WorkflowContext[Never, str]) -> None:
|
||||
user_query = responses[0].full_conversation[0].text
|
||||
|
||||
# Extract findings from all agent responses
|
||||
agent_findings = self._extract_agent_findings(responses)
|
||||
summary_text = (
|
||||
"\n".join(agent_findings) if agent_findings else "No specific findings were provided by the agents."
|
||||
)
|
||||
|
||||
# Generate comprehensive travel plan summary
|
||||
messages = [
|
||||
Message(
|
||||
role="system",
|
||||
contents=[
|
||||
"You are a travel planning coordinator. Summarize findings from multiple specialized travel agents and provide a clear, comprehensive travel plan based on the user's query."
|
||||
],
|
||||
),
|
||||
Message(
|
||||
role="user",
|
||||
contents=[
|
||||
f"Original query: {user_query}\n\nFindings from specialized travel agents:\n{summary_text}\n\nPlease provide a comprehensive travel plan based on these findings."
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
try:
|
||||
final_response = await self.agent.run(messages)
|
||||
output_text = (
|
||||
final_response.messages[-1].text
|
||||
if final_response.messages and final_response.messages[-1].text
|
||||
else f"Based on the available findings, here's your travel plan for '{user_query}': {summary_text}"
|
||||
)
|
||||
except Exception:
|
||||
output_text = f"Based on the available findings, here's your travel plan for '{user_query}': {summary_text}"
|
||||
|
||||
await ctx.yield_output(output_text)
|
||||
|
||||
def _extract_agent_findings(self, responses: list[AgentExecutorResponse]) -> list[str]:
|
||||
"""Extract findings from agent responses."""
|
||||
agent_findings = []
|
||||
|
||||
for response in responses:
|
||||
findings = []
|
||||
if response.agent_response and response.agent_response.messages:
|
||||
for msg in response.agent_response.messages:
|
||||
if msg.role == "assistant" and msg.text and msg.text.strip():
|
||||
findings.append(msg.text.strip())
|
||||
|
||||
if findings:
|
||||
combined_findings = " ".join(findings)
|
||||
agent_findings.append(f"[{response.executor_id}]: {combined_findings}")
|
||||
|
||||
return agent_findings
|
||||
|
||||
|
||||
async def run_workflow_with_response_tracking(
|
||||
query: str, client: FoundryChatClient | None = None, model: str | None = None
|
||||
) -> dict:
|
||||
"""Run multi-agent workflow and track conversation IDs, response IDs, and interaction sequence.
|
||||
|
||||
Args:
|
||||
query: The user query to process through the multi-agent workflow
|
||||
client: Optional FoundryChatClient instance
|
||||
model: Optional model for the workflow agents
|
||||
|
||||
Returns:
|
||||
Dictionary containing interaction sequence, conversation/response IDs, and conversation analysis
|
||||
"""
|
||||
if client is None:
|
||||
try:
|
||||
async with DefaultAzureCredential() as credential:
|
||||
project_client = AIProjectClient(
|
||||
endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
credential=credential,
|
||||
)
|
||||
|
||||
async with project_client:
|
||||
client = FoundryChatClient(project_client=project_client, model=model)
|
||||
return await _run_workflow_with_client(query, client)
|
||||
except Exception as e:
|
||||
print(f"Error during workflow execution: {e}")
|
||||
raise
|
||||
else:
|
||||
return await _run_workflow_with_client(query, client)
|
||||
|
||||
|
||||
async def _run_workflow_with_client(query: str, client: FoundryChatClient) -> dict:
|
||||
"""Execute workflow with given client and track all interactions."""
|
||||
|
||||
# Initialize tracking variables - use lists to track multiple responses per agent
|
||||
conversation_ids: dict[str, list[str]] = defaultdict(list)
|
||||
response_ids: dict[str, list[str]] = defaultdict(list)
|
||||
|
||||
# Create workflow components using a single shared client
|
||||
workflow, agent_map = await _create_workflow(client)
|
||||
|
||||
def track_ids(event: WorkflowEvent) -> WorkflowEvent:
|
||||
"""Transform hook that tracks response/conversation IDs from AgentResponseUpdate events."""
|
||||
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
_track_agent_ids(event, event.executor_id, response_ids, conversation_ids)
|
||||
return event
|
||||
|
||||
# Process workflow events using a transform hook for ID tracking
|
||||
stream = workflow.run(query, stream=True).with_transform_hook(track_ids)
|
||||
result = await stream.get_final_response()
|
||||
|
||||
workflow_output = result.get_outputs()[-1] if result.get_outputs() else None
|
||||
if workflow_output:
|
||||
print(f"\nWorkflow Output: {workflow_output}\n")
|
||||
|
||||
return {
|
||||
"conversation_ids": dict(conversation_ids),
|
||||
"response_ids": dict(response_ids),
|
||||
"output": workflow_output,
|
||||
"query": query,
|
||||
}
|
||||
|
||||
|
||||
async def _create_workflow(client: FoundryChatClient):
|
||||
"""Create the multi-agent travel planning workflow with specialized agents.
|
||||
|
||||
Uses a single shared FoundryChatClient for all agents.
|
||||
"""
|
||||
|
||||
final_coordinator = ResearchLead(client=client, id="final-coordinator")
|
||||
|
||||
# Agent 1: Travel Request Handler (initial coordinator)
|
||||
travel_request_handler = Agent(
|
||||
client=client,
|
||||
id="travel-request-handler",
|
||||
instructions=(
|
||||
"You receive user travel queries and relay them to specialized agents. Extract key information: destination, dates, budget, and preferences. Pass this information forward clearly to the next agents."
|
||||
),
|
||||
name="travel-request-handler",
|
||||
)
|
||||
|
||||
# Agent 2: Hotel Search Executor
|
||||
hotel_search_agent = Agent(
|
||||
client=client,
|
||||
id="hotel-search-agent",
|
||||
instructions=(
|
||||
"You are a hotel search specialist. Your task is ONLY to search for and provide hotel information. Use search_hotels to find options, get_hotel_details for specifics, and check_availability to verify rooms. Output format: List hotel names, prices per night, total cost for the stay, locations, ratings, amenities, and addresses. IMPORTANT: Only provide hotel information without additional commentary."
|
||||
),
|
||||
name="hotel-search-agent",
|
||||
tools=[search_hotels, get_hotel_details, check_hotel_availability],
|
||||
)
|
||||
|
||||
# Agent 3: Flight Search Executor
|
||||
flight_search_agent = Agent(
|
||||
client=client,
|
||||
id="flight-search-agent",
|
||||
instructions=(
|
||||
"You are a flight search specialist. Your task is ONLY to search for and provide flight information. Use search_flights to find options, get_flight_details for specifics, and check_availability for seats. Output format: List flight numbers, airlines, departure/arrival times, prices, durations, and cabin class. IMPORTANT: Only provide flight information without additional commentary."
|
||||
),
|
||||
name="flight-search-agent",
|
||||
tools=[search_flights, get_flight_details, check_flight_availability],
|
||||
)
|
||||
|
||||
# Agent 4: Activity Search Executor
|
||||
activity_search_agent = Agent(
|
||||
client=client,
|
||||
id="activity-search-agent",
|
||||
instructions=(
|
||||
"You are an activities specialist. Your task is ONLY to search for and provide activity information. Use search_activities to find options for activities. Output format: List activity names, descriptions, prices, durations, ratings, and categories. IMPORTANT: Only provide activity information without additional commentary."
|
||||
),
|
||||
name="activity-search-agent",
|
||||
tools=[search_activities],
|
||||
)
|
||||
|
||||
# Agent 5: Booking Confirmation Executor
|
||||
booking_confirmation_agent = Agent(
|
||||
client=client,
|
||||
id="booking-confirmation-agent",
|
||||
instructions=(
|
||||
"You confirm bookings. Use check_hotel_availability and check_flight_availability to verify slots, then confirm_booking to finalize. Provide ONLY: confirmation numbers, booking references, and confirmation status."
|
||||
),
|
||||
name="booking-confirmation-agent",
|
||||
tools=[confirm_booking, check_hotel_availability, check_flight_availability],
|
||||
)
|
||||
|
||||
# Agent 6: Booking Payment Executor
|
||||
booking_payment_agent = Agent(
|
||||
client=client,
|
||||
id="booking-payment-agent",
|
||||
instructions=(
|
||||
"You process payments. Use validate_payment_method to verify payment, then process_payment to complete transactions. Provide ONLY: payment confirmation status, transaction IDs, and payment amounts."
|
||||
),
|
||||
name="booking-payment-agent",
|
||||
tools=[process_payment, validate_payment_method],
|
||||
)
|
||||
|
||||
# Agent 7: Booking Information Aggregation Executor
|
||||
booking_info_aggregation_agent = Agent(
|
||||
client=client,
|
||||
id="booking-info-aggregation-agent",
|
||||
instructions=(
|
||||
"You aggregate hotel and flight search results. Receive options from search agents and organize them. Provide: top 2-3 hotel options with prices and top 2-3 flight options with prices in a structured format."
|
||||
),
|
||||
name="booking-info-aggregation-agent",
|
||||
)
|
||||
|
||||
# Build workflow with logical booking flow:
|
||||
# 1. start_executor → travel_request_handler
|
||||
# 2. travel_request_handler → hotel_search, flight_search, activity_search (fan-out)
|
||||
# 3. hotel_search → booking_info_aggregation
|
||||
# 4. flight_search → booking_info_aggregation
|
||||
# 5. booking_info_aggregation → booking_confirmation
|
||||
# 6. booking_confirmation → booking_payment
|
||||
# 7. booking_info_aggregation, booking_payment, activity_search → final_coordinator (final aggregation, fan-in)
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder(name="Travel Planning Workflow", start_executor=start_executor)
|
||||
.add_edge(start_executor, travel_request_handler)
|
||||
.add_fan_out_edges(travel_request_handler, [hotel_search_agent, flight_search_agent, activity_search_agent])
|
||||
.add_edge(hotel_search_agent, booking_info_aggregation_agent)
|
||||
.add_edge(flight_search_agent, booking_info_aggregation_agent)
|
||||
.add_edge(booking_info_aggregation_agent, booking_confirmation_agent)
|
||||
.add_edge(booking_confirmation_agent, booking_payment_agent)
|
||||
.add_fan_in_edges(
|
||||
[booking_info_aggregation_agent, booking_payment_agent, activity_search_agent], final_coordinator
|
||||
)
|
||||
.build()
|
||||
)
|
||||
|
||||
# Return workflow and agent map for thread ID extraction
|
||||
agent_map = {
|
||||
"travel_request_handler": travel_request_handler,
|
||||
"hotel-search-agent": hotel_search_agent,
|
||||
"flight-search-agent": flight_search_agent,
|
||||
"activity-search-agent": activity_search_agent,
|
||||
"booking-confirmation-agent": booking_confirmation_agent,
|
||||
"booking-payment-agent": booking_payment_agent,
|
||||
"booking-info-aggregation-agent": booking_info_aggregation_agent,
|
||||
"final-coordinator": final_coordinator.agent,
|
||||
}
|
||||
|
||||
return workflow, agent_map
|
||||
|
||||
|
||||
def _track_agent_ids(event, agent, response_ids, conversation_ids):
|
||||
"""Track agent response and conversation IDs - supporting multiple responses per agent."""
|
||||
update = event.data
|
||||
|
||||
# response_id is directly on AgentResponseUpdate
|
||||
if update.response_id and update.response_id not in response_ids[agent]:
|
||||
response_ids[agent].append(update.response_id)
|
||||
|
||||
# conversation_id is on the underlying ChatResponseUpdate (raw_representation)
|
||||
raw = update.raw_representation
|
||||
if (
|
||||
raw
|
||||
and hasattr(raw, "conversation_id")
|
||||
and raw.conversation_id
|
||||
and raw.conversation_id not in conversation_ids[agent]
|
||||
):
|
||||
conversation_ids[agent].append(raw.conversation_id)
|
||||
|
||||
|
||||
async def create_and_run_workflow(model: str | None = None):
|
||||
"""Run the workflow evaluation and display results.
|
||||
|
||||
Args:
|
||||
model: Optional model for the workflow agents
|
||||
|
||||
Returns:
|
||||
Dictionary containing agents data with conversation IDs, response IDs, and query information
|
||||
"""
|
||||
example_queries = [
|
||||
"Plan a 3-day trip to Paris from December 15-18, 2025. Budget is $2000. Need hotel near Eiffel Tower, round-trip flights from New York JFK, and recommend 2-3 activities per day.",
|
||||
"Find a budget hotel in Tokyo for January 5-10, 2026 under $150/night near Shibuya station, book activities including a sushi making class",
|
||||
"Search for round-trip flights from Los Angeles to London departing March 20, 2026, returning March 27, 2026. Economy class, 2 passengers. Recommend tourist attractions and museums.",
|
||||
]
|
||||
|
||||
query = example_queries[0]
|
||||
print(f"Query: {query}\n")
|
||||
|
||||
result = await run_workflow_with_response_tracking(query, model=model)
|
||||
|
||||
# Create output data structure
|
||||
output_data = {"agents": {}, "query": result["query"], "output": result.get("output", "")}
|
||||
|
||||
# Create agent-specific mappings - now with lists of IDs
|
||||
all_agents = set(result["conversation_ids"].keys()) | set(result["response_ids"].keys())
|
||||
for agent_name in all_agents:
|
||||
output_data["agents"][agent_name] = {
|
||||
"conversation_ids": result["conversation_ids"].get(agent_name, []),
|
||||
"response_ids": result["response_ids"].get(agent_name, []),
|
||||
"response_count": len(result["response_ids"].get(agent_name, [])),
|
||||
}
|
||||
|
||||
print(f"\nTotal agents tracked: {len(output_data['agents'])}")
|
||||
|
||||
# Print summary of multiple responses
|
||||
print("\n=== Multi-Response Summary ===")
|
||||
for agent_name, agent_data in output_data["agents"].items():
|
||||
response_count = agent_data["response_count"]
|
||||
print(f"{agent_name}: {response_count} response(s)")
|
||||
|
||||
return output_data
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(create_and_run_workflow())
|
||||
@@ -0,0 +1,240 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
# type: ignore
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from azure.ai.projects import AIProjectClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from create_workflow import create_and_run_workflow
|
||||
from dotenv import load_dotenv
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openai import OpenAI
|
||||
from openai.types import EvalCreateResponse
|
||||
from openai.types.evals import RunCreateResponse
|
||||
|
||||
"""
|
||||
Script to run multi-agent travel planning workflow and evaluate agent responses.
|
||||
|
||||
This script:
|
||||
1. Runs the multi-agent travel planning workflow
|
||||
2. Displays a summary of tracked agent responses
|
||||
3. Fetches and previews final agent responses
|
||||
4. Creates an evaluation with multiple evaluators
|
||||
5. Runs the evaluation on selected agent responses
|
||||
6. Monitors evaluation progress and displays results
|
||||
"""
|
||||
|
||||
|
||||
def create_openai_client() -> OpenAI:
|
||||
project_client = AIProjectClient(
|
||||
endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
return project_client.get_openai_client()
|
||||
|
||||
|
||||
def print_section(title: str):
|
||||
"""Print a formatted section header."""
|
||||
print(f"\n{'=' * 80}")
|
||||
print(f"{title}")
|
||||
print(f"{'=' * 80}")
|
||||
|
||||
|
||||
async def run_workflow(model: str | None = None) -> dict[str, Any]:
|
||||
"""Execute the multi-agent travel planning workflow.
|
||||
|
||||
Args:
|
||||
model: Optional model for the workflow agents
|
||||
|
||||
Returns:
|
||||
Dictionary containing workflow data with agent response IDs
|
||||
"""
|
||||
print("Executing multi-agent travel planning workflow...")
|
||||
print("This may take a few minutes...")
|
||||
|
||||
workflow_data = await create_and_run_workflow(model=model)
|
||||
|
||||
print("Workflow execution completed")
|
||||
return workflow_data
|
||||
|
||||
|
||||
def display_response_summary(workflow_data: dict) -> None:
|
||||
"""Display summary of response data."""
|
||||
print(f"Query: {workflow_data['query']}")
|
||||
print(f"\nAgents tracked: {len(workflow_data['agents'])}")
|
||||
|
||||
for agent_name, agent_data in workflow_data["agents"].items():
|
||||
response_count = agent_data["response_count"]
|
||||
print(f" {agent_name}: {response_count} response(s)")
|
||||
|
||||
|
||||
def fetch_agent_responses(openai_client: OpenAI, workflow_data: dict[str, Any], agent_names: list[str]) -> None:
|
||||
"""Fetch and display final responses from specified agents."""
|
||||
for agent_name in agent_names:
|
||||
if agent_name not in workflow_data["agents"]:
|
||||
continue
|
||||
|
||||
agent_data = workflow_data["agents"][agent_name]
|
||||
if not agent_data["response_ids"]:
|
||||
continue
|
||||
|
||||
final_response_id = agent_data["response_ids"][-1]
|
||||
print(f"\n{agent_name}")
|
||||
print(f" Response ID: {final_response_id}")
|
||||
|
||||
try:
|
||||
response = openai_client.responses.retrieve(response_id=final_response_id)
|
||||
content = response.output[-1].content[-1].text
|
||||
truncated = content[:300] + "..." if len(content) > 300 else content
|
||||
print(f" Content preview: {truncated}")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
|
||||
|
||||
def create_evaluation(openai_client: OpenAI, model: str | None = "gpt-5.2") -> EvalCreateResponse:
|
||||
"""Create evaluation with multiple evaluators."""
|
||||
model = os.environ.get("FOUNDRY_MODEL", model)
|
||||
data_source_config = {"type": "azure_ai_source", "scenario": "responses"}
|
||||
|
||||
testing_criteria = [
|
||||
{
|
||||
"type": "azure_ai_evaluator",
|
||||
"name": "relevance",
|
||||
"evaluator_name": "builtin.relevance",
|
||||
"initialization_parameters": {"deployment_name": model},
|
||||
},
|
||||
{
|
||||
"type": "azure_ai_evaluator",
|
||||
"name": "groundedness",
|
||||
"evaluator_name": "builtin.groundedness",
|
||||
"initialization_parameters": {"deployment_name": model},
|
||||
},
|
||||
{
|
||||
"type": "azure_ai_evaluator",
|
||||
"name": "tool_call_accuracy",
|
||||
"evaluator_name": "builtin.tool_call_accuracy",
|
||||
"initialization_parameters": {"deployment_name": model},
|
||||
},
|
||||
{
|
||||
"type": "azure_ai_evaluator",
|
||||
"name": "tool_output_utilization",
|
||||
"evaluator_name": "builtin.tool_output_utilization",
|
||||
"initialization_parameters": {"deployment_name": model},
|
||||
},
|
||||
]
|
||||
|
||||
eval_object = openai_client.evals.create(
|
||||
name="Travel Workflow Multi-Evaluator Assessment",
|
||||
data_source_config=data_source_config,
|
||||
testing_criteria=testing_criteria,
|
||||
)
|
||||
|
||||
evaluator_names = [criterion["name"] for criterion in testing_criteria]
|
||||
print(f"Evaluation created: {eval_object.id}")
|
||||
print(f"Evaluators ({len(evaluator_names)}): {', '.join(evaluator_names)}")
|
||||
|
||||
return eval_object
|
||||
|
||||
|
||||
def run_evaluation(
|
||||
openai_client: OpenAI, eval_object: EvalCreateResponse, workflow_data: dict[str, Any], agent_names: list[str]
|
||||
) -> RunCreateResponse:
|
||||
"""Run evaluation on selected agent responses."""
|
||||
selected_response_ids = []
|
||||
for agent_name in agent_names:
|
||||
if agent_name in workflow_data["agents"]:
|
||||
agent_data = workflow_data["agents"][agent_name]
|
||||
if agent_data["response_ids"]:
|
||||
selected_response_ids.append(agent_data["response_ids"][-1])
|
||||
|
||||
print(f"Selected {len(selected_response_ids)} responses for evaluation")
|
||||
|
||||
data_source = {
|
||||
"type": "azure_ai_responses",
|
||||
"item_generation_params": {
|
||||
"type": "response_retrieval",
|
||||
"data_mapping": {"response_id": "{{item.resp_id}}"},
|
||||
"source": {
|
||||
"type": "file_content",
|
||||
"content": [{"item": {"resp_id": resp_id}} for resp_id in selected_response_ids],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
eval_run = openai_client.evals.runs.create(
|
||||
eval_id=eval_object.id, name="Multi-Agent Response Evaluation", data_source=data_source
|
||||
)
|
||||
|
||||
print(f"Evaluation run created: {eval_run.id}")
|
||||
|
||||
return eval_run
|
||||
|
||||
|
||||
def monitor_evaluation(openai_client: OpenAI, eval_object: EvalCreateResponse, eval_run: RunCreateResponse):
|
||||
"""Monitor evaluation progress and display results."""
|
||||
print("Waiting for evaluation to complete...")
|
||||
|
||||
while eval_run.status not in ["completed", "failed"]:
|
||||
eval_run = openai_client.evals.runs.retrieve(run_id=eval_run.id, eval_id=eval_object.id)
|
||||
print(f"Status: {eval_run.status}")
|
||||
time.sleep(5)
|
||||
|
||||
if eval_run.status == "completed":
|
||||
print("\nEvaluation completed successfully")
|
||||
print(f"Result counts: {eval_run.result_counts}")
|
||||
print(f"\nReport URL: {eval_run.report_url}")
|
||||
else:
|
||||
print("\nEvaluation failed")
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main execution flow."""
|
||||
load_dotenv()
|
||||
openai_client = create_openai_client()
|
||||
|
||||
# Model configuration
|
||||
workflow_agent_model = os.environ.get("FOUNDRY_MODEL_WORKFLOW", "gpt-4.1-nano")
|
||||
eval_model = os.environ.get("FOUNDRY_MODEL_EVAL", "gpt-5.2")
|
||||
|
||||
# Focus on these agents, uncomment other ones you want to have evals run on
|
||||
agents_to_evaluate = [
|
||||
"hotel-search-agent",
|
||||
"flight-search-agent",
|
||||
"activity-search-agent",
|
||||
# "booking-payment-agent",
|
||||
# "booking-info-aggregation-agent",
|
||||
# "travel-request-handler",
|
||||
# "booking-confirmation-agent",
|
||||
]
|
||||
|
||||
print_section("Travel Planning Workflow Evaluation")
|
||||
|
||||
print_section("Step 1: Running Workflow")
|
||||
workflow_data = await run_workflow(model=workflow_agent_model)
|
||||
|
||||
print_section("Step 2: Response Data Summary")
|
||||
display_response_summary(workflow_data)
|
||||
|
||||
print_section("Step 3: Fetching Agent Responses")
|
||||
fetch_agent_responses(openai_client, workflow_data, agents_to_evaluate)
|
||||
|
||||
print_section("Step 4: Creating Evaluation")
|
||||
eval_object = create_evaluation(openai_client, model=eval_model)
|
||||
|
||||
print_section("Step 5: Running Evaluation")
|
||||
eval_run = run_evaluation(openai_client, eval_object, workflow_data, agents_to_evaluate)
|
||||
|
||||
print_section("Step 6: Monitoring Evaluation")
|
||||
monitor_evaluation(openai_client, eval_object, eval_run)
|
||||
|
||||
print_section("Complete")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user