brian-yu-nexusflow commited on
Commit
5d95723
Β·
1 Parent(s): 0104266

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +235 -484
app.py CHANGED
@@ -1,531 +1,282 @@
1
- from typing import Any, Callable, List, Tuple
 
2
 
3
- import huggingface_hub
4
 
5
- from dataclasses import dataclass
 
 
6
 
7
- from datetime import datetime
8
 
9
- from time import sleep
10
 
11
- import inspect
12
 
13
- from random import randint
14
 
15
- from urllib.parse import quote
16
 
17
- from black import Mode, format_str
18
 
19
- import gradio as gr
 
 
20
 
21
- from huggingface_hub import InferenceClient
 
22
 
23
- from constants import *
24
- from config import DemoConfig
25
- from tools import Tools
26
-
27
-
28
- @dataclass
29
- class Function:
30
- name: str
31
- short_description: str
32
- description_function: Callable[[Any], str]
33
- explanation_function: Callable[[Any], str]
34
-
35
-
36
- FUNCTIONS = [
37
- Function(
38
- name="get_current_location",
39
- short_description="Finding your city",
40
- description_function=lambda *_, **__: "Finding your city",
41
- explanation_function=lambda result: f"Found you in {result}!",
42
- ),
43
- Function(
44
- name="sort_results",
45
- short_description="Sorting results",
46
- description_function=lambda places, sort, descending=True, first_n=None: f"Sorting results by {sort} from "
47
- + ("lowest to highest" if not descending else "highest to lowest"),
48
- explanation_function=lambda result: "Done!",
49
- ),
50
- Function(
51
- name="get_latitude_longitude",
52
- short_description="Convert to coordinates",
53
- description_function=lambda location: f"Converting {location} into latitude and longitude coordinates",
54
- explanation_function=lambda result: "Converted!",
55
- ),
56
- Function(
57
- name="get_distance",
58
- short_description="Calcuate distance",
59
- description_function=lambda place_1, place_2: "Calculating distances",
60
- explanation_function=lambda result: result[2],
61
- ),
62
- Function(
63
- name="get_recommendations",
64
- short_description="Read recommendations",
65
- description_function=lambda topics, **__: f"Reading recommendations for the following "
66
- + (
67
- f"topics: {', '.join(topics)}" if len(topics) > 1 else f"topic: {topics[0]}"
68
- ),
69
- explanation_function=lambda result: f"Read {len(result)} recommendations",
70
- ),
71
- Function(
72
- name="find_places_near_location",
73
- short_description="Look for places",
74
- description_function=lambda type_of_place, location, radius_miles=50: f"Looking for places near {location} within {radius_miles} with the following "
75
- + (
76
- f"types: {', '.join(type_of_place)}"
77
- if isinstance(type_of_place, list)
78
- else f"type: {type_of_place}"
79
- ),
80
- explanation_function=lambda result: f"Found "
81
- + (f"{len(result)} places!" if len(result) > 1 else f"1 place!"),
82
- ),
83
- Function(
84
- name="get_some_reviews",
85
- short_description="Fetching reviews",
86
- description_function=lambda place_names, **_: f"Fetching reviews for the requested items",
87
- explanation_function=lambda result: f"Fetched {len(result)} reviews!",
88
- ),
89
- ]
90
-
91
-
92
- class FunctionsHelper:
93
- FUNCTION_DEFINITION_TEMPLATE = '''Function:
94
- def {name}{signature}:
95
- """
96
- {docstring}
97
- """
98
 
99
- '''
100
- PROMPT_TEMPLATE = """{function_definitions}User Query: {query}<human_end>Call:"""
 
 
101
 
102
- def __init__(self, tools: Tools) -> None:
103
- self.tools = tools
 
 
104
 
105
- function_definitions = ""
106
- for function in FUNCTIONS:
107
- f = getattr(tools, function.name)
108
- signature = inspect.signature(f)
109
- docstring = inspect.getdoc(f)
110
 
111
- function_str = self.FUNCTION_DEFINITION_TEMPLATE.format(
112
- name=function.name, signature=signature, docstring=docstring
113
- )
114
- function_definitions += function_str
115
 
116
- self.prompt_without_query = self.PROMPT_TEMPLATE.format(
117
- function_definitions=function_definitions, query="{query}"
 
 
118
  )
119
 
