DEV Community

Cover image for How to Build a Travel Agent with OpenAI API
xbb
xbb

Posted on

How to Build a Travel Agent with OpenAI API

Introduction

Building an AI Agent has never been easier, thanks to the OpenAI API. In this guide, we'll show you how to create your own AI agent to plan trips and provide travel tips. This step-by-step tutorial will help you build an AI-driven travel agent quickly. If you're wondering how to build an AI from scratch, this guide will walk you through the process. Let's get started!

Who is this for?

This tutorial is intended for developers with basic knowledge of JavaScript and React who want to integrate OpenAI's capabilities into their applications to create intelligent, conversational agents.

What will be covered?

  1. React useEffect and useState Hooks
  2. How to access weather API through OpenAI
  3. Implementing weather, flight, and hotel data fetching using OpenAI.

You can find the complete project code on GitHub.

Step-by-Step Guide

1. Initialize the Project with Vite

Create a new Vite project:

npm create vite@latest travel-agent -- --template react
cd travel-agent
Enter fullscreen mode Exit fullscreen mode

2. Configure Environment Variables

Create a .env file in the root of your project and add your API keys:

VITE_OPENAI_API_KEY=your-openai-api-key
VITE_OPENAI_BASE_URL=https://api.openai.com/v1
VITE_WEATHER_API_KEY=your-openweathermap-api-key
Enter fullscreen mode Exit fullscreen mode

3. Install Necessary Dependencies

Install dependencies for TailwindCSS, React Router, DatePicker, and other utilities:

npm install tailwindcss postcss autoprefixer react-router-dom react-datepicker openai

Enter fullscreen mode Exit fullscreen mode

4. Set Up TailwindCSS

Initialize TailwindCSS:

npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

Configure tailwind.config.js:

module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
Enter fullscreen mode Exit fullscreen mode

Add Tailwind directives to src/index.css:

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

5. Project Directory Structure

Organize your project files as follows:

src/
├── components/
│   └── TravellerCounter.jsx
├── pages/
│   ├── Plan.jsx
│   └── Suggestion.jsx
├── utils/
│   ├── openai.js
│   ├── tools.js
│   └── weather.js
├── App.jsx
├── main.jsx
└── index.css
Enter fullscreen mode Exit fullscreen mode

6. TravellerCounter Component

src/components/TravellerCounter.jsx

import React from 'react';
import PropTypes from 'prop-types';

function TravellerCounter({ count, setCount }) {
  // Function to increment the traveler count
  const increment = () => setCount(count + 1);

  // Function to decrement the traveler count, ensuring it doesn't go below 1
  const decrement = () => setCount(count > 1 ? count - 1 : 1);

  return (
    <div className="max-w-xs mb-4">
      <label htmlFor="quantity-input" className="block mb-2 text-sm font-medium text-gray-900">Number of Travellers</label>
      <div className="relative flex items-center max-w-[8rem]">
        <button
          type="button"
          id="decrement-button"
          onClick={decrement}
          className="bg-gray-100 hover:bg-gray-200 border border-gray-300 rounded-s-lg p-3 h-11">
          <svg className="w-3 h-3 text-gray-900" aria-hidden="true" viewBox="0 0 18 2">
            <path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M1 1h16" />
          </svg>
        </button>
        <div className="bg-gray-50 border-x-0 border-gray-300 h-11 text-center text-gray-900 text-sm w-full py-2.5">{count}</div>
        <button
          type="button"
          onClick={increment}
          id="increment-button"
          className="bg-gray-100 hover:bg-gray-200 border border-gray-300 rounded-e-lg p-3 h-11">
          <svg className="w-3 h-3 text-gray-900" aria-hidden="true" viewBox="0 0 18 18">
            <path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 1v16M1 9h16" />
          </svg>
        </button>
      </div>
    </div>
  );
}

TravellerCounter.propTypes = {
  count: PropTypes.number.isRequired, // Ensure count is a required number
  setCount: PropTypes.func.isRequired, // Ensure setCount is a required function
};

export default TravellerCounter;
Enter fullscreen mode Exit fullscreen mode

This component renders a label and buttons to increment or decrement the traveler count. The decrement button ensures the count doesn't go below 1. The buttons use SVGs to represent plus and minus symbols. The component also uses PropTypes to enforce the types of its props.

7. Plan Page

src/pages/Plan.jsx

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import TravellerCounter from '../components/TravellerCounter';

