Spaces:
Runtime error
Runtime error
File size: 14,088 Bytes
e67043b |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 |
import os
import re
import json
import requests
from ..tool import Tool
from typing import Optional
from bs4 import BeautifulSoup
from serpapi import GoogleSearch
from datetime import datetime, timedelta
from amadeus import Client, ResponseError
def build_tool(config) -> Tool:
tool = Tool(
"Travel Info",
"Look up travel infomation about lodging, flight, car rental and landscape",
name_for_model="Travel",
description_for_model="""This is a plugin for look up real travel infomation. Results from this API are inaccessible for users. Please organize and re-present them.""",
logo_url="https://your-app-url.com/.well-known/logo.png",
contact_email="[email protected]",
legal_info_url="[email protected]",
)
SERPAPI_KEY = os.environ.get("SERPAPI_KEY", "")
if SERPAPI_KEY == "":
raise RuntimeError(
"SERPAPI_KEY not provided, please register one at https://serpapi.com/search-api and add it to environment variables."
)
AMADEUS_ID = os.environ.get("AMADEUS_ID", "")
if AMADEUS_ID == "":
raise RuntimeError(
"AMADEUS_ID not provided, please register one following https://developers.amadeus.com/ and add it to environment variables."
)
AMADEUS_KEY = os.environ.get("AMADEUS_KEY", "")
if AMADEUS_KEY == "":
raise RuntimeError(
"AMADEUS_KEY not provided, please register one following https://developers.amadeus.com/ and add it to environment variables."
)
def cName2coords(place_name: str, limits: Optional[int] = 1):
"""
This function accepts a place name and returns its coordinates.
:param (str) place_name: a string standing for target city for locating.
:param (str) limits: number of searching results. Usually 1 is enough.
:return: (longitude, latitude)
"""
url = f"https://serpapi.com/locations.json?q={place_name}&limit={limits}"
response = requests.get(url)
if response.status_code == 200:
if response.json():
locations = response.json()
return locations[0]["gps"]
# if not a city, use google map to find this place
else:
params = {
"engine": "google_maps",
"q": place_name,
"type": "search",
"api_key": SERPAPI_KEY,
}
search = GoogleSearch(params)
results = search.get_dict()
coords = results["place_results"]["gps_coordinates"]
return coords["longitude"], coords["latitude"]
else:
return None
def cName2IATA(city_name: str):
"""
This function accepts a city name and returns a IATA pilot code of the local airport.
:param (str) city_name: city name a s a string like 'Beijing'
:return: 3-letter IATA code like 'PEK'
"""
try:
url = f"https://www.iata.org/en/publications/directories/code-search/?airport.search={city_name}"
response = requests.get(url)
html_content = response.content
soup = str(BeautifulSoup(html_content, "html.parser"))
head = soup.find(f"<td>{city_name}</td>")
string = soup[head:]
pattern = r"<td>(.*?)</td>"
matches = re.findall(pattern, string)
# Extract the desired value
desired_value = matches[2] # Index 2 corresponds to the third match (WUH)
return desired_value # Output: WUH
except:
raise ValueError("The input city may not have an IATA registered air-port.")
@tool.get("/lodgingProducts")
def lodgingProducts(
destination: str,
exhibit_maxnum: Optional[int] = 3,
):
"""
This function returns the lodging resources near a given location.
:param (str) destination: the destination can be a city, address, airport, or a landmark.
:param (int, optional) exhibit_maxnum: int like 2, 3, 4. It determines how much items to exhibit.
:return: lodging information at a given destination.
"""
try:
# Convert destination to geographic coordinates
coords = cName2coords(destination)
# Set parameters for Google search
params = {
"engine": "google_maps",
"q": "hotel",
"ll": f"@{coords[1]},{coords[0]},15.1z",
"type": "search",
"api_key": SERPAPI_KEY,
}
# Call GoogleSearch API with given parameters
search = GoogleSearch(params)
results = search.get_dict()
local_results = results["local_results"]
# hotel with website links are preferred
filtered_results = sorted(
local_results, key=lambda x: 1 if "website" in x else 0, reverse=True
)[:exhibit_maxnum]
# Return error message if no results match criteria
if not filtered_results:
return {"error": "No searching results satisfy user's demand."}
# Parse and format relevant information for each result
final_results = []
for item in filtered_results:
info = {}
for metadata in (
"title",
"website",
"address",
"description",
"gps_coordinates",
"open_state",
"thumbnail",
):
if metadata in item:
info[metadata] = item[metadata]
final_results.append(info)
# Return formatted results along with recommendations for next steps
return json.dumps({"data": final_results})
except ResponseError as error:
print("ResponseError", error)
@tool.get("/flightProducts")
def flightProducts(
origin: str,
destination: str,
departureDate: Optional[str] = None,
adult_num: Optional[str] = None,
exhibit_maxnum: Optional[int] = 3,
):
"""
This function returns the flight information between two cities.
:param origin: (str) city name, origin of departure
:param destination: (str) city name, destination of arrival
:param departureDate: (str, optional) Date formatted as "yyyy-mm-dd". It shoule be LATER than the PRESENT date. Pass None if not sure about this.
:param adult_num: (str, optional) Number of adults for flight tickets
:param exhibit_maxnum: (int, optional) Maximum number of items to show
:return: (dict) information about flight.
"""
amadeus = Client(
client_id=AMADEUS_ID,
client_secret=AMADEUS_KEY,
)
# set default date if none or past date is given
defaultDate = f"{(datetime.now() + timedelta(days=1)).year}-{'0' + str((datetime.now() + timedelta(days=1)).month) if (datetime.now() + timedelta(days=1)).month < 10 else (datetime.now() + timedelta(days=1)).month}-{(datetime.now() + timedelta(days=1)).day}"
if not departureDate or departureDate < defaultDate:
departureDate = defaultDate
try:
# Call API to search flight offers
response = amadeus.shopping.flight_offers_search.get(
originLocationCode=cName2IATA(origin),
destinationLocationCode=cName2IATA(destination),
departureDate=departureDate,
adults=adult_num,
)
# Filter results based on exhibit_maxnum
filterd_results = response.data[:exhibit_maxnum]
# If no results found return error message
if not filterd_results:
return {"error": "No search results satisfy user's demand."}
final_results = []
for item in filterd_results:
info = {}
metadata = [
"itineraries",
"travelerPricings",
"lastTicketingDate",
"numberOfBookableSeats",
"source",
]
# Only include relevant metadata in info
for key in metadata:
if key in item:
info[key] = item[key]
final_results.append(info)
# Return formatted results along with recommendations for next steps
return json.dumps({"data": final_results})
except ResponseError as error:
print("ResponseError", error)
@tool.get("/landscapeProducts")
def landscapeProducts(
destination: str,
exhibit_maxnum: Optional[int] = 3,
):
"""
This function returns the scenic spot information given a destination.
:param (str) destination: string of cityname, destination of arrival
:param (int, optional) exhibit_maxnum: int like 3, 4, 5. It determines how many spots to display.
:return: Information about landscape around that destination.
"""
try:
# Get the coordinates of the destination using the cName2coords function
coords = cName2coords(destination)
# Set parameters for the GoogleSearch API call
params = {
"engine": "google_maps",
"q": "tourist attractions",
"ll": f"@{coords[1]},{coords[0]},15.1z",
"type": "search",
"api_key": SERPAPI_KEY,
}
# Call the GoogleSearch API
search = GoogleSearch(params)
results = search.get_dict()
local_results = results["local_results"]
# Sort the results by the specified keyword if provided
sorting_keywords = "reviews"
if sorting_keywords:
local_results = sorted(
local_results,
key=lambda x: x[sorting_keywords] if sorting_keywords in x else 0,
reverse=True,
)
# Filter the results to exhibit_maxnum number of items
filterd_results = local_results[:exhibit_maxnum]
# Return an error message if no results are found
if not filterd_results:
return {"error": "No searching results satisfy user's demand."}
# Format the results into a dictionary to be returned to the user
final_results = []
for item in filterd_results:
final_results.append({"spot_name": item["title"]})
for keywords in (
"description",
"address",
"website",
"rating",
"thumbnail",
):
if keywords in item:
final_results[-1][keywords] = item[keywords]
# Return the formatted data and some extra information to the user
return json.dumps({"data": final_results})
except ResponseError as error:
print("ResponseError", error)
@tool.get("/carProducts")
async def carProducts(
pickup_location: str,
exhibit_maxnum: Optional[int] = 3,
):
"""
Given a pickup location, returns a list of car rentals nearby.
:param pickup_location: string of city name or location for the car rental pickups.
:param exhibit_maxnum: number of rental cars to display.
:return: a dict of data and some extra-information for the LLM.
"""
try:
coords = cName2coords(pickup_location)
# Construct search query params with SERPAPI_KEY
params = {
"engine": "google_maps",
"q": "car rentals",
"ll": f"@{coords[1]},{coords[0]},15.1z",
"type": "search",
"api_key": SERPAPI_KEY,
}
# Search for nearby car rentals on SERPAPI
search = GoogleSearch(params)
results = search.get_dict()
local_results = results["local_results"]
# Sort results by rating or reviews keywords if sorting_keywords is specified
sorting_keywords = "reviews"
if sorting_keywords:
local_results = sorted(
local_results,
key=lambda x: x.get(sorting_keywords, 0),
reverse=True,
)
# Make sure spots with a website appear first
local_results = sorted(
local_results, key=lambda x: "website" in x, reverse=True
)
# Choose the exhibit_maxnum rentals to display
filtered_results = local_results[:exhibit_maxnum]
# Return an error if there are no results
if not filtered_results:
return {"error": "No results found."}
# Format the output dictionary with relevant data
final_results = []
for item in filtered_results:
spot = {"spot_name": item["title"]}
for keyword in (
"description",
"address",
"website",
"rating",
"thumbnail",
):
if keyword in item:
spot[keyword] = item[keyword]
final_results.append(spot)
# Return the formatted output dictionary with extra information
# Return formatted results along with recommendations for next steps
return json.dumps({"data": final_results})
except ResponseError: # Handle response error exceptions
return {"error": "Response error."}
return tool
|