120
- def get_prompt(self, query: str):
121
- return self.prompt_without_query.format(query=query)
122
-
123
- def get_function_call_plan(self, function_call_str: str) -> List[str]:
124
- function_call_list = []
125
- locals_to_pass = {"function_call_list": function_call_list}
126
- for f in FUNCTIONS:
127
- name = f.name
128
- exec(
129
- f"def {name}(**_):\n\tfunction_call_list.append('{f.short_description}')",
130
- locals_to_pass,
131
- )
132
- calls = [c.strip() for c in function_call_str.split(";") if c.strip()]
133
- [eval(call, locals_to_pass) for call in calls]
134
- return function_call_list
135
-
136
- def run_function_call(self, function_call_str: str):
137
- function_call_list = []
138
- locals_to_pass = {"function_call_list": function_call_list, "tools": self.tools}
139
- for f in FUNCTIONS:
140
- name = f.name
141
-
142
- locals_to_pass[f"{name}_description_function"] = f.description_function
143
- locals_to_pass[f"{name}_explanation_function"] = f.explanation_function
144
-
145
- function_definition = f"""
146
- def {name}(**kwargs):
147
- result = tools.{f.name}(**kwargs)
148
- function_call_list.append(({name}_description_function(**kwargs), {name}_explanation_function(result)))
149
- return result
150
- """
151
- exec(function_definition, locals_to_pass)
152
-
153
- calls = [c.strip() for c in function_call_str.split(";") if c.strip()]
154
- for call in calls:
155
- locals_to_pass["function_call_list"] = function_call_list = []
156
- result = eval(call, locals_to_pass)
157
- yield result, function_call_list
158
 
 
 
 
159
 
160
- class RavenDemo(gr.Blocks):
161
- def __init__(self, config: DemoConfig) -> None:
162
- theme = gr.themes.Soft(
163
- primary_hue=gr.themes.colors.blue,
164
- secondary_hue=gr.themes.colors.blue,
165
- )
166
- super().__init__(theme=theme, css=CSS, title="NexusRaven V2 Demo")
167
-
168
- self.config = config
169
- self.tools = Tools(config)
170
- self.functions_helper = FunctionsHelper(self.tools)
171
 
172
- self.raven_client = InferenceClient(
173
- model=config.raven_endpoint, token=config.hf_token
 
174
  )
175
- self.summary_model_client = InferenceClient(config.summary_model_endpoint)
176
-
177
- self.max_num_steps = 20
178
-
179
- with self:
180
- gr.HTML(HEADER_HTML)
181
- with gr.Row():
182
- gr.Image(
183
- "NexusRaven.png",
184
- show_label=False,
185
- show_share_button=True,
186
- min_width=200,
187
- scale=1,
188
- )
189
- with gr.Column(scale=4, min_width=800):
190
- gr.Markdown(INTRO_TEXT, elem_classes="inner-large-font")
191
- with gr.Row():
192
- examples = [
193
- gr.Button(query_name) for query_name in EXAMPLE_QUERIES
194
- ]
195
-
196
- user_input = gr.Textbox(
197
- placeholder="Ask me anything!",
198
- show_label=False,
199
- autofocus=True,
200
- )
201
 
202
- raven_function_call = gr.Code(
203
- label="πŸ¦β€β¬› NexusRaven V2 13B generated function call",
204
- language="python",
205
- interactive=False,
206
- lines=10,
207
- )
208
- with gr.Accordion(
209
- "Executing plan generated by πŸ¦β€β¬› NexusRaven V2 13B", open=True
210
- ) as steps_accordion:
211
- steps = [
212
- gr.Textbox(visible=False, show_label=False)
213
- for _ in range(self.max_num_steps)
214
- ]
215
-
216
- with gr.Column():
217
- initial_relevant_places = self.get_relevant_places([])
218
- relevant_places = gr.State(initial_relevant_places)
219
- place_dropdown_choices = self.get_place_dropdown_choices(
220
- initial_relevant_places
221
- )
222
- places_dropdown = gr.Dropdown(
223
- choices=place_dropdown_choices,
224
- value=place_dropdown_choices[0],
225
- label="Relevant places",
226
- )
227
- gmaps_html = gr.HTML(self.get_gmaps_html(initial_relevant_places[0]))
228
-
229
- summary_model_summary = gr.Textbox(
230
- label="Chat summary",
231
- interactive=False,
232
- show_copy_button=True,
233
- lines=10,
234
- max_lines=1000,
235
- autoscroll=False,
236
- elem_classes="inner-large-font",
237
- )
238
 
