通过 OpenAI API 来实现一个旅行规划 Agent

424 阅读6分钟

介绍

由于OpenAI API的出现,构建AI代理从未如此简单。在本指南中,我们将向您展示如何创建自己的AI代理来规划旅行并提供旅行建议。本教程将帮助您快速构建一个AI驱动的旅行代理。如果您想知道如何从零开始构建AI,本指南将引导您完成整个过程。让我们开始吧!

适用人群

本教程适用于具有JavaScript和React基础知识的开发者,他们希望将OpenAI的功能集成到自己的应用程序中,以创建智能的对话代理。

将要覆盖的内容

  1. React useEffect 和 useState 钩子
  2. 如何通过OpenAI访问天气API
  3. 使用OpenAI实现天气、航班和酒店数据的获取

完整项目代码可在 GitHub 上找到。

分步指南

1. 使用Vite初始化项目

创建一个新的Vite项目:

npm create vite@latest travel-agent -- --template react
cd travel-agent

2. 配置环境变量

在项目根目录下创建一个.env文件,并添加您的API密钥:

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. 安装必要的依赖

安装TailwindCSS、React Router、DatePicker和其他工具的依赖:

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

4. 设置TailwindCSS

初始化TailwindCSS:

npx tailwindcss init -p

配置tailwind.config.js

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

src/index.css中添加Tailwind指令:

@tailwind base;
@tailwind components;
@tailwind utilities;

5. 项目目录结构

将项目文件组织如下:

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

6. TravellerCounter组件

src/components/TravellerCounter.jsx

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

function TravellerCounter({ count, setCount }) {
  const increment = () => setCount(count + 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,
  setCount: PropTypes.func.isRequired,
};

export default TravellerCounter;

该组件渲染一个标签和按钮,用于增加或减少旅行者人数。减少按钮确保数量不会低于1。按钮使用SVG表示加号和减号符号。组件还使用PropTypes来强制执行其属性的类型。

7. Plan页面

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: '' });

  const validateCity = (city) => /^[a-zA-Z\s]+$/.test(city);
  const validateBudget = (budget) => !isNaN(budget) && budget > 0;

  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('/suggestion', {
        state: { flyingFrom, flyingTo, fromDate, toDate, budget, travelers }
      });
    } else {
      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;

该代码定义了 Plan 组件,包括:

  • 用于管理输入值的状态钩子(flyingFrom, flyingTo, fromDate, toDate, budgettravelers)。
  • 验证函数以确保城市名称有效且预算为正数。
  • 一个 handleSubmit 函数,用于验证表单输入并在所有输入有效时导航到 Suggestion 页面并传递表单数据。如果无效,则设置相应的错误消息。
  • 表单的 JSX 结构,包括输入字段和用于管理旅行者数量的 TravellerCounter 组件。

8. Suggestion页面

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

  const [weather, setWeather] = useState('');
  const [hotel, setHotel] = useState('');
  const [flights, setFlights] = useState('');
  const [loading, setLoading] = useState({ weather: true, flights: true, hotel: true });

  useEffect(() => {
    if (!state) {
      return;
    }

    if (!flyingFrom || !flyingTo) {
      return;
    }

    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);
      } catch (err) {
        console.error(err);
        setWeather('Failed to fetch weather');
      } finally {
        setLoading(prev => ({ ...prev, weather: false }));
      }
    };

    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);
      } catch (err) {
        console.error(err);
        setFlights('Failed to fetch flights');
      } finally {
        setLoading(prev => ({ ...prev, flights: false }));
      }
    };

    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);
      } catch (err) {
        console.error(err);
        setHotel('Failed to fetch hotels');
      } finally {
        setLoading(prev => ({ ...prev, hotel: false }));
      }
    };

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

  }, [state, flyingFrom, flyingTo, travelers, budget]);

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

  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>
      </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>
      </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>
      </div>
    </div>
  );
}

export default Suggestion;

Suggestion 组件:

  • 使用 react-router-domuseLocation 钩子访问从 Plan 页面传递的状态。
  • 初始化状态变量以存储天气、航班和酒店信息,以及每种数据类型的加载状态。
  • 使用 useEffect 钩子在组件挂载时或任何依赖项更改时获取天气、航班和酒店信息。
  • 使用OpenAI API获取数据并设置状态。
  • 渲染旅行细节、天气、航班和酒店信息。如果数据仍在获取中,显示加载消息。

有关runTools功能的详细使用说明,请参阅文档

9. OpenAI客户端配置

src/utils/openai.js

import OpenAI from "openai";

const openAIApiKey = import.meta.env.VITE_OPENAI_API_KEY;
const openAIUrl = import.meta.env.VITE_OPENAI_BASE_URL;

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

此代码使用来自环境变量的API密钥和基本URL配置OpenAI客户端。客户端被导出以便在应用程序的其他部分使用。

10. 工具配置

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']
      },
    },
  }
];

此代码定义了一个包含天气获取功能的工具配置。 getWeather 函数从 weather.js 模块导入。工具配置指定了预期的输入参数以及如何解析它们。

11. 天气工具

src/utils/weather.js

您需要在 openweathermap 上创建一个API密钥。



const weatherAIApiKey = import.meta.env.VITE_WEATHER_API_KEY;

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';
  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');
}

此模块定义了两个函数: getWeatherdisplayWeather

  • getWeather:使用提供的城市名称从OpenWeatherMap API获取天气数据。它处理API响应并提取相关的天气信息。
  • displayWeather:过滤并格式化一天中特定时间(12:00:00)的天气数据,并返回接下来五天的天气详细信息字符串。

12. 运行应用程序

  1. 启动开发服务器:

    npm run dev
    
  2. 访问应用程序: 打开浏览器并导航到 http://localhost:5173

恭喜!您已经成功使用Vite和TailwindCSS构建了一个旅行代理应用程序。本指南应帮助您理解本项目中实现的基本结构和功能。

结论

在本教程中,我们使用OpenAI API构建了一个旅行代理,简化了通过提供个性化建议来规划旅行的过程。该代理根据用户输入获取航班选项、酒店推荐和天气预报。

参考

  1. GitHub Repo
  2. 安装 Tailwind CSS 与 Vite
  3. OpenWeatherMap API
  4. OpenAI 自动化功能调用