Chapter 11: Capstone Project: Building a Travel Agent AI
The Travel Planning Challenge
Planning the perfect trip involves juggling multiple factors: finding destinations that match your interests, staying within budget, creating an itinerary with appealing activities, and booking suitable accommodations and transportation. This complexity makes travel planning an ideal application for an AI agent.
In this capstone project, we'll build a Travel Agent AI that guides users through the entire trip planning process—from initial concept to actionable booking information. Our agent will demonstrate key capabilities including:
- Maintaining conversational context (remembering preferences throughout the planning process)
- Tool usage (researching destinations, calculating costs, finding flights/hotels)
- Planning (creating coherent multi-day itineraries)
- Decision-making (recommending destinations based on multiple criteria)
By completing this project, you'll synthesize concepts from previous chapters into a practical agent that delivers real value. Let's begin!
Project Requirements and Planning
Core Requirements
Our Travel Agent AI must:
- Gather Trip Requirements: Conversationally collect key information about the user's trip (budget, dates, group size, interests, preferred climate, etc.)
- Research Destinations: Find and evaluate potential destinations matching the user's criteria
- Generate Itineraries: Create detailed day-by-day plans with activities and highlights
- Find Travel Options: Research and recommend flights and accommodations
- Present Recommendations: Deliver findings in a clear, organized format
- Maintain Context: Remember user preferences throughout the conversation
Project Architecture
We'll build our agent with these key components:
- Memory Module: Stores conversation history and trip requirements
- Research Tools: Integration with search APIs for destination research
- Itinerary Generator: Creates structured travel plans based on destinations and preferences
- Travel Options Finder: Searches for flights and accommodations
- Decision Engine: Manages the conversation flow and determines next steps
- Natural Language Interface: Handles user communication
Development Plan
We'll tackle this project in phases:
- Set up our development environment
- Build the core agent with memory and conversation management
- Implement the destination research capability
- Create the itinerary generation system
- Add travel booking information tools
- Integrate all components and test the complete agent
Let's dive into implementation!
Setting Up the Environment
First, we need to set up our project with the necessary dependencies.
Creating the Project Structure
- Create a new directory for your project:
mkdir travel-agent-ai
cd travel-agent-ai
- Set up a virtual environment:
python -m venv venv
- Activate the virtual environment:
- On Windows:
venv\Scripts\activate
- On macOS/Linux:
source venv/bin/activate
- Create a basic file structure:
mkdir -p src/tools
touch src/init.py
touch src/agent.py
touch src/memory.py
touch src/tools/init.py
touch src/tools/destination_research.py
touch src/tools/itinerary_generator.py
touch src/tools/travel_finder.py
touch main.py
touch .env
Installing Dependencies
Create a requirements.txt file with the following content:
openai==1.3.0
python-dotenv==1.0.0
requests==2.31.0
serpapi==0.1.0
Install these dependencies:
pip install -r requirements.txt
Setting Up API Keys
For this project, we'll need:
- OpenAI API key (for the language model)
- SerpAPI key (for web searches)
Sign up for API keys if you don't have them already:
Add your API keys to the .env file:
OPENAIAPIKEY=youropenaiapikeyhere
SERPAPIKEY=yourserpapikeyhere
Building the Core Agent
Let's start by implementing the memory and core conversation capabilities of our agent.
Creating the Memory Module
First, let's create our memory system to track conversation history and trip requirements.
Edit src/memory.py:
class Memory:
def init(self):
# Conversation history
self.messages = []
# Trip requirements (will be populated during conversation)
self.trip_requirements = {
"budget": None,
"duration": None,
"travelers": None,
"departure_date": None,
"departure_location": None,
"interests": [],
"climate_preference": None,
"accommodation_preference": None
}
# Generated destination options
self.destination_options = []
# Selected destination
self.selected_destination = None
# Generated itinerary
self.itinerary = []
# Travel booking information
self.flight_options = []
self.accommodation_options = []
def add_message(self, role, content):
"""Add a message to the conversation history."""
self.messages.append({"role": role, "content": content})
def getmessages(self, includesystem=True):
"""Get all conversation messages."""
if include_system:
return self.messages
else:
return [msg for msg in self.messages if msg["role"] != "system"]
def getlastnmessages(self, n=10, includesystem=True):
"""Get the last n messages from conversation history."""
if include_system:
return self.messages[-n:]
else:
non_system = [msg for msg in self.messages if msg["role"] != "system"]
return non_system[-n:]
def updatetriprequirement(self, key, value):
"""Update a specific trip requirement."""
if key in self.trip_requirements:
self.trip_requirements[key] = value
def gettriprequirements(self):
"""Get all trip requirements."""
return self.trip_requirements
def isrequirementscomplete(self):
"""Check if all essential trip requirements are filled."""
essentialkeys = ["budget", "duration", "travelers", "departuredate", "departure_location"]
return all(self.triprequirements[key] for key in essentialkeys) and len(self.trip_requirements["interests"]) > 0
def storedestinationoptions(self, destinations):
"""Store researched destination options."""
self.destination_options = destinations
def select_destination(self, destination):
"""Set the selected destination."""
self.selected_destination = destination
def store_itinerary(self, itinerary):
"""Store the generated itinerary."""
self.itinerary = itinerary
def storeflightoptions(self, flights):
"""Store found flight options."""
self.flight_options = flights
def storeaccommodationoptions(self, accommodations):
"""Store found accommodation options."""
self.accommodation_options = accommodations
Implementing the Destination Research Tool
Now let's create our first tool for researching potential destinations.
Edit src/tools/destination_research.py
:
import os
import json
import requests
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
class DestinationResearch:
def init(self):
self.serpapikey = os.getenv("SERPAPIKEY")
def searchdestinations(self, triprequirements):
"""
Research destinations based on trip requirements.
Returns a list of potential destinations with information.
"""
# Construct search query based on trip requirements
query_parts = []
if triprequirements["climatepreference"]:
queryparts.append(triprequirements["climate_preference"])
if trip_requirements["interests"]:
interests = " ".join(trip_requirements["interests"][:3]) # Use top 3 interests
query_parts.append(interests)
query_parts.append("travel destination")
query_parts.append("tourist attractions")
if trip_requirements["budget"]:
# Convert budget to terms like "budget", "affordable", "luxury"
budgetamt = triprequirements["budget"]
if isinstance(budget_amt, str):
try:
budgetamt = int(budgetamt.replace("$", "").replace(",", ""))
except:
budget_amt = 3000 # Default if parsing fails
budget_term = "budget"
if budget_amt > 5000:
budget_term = "luxury"
elif budget_amt > 2000:
budget_term = "mid-range"
queryparts.append(budgetterm)
# Construct the final search query
searchquery = " ".join(queryparts)
# Perform the search using SerpAPI
searchresults = self.performsearch(searchquery)
# Process and extract destination information
destinations = self.extractdestinations(searchresults, triprequirements)
return destinations
def performsearch(self, query):
"""
Perform a search using SerpAPI.
"""
# For demo purposes, we're using a simplified approach with SerpAPI
url = "https://serpapi.com/search.json"
params = {
"q": query,
"apikey": self.serpapikey,
"engine": "google",
"num": 10 # Number of results
}
response = requests.get(url, params=params)
if response.status_code == 200:
return response.json()
else:
# For demonstration, return sample data if API call fails
return self.getsample_data()
def extractdestinations(self, searchresults, triprequirements):
"""
Extract structured destination information from search results.
"""
# In a full implementation, this would parse the search results
# to extract destination names, descriptions, and other details
# For demonstration, we'll return structured data based on the search
# or fallback to sample data if processing fails
try:
destinations = []
# Extract organic results
organicresults = searchresults.get("organic_results", [])
for result in organic_results[:5]: # Process top 5 results
title = result.get("title", "")
link = result.get("link", "")
snippet = result.get("snippet", "")
# Basic filtering to identify destination names
# This is a simplified approach - real implementation would be more sophisticated
# Skip results that don't look like destinations
if "hotel" in title.lower() or "booking" in title.lower():
continue
# Extract potential destination name
destination_name = title.split("-")[0].split("|")[0].split(":")[0].strip()
# Skip if destination name is too long (likely not a real destination)
if len(destination_name.split()) > 4:
continue
destinations.append({
"name": destination_name,
"description": snippet,
"url": link,
"highlights": self.extracthighlights(snippet),
"estimatedcost": self.estimatecost(destinationname, trip_requirements)
})
# If we couldn't extract meaningful destinations, use sample data
if len(destinations) < 3:
return self.getsampledestinations(triprequirements)
return destinations
except:
# Fallback to sample data if extraction fails
return self.getsampledestinations(triprequirements)
def extracthighlights(self, text):
"""
Extract potential highlights from the description.
"""
# In a real implementation, this would use NLP to extract key attractions
# For demonstration, we'll extract phrases that might indicate attractions
highlights = []
sentences = text.split(". ")
for sentence in sentences:
lower_sentence = sentence.lower()
if any(keyword in lower_sentence for keyword in [
"famous", "popular", "attraction", "visit", "experience", "enjoy"
]):
highlights.append(sentence)
return highlights[:3] # Return up to 3 highlights
def estimatecost(self, destination, trip_requirements):
"""
Estimate the cost of visiting this destination based on requirements.
"""
# In a real implementation, this would use actual data from travel APIs
# For demonstration, we'll use a simple heuristic
# Base cost per day per person
base_costs = {
"Bali": 50,
"Paris": 150,
"Tokyo": 120,
"New York": 200,
"Bangkok": 40,
"London": 180,
"Rome": 130,
"Cancun": 100,
"Sydney": 140,
"Dubai": 160,
"Amsterdam": 140,
"Singapore": 110,
"Las Vegas": 150,
"Miami": 170,
"Barcelona": 120
}
# Default cost if destination not in our database
daily_cost = 100
# Try to match destination with our database
for knowndest, cost in basecosts.items():
if known_dest.lower() in destination.lower():
daily_cost = cost
break
# Adjust based on accommodation preference
accommodation_factor = 1.0
if triprequirements["accommodationpreference"] == "luxury":
accommodation_factor = 1.5
elif triprequirements["accommodationpreference"] == "budget":
accommodation_factor = 0.7
# Calculate total
travelers = int(triprequirements["travelers"]) if triprequirements["travelers"] else 1
duration = int(triprequirements["duration"]) if triprequirements["duration"] else 7
totalestimate = dailycost travelers duration * accommodation_factor
return round(total_estimate)
def getsample_data(self):
"""
Return sample search data for demonstration purposes.
"""
# This would contain sample search results JSON
return {
"organic_results": [
{
"title": "Bali, Indonesia - Top Tropical Destination",
"link": "https://example.com/bali",
"snippet": "Bali offers beautiful beaches, lush rice terraces, and vibrant cultural experiences. Visit the famous Ubud Monkey Forest or relax at Kuta Beach."
},
{
"title": "Paris, France - City of Lights",
"link": "https://example.com/paris",
"snippet": "Experience the romance of Paris with its iconic Eiffel Tower, world-class museums like the Louvre, and charming cafes along the Seine River."
},
{
"title": "Tokyo, Japan - Modern Meets Traditional",
"link": "https://example.com/tokyo",
"snippet": "Tokyo blends ultramodern and traditional, from neon-lit skyscrapers to historic temples. Enjoy shopping in Shibuya, visit the tranquil Meiji Shrine, and experience the world-famous cuisine."
},
{
"title": "10 Best Hotels in Bangkok - Booking.com",
"link": "https://example.com/bangkok-hotels",
"snippet": "Find deals at Bangkok's best hotels with pool and breakfast included. Book your hotel in Bangkok now."
},
{
"title": "New York City - The Big Apple",
"link": "https://example.com/nyc",
"snippet": "New York offers world-famous attractions like Times Square, Central Park and the Statue of Liberty. Enjoy broadway shows, diverse dining options, and incredible museums."
}
]
}
def getsampledestinations(self, triprequirements):
"""
Generate sample destination data based on user requirements.
"""
destinations = []
# Select destinations based on climate preference
climate_destinations = {
"tropical": ["Bali, Indonesia", "Cancun, Mexico", "Phuket, Thailand"],
"warm": ["Barcelona, Spain", "Sydney, Australia", "Miami, USA"],
"moderate": ["Paris, France", "Tokyo, Japan", "San Francisco, USA"],
"cold": ["Reykjavik, Iceland", "Aspen, Colorado", "Queenstown, New Zealand"]
}
# Default to a mix if no preference specified
selecteddestinations = climatedestinations.get(
triprequirements["climatepreference"],
["Paris, France", "Bali, Indonesia", "Tokyo, Japan", "New York City, USA"]
)
# Generate destination data
for dest in selected_destinations[:3]: # Limit to 3 destinations
highlights = []
# Add interest-based highlights
if "beach" in trip_requirements["interests"] and dest in ["Bali, Indonesia", "Cancun, Mexico", "Miami, USA"]:
highlights.append("Beautiful beaches with crystal clear water")
if "culture" in trip_requirements["interests"] and dest in ["Paris, France", "Tokyo, Japan", "Bali, Indonesia"]:
highlights.append("Rich cultural experiences and historical sites")
if "food" in trip_requirements["interests"]:
highlights.append("World-renowned culinary scene with local specialties")
if "adventure" in trip_requirements["interests"] and dest in ["Queenstown, New Zealand", "Bali, Indonesia"]:
highlights.append("Thrilling outdoor activities and adventures")
# Add some default highlights if needed
destination_highlights = {
"Bali, Indonesia": ["Sacred temples and ceremonies", "Ubud Monkey Forest", "Beautiful rice terraces"],
"Paris, France": ["Eiffel Tower", "The Louvre Museum", "Notre Dame Cathedral"],
"Tokyo, Japan": ["Shibuya Crossing", "Meiji Shrine", "Tokyo Skytree"],
"New York City, USA": ["Times Square", "Central Park", "Statue of Liberty"],
"Cancun, Mexico": ["Pristine Caribbean beaches", "Chichen Itza ruins", "Vibrant nightlife"],
"Reykjavik, Iceland": ["Northern Lights", "Blue Lagoon", "Golden Circle tour"]
}
if not highlights and dest in destination_highlights:
highlights = destination_highlights[dest]
elif not highlights:
highlights = ["Popular tourist attractions", "Local experiences", "Natural beauty"]
# Create destination entry
destinations.append({
"name": dest,
"description": f"{dest} is a wonderful destination with something for everyone. "
f"It offers a perfect balance of attractions, experiences, and local culture.",
"url": f"https://example.com/{dest.split(',')[0].lower().replace(' ', '-')}",
"highlights": highlights,
"estimatedcost": self.estimatecost(dest, triprequirements)
})
return destinations
Creating the Itinerary Generator
Next, let's implement the tool for generating personalized itineraries.
Edit src/tools/itinerary_generator.py
:
class ItineraryGenerator:
def init(self):
self.activitydatabase = self.initializeactivitydatabase()
def generateitinerary(self, destination, triprequirements):
"""
Generate a day-by-day itinerary based on the selected destination
and trip requirements.
"""
# Extract relevant information
duration = int(triprequirements["duration"]) if triprequirements["duration"] else 7
interests = trip_requirements["interests"]
# Get destination-specific activities
availableactivities = self.getactivitiesfor_destination(destination, interests)
# Generate itinerary
itinerary = []
for day in range(1, duration + 1):
# Create a balanced day with morning, afternoon, and evening activities
day_plan = {
"day": day,
"morning": self.selectactivity(available_activities, "morning", interests),
"afternoon": self.selectactivity(available_activities, "afternoon", interests),
"evening": self.selectactivity(available_activities, "evening", interests),
"meals": self.suggestmeals(destination, day)
}
# Remove used activities to avoid duplication
self.removeusedactivities(availableactivities, [
dayplan["morning"], dayplan["afternoon"], day_plan["evening"]
])
itinerary.append(day_plan)
return itinerary
def initializeactivity_database(self):
"""
Initialize a database of activities for different destinations.
In a real implementation, this would connect to a travel API or database.
"""
return {
"Bali, Indonesia": [
{"name": "Ubud Monkey Forest", "type": "nature", "time": "morning", "description": "Sacred sanctuary with hundreds of monkeys"},
{"name": "Tegallalang Rice Terraces", "type": "sightseeing", "time": "morning", "description": "Stunning stepped rice paddies"},
{"name": "Uluwatu Temple", "type": "culture", "time": "afternoon", "description": "Ancient sea temple with spectacular cliff views"},
{"name": "Seminyak Beach", "type": "beach", "time": "afternoon", "description": "Popular beach with great sunsets"},
{"name": "Ubud Art Market", "type": "shopping", "time": "afternoon", "description": "Traditional market with local crafts"},
{"name": "Kecak Fire Dance", "type": "culture", "time": "evening", "description": "Traditional Balinese dance performance"},
{"name": "Jimbaran Bay Seafood Dinner", "type": "food", "time": "evening", "description": "Fresh seafood dinner on the beach"},
{"name": "Bali Swing", "type": "adventure", "time": "morning", "description": "Swing over the jungle canopy"},
{"name": "Tegenungan Waterfall", "type": "nature", "time": "morning", "description": "Beautiful waterfall with swimming area"},
{"name": "Mount Batur Sunrise Trek", "type": "adventure", "time": "morning", "description": "Hiking up an active volcano for sunrise views"},
{"name": "Sacred Monkey Forest Sanctuary", "type": "nature", "time": "afternoon", "description": "Nature reserve and Hindu temple complex"},
{"name": "Potato Head Beach Club", "type": "leisure", "time": "afternoon", "description": "Trendy beach club with infinity pool"},
{"name": "Tanah Lot Temple", "type": "culture", "time": "afternoon", "description": "Iconic temple perched on a rock formation"},
{"name": "Seminyak Nightlife", "type": "nightlife", "time": "evening", "description": "Bars and clubs in Bali's fashionable district"}
],
"Paris, France": [
{"name": "Eiffel Tower Visit", "type": "sightseeing", "time": "morning", "description": "Iconic iron tower with city views"},
{"name": "Louvre Museum", "type": "culture", "time": "morning", "description": "World's largest art museum and home to the Mona Lisa"},
{"name": "Notre-Dame Cathedral", "type": "culture", "time": "morning", "description": "Medieval Catholic cathedral with Gothic architecture"},
{"name": "Seine River Cruise", "type": "leisure", "time": "afternoon", "description": "Boat tour along the Seine River"},
{"name": "Montmartre Walking Tour", "type": "culture", "time": "afternoon", "description": "Bohemian neighborhood with Sacré-Cœur Basilica"},
{"name": "Champs-Élysées Shopping", "type": "shopping", "time": "afternoon", "description": "Luxury shopping on Paris's most famous avenue"},
{"name": "Moulin Rouge Show", "type": "nightlife", "time": "evening", "description": "Famous cabaret show"},
{"name": "Fine Dining Experience", "type": "food", "time": "evening", "description": "Dinner at a classic French restaurant"},
{"name": "Arc de Triomphe", "type": "sightseeing", "time": "afternoon", "description": "Historic monument at the center of Place Charles de Gaulle"},
{"name": "Luxembourg Gardens", "type": "leisure", "time": "afternoon", "description": "Beautiful park with fountains and statues"},
{"name": "Musée d'Orsay", "type": "culture", "time": "morning", "description": "Museum holding mainly French art"},
{"name": "Le Marais Exploration", "type": "culture", "time": "afternoon", "description": "Historic district with boutiques and cafes"},
{"name": "Paris Catacombs", "type": "culture", "time": "morning", "description": "Underground ossuaries holding the remains of more than six million people"},
{"name": "Opera Garnier", "type": "culture", "time": "evening", "description": "Opulent 19th-century opera house"}
],
"Tokyo, Japan": [
{"name": "Meiji Shrine", "type": "culture", "time": "morning", "description": "Shinto shrine dedicated to Emperor Meiji"},
{"name": "Tsukiji Outer Market", "type": "food", "time": "morning", "description": "Food market with fresh seafood and local specialties"},
{"name": "Shibuya Crossing", "type": "sightseeing", "time": "afternoon", "description": "Famous busy intersection known for its scramble crossing"},
{"name": "Harajuku Shopping", "type": "shopping", "time": "afternoon", "description": "Trendy district known for fashion and youth culture"},
{"name": "Robot Restaurant Show", "type": "nightlife", "time": "evening", "description": "Eccentric performance with robots and dancers"},
{"name": "Izakaya Hopping in Shinjuku", "type": "food", "time": "evening", "description": "Visit multiple Japanese pubs for food and drinks"},
{"name": "Tokyo Skytree", "type": "sightseeing", "time": "afternoon", "description": "Tallest tower in Japan with observation decks"},
{"name": "Sensō-ji Temple", "type": "culture", "time": "morning", "description": "Ancient Buddhist temple in Asakusa"},
{"name": "Akihabara Electric Town", "type": "shopping", "time": "afternoon", "description": "District famous for electronics, anime, and manga"},
{"name": "Imperial Palace Gardens", "type": "nature", "time": "morning", "description": "Historic park surrounding the Imperial Palace"},
{"name": "TeamLab Borderless Museum", "type": "culture", "time": "afternoon", "description": "Digital art museum with interactive installations"},
{"name": "Karaoke in Shinjuku", "type": "nightlife", "time": "evening", "description": "Japanese-style karaoke in private rooms"}
]
}
def getactivitiesfordestination(self, destination, interests):
"""
Get activities for a specific destination, filtered by user interests.
"""
# Extract destination name
destination_name = destination["name"] if isinstance(destination, dict) else destination
# Check if we have activities for this destination
if destinationname in self.activitydatabase:
activities = self.activitydatabase[destinationname]
else:
# For unknown destinations, generate generic activities
activities = self.generategeneric_activities()
# If we have specific interests, prioritize those activities
if interests:
# Map interests to activity types
interest_mapping = {
"beach": ["beach", "nature", "leisure"],
"history": ["culture", "sightseeing"],
"food": ["food"],
"shopping": ["shopping"],
"nightlife": ["nightlife"],
"culture": ["culture", "sightseeing"],
"adventure": ["adventure", "nature"],
"relaxation": ["leisure", "beach", "nature"],
"museums": ["culture"],
"photography": ["sightseeing", "nature"]
}
# Flatten the mapping for the user's interests
preferred_types = []
for interest in interests:
if interest in interest_mapping:
preferredtypes.extend(interestmapping[interest])
# If we have preferences, sort activities to prioritize those types
if preferred_types:
# Sort activities to put preferred types first
activities = sorted(activities, key=lambda x: x["type"] not in preferred_types)
return activities.copy() # Return a copy to avoid modifying the original
def selectactivity(self, activities, timeofday, interests):
"""
Select an appropriate activity for a specific time of day.
"""
# Filter activities for the given time of day
timeactivities = [a for a in activities if a["time"] == timeof_day]
# If no activities for this time, use any activities
if not time_activities:
time_activities = activities
# If we have activities, select the first one (they're already sorted by preference)
if time_activities:
return time_activities[0]
else:
# Create a generic activity if nothing is available
return {
"name": f"Explore Local Area",
"type": "leisure",
"time": timeofday,
"description": "Take time to explore the surrounding area and discover hidden gems"
}
def removeusedactivities(self, activities, usedactivities):
"""
Remove used activities from the available list to avoid duplication.
"""
for used in used_activities:
if used in activities:
activities.remove(used)
def suggestmeals(self, destination, day):
"""
Suggest meals based on the destination.
"""
destination_name = destination["name"] if isinstance(destination, dict) else destination
# Destination-specific meal suggestions
mealsbydestination = {
"Bali, Indonesia": [
{"breakfast": "Balinese Bubur Ayam (chicken rice porridge)", "lunch": "Nasi Campur (mixed rice)", "dinner": "Fresh Seafood at Jimbaran Bay"},
{"breakfast": "Pisang Goreng (fried bananas) with coffee", "lunch": "Babi Guling (suckling pig)", "dinner": "Bebek Betutu (slow-cooked duck)"},
{"breakfast": "Fresh tropical fruit platter", "lunch": "Mie Goreng (fried noodles)", "dinner": "Seafood BBQ on the beach"}
],
"Paris, France": [
{"breakfast": "Croissants and café au lait", "lunch": "Croque Monsieur at a sidewalk café", "dinner": "Steak frites at a traditional bistro"},
{"breakfast": "Pain au chocolat with espresso", "lunch": "French onion soup and a baguette", "dinner": "Duck confit with wine pairing"},
{"breakfast": "Fresh pastries from a local boulangerie", "lunch": "Salade Niçoise", "dinner": "Beef Bourguignon at a classic restaurant"}
],
"Tokyo, Japan": [
{"breakfast": "Traditional Japanese breakfast set", "lunch": "Ramen at a local shop", "dinner": "Sushi at a conveyor belt restaurant"},
{"breakfast": "Onigiri (rice balls) from a convenience store", "lunch": "Katsu curry set", "dinner": "Izakaya experience with yakitori and sake"},
{"breakfast": "Fluffy Japanese pancakes", "lunch": "Tonkatsu (breaded pork cutlet)", "dinner": "Shabu-shabu hot pot"}
]
}
# Get meals for the destination or use generic
if destinationname in mealsby_destination:
destinationmeals = mealsbydestination[destinationname]
# Select meals, cycling through available options
mealindex = (day - 1) % len(destinationmeals)
return destinationmeals[mealindex]
else:
# Generic meals
generic_meals = [
{"breakfast": "Breakfast at hotel", "lunch": "Lunch at local restaurant", "dinner": "Dinner exploring local cuisine"},
{"breakfast": "Continental breakfast", "lunch": "Street food sampling", "dinner": "Dinner with local specialties"},
{"breakfast": "Café breakfast", "lunch": "Light lunch at a bistro", "dinner": "Fine dining experience"}
]
mealindex = (day - 1) % len(genericmeals)
return genericmeals[mealindex]
def generategeneric_activities(self):
"""
Generate generic activities for destinations not in our database.
"""
return [
{"name": "City Walking Tour", "type": "sightseeing", "time": "morning", "description": "Explore the main sights of the city on foot"},
{"name": "Museum Visit", "type": "culture", "time": "morning", "description": "Visit the city's most important museum"},
{"name": "Historic District Exploration", "type": "culture", "time": "morning", "description": "Wander through the historic quarter"},
{"name": "Local Market", "type": "food", "time": "morning", "description": "Experience local produce and street food"},
{"name": "Landmark Visit", "type": "sightseeing", "time": "afternoon", "description": "Visit the city's most famous landmark"},
{"name": "Park Relaxation", "type": "leisure", "time": "afternoon", "description": "Relax in the city's central park"},
{"name": "Shopping District", "type": "shopping", "time": "afternoon", "description": "Shop for souvenirs and local goods"},
{"name": "Scenic Viewpoint", "type": "sightseeing", "time": "afternoon", "description": "Visit a viewpoint offering panoramic views"},
{"name": "Local Dining Experience", "type": "food", "time": "evening", "description": "Dinner featuring local specialties"},
{"name": "Night Tour", "type": "sightseeing", "time": "evening", "description": "See the city illuminated at night"},
{"name": "Cultural Performance", "type": "culture", "time": "evening", "description": "Enjoy a local music or dance performance"},
{"name": "Local Nightlife", "type": "nightlife", "time": "evening", "description": "Experience bars and venues popular with locals"}
]
Creating the Travel Finder Tool
Next, let's implement the tool for finding flights and accommodations.
Edit src/tools/travel_finder.py
:
import random
from datetime import datetime, timedelta
class TravelFinder:
def init(self):
pass
def findflights(self, departurelocation, destination, departuredate, returndate=None, travelers=1):
"""
Find flight options based on the given parameters.
In a real implementation, this would connect to a flight API.
"""
# Parse destination name if it's in a dictionary
if isinstance(destination, dict):
destination_name = destination["name"]
else:
destination_name = destination
# For demonstration, generate sample flight data
destinationcity = destinationname.split(',')[0].strip()
# Parse dates
try:
if isinstance(departure_date, str):
departuredate = datetime.strptime(departuredate, "%Y-%m-%d")
if returndate and isinstance(returndate, str):
returndate = datetime.strptime(returndate, "%Y-%m-%d")
elif not returndate and isinstance(departuredate, datetime):
# If no return date specified, assume 7 days later
returndate = departuredate + timedelta(days=7)
except ValueError:
# If date parsing fails, use placeholders
departure_date = datetime.now() + timedelta(days=30)
returndate = departuredate + timedelta(days=7)
# Generate sample flights
flights = []
# Major airlines
airlines = ["Delta Air Lines", "United Airlines", "American Airlines", "Lufthansa",
"Emirates", "Singapore Airlines", "Air France", "British Airways"]
# Generate 3-5 outbound flight options
num_options = random.randint(3, 5)
for i in range(num_options):
# Generate random flight details
airline = random.choice(airlines)
flight_number = f"{airline[:3].upper()}{random.randint(100, 999)}"
# Departure time (morning to evening)
departure_hour = random.randint(6, 21)
departuretime = departuredate.replace(hour=departure_hour, minute=random.choice([0, 15, 30, 45]))
# Flight duration (depends on destination - roughly based on distance)
duration_mapping = {
"Bali": {"hours": 20, "variance": 4}, # Long-haul
"Paris": {"hours": 8, "variance": 2}, # Medium-haul
"Tokyo": {"hours": 14, "variance": 3}, # Long-haul
"New York": {"hours": 6, "variance": 1}, # Medium-haul
"London": {"hours": 8, "variance": 2}, # Medium-haul
"Rome": {"hours": 9, "variance": 2}, # Medium-haul
"Cancun": {"hours": 4, "variance": 1}, # Short/Medium-haul
}
# Default duration (medium-haul)
duration_hours = 8
variance = 2
# Try to find a matching destination for better duration estimate
for key, value in duration_mapping.items():
if key in destination_city:
duration_hours = value["hours"]
variance = value["variance"]
break
# Add some randomness to duration
actualduration = durationhours + random.randint(-variance, variance)
actualduration = max(1, actualduration) # Ensure at least 1 hour
# Calculate arrival time
arrivaltime = departuretime + timedelta(hours=actual_duration)
# Determine if it's a direct flight or has connections
has_connection = random.choice([True, False, False]) # 1/3 chance of connection
connection_city = None
if has_connection:
# Major hub airports
hubs = ["Atlanta", "Chicago", "London", "Frankfurt", "Dubai", "Singapore", "Tokyo"]
connection_city = random.choice(hubs)
# Price calculation - base fare + distance factor + random variance
base_fare = 300
distancefactor = durationhours * 50 # $50 per flight hour
random_variance = random.randint(-100, 200)
# First/business class prices are higher
seat_class = random.choice(["Economy", "Economy", "Economy", "Premium Economy", "Business"])
class_multiplier = 1.0
if seat_class == "Premium Economy":
class_multiplier = 1.7
elif seat_class == "Business":
class_multiplier = 3.5
price = (basefare + distancefactor + randomvariance) * classmultiplier
# Round to nearest 10
price = round(price / 10) * 10
# Multiple by number of travelers
total_price = price * travelers
# Create flight object
flight = {
"airline": airline,
"flightnumber": flightnumber,
"departureairport": f"{departurelocation} Airport",
"departuretime": departuretime.strftime("%Y-%m-%d %H:%M"),
"arrivalairport": f"{destinationcity} Airport",
"arrivaltime": arrivaltime.strftime("%Y-%m-%d %H:%M"),
"durationhours": actualduration,
"seatclass": seatclass,
"hasconnection": hasconnection,
"connectioncity": connectioncity,
"priceperperson": price,
"totalprice": totalprice,
"bookingurl": f"https://example.com/book-flight/{flightnumber}"
}
flights.append(flight)
# Sort by price (cheapest first)
flights.sort(key=lambda x: x["total_price"])
return flights
def findaccommodations(self, destination, checkindate, checkout_date, travelers=1, preference=None):
"""
Find accommodation options based on the given parameters.
In a real implementation, this would connect to a hotel API.
"""
# Parse destination name if it's in a dictionary
if isinstance(destination, dict):
destination_name = destination["name"]
else:
destination_name = destination
# Extract city
destinationcity = destinationname.split(',')[0].strip()
# Parse dates
try:
if isinstance(checkindate, str):
checkindate = datetime.strptime(checkindate, "%Y-%m-%d")
if isinstance(checkoutdate, str):
checkoutdate = datetime.strptime(checkoutdate, "%Y-%m-%d")
except ValueError:
# If date parsing fails, use placeholders
checkindate = datetime.now() + timedelta(days=30)
checkoutdate = checkindate + timedelta(days=7)
# Calculate number of nights
if isinstance(checkindate, datetime) and isinstance(checkoutdate, datetime):
nights = (checkoutdate - checkindate).days
else:
nights = 7 # Default
# Validate preference
valid_preferences = ["budget", "mid-range", "luxury"]
if not preference or preference.lower() not in valid_preferences:
preference = "mid-range" # Default
# Generate sample accommodations
accommodations = []
# Accommodation types based on preference
accommodation_types = {
"budget": ["Hostel", "Budget Hotel", "Guest House", "Budget Inn"],
"mid-range": ["Hotel", "Boutique Hotel", "Resort", "Apartment"],
"luxury": ["Luxury Hotel", "Premium Resort", "Villa", "Spa Resort"]
}
# Star ratings based on preference
star_ratings = {
"budget": [2, 3],
"mid-range": [3, 4],
"luxury": [4, 5]
}
# Price ranges per night based on destination and preference
base_prices = {
"Bali": {"budget": (30, 50), "mid-range": (80, 150), "luxury": (200, 500)},
"Paris": {"budget": (70, 120), "mid-range": (150, 250), "luxury": (300, 800)},
"Tokyo": {"budget": (60, 100), "mid-range": (120, 200), "luxury": (250, 600)},
"New York": {"budget": (100, 150), "mid-range": (200, 350), "luxury": (400, 1000)},
"London": {"budget": (80, 130), "mid-range": (180, 300), "luxury": (350, 900)},
"Rome": {"budget": (60, 110), "mid-range": (130, 240), "luxury": (280, 700)},
"Cancun": {"budget": (50, 90), "mid-range": (120, 250), "luxury": (300, 800)},
}
# Default price range if destination not found
default_price = {"budget": (50, 100), "mid-range": (120, 250), "luxury": (300, 800)}
# Find price range for destination
pricerange = defaultprice[preference.lower()]
for city, prices in base_prices.items():
if city in destination_city:
price_range = prices[preference.lower()]
break
# Generate 3-5 accommodation options
num_options = random.randint(3, 5)
for i in range(num_options):
# Generate random accommodation details
acctype = random.choice(accommodationtypes[preference.lower()])
starrating = random.choice(starratings[preference.lower()])
# Generate hotel name
hotel_prefixes = ["Grand", "Royal", "Golden", "Pacific", "Central", "Harbor", "City", "Plaza"]
hotel_suffixes = ["Hotel", "Resort", "Suites", "Inn", "Lodge", "Place", "View"]
if acc_type in ["Hostel", "Guest House", "Villa", "Apartment"]:
hotelname = f"{destinationcity} {acc_type}"
else:
prefix = random.choice(hotel_prefixes)
suffix = random.choice(hotel_suffixes)
hotelname = f"{prefix} {destinationcity} {suffix}"
# Price calculation
baseprice = random.randint(pricerange[0], price_range[1])
# Add some randomness
pricepernight = base_price + random.randint(-20, 30)
pricepernight = max(20, pricepernight) # Ensure at least $20
# Total price for stay
totalprice = priceper_night * nights
# Amenities based on preference
basic_amenities = ["WiFi", "Air Conditioning"]
mid_amenities = ["Swimming Pool", "Restaurant", "Fitness Center"]
luxury_amenities = ["Spa", "Room Service", "Ocean View", "Private Balcony"]
amenities = basic_amenities.copy()
if preference.lower() in ["mid-range", "luxury"]:
amenities.extend(random.sample(midamenities, k=random.randint(1, len(midamenities))))
if preference.lower() == "luxury":
amenities.extend(random.sample(luxuryamenities, k=random.randint(1, len(luxuryamenities))))
# Create accommodation object
accommodation = {
"name": hotel_name,
"type": acc_type,
"starrating": starrating,
"pricepernight": pricepernight,
"totalprice": totalprice,
"location": f"{random.choice(['Downtown', 'Central', 'Beachfront', 'Historic District'])} {destination_city}",
"amenities": amenities,
"available_rooms": random.randint(1, 10),
"bookingurl": f"https://example.com/book-hotel/{hotelname.lower().replace(' ', '-')}"
}
accommodations.append(accommodation)
# Sort by price (cheapest first)
accommodations.sort(key=lambda x: x["total_price"])
return accommodations
Creating the Core Agent
Now let's build the main agent class that will integrate all of these components:
Edit src/agent.py
:
import os
import json
import openai
from dotenv import load_dotenv
from datetime import datetime, timedelta
from .memory import Memory
from .tools.destination_research import DestinationResearch
from .tools.itinerary_generator import ItineraryGenerator
from .tools.travel_finder import TravelFinder
# Load environment variables
load_dotenv()
openai.apikey = os.getenv("OPENAIAPI_KEY")
class TravelAgent:
def init(self):
self.memory = Memory()
self.destination_research = DestinationResearch()
self.itinerary_generator = ItineraryGenerator()
self.travel_finder = TravelFinder()
# Current state of the conversation
self.state = "greeting"
# Initialize system instructions
self.initializesystem_instructions()
def initializesystem_instructions(self):
"""Initialize the system instructions for the agent."""
system_instructions = """You are an expert travel agent AI. Your job is to help users plan their perfect trip by guiding them through a structured process:
1. Gather trip requirements (budget, dates, travelers, interests, etc.)
2. Research and suggest destinations that match their needs
3. Create a detailed itinerary once a destination is chosen
4. Find travel options (flights and accommodations)
Maintain a conversational, helpful tone throughout. Ask for missing information when needed, but don't overwhelm the user with too many questions at once. Focus on understanding their travel goals and preferences.
The conversation will proceed through several states:
- greeting: Initial welcome and introduction
- gathering_requirements: Collecting necessary trip information
- suggesting_destinations: Presenting destination options based on requirements
- creating_itinerary: Generating a detailed day-by-day plan for the chosen destination
- findingtraveloptions: Searching for flights and accommodations
- finalizing: Wrapping up the conversation and providing next steps
Adapt to the user's inputs and be flexible when they want to change direction or revisit previous steps.
"""
self.memory.addmessage("system", systeminstructions)
def processmessage(self, usermessage):
"""
Process a user message and advance the conversation.
Returns the agent's response.
"""
# Add user message to memory
self.memory.addmessage("user", usermessage)
# Process user message based on current state
if self.state == "greeting":
response = self.handlegreeting(user_message)
elif self.state == "gathering_requirements":
response = self.handlegatheringrequirements(usermessage)
elif self.state == "suggesting_destinations":
response = self.handlesuggestingdestinations(usermessage)
elif self.state == "creating_itinerary":
response = self.handlecreatingitinerary(usermessage)
elif self.state == "findingtraveloptions":
response = self.handlefindingtraveloptions(user_message)
elif self.state == "finalizing":
response = self.handlefinalizing(user_message)
else:
# Default fallback
response = self.getllmresponse(usermessage)
# Add agent response to memory
self.memory.add_message("assistant", response)
return response
def handlegreeting(self, user_message):
"""
Handle the initial greeting and transition to gathering requirements.
"""
# Transition to gathering requirements
self.state = "gathering_requirements"
# Generate response with LLM
prompt = """The user has just begun a conversation. Warmly welcome them and begin the trip planning process.
Introduce yourself as a travel planning assistant, explain that you'll help them plan the perfect trip,
and start by asking for key information like their budget, travel dates, number of travelers,
and interests. Keep the message conversational and engaging."""
response = self.getllm_response(prompt)
return response
def handlegatheringrequirements(self, usermessage):
"""
Handle gathering trip requirements from the user.
Parse requirements from the message and decide when to transition to suggesting destinations.
"""
# Extract trip requirements from user message
self.extracttriprequirements(usermessage)
# Check if we have enough requirements to suggest destinations
if self.memory.isrequirementscomplete():
# Transition to suggesting destinations
self.state = "suggesting_destinations"
# Research destinations based on requirements
destinations = self.destinationresearch.searchdestinations(self.memory.gettriprequirements())
# Store destinations in memory
self.memory.storedestinationoptions(destinations)
# Generate response with destinations
prompt = """Based on the user's requirements, you've found some destination options to present.
Create a helpful, informative response that:
1. Briefly summarizes their requirements
2. Presents 3 destination options with key highlights
3. For each option, mention estimated costs, key attractions, and why it might be a good fit for their interests
4. Ask them which destination they prefer or if they'd like more options
Make the response conversational and enthusiastic. Present each destination clearly so they can make an informed choice.
"""
# Add context about destinations
prompt += "\n\nDestinations to present:\n"
for i, dest in enumerate(destinations[:3], 1):
prompt += f"\nOption {i}: {dest['name']}\n"
prompt += f"Description: {dest['description']}\n"
prompt += f"Highlights: {', '.join(dest['highlights'][:3])}\n"
prompt += f"Estimated cost: ${dest['estimated_cost']} for their trip\n"
response = self.getllm_response(prompt)
else:
# Continue gathering requirements
# Check what requirements we're missing
requirements = self.memory.gettriprequirements()
missing_requirements = []
essential_requirements = {
"budget": "their budget",
"duration": "how long they plan to travel",
"travelers": "how many people are traveling",
"departure_date": "when they want to travel",
"departure_location": "where they'll be traveling from"
}
for key, description in essential_requirements.items():
if not requirements[key]:
missing_requirements.append(description)
if not requirements["interests"]:
missing_requirements.append("their interests or what they enjoy while traveling")
# Generate prompt based on what's missing
if missing_requirements:
prompt = f"""The user is providing information about their trip, but you still need to gather more details.
You need to learn about: {', '.join(missing_requirements)}.
What you already know:
"""
# Add what we already know
for key, value in requirements.items():
if value:
if key == "interests":
prompt += f"\n- Interests: {', '.join(value)}"
else:
prompt += f"\n- {key.replace('_', ' ').title()}: {value}"
prompt += """
Create a friendly response that:
1. Acknowledges what they've told you so far
2. Asks for the missing information in a conversational way
3. Doesn't overwhelm them with too many questions at once (focus on 2-3 most important missing items)
"""
else:
# If we have basic info but want to get more preferences
prompt = """The user has provided the essential trip information, but you could gather more preferences
to make better recommendations. Ask about their accommodation preferences (luxury, mid-range, budget),
what climate they prefer, or any specific activities they're interested in.
Create a response that:
1. Thanks them for the information provided
2. Asks 1-2 additional preference questions
3. Lets them know you're almost ready to suggest destinations
"""
response = self.getllm_response(prompt)
return response
def handlesuggestingdestinations(self, usermessage):
"""
Handle the destination suggestion phase.
Process user's choice and transition to creating itinerary when appropriate.
"""
# Check if user has selected a destination
destinationoptions = self.memory.destinationoptions
# Look for destination selection in user message
selected_destination = None
# Check if user explicitly mentions a destination
for dest in destination_options:
dest_name = dest["name"].lower()
if destname.split(",")[0].lower() in usermessage.lower():
selected_destination = dest
break
if "option 1" in usermessage.lower() or "first option" in usermessage.lower() and destination_options:
selecteddestination = destinationoptions[0]
elif "option 2" in usermessage.lower() or "second option" in usermessage.lower() and len(destination_options) > 1:
selecteddestination = destinationoptions[1]
elif "option 3" in usermessage.lower() or "third option" in usermessage.lower() and len(destination_options) > 2:
selecteddestination = destinationoptions[2]
# If user wants more options, research more destinations
if "more options" in usermessage.lower() or "other options" in usermessage.lower():
# Research more destinations
newdestinations = self.destinationresearch.searchdestinations(self.memory.gettrip_requirements())
# Filter out destinations already shown
existingnames = [d["name"] for d in destinationoptions]
filterednewdestinations = [d for d in newdestinations if d["name"] not in existingnames]
# Add new options to memory
self.memory.destinationoptions.extend(filterednew_destinations[:3])
# Generate response with new destinations
prompt = """The user has asked for more destination options. Present 2-3 new options that weren't
previously mentioned. Format these clearly with key highlights, estimated costs, and why they might
be a good fit for the user's interests. Ask them which destination they prefer."""
# Add context about new destinations
prompt += "\n\nNew destinations to present:\n"
for i, dest in enumerate(filterednewdestinations[:3], len(destination_options) + 1):
prompt += f"\nOption {i}: {dest['name']}\n"
prompt += f"Description: {dest['description']}\n"
prompt += f"Highlights: {', '.join(dest['highlights'][:3])}\n"
prompt += f"Estimated cost: ${dest['estimated_cost']} for their trip\n"
response = self.getllm_response(prompt)
# If the user has selected a destination, move to creating itinerary
elif selected_destination:
# Store selected destination
self.memory.selectdestination(selecteddestination)
# Transition to creating itinerary
self.state = "creating_itinerary"
# Generate itinerary for selected destination
itinerary = self.itinerarygenerator.generateitinerary(
selected_destination,
self.memory.gettriprequirements()
)
# Store itinerary in memory
self.memory.store_itinerary(itinerary)
# Generate response with itinerary
prompt = f"""The user has selected {selected_destination['name']} as their destination.
You've created a detailed itinerary for their trip. Present this itinerary in an engaging,
well-formatted way. Include:
1. An enthusiastic introduction to their chosen destination
2. The day-by-day itinerary with morning, afternoon, and evening activities
3. Meal suggestions for each day
4. Ask if they'd like to make any changes to the itinerary or if they're ready to look at flights and accommodations
Make the response conversational and excited about their trip.
"""
# Add context about the itinerary
prompt += "\n\nItinerary details:\n"
for day in itinerary:
prompt += f"\nDay {day['day']}:\n"
prompt += f"Morning: {day['morning']['name']} - {day['morning']['description']}\n"
prompt += f"Afternoon: {day['afternoon']['name']} - {day['afternoon']['description']}\n"
prompt += f"Evening: {day['evening']['name']} - {day['evening']['description']}\n"
prompt += f"Meals: Breakfast - {day['meals']['breakfast']}, Lunch - {day['meals']['lunch']}, Dinner - {day['meals']['dinner']}\n"
response = self.getllm_response(prompt)
# Otherwise, the user might need more information or clarity
else:
prompt = """The user hasn't clearly selected a destination yet. They might be asking questions
or need more information. Provide helpful guidance to assist their decision, addressing any
specific questions they've asked. If appropriate, remind them of the destination options and
gently ask which one they prefer."""
response = self.getllm_response(prompt)
return response
def handlecreatingitinerary(self, usermessage):
"""
Handle the itinerary creation phase.
Process feedback and move to finding travel options when appropriate.
"""
# Check if user is ready to move to travel options
ready_signals = [
"looks good", "sounds good", "perfect", "great itinerary",
"find flights", "flight options", "accommodations", "book",
"travel options", "let's proceed", "next step"
]
isready = any(signal in usermessage.lower() for signal in ready_signals)
if is_ready:
# Transition to finding travel options
self.state = "findingtraveloptions"
# Get selected destination and trip requirements
destination = self.memory.selected_destination
requirements = self.memory.gettriprequirements()
# Parse dates
departuredatestr = requirements["departure_date"]
# Convert string date to datetime if needed
if isinstance(departuredatestr, str):
try:
departuredate = datetime.strptime(departuredate_str, "%Y-%m-%d")
except ValueError:
# Try different format
try:
departuredate = datetime.strptime(departuredate_str, "%m/%d/%Y")
except ValueError:
# Default to 30 days from now
departure_date = datetime.now() + timedelta(days=30)
else:
# Default if no valid date
departure_date = datetime.now() + timedelta(days=30)
# Calculate return date based on duration
duration = int(requirements["duration"]) if requirements["duration"] else 7
returndate = departuredate + timedelta(days=duration)
# Search for flights
flights = self.travelfinder.findflights(
requirements["departure_location"],
destination["name"],
departure_date,
return_date,
int(requirements["travelers"]) if requirements["travelers"] else 1
)
# Search for accommodations
accommodations = self.travelfinder.findaccommodations(
destination["name"],
departure_date,
return_date,
int(requirements["travelers"]) if requirements["travelers"] else 1,
requirements["accommodation_preference"]
)
# Store results in memory
self.memory.storeflightoptions(flights)
self.memory.storeaccommodationoptions(accommodations)
# Generate response with travel options
prompt = f"""The user is ready to see travel options for their trip to {destination['name']}.
You've found several flight and accommodation options for them.
Present these options in a clear, organized way:
1. Show 2-3 flight options with airline, times, price, and any connections
2. Show 2-3 accommodation options with name, type, amenities, and price
3. Provide the total estimated price of the trip (flight + accommodation)
4. Ask if they would like to proceed with booking or if they have any questions
Make the response helpful and informative.
"""
# Add context about flight options
prompt += "\n\nFlight options:\n"
for i, flight in enumerate(flights[:3], 1):
prompt += f"\nOption {i}:\n"
prompt += f"Airline: {flight['airline']}\n"
prompt += f"Departure: {flight['departuretime']} from {flight['departureairport']}\n"
prompt += f"Arrival: {flight['arrivaltime']} at {flight['arrivalairport']}\n"
prompt += f"Duration: {flight['duration_hours']} hours\n"
if flight['has_connection']:
prompt += f"Connection: Yes, via {flight['connection_city']}\n"
else:
prompt += "Connection: No, direct flight\n"
prompt += f"Class: {flight['seat_class']}\n"
prompt += f"Price: ${flight['totalprice']} (${flight['priceper_person']} per person)\n"
prompt += f"Booking URL: {flight['booking_url']}\n"
# Add context about accommodation options
prompt += "\n\nAccommodation options:\n"
for i, accommodation in enumerate(accommodations[:3], 1):
prompt += f"\nOption {i}:\n"
prompt += f"Name: {accommodation['name']}\n"
prompt += f"Type: {accommodation['type']} ({accommodation['star_rating']}-star)\n"
prompt += f"Location: {accommodation['location']}\n"
prompt += f"Amenities: {', '.join(accommodation['amenities'])}\n"
prompt += f"Price: ${accommodation['totalprice']} (${accommodation['priceper_night']} per night)\n"
prompt += f"Booking URL: {accommodation['booking_url']}\n"
response = self.getllm_response(prompt)
# Transition to finalizing
self.state = "finalizing"
# User might be requesting modifications to the itinerary
elif any(word in user_message.lower() for word in ["change", "modify", "update", "swap", "replace", "different"]):
# Generate a response addressing their change request
prompt = f"""The user wants to make changes to the itinerary. Their message: "{user_message}"
Create a helpful response that:
1. Acknowledges their request for changes
2. Suggests modifications to the itinerary based on their feedback
3. Explains that once they're happy with the itinerary, you can look for flights and accommodations
Make the response empathetic and flexible.
"""
response = self.getllm_response(prompt)
# Otherwise, they may have questions or need clarification
else:
prompt = f"""The user is reviewing the itinerary and has sent: "{user_message}"
They haven't explicitly requested changes or said they're ready to proceed. Create a response that:
1. Addresses any questions or comments in their message
2. Provides any clarification they might need about the itinerary
3. Gently asks if they're happy with the itinerary or would like any changes
4. Mentions that once they're satisfied, you can look for flights and accommodations
Make the response helpful and conversational.
"""
response = self.getllm_response(prompt)
return response
def handlefindingtraveloptions(self, user_message):
"""
Handle the phase of finding travel options.
Process feedback and transition to finalizing when appropriate.
"""
# Check if the user is selecting specific travel options
flight_preference = None
accommodation_preference = None
# Look for flight selection
if "flight 1" in usermessage.lower() or "first flight" in usermessage.lower():
flight_preference = 0
elif "flight 2" in usermessage.lower() or "second flight" in usermessage.lower():
flight_preference = 1
elif "flight 3" in usermessage.lower() or "third flight" in usermessage.lower():
flight_preference = 2
# Look for accommodation selection
if "hotel 1" in usermessage.lower() or "first hotel" in usermessage.lower() or "accommodation 1" in user_message.lower():
accommodation_preference = 0
elif "hotel 2" in usermessage.lower() or "second hotel" in usermessage.lower() or "accommodation 2" in user_message.lower():
accommodation_preference = 1
elif "hotel 3" in usermessage.lower() or "third hotel" in usermessage.lower() or "accommodation 3" in user_message.lower():
accommodation_preference = 2
# Generate response based on selections
if flightpreference is not None or accommodationpreference is not None:
# Access the options from memory
flights = self.memory.flight_options
accommodations = self.memory.accommodation_options
# Generate prompt with selections
prompt = f"""The user has indicated some preferences about flights or accommodations.
Create a response that:
"""
if flightpreference is not None and flightpreference < len(flights):
selectedflight = flights[flightpreference]
prompt += f"\n1. Confirms their selection of {selectedflight['airline']} flight for ${selectedflight['total_price']}"
else:
prompt += "\n1. Asks which flight option they prefer (if they haven't specified)"
if accommodationpreference is not None and accommodationpreference < len(accommodations):
selectedaccommodation = accommodations[accommodationpreference]
prompt += f"\n2. Confirms their selection of {selectedaccommodation['name']} for ${selectedaccommodation['total_price']}"
else:
prompt += "\n2. Asks which accommodation option they prefer (if they haven't specified)"
# Add booking information and next steps
prompt += """
3. Provides booking links for their selected options
4. Summarizes the total trip cost (flight + accommodation)
5. Provides next steps (e.g., booking directly through the links)
Make the response excited about their trip and helpful with the booking process.
"""
response = self.getllm_response(prompt)
# Transition to finalizing
self.state = "finalizing"
# User might be asking for more options
elif "more options" in usermessage.lower() or "other options" in usermessage.lower():
# Check if they're asking about flights or accommodations
if "flight" in user_message.lower():
# Generate more flight options
requirements = self.memory.gettriprequirements()
destination = self.memory.selected_destination
departuredatestr = requirements["departure_date"]
try:
departuredate = datetime.strptime(departuredate_str, "%Y-%m-%d")
except ValueError:
# Default to 30 days from now
departure_date = datetime.now() + timedelta(days=30)
duration = int(requirements["duration"]) if requirements["duration"] else 7
returndate = departuredate + timedelta(days=duration)
# Get more flights with different search parameters
newflights = self.travelfinder.find_flights(
requirements["departure_location"],
destination["name"],
departure_date,
return_date,
int(requirements["travelers"]) if requirements["travelers"] else 1
)
# Update memory with new flights
self.memory.storeflightoptions(new_flights)
# Generate response with new flight options
prompt = """The user has asked for more flight options. Present 2-3 new options with
airline, times, price, and any connections. Ask which option they prefer."""
# Add context about new flights
prompt += "\n\nNew flight options:\n"
for i, flight in enumerate(new_flights[:3], 1):
prompt += f"\nOption {i}:\n"
prompt += f"Airline: {flight['airline']}\n"
prompt += f"Departure: {flight['departuretime']} from {flight['departureairport']}\n"
prompt += f"Arrival: {flight['arrivaltime']} at {flight['arrivalairport']}\n"
prompt += f"Duration: {flight['duration_hours']} hours\n"
if flight['has_connection']:
prompt += f"Connection: Yes, via {flight['connection_city']}\n"
else:
prompt += "Connection: No, direct flight\n"
prompt += f"Class: {flight['seat_class']}\n"
prompt += f"Price: ${flight['totalprice']} (${flight['priceper_person']} per person)\n"
response = self.getllm_response(prompt)
elif "hotel" in usermessage.lower() or "accommodation" in usermessage.lower():
# Generate more accommodation options
requirements = self.memory.gettriprequirements()
destination = self.memory.selected_destination
departuredatestr = requirements["departure_date"]
try:
departuredate = datetime.strptime(departuredate_str, "%Y-%m-%d")
except ValueError:
# Default to 30 days from now
departure_date = datetime.now() + timedelta(days=30)
duration = int(requirements["duration"]) if requirements["duration"] else 7
returndate = departuredate + timedelta(days=duration)
# Get more accommodations
newaccommodations = self.travelfinder.find_accommodations(
destination["name"],
departure_date,
return_date,
int(requirements["travelers"]) if requirements["travelers"] else 1,
requirements["accommodation_preference"]
)
# Update memory with new accommodations
self.memory.storeaccommodationoptions(new_accommodations)
# Generate response with new accommodation options
prompt = """The user has asked for more accommodation options. Present 2-3 new options with
name, type, amenities, location, and price. Ask which option they prefer."""
# Add context about new accommodations
prompt += "\n\nNew accommodation options:\n"
for i, accommodation in enumerate(new_accommodations[:3], 1):
prompt += f"\nOption {i}:\n"
prompt += f"Name: {accommodation['name']}\n"
prompt += f"Type: {accommodation['type']} ({accommodation['star_rating']}-star)\n"
prompt += f"Location: {accommodation['location']}\n"
prompt += f"Amenities: {', '.join(accommodation['amenities'])}\n"
prompt += f"Price: ${accommodation['totalprice']} (${accommodation['priceper_night']} per night)\n"
response = self.getllm_response(prompt)
else:
# Default to both if not specified
prompt = """The user has asked for more options but didn't specify if they mean flights,
accommodations, or both. Create a response that asks for clarification and reminds them
of the current options."""
response = self.getllm_response(prompt)
# Otherwise, they may have questions or need clarification
else:
prompt = f"""The user is reviewing the travel options and has sent: "{user_message}"
They haven't explicitly selected options or asked for more. Create a response that:
1. Addresses any questions or comments in their message
2. Provides any clarification they might need about the travel options
3. Gently asks which flight and accommodation they prefer
Make the response helpful and conversational.
"""
response = self.getllm_response(prompt)
return response
def handlefinalizing(self, user_message):
"""
Handle the finalizing phase.
Provide booking information and wrap up the conversation.
"""
# Generate a final wrap-up response
prompt = f"""The user has completed their travel planning process, and you're helping wrap up.
Their latest message: "{user_message}"
Create a helpful, friendly final response that:
1. Thanks them for using your travel planning service
2. Summarizes their trip details (destination, dates, accommodation, flights)
3. Provides any final information they need based on their message
4. Wishes them a wonderful trip
5. Invites them to return if they need any more travel assistance
Make the response warm and personalized."""
response = self.getllm_response(prompt)
return response
def extracttriprequirements(self, usermessage):
"""
Extract trip requirements from the user message using the LLM.
"""
# Prepare the prompt for extracting requirements
existingrequirements = self.memory.gettrip_requirements()
prompt = f"""Extract travel requirements from the user's message: "{user_message}"
Current known requirements:
- Budget: {existing_requirements['budget'] or 'Unknown'}
- Duration: {existing_requirements['duration'] or 'Unknown'}
- Travelers: {existing_requirements['travelers'] or 'Unknown'}
- Departure Date: {existingrequirements['departuredate'] or 'Unknown'}
- Departure Location: {existingrequirements['departurelocation'] or 'Unknown'}
- Interests: {', '.join(existingrequirements['interests']) if existingrequirements['interests'] else 'Unknown'}
- Climate Preference: {existingrequirements['climatepreference'] or 'Unknown'}
- Accommodation Preference: {existingrequirements['accommodationpreference'] or 'Unknown'}
Extract any new or updated requirements from the user's message. Format the response as a JSON object with only the fields that are mentioned in the message. If a requirement isn't mentioned, don't include it in the JSON.
For example, if they mention budget and interests only, return:
{{"budget": "3000", "interests": ["beach", "food"]}}
"""
# Get requirements from LLM
requirementsjson = self.getllmresponse(prompt, responseformat={"type": "jsonobject"})
try:
# Parse the JSON response
requirementsdata = json.loads(requirementsjson)
# Update requirements in memory
for key, value in requirements_data.items():
if key == "interests":
existing_requirements["interests"] = value
else:
self.memory.updatetriprequirement(key, value)
except:
# If parsing fails, log the error (in a real system) but continue
pass
def getllmresponse(self, prompt=None, responseformat=None):
"""
Get a response from the language model.
If prompt is provided, use it to generate a response.
Otherwise, use the conversation history.
"""
messages = self.memory.get_messages()
# If a specific prompt is provided, add it as a system message
if prompt:
if messages and messages[0]["role"] == "system":
# Replace the original system message with our new one for this request
request_messages = [{"role": "system", "content": prompt}] + messages[1:]
else:
# Add a new system message
request_messages = [{"role": "system", "content": prompt}] + messages
else:
# Use the existing messages
request_messages = messages
# Make the API call to the language model
try:
if response_format:
response = openai.chat.completions.create(
model="gpt-4", # Use an appropriate model
messages=request_messages,
responseformat=responseformat
)
else:
response = openai.chat.completions.create(
model="gpt-4", # Use an appropriate model
messages=request_messages
)
return response.choices[0].message.content
except Exception as e:
# Handle API errors (in a real system, implement proper error handling)
return f"I'm sorry, I encountered an error. Please try again in a moment."
Creating the Main Script
Finally, let's create the main script that will run our agent:
Edit main.py
:
import os
from dotenv import load_dotenv
from src.agent import TravelAgent
# Load environment variables
load_dotenv()
def main():
"""
Run the Travel Agent AI in an interactive console.
"""
print("\n=== Travel Agent AI ===")
print("Let me help you plan your perfect trip!\n")
# Initialize agent
agent = TravelAgent()
# Start conversation with a greeting
user_input = input("👤 You: ")
while True:
# Process the message
response = agent.processmessage(userinput)
# Print the response
print(f"\n🤖 Travel Agent: {response}\n")
# Get next user input
user_input = input("👤 You: ")
# Exit if user types 'exit' or 'quit'
if user_input.lower() in ['exit', 'quit', 'bye']:
print("\n🤖 Travel Agent: Thank you for planning your trip with me! Have a wonderful journey! Goodbye!\n")
break
if name == "main":
main()
Testing & Refinement
Now let's test our Travel Agent AI to ensure it works as expected.
Running the Agent
- Make sure your .env file is properly set up with the necessary API keys:
OPENAIAPIKEY=youropenaiapikeyhere
SERPAPIKEY=yourserpapikeyhere
Run the main script:
python main.py
Interact with the agent by providing your travel preferences, selecting destinations, and going through the trip planning process.
Testing Scenarios
Try testing your Travel Agent AI with these scenarios:
- Beach vacation: Ask for a tropical destination with beaches and relaxation
- City adventure: Request a cultural trip to explore museums and historical sites
- Budget constraints: Specify a limited budget and see how the agent adapts
- Special requirements: Mention traveling with children or needing accessibility options
Debugging Common Issues
If you encounter problems:
- API Key Errors: Ensure your OpenAI and SerpAPI keys are correctly set in the .env file
- Module Import Errors: Check your project structure matches the described setup
- JSON Parsing Errors: These can occur if the LLM returns malformed JSON. Add better error handling for robust operation
- Rate Limiting: If you hit API rate limits, add delays between requests or implement backoff strategies
Extensions & Customizations
Once you have a working Travel Agent AI, consider these enhancements:
Add More Data Sources
- Integrate with real travel APIs like:
- Amadeus for flight data
- Booking.com or Expedia for accommodation information
- TripAdvisor for attraction recommendations
- Replace the sample data with real-time information for more accurate planning.
Improve the User Interface
- Create a web-based interface using Flask or Streamlit:
# Example Streamlit app (save as app.py)
import streamlit as st
from src.agent import TravelAgent
st.title("Travel Agent AI")
# Initialize the agent
if "agent" not in st.session_state:
st.session_state.agent = TravelAgent()
st.session_state.messages = []
# Display chat history
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
# Input for new message
if prompt := st.chat_input("What are your travel plans?"):
# Add user message to chat history
st.session_state.messages.append({"role": "user", "content": prompt})
# Display user message
with st.chat_message("user"):
st.markdown(prompt)
# Get agent response
response = st.sessionstate.agent.processmessage(prompt)
# Add assistant response to chat history
st.session_state.messages.append({"role": "assistant", "content": response})
# Display assistant response
with st.chat_message("assistant"):
st.markdown(response)
Add visualization components for itineraries and destination information.
Add More Specialized Features
- Visa requirement checking based on traveler nationality and destination
- Weather forecasts for the travel dates
- Language translation phrases for the destination
- Budget tracking and expense estimation
- Travel insurance recommendations
Key Learnings & Takeaways
By building this Travel Agent AI, you've learned how to:
- Design an AI Agent Architecture:
- Create a memory system for tracking conversation and storing trip information
- Implement a state machine to manage different stages of the conversation
- Integrate tools for specialized tasks (research, itinerary generation, travel booking)
- Implement Natural Language Understanding:
- Use a language model to interpret user requests
- Extract structured information from natural language
- Generate conversational responses based on agent state
- Develop Tool-Using AI Capabilities:
- Design interfaces between the agent and specialized tools
- Pass information between components effectively
- Make decisions about which tools to use when
- Apply Best Practices in Agent Design:
- Manage conversation state effectively
- Gracefully handle edge cases and errors
- Create a natural, helpful user experience
These skills can be applied to a wide range of AI agent applications beyond travel planning. The same architectural patterns work for customer service agents, educational assistants, productivity tools, and many other applications.
Conclusion
In this capstone project, you've built a complete AI Travel Agent that demonstrates key capabilities of modern AI agents:
- Conversation management and context tracking
- Tool usage for specialized tasks
- Goal-oriented behavior to help users through a complex process
- Natural language understanding and generation
Your Travel Agent AI guides users from initial travel ideas through destination research, itinerary planning, and booking information—all while maintaining a helpful, conversational experience. While using simulated data for this educational project, the same principles apply when connecting to real travel APIs and data sources.
By completing this project, you've synthesized concepts from previous chapters and gained hands-on experience with the full lifecycle of AI agent development. You're now equipped to design and build your own AI agents for a wide range of applications!
Next Steps
To continue your journey with AI agents:
- Connect to Real APIs: Replace the simulated data with actual travel information from public APIs
- Add User Management: Create account systems to save trip plans for later
- Expand Domain Knowledge: Add more specialized knowledge about destinations, travel requirements, etc.
- Implement a Web or Mobile Interface: Build a polished user experience beyond the console
- Create Multi-Agent Systems: Develop specialized sub-agents for different aspects of travel planning
By building on this foundation, you can create increasingly sophisticated AI systems that deliver real value to users.