239
- with gr.Accordion("Raven inputs", open=False):
240
- gr.Textbox(
241
- label="Available functions",
242
- value="`" + "`, `".join(f.name for f in FUNCTIONS) + "`",
243
- interactive=False,
244
- show_copy_button=True,
245
- )
246
- gr.Textbox(
247
- label="Raven prompt",
248
- value=self.functions_helper.get_prompt("{query}"),
249
- interactive=False,
250
- show_copy_button=True,
251
- lines=20,
252
- )
253
-
254
- user_input.submit(
255
- fn=self.on_submit,
256
- inputs=[user_input],
257
- outputs=[
258
- user_input,
259
- raven_function_call,
260
- summary_model_summary,
261
- relevant_places,
262
- places_dropdown,
263
- gmaps_html,
264
- steps_accordion,
265
- *steps,
266
- ],
267
- concurrency_limit=20, # not a hyperparameter
268
- api_name=False,
269
- )
270
 
271
- for i, button in enumerate(examples):
272
- button.click(
273
- fn=EXAMPLE_QUERIES.get,
274
- inputs=button,
275
- outputs=user_input,
276
- api_name=f"button_click_{i}",
277
- )
278
-
279
- places_dropdown.input(
280
- fn=self.get_gmaps_html_from_dropdown,
281
- inputs=[places_dropdown, relevant_places],
282
- outputs=gmaps_html,
283
- )
284
 
285
- def on_submit(self, query: str, request: gr.Request):
286
- def get_returns():
287
- return (
288
- user_input,
289
- raven_function_call,
290
- summary_model_summary,
291
- relevant_places,
292
- places_dropdown,
293
- gmaps_html,
294
- steps_accordion,
295
- *steps,
296
- )
297
 
298
- user_input = gr.Textbox(interactive=False)
299
- raven_function_call = ""
300
- summary_model_summary = ""
301
- relevant_places = []
302
- places_dropdown = ""
303
- gmaps_html = ""
304
- steps_accordion = gr.Accordion(open=True)
305
- steps = [gr.Textbox(value="", visible=False) for _ in range(self.max_num_steps)]
306
- yield get_returns()
307
-
308
- raven_prompt = self.functions_helper.get_prompt(
309
- query.replace("'", r"\'").replace('"', r"\"")
310
- )
311
- print(f"{'-' * 80}\nPrompt sent to Raven\n\n{raven_prompt}\n\n{'-' * 80}\n")
312
- stream = self.raven_client.text_generation(
313
- raven_prompt, **RAVEN_GENERATION_KWARGS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  )
315
- for s in stream:
316
- for c in s:
317
- raven_function_call += c
318
- raven_function_call = raven_function_call.removesuffix("<bot_end>")
319
- yield get_returns()
320
 
321
- print(f"Raw Raven response before formatting: {raven_function_call}")
 
 
 
 
322
 
323
- r_calls = [c.strip() for c in raven_function_call.split(";") if c.strip()]
324
- f_r_calls = []
325
- for r_c in r_calls:
326
- f_r_call = format_str(r_c.strip(), mode=Mode())
327
- f_r_calls.append(f_r_call)
328
 
329
- raven_function_call = "; ".join(f_r_calls)
 
 
 
 
 
 
 
 
 
 
 
 
 
330
 
331
- yield get_returns()
 
 
 
 
332
 
333
- self._set_client_ip(request)
334
- function_call_plan = self.functions_helper.get_function_call_plan(
335
- raven_function_call
336
- )
337
- for i, v in enumerate(function_call_plan):
338
- steps[i] = gr.Textbox(value=f"{i+1}. {v}", visible=True)
339
- yield get_returns()
340
- sleep(0.1)
341
-
342
- results_gen = self.functions_helper.run_function_call(raven_function_call)
343
- results = []
344
- previous_num_calls = 0
345
- for result, function_call_list in results_gen:
346
- results.extend(result)
347
- for i, (description, explanation) in enumerate(function_call_list):
348
- i = i + previous_num_calls
349
-
350
- if len(description) > 100:
351
- description = function_call_plan[i]
352
- to_stream = f"{i+1}. {description} ..."
353
- steps[i] = ""
354
- for c in to_stream:
355
- steps[i] += c
356
- sleep(0.005)
357
- yield get_returns()
358
-
359
- to_stream = "." * randint(0, 5)
360
- for c in to_stream:
361
- steps[i] += c
362
- sleep(0.2)
363
- yield get_returns()
364
-
365
- to_stream = f" {explanation}"
366
- for c in to_stream:
367
- steps[i] += c
368
- sleep(0.005)
369
- yield get_returns()
370
-
371
- previous_num_calls += len(function_call_list)
372
-
373
- relevant_places = self.get_relevant_places(results)
374
- gmaps_html = self.get_gmaps_html(relevant_places[0])
375
- places_dropdown_choices = self.get_place_dropdown_choices(relevant_places)
376
- places_dropdown = gr.Dropdown(
377
- choices=places_dropdown_choices, value=places_dropdown_choices[0]
378
  )
