xbb
Posted on June 24, 2024
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?
- React useEffect and useState Hooks
- How to access weather API through OpenAI
- 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
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
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
4. Set Up TailwindCSS
Initialize TailwindCSS:
npx tailwindcss init -p
Configure tailwind.config.js
:
module.exports = {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
Add Tailwind directives to src/index.css
:
@tailwind base;
@tailwind components;
@tailwind utilities;
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
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;
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;
This code defines the Plan
component, which includes:
- State hooks to manage input values (
flyingFrom
,flyingTo
,fromDate
,toDate
,budget
, andtravelers
). - 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 theSuggestion
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;
The Suggestion
component:
- Uses the
useLocation
hook fromreact-router-dom
to access the state passed from thePlan
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,
});
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']
},
},
}
];
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');
}
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
-
Start the Development Server:
npm run dev
Access the Application:
Open your browser and navigate tohttp://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
Posted on June 24, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.