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="hello@contact.com", legal_info_url="hello@legal.com", ) 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"{city_name}") string = soup[head:] pattern = r"(.*?)" 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