379
- steps_accordion = gr.Accordion(open=False)
380
- yield get_returns()
381
-
382
- while True:
383
- try:
384
- summary_model_prompt = self.get_summary_model_prompt(results, query)
385
- print(
386
- f"{'-' * 80}\nPrompt sent to summary model\n\n{summary_model_prompt}\n\n{'-' * 80}\n"
387
- )
388
- stream = self.summary_model_client.text_generation(
389
- summary_model_prompt, **SUMMARY_MODEL_GENERATION_KWARGS
390
- )
391
- for s in stream:
392
- for c in s:
393
- summary_model_summary += c
394
- summary_model_summary = (
395
- summary_model_summary.lstrip().removesuffix(
396
- "<|end_of_turn|>"
397
- )
398
- )
399
- yield get_returns()
400
- except huggingface_hub.inference._text_generation.ValidationError:
401
- if len(results) > 1:
402
- new_length = (3 * len(results)) // 4
403
- results = results[:new_length]
404
- continue
405
- else:
406
- break
407
-
408
- break
409
-
410
- user_input = gr.Textbox(interactive=True)
411
- yield get_returns()
412
-
413
- def get_summary_model_prompt(self, results: List, query: str) -> None:
414
- # TODO check what outputs are returned and return them properly
415
- ALLOWED_KEYS = [
416
- "author_name",
417
- "text",
418
- "for_location",
419
- "time",
420
- "author_url",
421
- "language",
422
- "original_language",
423
- "name",
424
- "opening_hours",
425
- "rating",
426
- "user_ratings_total",
427
- "vicinity",
428
- "distance",
429
- "formatted_address",
430
- "price_level",
431
- "types",
432
- ]
433
- ALLOWED_KEYS = set(ALLOWED_KEYS)
434
-
435
- results_str = ""
436
- for idx, res in enumerate(results):
437
- if isinstance(res, str):
438
- results_str += f"{res}\n"
439
  continue
440
 
441
- assert isinstance(res, dict)
 
 
442
 
443
- item_str = ""
444
- for key, value in res.items():
445
- if key not in ALLOWED_KEYS:
446
- continue
447
 
448
- key = key.replace("_", " ").capitalize()
449
- item_str += f"\t{key}: {value}\n"
450
 
451
- results_str += f"Result {idx + 1}\n{item_str}\n"
452
-
453
- current_time = datetime.now().strftime("%b %d, %Y %H:%M:%S")
454
- current_location = self.tools.get_current_location()
455
-
456
- prompt = SUMMARY_MODEL_PROMPT.format(
457
- current_location=current_location,
458
- current_time=current_time,
459
- results=results_str,
460
- query=query,
461
- )
462
- return prompt
463
-
464
- def get_relevant_places(self, results: List) -> List[Tuple[str, str]]:
465
  """
466
- Returns
467
- -------
468
- relevant_places: List[Tuple[str, str]]
469
- A list of tuples, where each tuple is (address, name)
470
 
 
 
471
  """
