Spaces:
Runtime error
Runtime error
| """ | |
| These are all the tools used in the NexusRaven V2 demo! You can provide any tools you want to Raven. | |
| Nothing in this file is specific to Raven, code/information related to Raven can be found in the `raven_demo.py` file. | |
| For more information about the Google Maps Places API Python client, see https://github.com/googlemaps/google-maps-services-python | |
| """ | |
| from typing import Any, Dict, List | |
| from math import radians, cos, sin, asin, sqrt | |
| import random | |
| import requests | |
| from googlemaps import Client | |
| from config import DemoConfig | |
| class Tools: | |
| def __init__(self, config: DemoConfig) -> None: | |
| self.config = config | |
| self.gmaps = Client(config.gmaps_client_key) | |
| self.client_ip: str | None = None | |
| def haversine(self, lon1, lat1, lon2, lat2) -> float: | |
| """ | |
| Calculate the great circle distance in kilometers between two points on the earth (specified in decimal degrees). | |
| """ | |
| # convert decimal degrees to radians | |
| lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2]) | |
| # haversine formula | |
| dlon = lon2 - lon1 | |
| dlat = lat2 - lat1 | |
| a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2 | |
| c = 2 * asin(sqrt(a)) | |
| r = 6371 # Radius of Earth in kilometers. Use 3956 for miles | |
| return round(c * r, 2) | |
| def get_current_location(self) -> str: | |
| """ | |
| Returns the current location. ONLY use this if the user has not provided an explicit location in the query. | |
| Do not use this function if the user has provided a city location (like "Austin" or "San Jose"). | |
| Do not use this function if the user has provided a place of interest location (like "Louvre" or "Levi's Stadium"). | |
| """ | |
| location_data = self._get_current_location_information() | |
| city = location_data["city"] | |
| region = location_data["regionName"] | |
| country = location_data["countryCode"] | |
| location = f"{city}, {region}, {country}" | |
| return [location] | |
| def _get_current_location_information(self) -> Dict[str, Any] | None: | |
| default_response = { | |
| "lat": "37.7577607", | |
| "lon": "-122.4788854", | |
| "city": "San Francisco", | |
| "regionName": "California", | |
| "countryCode": "US", | |
| "country": "United States", | |
| "region": "CA", | |
| } | |
| try: | |
| response = requests.get( | |
| f"https://pro.ip-api.com/json/{self.client_ip}?key={self.config.ip_api_key}" | |
| ) | |
| if not response.ok: | |
| print(f"Not able to find user. Defaulting to {default_response}") | |
| return default_response | |
| response = response.json() | |
| if response["status"] != "success": | |
| print(f"Not able to find user. Defaulting to {default_response}") | |
| return default_response | |
| except: | |
| print(f"Not able to find user. Defaulting to {default_response}") | |
| return default_response | |
| print(f"User successfully located in {response}") | |
| return response | |
| def sort_results( | |
| self, places: list, sort: str, descending: bool = True, first_n: int = None | |
| ) -> List: | |
| """ | |
| Sorts the results by either 'distance', 'rating' or 'price'. | |
| - places (list): The output list from the recommendations. | |
| - sort (str): If set, sorts by either 'distance' or 'rating' or 'price'. ONLY supports 'distance' or 'rating' or 'price'. | |
| - descending (bool): If descending is set, setting this boolean to true will sort the results such that the highest values are first. | |
| - first_n (int): If provided, only retains the first n items in the final sorted list. | |
| When people ask for 'closest' or 'nearest', sort by 'distance'. | |
| When people ask for 'cheapest' or 'most expensive', sort by 'price'. | |
| When people ask for 'best' or 'highest rated', sort by rating. | |
| """ | |
| if not sort: | |
| return places | |
| if sort == "price": | |
| sort = "price_level" | |
| items = sorted( | |
| places, | |
| key=lambda x: x.get(sort, float("inf")), | |
| reverse=descending, | |
| ) | |
| if first_n: | |
| items = items[:first_n] | |
| return items | |
| def get_latitude_longitude(self, location: str) -> List: | |
| """ | |
| Given a city name, this function provides the latitude and longitude of the specific location. | |
| - location: This can be a city like 'Austin', or a place like 'Austin Airport', etc. | |
| """ | |
| if ( | |
| isinstance(location, list) | |
| and len(location) != 0 | |
| and isinstance(location[0], dict) | |
| ): | |
| return location | |
| current_loc_info = self._get_current_location_information() | |
| lat = current_loc_info["lat"] | |
| lng = current_loc_info["lon"] | |
| radius_miles = 100 # Not a hyperparameter | |
| radius_meters = radius_miles * 1609.34 | |
| # For response content, see https://developers.google.com/maps/documentation/places/web-service/search-find-place#find-place-responses | |
| results = self.gmaps.find_place( | |
| location, | |
| input_type="textquery", | |
| location_bias=f"circle:{radius_meters}@{lat},{lng}", | |
| ) | |
| if results["status"] != "OK": | |
| return [] | |
| # We always use the first candidate | |
| place_id = results["candidates"][0]["place_id"] | |
| # For response format, see https://developers.google.com/maps/documentation/places/web-service/details#PlaceDetailsResponses | |
| place_details = self.gmaps.place(place_id=place_id)["result"] | |
| return [place_details] | |
| def get_distance(self, place_1: str, place_2: str): | |
| """ | |
| Provides distance between two locations. Do NOT provide latitude longitude, but rather, provide the string descriptions. | |
| Allows you to provide output from the get_recommendations API. | |
| - place_1: The first location. | |
| - place_2: The second location. | |
| """ | |
| if isinstance(place_1, list) and len(place_1) > 0: | |
| place_1 = place_1[0] | |
| if isinstance(place_2, list) and len(place_2) > 0: | |
| place_2 = place_2[0] | |
| if isinstance(place_1, dict): | |
| place_1: str = place_1["name"] | |
| if isinstance(place_2, dict): | |
| place_2: str = place_2["name"] | |
| latlong_1 = self.get_latitude_longitude(place_1) | |
| if len(latlong_1) == 0: | |
| return f"No place found for `{place_1}`. Please be more explicit." | |
| latlong_2 = self.get_latitude_longitude(place_2) | |
| if len(latlong_2) == 0: | |
| return f"No place found for `{place_2}`. Please be more explicit." | |
| latlong_1 = latlong_1[0] | |
| latlong_2 = latlong_2[0] | |
| latlong_values_1 = latlong_1["geometry"]["location"] | |
| latlong_values_2 = latlong_2["geometry"]["location"] | |
| dist = self.haversine( | |
| latlong_values_1["lng"], | |
| latlong_values_1["lat"], | |
| latlong_values_2["lng"], | |
| latlong_values_2["lat"], | |
| ) | |
| dist = dist * 0.621371 | |
| return [ | |
| latlong_1, | |
| latlong_2, | |
| f"The distance between {place_1} and {place_2} is {dist:.3f} miles", | |
| ] | |
| def get_recommendations(self, topics: list, lat_long: tuple): | |
| """ | |
| Returns the recommendations for a specific topic that is of interest. Remember, a topic IS NOT an establishment. For establishments, please use another function. | |
| - topics (list): A list of topics of interest to pull recommendations for. Can be multiple words. | |
| - lat_long (tuple): The lat_long of interest. | |
| """ | |
| if len(lat_long) == 0: | |
| return [] | |
| topic = " ".join(topics) | |
| latlong = lat_long[0]["geometry"]["location"] | |
| # For response format, see https://developers.google.com/maps/documentation/places/web-service/search-find-place#find-place-responses | |
| results = self.gmaps.places( | |
| query=topic, | |
| location=latlong, | |
| ) | |
| return results["results"] | |
| def find_places_near_location( | |
| self, type_of_place: list, location: str, radius_miles: int = 50 | |
| ) -> List[Dict]: | |
| """ | |
| Find places close to a very defined location. | |
| - type_of_place (list): The type of place. This can be something like 'restaurant' or 'airport'. Make sure that it is a physical location. You can provide multiple words. | |
| - location (str): The location for the search. This can be a city's name, region, or anything that specifies the location. | |
| - radius_miles (int): Optional. The max distance from the described location to limit the search. Distance is specified in miles. | |
| """ | |
| place_details = self.get_latitude_longitude(location) | |
| if len(place_details) == 0: | |
| return [] | |
| place_details = place_details[0] | |
| location = place_details["name"] | |
| latlong = place_details["geometry"]["location"] | |
| type_of_place = " ".join(type_of_place) | |
| # Perform the search using Google Places API | |
| # For response format, see https://developers.google.com/maps/documentation/places/web-service/search-nearby#nearby-search-responses | |
| places_nearby = self.gmaps.places_nearby( | |
| location=(latlong["lat"], latlong["lng"]), | |
| keyword=type_of_place, | |
| radius=radius_miles * 1609.34, | |
| ) | |
| if places_nearby["status"] != "OK": | |
| print (f"Found nothing!") | |
| return [] | |
| places_nearby = places_nearby["results"] | |
| places = [] | |
| for place_nearby in places_nearby: | |
| place_location = place_nearby["geometry"]["location"] | |
| distance = self.haversine( | |
| latlong["lng"], | |
| latlong["lat"], | |
| place_location["lng"], | |
| place_location["lat"], | |
| ) | |
| if distance == 0.0: | |
| continue | |
| distance = distance * 0.621371 | |
| place_nearby["distance"] = f"{distance} miles from {location}" | |
| places.append(place_nearby) | |
| if len(places) == 0: | |
| return [] | |
| return self.sort_results(places, sort="distance", descending=False) | |
| def out_of_domain(self, user_query : str) -> str: | |
| """ | |
| This function is designed to handle out of domain queries from the user. | |
| If the user provides any input user query that is out of domain of the other APIs provided above, | |
| please use this function with the input user query as the string. | |
| This function SHOULD NOT be used as input to another function.This should always be called as the only function. | |
| This function should not be used as input to another function. This should always be called as the only function. | |
| - user_query (str): The input string that is out of domain. | |
| Returns nothing. | |
| """ | |
| return ["This query is not answerable. Please provide queries regarding locations, reviews, or recommendations!"] | |
| def get_some_reviews(self, place_names: list, location: str = None): | |
| """ | |
| Given an establishment (or place) name, return reviews about the establishment. | |
| - place_names (list): The name of the establishment. This should be a physical location name. You can provide multiple inputs. | |
| - location (str) : The location where the restaurant is located. Optional argument. | |
| """ | |
| all_reviews = [] | |
| if location and isinstance(location, list) and len(location) > 0 and isinstance(location[0], dict): | |
| # Output of getlatlong likely. Let's reformat it. | |
| location = location[0] | |
| location = location['address_components'][0]['long_name'] | |
| for place_name in place_names: | |
| if isinstance(place_name, str): | |
| if location and isinstance(location, list) and len(location) > 0: | |
| # Sometimes location will be a list of relevant places from the API. | |
| # We just use the first one. | |
| location = location[0] | |
| elif location and isinstance(location, list): | |
| # No matching spaces found in the API, len of 0 | |
| if len(location) == 0: | |
| location = None | |
| elif len(location) == 1 and isinstance(location[0], dict): | |
| # Likely a latitude and longitude tuple | |
| latlong = location["geometry"]["location"] | |
| location = f"lat: {latlong['lat']}, long: {location['lng']}" | |
| else: | |
| location = None | |
| if location and isinstance(location, dict): | |
| # Weird response from the API, likely a timeout error, disable geoloc | |
| location = None | |
| if location and isinstance(location, str): | |
| place_name += " , " + location | |
| elif ( | |
| isinstance(place_name, dict) | |
| and "results" in place_name | |
| and "name" in place_name["results"] | |
| ): | |
| if "vicinity" in place_name["results"]: | |
| vicinity = ", " + place_name["results"]["vicinity"] | |
| else: | |
| vicinity = "" | |
| place_name = place_name["results"]["name"] + vicinity | |
| elif isinstance(place_name, dict) and "name" in place_name: | |
| if "vicinity" in place_name: | |
| vicinity = ", " + place_name["vicinity"] | |
| else: | |
| vicinity = "" | |
| place_name = place_name["name"] + vicinity | |
| print (f"Reformatted it as {place_name}!") | |
| place_details = self.get_latitude_longitude(place_name) | |
| if len(place_details) == 0: | |
| continue | |
| place_details = place_details[0] | |
| reviews = place_details.get("reviews", []) | |
| for review in reviews: | |
| review["for_location"] = place_name | |
| review["formatted_address"] = place_details["formatted_address"] | |
| all_reviews.extend(reviews) | |
| random.shuffle(all_reviews) | |
| # if the length > 20, limit it | |
| all_reviews = all_reviews[:20] | |
| return all_reviews |