Dashboard
Lessons
Chapter 11: Capstone Project: Building a Travel Agent AI

Chapter 11: Capstone Project: Building a Travel Agent AI

Skills
AI Agents

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:

  1. Gather Trip Requirements: Conversationally collect key information about the user's trip (budget, dates, group size, interests, preferred climate, etc.)
  2. Research Destinations: Find and evaluate potential destinations matching the user's criteria
  3. Generate Itineraries: Create detailed day-by-day plans with activities and highlights
  4. Find Travel Options: Research and recommend flights and accommodations
  5. Present Recommendations: Deliver findings in a clear, organized format
  6. Maintain Context: Remember user preferences throughout the conversation

Project Architecture

We'll build our agent with these key components:

  1. Memory Module: Stores conversation history and trip requirements
  2. Research Tools: Integration with search APIs for destination research
  3. Itinerary Generator: Creates structured travel plans based on destinations and preferences
  4. Travel Options Finder: Searches for flights and accommodations
  5. Decision Engine: Manages the conversation flow and determines next steps
  6. Natural Language Interface: Handles user communication

Development Plan

We'll tackle this project in phases:

  1. Set up our development environment
  2. Build the core agent with memory and conversation management
  3. Implement the destination research capability
  4. Create the itinerary generation system
  5. Add travel booking information tools
  6. 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

  1. Create a new directory for your project:

mkdir travel-agent-ai

cd travel-agent-ai

  1. Set up a virtual environment:

python -m venv venv

  1. Activate the virtual environment:
    • On Windows:

venv\Scripts\activate

    • On macOS/Linux:

source venv/bin/activate

  1. 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

  1. 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:

  1. Beach vacation: Ask for a tropical destination with beaches and relaxation
  2. City adventure: Request a cultural trip to explore museums and historical sites
  3. Budget constraints: Specify a limited budget and see how the agent adapts
  4. Special requirements: Mention traveling with children or needing accessibility options

Debugging Common Issues

If you encounter problems:

  1. API Key Errors: Ensure your OpenAI and SerpAPI keys are correctly set in the .env file
  2. Module Import Errors: Check your project structure matches the described setup
  3. JSON Parsing Errors: These can occur if the LLM returns malformed JSON. Add better error handling for robust operation
  4. 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

  1. Integrate with real travel APIs like:
    1. Amadeus for flight data
    2. Booking.com or Expedia for accommodation information
    3. TripAdvisor for attraction recommendations
  2. Replace the sample data with real-time information for more accurate planning.

Improve the User Interface

  1. 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

  1. Visa requirement checking based on traveler nationality and destination
  2. Weather forecasts for the travel dates
  3. Language translation phrases for the destination
  4. Budget tracking and expense estimation
  5. 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:

  1. Connect to Real APIs: Replace the simulated data with actual travel information from public APIs
  2. Add User Management: Create account systems to save trip plans for later
  3. Expand Domain Knowledge: Add more specialized knowledge about destinations, travel requirements, etc.
  4. Implement a Web or Mobile Interface: Build a polished user experience beyond the console
  5. 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.

Table of contents
Teacher
Astro
All
Astro
lessons

https://forwardfuture.ai/lessons/chapter-11-capstone-project-building-a-travel-agent-ai