472
- # We use a dict to preserve ordering, while enforcing uniqueness
473
- relevant_places = dict()
474
- for result in results:
475
- if "formatted_address" in result and "name" in result:
476
- relevant_places[(result["formatted_address"], result["name"])] = None
477
- elif "formatted_address" in result and "for_location" in result:
478
- relevant_places[
479
- (result["formatted_address"], result["for_location"])
480
- ] = None
481
- elif "vicinity" in result and "name" in result:
482
- relevant_places[(result["vicinity"], result["name"])] = None
483
-
484
- relevant_places = list(relevant_places.keys())
485
-
486
- if not relevant_places:
487
- current_location = self.tools.get_current_location()
488
- relevant_places.append((current_location, current_location))
489
-
490
- return relevant_places
491
-
492
- def get_place_dropdown_choices(
493
- self, relevant_places: List[Tuple[str, str]]
494
- ) -> List[str]:
495
- return [p[1] for p in relevant_places]
496
-
497
- def get_gmaps_html(self, relevant_place: Tuple[str, str]) -> str:
498
- address, name = relevant_place
499
- return GMAPS_EMBED_HTML_TEMPLATE.format(
500
- address=quote(address), location=quote(name)
501
- )
502
-
503
- def get_gmaps_html_from_dropdown(
504
- self, place_name: str, relevant_places: List[Tuple[str, str]]
505
- ) -> str:
506
- relevant_place = [p for p in relevant_places if p[1] == place_name][0]
507
- return self.get_gmaps_html(relevant_place)
508
 
509
- def _set_client_ip(self, request: gr.Request) -> None:
510
- client_ip = request.client.host
511
- if (
512
- "headers" in request.kwargs
513
- and "x-forwarded-for" in request.kwargs["headers"]
514
- ):
515
- x_forwarded_for = request.kwargs["headers"]["x-forwarded-for"]
516
- else:
517
- x_forwarded_for = request.headers.get("x-forwarded-for", None)
518
- if x_forwarded_for:
519
- client_ip = x_forwarded_for.split(",")[0].strip()
520
 
521
- self.tools.client_ip = client_ip
 
 
522
 
 
523
 
524
- demo = RavenDemo(DemoConfig.load_from_env())
525
 
526
- if __name__ == "__main__":
527
- demo.launch(
528
- share=True,
529
- allowed_paths=["logo.png", "NexusRaven.png"],
530
- favicon_path="logo.png",
531
- )
 
1
+ """
2
+ These are all the tools used in the NexusRaven V2 demo! You can provide any tools you want to Raven.
3
 
4
+ Nothing in this file is specific to Raven, code/information related to Raven can be found in the `raven_demo.py` file.
5
 
6
+ For more information about the Google Maps Places API Python client, see https://github.com/googlemaps/google-maps-services-python
7
+ """
8
+ from typing import Dict, List
9
 
10
+ from math import radians, cos, sin, asin, sqrt
11
 
12
+ import random
13
 
14
+ import requests
15
 
16
+ from googlemaps import Client
17
 
18
+ from config import DemoConfig
19
 
 
20
 
21
+ class Tools:
22
+ def __init__(self, config: DemoConfig) -> None:
23
+ self.config = config
24
 
25
+ self.gmaps = Client(config.gmaps_client_key)
26
+ self.client_ip: str | None = None
27
 
28
+ def haversine(self, lon1, lat1, lon2, lat2) -> float:
29
+ """
30
+ Calculate the great circle distance in kilometers between two points on the earth (specified in decimal degrees).
31
+ """
32
+ # convert decimal degrees to radians
33
+ lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])
34
+
35
+ # haversine formula
36
+ dlon = lon2 - lon1
37
+ dlat = lat2 - lat1
38
+ a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
39
+ c = 2 * asin(sqrt(a))
40
+ r = 6371 # Radius of Earth in kilometers. Use 3956 for miles
41
+ return round(c * r, 2)
42
+
43
+ def get_current_location(self) -> str:
44
+ """
45
+ Returns the current location. ONLY use this if the user has not provided an explicit location in the query.
46
+ """
47
+ try:
48
+ response = requests.get(f"http://ip-api.com/json/{self.client_ip}")
49
+ location_data = response.json()
50
+ city = location_data["city"]
51
+ region = location_data["regionName"]
52
+ country = location_data["countryCode"]
53
+ location = f"{city}, {region}, {country}"
54
+ print(f"User successfully located in {location}")
55
+ except:
56
+ location = "San Francisco, California, US"
57
+ print(f"Not able to find user. Defaulting to {location}")
58
+ return location
59
+
60
+ def sort_results(
61
+ self, places: list, sort: str, descending: bool = True, first_n: int = None
62
+ ) -> List:
63
+ """
64
+ Sorts the results by either 'distance', 'rating' or 'price'.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
+ - places (list): The output list from the recommendations.
67
+ - sort (str): If set, sorts by either 'distance' or 'rating' or 'price'. ONLY supports 'distance' or 'rating' or 'price'.
68
+ - descending (bool): If descending is set, setting this boolean to true will sort the results such that the highest values are first.
69
+ - first_n (int): If provided, only retains the first n items in the final sorted list.
70
 
71
+ When people ask for 'closest' or 'nearest', sort by 'distance'.
72
+ When people ask for 'cheapest' or 'most expensive', sort by 'price'.
73
+ When people ask for 'best' or 'highest rated', sort by rating.
74
+ """
75
 