function Plan() {
  const navigate = useNavigate();
  const [flyingFrom, setFlyingFrom] = useState('Shanghai');
  const [flyingTo, setFlyingTo] = useState('Tokyo');
  const [fromDate, setFromDate] = useState(new Date());
  const [toDate, setToDate] = useState(new Date(new Date().setDate(new Date().getDate() + 4)));
  const [budget, setBudget] = useState(1000);
  const [travelers, setTravelers] = useState(1);
  const [errors, setErrors] = useState({ flyingFrom: '', flyingTo: '', fromDate: '', toDate: '', budget: '' });

  // Validate city name using a regex that only allows letters and spaces
  const validateCity = (city) => /^[a-zA-Z\\s]+$/.test(city);
  // Validate budget to ensure it's a positive number
  const validateBudget = (budget) => !isNaN(budget) && budget > 0;

  // Handle form submission
  const handleSubmit = (e) => {
    e.preventDefault();
    const isValidFlyingFrom = validateCity(flyingFrom);
    const isValidFlyingTo = validateCity(flyingTo);
    const isValidBudget = validateBudget(budget);
    const isValidDates = fromDate <= toDate;

    if (isValidFlyingFrom && isValidFlyingTo && isValidBudget && isValidDates) {
      // Navigate to the suggestion page with the form data
      navigate('/suggestion', {
        state: { flyingFrom, flyingTo, fromDate, toDate, budget, travelers }
      });
    } else {
      // Set error messages for invalid inputs
      setErrors({
        flyingFrom: isValidFlyingFrom ? '' : 'Invalid city name',
        flyingTo: isValidFlyingTo ? '' : 'Invalid city name',
        fromDate: isValidDates ? '' : 'From Date should be less than or equal to To Date',
        toDate: isValidDates ? '' : 'To Date should be greater than or equal to From Date',
        budget: isValidBudget ? '' : 'Invalid budget amount',
      });
    }
  };

  return (
    <div className="flex flex-col items-center py-8 mx-auto max-w-md">
      <h1 className="mb-4 text-2xl font-bold text-center">Travel Agent</h1>
      <form className="w-full" onSubmit={handleSubmit} noValidate>
        <TravellerCounter count={travelers} setCount={setTravelers} />
        <div className="mb-4">
          <label className="block mb-1 text-gray-700">Flying from</label>
          <input
            type="text"
            value={flyingFrom}
            onChange={(e) => setFlyingFrom(e.target.value)}
            className="w-full px-3 py-2 border rounded-md"
          />
          {errors.flyingFrom && <p className="mt-1 text-red-500">{errors.flyingFrom}</p>}
        </div>
        <div className="mb-4">
          <label className="block mb-1 text-gray-700">Flying to</label>
          <input
            type="text"
            value={flyingTo}
            onChange={(e) => setFlyingTo(e.target.value)}
            className="w-full px-3 py-2 border rounded-md"
          />
          {errors.flyingTo && <p className="mt-1 text-red-500">{errors.flyingTo}</p>}
        </div>
        <div className="mb-4">
          <label className="block mb-1 text-gray-700">From Date</label>
          <DatePicker selected={fromDate} onChange={(date) => setFromDate(date)} className="w-full px-3 py-2 border rounded-md" />
          {errors.fromDate && <p className="mt-1 text-red-500">{errors.fromDate}</p>}
        </div>
        <div className="mb-4">
          <label className="block mb-1 text-gray-700">To Date</label>
          <DatePicker selected={toDate} onChange={(date) => setToDate(date)} className="w-full px-3 py-2 border rounded-md" />
          {errors.toDate && <p className="mt-1 text-red-500">{errors.toDate}</p>}
        </div>
        <div className="mb-4">
          <label className="block mb-1 text-gray-700">Budget ($)</label>
          <input
            type="number"
            value={budget}
            onChange={(e) => setBudget(e.target.value)}
            className="w-full px-3 py-2 border rounded-md"
          />
          {errors.budget && <p className="mt-1 text-red-500">{errors.budget}</p>}
        </div>
        <button type="submit" className

="w-full px-4 py-2 text-white bg-green-500 rounded-md hover:bg-green-700">
          Plan my Trip!
        </button>
      </form>
    </div>
  );
}

export default Plan;
Enter fullscreen mode Exit fullscreen mode

This code defines the Plan component, which includes:

  • State hooks to manage input values (flyingFrom, flyingTo, fromDate, toDate, budget, and travelers).
  • Validation functions to ensure that city names are valid and budgets are positive numbers.
  • An handleSubmit function that validates the form inputs and navigates to the Suggestion page with the form data if all inputs are valid. If not, it sets appropriate error messages.
  • The JSX structure of the form, including input fields and a TravellerCounter component for managing the number of travelers.

