File size: 14,489 Bytes
5321b2e
 
 
 
6db0fcc
 
5321b2e
b2326e1
5321b2e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18a99a5
a428c9a
 
5321b2e
b2326e1
 
 
 
 
11d051f
5321b2e
b2326e1
 
 
 
 
 
 
 
 
 
0f867ca
 
 
 
 
 
 
 
 
 
 
 
 
b2326e1
 
 
 
 
 
6db0fcc
 
 
5321b2e
 
 
 
 
 
 
6db0fcc
5321b2e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d082a11
 
 
 
 
 
 
b2326e1
 
 
 
 
 
d082a11
 
b2326e1
 
 
d082a11
 
 
 
 
 
6db0fcc
d082a11
 
 
5321b2e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6db0fcc
5321b2e
6db0fcc
5321b2e
6db0fcc
 
 
5321b2e
6db0fcc
 
 
 
 
 
 
 
 
5321b2e
 
6db0fcc
 
 
 
5321b2e
 
 
6db0fcc
 
 
 
 
5321b2e
 
 
 
 
 
 
 
 
 
 
 
 
6db0fcc
5321b2e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d082a11
 
26acc8d
d082a11
 
 
5321b2e
 
6db0fcc
d082a11
 
 
 
5321b2e
d082a11
58994e5
d082a11
 
 
 
 
 
5321b2e
 
 
 
 
 
 
 
 
6db0fcc
d082a11
 
5321b2e
 
 
 
 
 
7ff240f
0a16124
7ff240f
 
0a16124
 
c179503
 
 
7ff240f
c179503
 
0a16124
11d051f
0a16124
5321b2e
 
 
 
 
 
 
 
9af5dac
 
 
 
 
5321b2e
 
d5968dd
 
 
 
 
 
97b846a
 
80aa8b6
97b846a
80aa8b6
 
97b846a
 
d5968dd
 
 
 
97b846a
6db0fcc
 
 
 
 
6b9a6c0
 
 
 
 
5321b2e
6b9a6c0
 
 
 
 
5321b2e
5560d6b
d082a11
 
 
 
5321b2e
d082a11
5321b2e
 
 
d082a11
5321b2e
 
 
 
 
12abad4
 
 
0a16124
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
"""
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