76
+ if not sort:
77
+ return places
 
 
 
78
 
79
+ if sort == "price":
80
+ sort = "price_level"
 
 
81
 
82
+ items = sorted(
83
+ places,
84
+ key=lambda x: x.get(sort, float("inf")),
85
+ reverse=descending,
86
  )
87
 
88
+ if first_n:
89
+ items = items[:first_n]
90
+ return items
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
+ def get_latitude_longitude(self, location: str) -> List:
93
+ """
94
+ Given a city name, this function provides the latitude and longitude of the specific location.
95
 
96
+ - location: This can be a city like 'Austin', or a place like 'Austin Airport', etc.
97
+ """
98
+ if (
99
+ isinstance(location, list)
100
+ and len(location) != 0
101
+ and isinstance(location[0], dict)
102
+ ):
103
+ return location
 
 
 
104
 
105
+ # For response content, see https://developers.google.com/maps/documentation/places/web-service/search-find-place#find-place-responses
106
+ results = self.gmaps.find_place(
107
+ location, input_type="textquery", location_bias="ipbias"
108
  )
109
+ if results["status"] != "OK":
110
+ return []
111
+ print(results)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
+ # We always use the first candidate
114
+ place_id = results["candidates"][0]["place_id"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
+ # For response format, see https://developers.google.com/maps/documentation/places/web-service/details#PlaceDetailsResponses
117
+ place_details = self.gmaps.place(place_id=place_id)["result"]
118
+ return [place_details]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
+ def get_distance(self, place_1: str, place_2: str):
121
+ """
122
+ Provides distance between two locations. Do NOT provide latitude longitude, but rather, provide the string descriptions.
 
 
 
 
 
 
 
 
 
 
123
 
124
+ Allows you to provide output from the get_recommendations API.
 
 
 
 
 
 
 
 
 
 
 
125
 
126
+ - place_1: The first location.
127
+ - place_2: The second location.
128
+ """
129
+ if isinstance(place_1, list) and len(place_1) > 0:
130
+ place_1 = place_1[0]
131
+ if isinstance(place_2, list) and len(place_2) > 0:
132
+ place_2 = place_2[0]
133
+
134
+ if isinstance(place_1, dict):
135
+ place_1: str = place_1["name"]
136
+ if isinstance(place_2, dict):
137
+ place_2: str = place_2["name"]
138
+
139
+ latlong_1 = self.get_latitude_longitude(place_1)
140
+ if len(latlong_1) == 0:
141
+ return f"No place found for `{place_1}`. Please be more explicit."
142
+
143
+ latlong_2 = self.get_latitude_longitude(place_2)
144
+ if len(latlong_2) == 0:
145
+ return f"No place found for `{place_2}`. Please be more explicit."
146
+
147
+ latlong_1 = latlong_1[0]
148
+ latlong_2 = latlong_2[0]
149
+
150
+ latlong_values_1 = latlong_1["geometry"]["location"]
151
+ latlong_values_2 = latlong_2["geometry"]["location"]
152
+
153
+ dist = self.haversine(
154
+ latlong_values_1["lng"],
155
+ latlong_values_1["lat"],
156
+ latlong_values_2["lng"],
157
+ latlong_values_2["lat"],
158
  )
159
+ dist = dist * 0.621371
 
 
 
 
160
 
161
+ return [
162
+ latlong_1,
163
+ latlong_2,
164
+ f"The distance between {place_1} and {place_2} is {dist:.3f} miles",
165
+ ]
166
 
167
+ def get_recommendations(self, topics: list, lat_long: tuple):
168
+ """
169
+ Returns the recommendations for a specific topic that is of interest. Remember, a topic IS NOT an establishment. For establishments, please use another function.
 
 
170
 
171
+ - topics (list): A list of topics of interest to pull recommendations for. Can be multiple words.
172
+ - lat_long (tuple): The lat_long of interest.
173
+ """
174
+ if len(lat_long) == 0:
175
+ return []
176
+
177
+ topic = " ".join(topics)
178
+ latlong = lat_long[0]["geometry"]["location"]
179
+ # For response format, see https://developers.google.com/maps/documentation/places/web-service/search-find-place#find-place-responses
180
+ results = self.gmaps.places(
181
+ query=topic,
182
+ location=latlong,
183
+ )
184
+ return results["results"]
185
 
186
+ def find_places_near_location(
187
+ self, type_of_place: list, location: str, radius_miles: int = 50
188
+ ) -> List[Dict]:
189
+ """
190
+ Find places close to a very defined location.
191
 
192
+ - 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.
193
+ - location (str): The location for the search. This can be a city's name, region, or anything that specifies the location.
194
+ - radius_miles (int): Optional. The max distance from the described location to limit the search. Distance is specified in miles.
195
+ """
196
+ place_details = self.get_latitude_longitude(location)
197
+ if len(place_details) == 0:
198
+ return []
199
+ place_details = place_details[0]
200
+ location = place_details["name"]
201
+ latlong = place_details["geometry"]["location"]
202
+
203
+ type_of_place = " ".join(type_of_place)
204
+ # Perform the search using Google Places API
205
+ # For response format, see https://developers.google.com/maps/documentation/places/web-service/search-nearby#nearby-search-responses
206
+ places_nearby = self.gmaps.places_nearby(
207
+ location=(latlong["lat"], latlong["lng"]),
208
+ keyword=type_of_place,
209
+ radius=radius_miles * 1609.34,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  )
211
+ if places_nearby["status"] != "OK":
212
+ return []
213
+
214
+ places_nearby = places_nearby["results"]
215
+ places = []
216
+ for place_nearby in places_nearby:
217
+ place_location = place_nearby["geometry"]["location"]
218
+ distance = self.haversine(
219
+ latlong["lng"],
220
+ latlong["lat"],
221
+ place_location["lng"],
222
+ place_location["lat"],
223
+ )
224
+ if distance == 0.0:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  continue
226
 
227
+ distance = distance * 0.621371
228
+ place_nearby["distance"] = f"{distance} miles from {location}"
229
+ places.append(place_nearby)
230
 
231
+ if len(places) == 0:
232
+ return []
 
 
233
 
234
+ return self.sort_results(places, sort="distance", descending=False)
 
235
 
236
+ def get_some_reviews(self, place_names: list, location: str = None):
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  """
238
+ Given an establishment (or place) name, return reviews about the establishment.
 
 
 
239
 
240
+ - place_names (list): The name of the establishment. This should be a physical location name. You can provide multiple inputs.
241
+ - location (str) : The location where the restaurant is located. Optional argument.
242
  """
243
+ all_reviews = []
244
+ for place_name in place_names:
245
+ if isinstance(place_name, str):
246
+ if location and isinstance(location, list) and len(location) > 0:
247
+ # Sometimes location will be a list of relevant places from the API.
248
+ # We just use the first one.
249
+ location = location[0]
250
+ elif location and isinstance(location, list):
251
+ # No matching spaces found in the API, len of 0
252
+ location = None
253
+ if location and isinstance(location, dict):
254
+ # Weird response from the API, likely a timeout error, disable geoloc
255
+ location = None
256
+ if location and isinstance(location, str):
257
+ place_name += " , " + location
258
+ elif (
259
+ isinstance(place_name, dict)
260
+ and "results" in place_name
261
+ and "name" in place_name["results"]
262
+ ):
263
+ place_name = place_name["results"]["name"]
264
+ elif isinstance(place_name, dict) and "name" in place_name:
265
+ place_name = place_name["name"]
266
+
267
+ place_details = self.get_latitude_longitude(place_name)
268
+ if len(place_details) == 0:
269
+ continue
270
+ place_details = place_details[0]
 
 
 
 
 
 
 
 
271
 
272
+ reviews = place_details.get("reviews", [])
 
 
 
 
 
 
 
 
 
 
273
 
274
+ for review in reviews:
275
+ review["for_location"] = place_name
276
+ review["formatted_address"] = place_details["formatted_address"]
277
 
278
+ all_reviews.extend(reviews)
279
 
280
+ random.shuffle(all_reviews)
281
 
282
+ return all_reviews