8. Suggestion Page

src/pages/Suggestion.jsx

import React, { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { format } from 'date-fns';
import { client } from '../utils/openai';
import { tools } from '../utils/tools';

const messages = [
  {
    role: "system", content: `
      You are a helpful AI agent. Transform technical data into engaging,
      conversational responses, but only include the normal information a
      regular person might want unless they explicitly ask for more. Provide
      highly specific answers based on the information you're given. Prefer
      to gather information with the tools provided to you rather than
      giving basic, generic answers.
      `
  },
];

function Suggestion() {
  const location = useLocation(); // Get the location object from react-router
  const { state } = location; // Extract state from the location object
  const { flyingFrom, flyingTo, fromDate, toDate, budget, travelers } = state || {}; // Destructure state properties

  // State variables to store API responses
  const [weather, setWeather] = useState('');
  const [hotel, setHotel] = useState('');
  const [flights, setFlights] = useState('');
  const [loading, setLoading] = useState({ weather: true, flights: true, hotel: true }); // Loading states for each data type

  useEffect(() => {
    if (!state) {
      return; // Early return if state is missing
    }

    if (!flyingFrom || !flyingTo) {
      return; // Early return if essential data is missing
    }

    // Fetch weather information
    const fetchWeather = async () => {
      try {
        const weatherMessages = [
          ...messages,
          { role: "user", content: `Get the weather for ${flyingTo}` }
        ];
        const weatherRunner = client.beta.chat.completions.runTools({
          model: "gpt-4-1106-preview",
          messages: weatherMessages,
          tools
        }).on("message", (message) => console.log(message));
        const weatherContent = await weatherRunner.finalContent();
        setWeather(weatherContent); // Set weather state
      } catch (err) {
        console.error(err);
        setWeather('Failed to fetch weather'); // Handle error
      } finally {
        setLoading(prev => ({ ...prev, weather: false })); // Set loading state to false
      }
    };

    // Fetch flight information
    const fetchFlights = async () => {
      try {
        const flightMessages = [
          { role: "system", content: `You are a helpful agent.` },
          { role: "user", content: `I need flight options from ${flyingFrom} to ${flyingTo}.` }
        ];
        const response = await client.chat.completions.create({
          model: "gpt-4-1106-preview",
          messages: flightMessages
        });
        const flightContent = response.choices[0].message.content;
        setFlights(flightContent); // Set flights state
      } catch (err) {
        console.error(err);
        setFlights('Failed to fetch flights'); // Handle error
      } finally {
        setLoading(prev => ({ ...prev, flights: false })); // Set loading state to false
      }
    };

    // Fetch hotel information
    const fetchHotels = async () => {
      try {
        const hotelMessages = [
          { role: "system", content: `You are a helpful agent.` },
          { role: "user", content: `I need hotel options in ${flyingTo} for ${travelers} travelers within a budget of ${budget} dollars.` }
        ];
        const response = await client.chat.completions.create({
          model: "gpt-4-1106-preview",
          messages: hotelMessages
        });
        const hotelContent = response.choices[0].message.content;
        setHotel(hotelContent); // Set hotel state
      } catch (err) {
        console.error(err);
        setHotel('Failed to fetch hotels'); // Handle error
      } finally {
        setLoading(prev => ({ ...prev, hotel: false })); // Set loading state to false
      }
    };

    fetchWeather();
    fetchFlights();
    fetchHotels();

  }, [state, flyingFrom, flyingTo, travelers, budget]); // Dependencies for useEffect

  if (!state) {
    return <div>Error: Missing state</div>; // Error message if state is missing
  }

  return (
    <div className="flex flex-col items-center py-8 mx-auto max-w-md">
      <h1 className="mb-4 text-2xl font-bold text-center">Your Trip</h1>
      <div className="flex justify-between w-full mb-4">
        <div className="px-3 py-2 text-white bg-green-500 rounded-md">{format(new Date(fromDate), 'dd MMM yyyy')}</div>
        <div className="px-3 py-2 text-white bg-green-500 rounded-md">{format(new Date(toDate), 'dd MMM yyyy')}</div>
      </div>
      <div className="w-full p-4 mb-4 border rounded-md">
        <h2 className="text-xl font-bold">{flyingFrom}{flyingTo}</h2>
      </div>
      <div className="w-full p-4 mb-4 border rounded-md">
        <h2 className="mb-2 text-xl font-bold">Weather</h2>
        <p>{loading.weather ? 'Fetching weather...' : weather}</p> {/* Display weather or loading message */}
      </div>
      <div className="w-full p-4 mb-4 border rounded-md">
        <h2 className="mb-2 text-xl font-bold">Flights</h2>
        <p>{loading.flights ? 'Fetching flights...' : flights}</p> {/* Display flights or loading message */}
      </div>
      <div className="w-full p-4 mb-4 border rounded-md">
        <h2 className="mb-2 text-xl font-bold">Hotel</h2>
        <p>{loading.hotel ? 'Fetching hotels...' : hotel}</p> {/* Display hotels or loading message */}
      </div>
    </div>
  );
}

export default Suggestion;
Enter fullscreen mode Exit fullscreen mode

The Suggestion component:

  • Uses the useLocation hook from react-router-dom to access the state passed from the Plan page.
  • Initializes state variables to store weather, flight, and hotel information, as well as loading states for each.
  • Uses the useEffect hook to fetch weather, flight, and hotel information when the component mounts or when any dependency changes.
  • Fetches data using the OpenAI API and sets the state with the fetched data.
  • Renders the trip details, weather, flight, and hotel information. If the data is still being fetched, it shows a loading message.

Please refer to the documentation for detailed usage instructions for the runTools feature.

9. OpenAI Client Configuration

src/utils/openai.js

import OpenAI from "openai";

const openAIApiKey = import.meta.env.VITE_OPENAI_API_KEY; // Import OpenAI API key from environment variables
const openAIUrl = import.meta.env.VITE_OPENAI_BASE_URL; // Import OpenAI base URL from environment variables

export const client = new OpenAI({
  apiKey: openAIApiKey,
  baseURL: openAIUrl,
  dangerouslyAllowBrowser: true,
});
Enter fullscreen mode Exit fullscreen mode

This code configures the OpenAI client using the API key and base URL from environment variables. The client is exported for use in other parts of the application.

10. Tools Configuration

src/utils/tools.js

import { getWeather } from './weather';

export const tools = [
  {
    type: 'function',
    function: {
      function: getWeather,
      parse: JSON.parse,
      parameters: {
        type: 'object',
        properties: {
          city: { type: 'string' },
        },
        required: ['city']
      },
    },
  }
];

Enter fullscreen mode Exit fullscreen mode

This code defines a tools configuration that includes a weather-fetching function. The getWeather function is imported from the weather.js module. The tool configuration specifies the expected input parameters and how to parse them.

11. Weather Utility

src/utils/weather.js

You need to create an API Key on the openweathermap.

const weatherAIApiKey = import.meta.env.VITE_WEATHER_API_KEY; // Import weather API key from environment variables

export async function getWeather({ city }) {
  try {
    const endpoint = `http://api.openweathermap.org/data/2.5/forecast?q=${city}&appid=${weatherAIApiKey}&units=

metric`;
    const response = await fetch(endpoint);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status} ${response.statusText}`);
    }
    const data = await response.json();
    const weatherReport = displayWeather(data);
    return { report: weatherReport };
  } catch (error) {
    console.error('Error fetching weather data:', error.message);
    throw error;
  }
}

export function displayWeather(data) {
  const targetTime = '12:00:00'; // Specific time to extract the weather data
  const dailyData = data.list.filter(entry => entry.dt_txt.includes(targetTime));

  return dailyData.slice(0, 5).map(entry => {
    const date = entry.dt_txt.split(' ')[0];
    const description = entry.weather[0].description;
    const temp_min = entry.main.temp_min;
    const temp_max = entry.main.temp_max;
    return `Date: ${date}, Weather: ${description}, Temperature: Min ${temp_min}°C, Max ${temp_max}°C`;
  }).join('\\n');
}

Enter fullscreen mode Exit fullscreen mode

This module defines two functions: getWeather and displayWeather.

  • getWeather: Fetches weather data from the OpenWeatherMap API using the provided city name. It processes the API response and extracts relevant weather information.
  • displayWeather: Filters and formats the weather data for a specific time of the day (12:00:00) and returns a string with weather details for the next five days.

12. Running the Application

  1. Start the Development Server:

    npm run dev
    
  2. Access the Application:
    Open your browser and navigate to http://localhost:5173.

Congratulations! You have successfully built a travel agent application using Vite and TailwindCSS. This guide should help you understand the basic structure and functionalities implemented in this project.

Conclusion

In this tutorial, we've built a travel agent using OpenAI API that simplifies the process of planning a trip by providing personalized suggestions. This agent fetches flight options, hotel recommendations, and weather forecasts based on user inputs.

References

  1. GitHub Repo
  2. Install Tailwind CSS with Vite
  3. OpenWeatherMap API
  4. OpenAI Automated function calls

Top comments (0)