Spaces:
Runtime error
Runtime error
brian-yu-nexusflow
commited on
Commit
β’
5321b2e
1
Parent(s):
f85ccfd
Upload 7 files
Browse files- .gitattributes +1 -0
- NexusRaven.png +3 -0
- config.py +22 -0
- constants.py +73 -0
- logo.png +0 -0
- raven_demo.py +515 -0
- requirements.txt +3 -0
- tools.py +230 -0
.gitattributes
CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
NexusRaven.png filter=lfs diff=lfs merge=lfs -text
|
NexusRaven.png
ADDED
Git LFS Details
|
config.py
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from dataclasses import dataclass
|
2 |
+
|
3 |
+
from os import getenv
|
4 |
+
|
5 |
+
|
6 |
+
@dataclass
|
7 |
+
class DemoConfig:
|
8 |
+
gmaps_client_key: str
|
9 |
+
|
10 |
+
raven_endpoint: str
|
11 |
+
hf_token: str
|
12 |
+
|
13 |
+
summary_model_endpoint: str
|
14 |
+
|
15 |
+
@classmethod
|
16 |
+
def load_from_env(cls) -> "DemoConfig":
|
17 |
+
return DemoConfig(
|
18 |
+
gmaps_client_key=getenv("GMAPS_CLIENT_KEY"),
|
19 |
+
raven_endpoint=getenv("RAVEN_ENDPOINT"),
|
20 |
+
hf_token=getenv("HF_TOKEN"),
|
21 |
+
summary_model_endpoint=getenv("SUMMARY_MODEL_ENDPOINT"),
|
22 |
+
)
|
constants.py
ADDED
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
RAVEN_GENERATION_KWARGS = {
|
2 |
+
"max_new_tokens": 200,
|
3 |
+
"do_sample": False,
|
4 |
+
"temperature": 0.001,
|
5 |
+
"return_full_text": False,
|
6 |
+
"stop_sequences": ["<bot_end>"],
|
7 |
+
"stream": True,
|
8 |
+
}
|
9 |
+
|
10 |
+
SUMMARY_MODEL_PROMPT = """<s>GPT4 Correct User:
|
11 |
+
I'm currently in {current_location} and the current time is {current_time}.
|
12 |
+
|
13 |
+
Search results for relevant areas of interest to the user query:
|
14 |
+
{results}
|
15 |
+
|
16 |
+
Now please answer the following query using the search results shown above. Please keep your answer concise.
|
17 |
+
Query: {query}<|end_of_turn|>GPT4 Correct Assistant:"""
|
18 |
+
|
19 |
+
SUMMARY_MODEL_GENERATION_KWARGS = {
|
20 |
+
"max_new_tokens": 1000,
|
21 |
+
"do_sample": False,
|
22 |
+
"temperature": 0.001,
|
23 |
+
"return_full_text": False,
|
24 |
+
"stream": True,
|
25 |
+
}
|
26 |
+
|
27 |
+
EXAMPLE_QUERIES = {
|
28 |
+
"Discover Your Locale": "Get me good food nearby?",
|
29 |
+
"Gather Opinions": "What are people saying about Golden Gate Park in San Francisco?",
|
30 |
+
"Compare Feedback": "Can you get me reviews for So Gong Dong Tofu house and Siam Thai Cuisine and compare them specifically regarding how tasty the food is? Summarize the answer. Please print the review texts you reference.",
|
31 |
+
"Tailored Recommendations": "Get me some good vegetarian Chinese food in San Francisco?",
|
32 |
+
"Proximity Searches": "Can you list me hostels that are cheaper than $200 per night? I need the place to be within 20 miles from San Francisco City Hall.",
|
33 |
+
"Deep Insights": "Can you please compare the reviews for Ippudo Ramen, Ramen Nagi and Yayoi Cupertino?",
|
34 |
+
}
|
35 |
+
|
36 |
+
INTRO_TEXT = """
|
37 |
+
# Google Places API Copilot Demo, Driven by NexusRaven-V2 13B
|
38 |
+
This demo presents a natural language interface to the Google Places API, showcasing Raven's capability to enable copilots and agents to use software tools. Raven transforms your plain English queries into function calls to your APIs. Type in your query and lets explore wonderful places and recommendations through Raven and the Places API!
|
39 |
+
|
40 |
+
πΊοΈ Google Places API searches for places of interest and returns information regarding location, reviews, and recommendations.
|
41 |
+
|
42 |
+
π¦ββ¬ NexusRaven-V2 13B, our function calling model, will execute the necessary API calls in the backend to get the information you need!
|
43 |
+
|
44 |
+
### Examples
|
45 |
+
"""
|
46 |
+
|
47 |
+
CSS = """
|
48 |
+
footer {
|
49 |
+
visibility: hidden;
|
50 |
+
}
|
51 |
+
.inner-large-font {
|
52 |
+
--text-md: 16px;
|
53 |
+
font-size: 20;
|
54 |
+
}
|
55 |
+
:root {
|
56 |
+
--text-sm: 18px;
|
57 |
+
--input-text-size: 18px;
|
58 |
+
}
|
59 |
+
.dark {
|
60 |
+
--text-sm: 18px;
|
61 |
+
--input-text-size: 18px;
|
62 |
+
}
|
63 |
+
|
64 |
+
"""
|
65 |
+
|
66 |
+
HEADER_HTML = """<img width="50" height="50" style="float:left; margin: 0px;" src="/file=logo.png">
|
67 |
+
<h1 style="overflow: hidden; padding-top: 17px; margin: 0px;">Nexusflow</h1>
|
68 |
+
"""
|
69 |
+
|
70 |
+
# Inputs must be encoded via urllib.parse.quote
|
71 |
+
GMAPS_EMBED_HTML_TEMPLATE = """
|
72 |
+
<iframe width="100%" height="600" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" src="https://maps.google.com/maps?width=100%25&height=600&hl=en&q={location}+{address}&t=&z=18&ie=UTF8&iwloc=B&output=embed">
|
73 |
+
"""
|
logo.png
ADDED
raven_demo.py
ADDED
@@ -0,0 +1,515 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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: f"Calculating the distance between various places...",
|
60 |
+
explanation_function=lambda result: result[0],
|
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 {len(result)} places!",
|
81 |
+
),
|
82 |
+
Function(
|
83 |
+
name="get_some_reviews",
|
84 |
+
short_description="Fetching reviews",
|
85 |
+
description_function=lambda place_names, **_: f"Fetching reviews for the requested items",
|
86 |
+
explanation_function=lambda result: f"Fetched {len(result)} reviews!",
|
87 |
+
),
|
88 |
+
]
|
89 |
+
|
90 |
+
|
91 |
+
class FunctionsHelper:
|
92 |
+
FUNCTION_DEFINITION_TEMPLATE = '''Function:
|
93 |
+
def {name}{signature}:
|
94 |
+
"""
|
95 |
+
{docstring}
|
96 |
+
"""
|
97 |
+
|
98 |
+
'''
|
99 |
+
PROMPT_TEMPLATE = """{function_definitions}User Query: {query}<human_end>Call:"""
|
100 |
+
|
101 |
+
def __init__(self, tools: Tools) -> None:
|
102 |
+
self.tools = tools
|
103 |
+
|
104 |
+
function_definitions = ""
|
105 |
+
for function in FUNCTIONS:
|
106 |
+
f = getattr(tools, function.name)
|
107 |
+
signature = inspect.signature(f)
|
108 |
+
docstring = inspect.getdoc(f)
|
109 |
+
|
110 |
+
function_str = self.FUNCTION_DEFINITION_TEMPLATE.format(
|
111 |
+
name=function.name, signature=signature, docstring=docstring
|
112 |
+
)
|
113 |
+
function_definitions += function_str
|
114 |
+
|
115 |
+
self.prompt_without_query = self.PROMPT_TEMPLATE.format(
|
116 |
+
function_definitions=function_definitions, query="{query}"
|
117 |
+
)
|
118 |
+
|
119 |
+
def get_prompt(self, query: str):
|
120 |
+
return self.prompt_without_query.format(query=query)
|
121 |
+
|
122 |
+
def get_function_call_plan(self, function_call_str: str) -> List[str]:
|
123 |
+
function_call_list = []
|
124 |
+
locals_to_pass = {"function_call_list": function_call_list}
|
125 |
+
for f in FUNCTIONS:
|
126 |
+
name = f.name
|
127 |
+
exec(
|
128 |
+
f"def {name}(**_):\n\tfunction_call_list.append('{f.short_description}')",
|
129 |
+
locals_to_pass,
|
130 |
+
)
|
131 |
+
calls = [c.strip() for c in function_call_str.split(";") if c.strip()]
|
132 |
+
[eval(call, locals_to_pass) for call in calls]
|
133 |
+
return function_call_list
|
134 |
+
|
135 |
+
def run_function_call(self, function_call_str: str):
|
136 |
+
function_call_list = []
|
137 |
+
locals_to_pass = {"function_call_list": function_call_list, "tools": self.tools}
|
138 |
+
for f in FUNCTIONS:
|
139 |
+
name = f.name
|
140 |
+
|
141 |
+
locals_to_pass[f"{name}_description_function"] = f.description_function
|
142 |
+
locals_to_pass[f"{name}_explanation_function"] = f.explanation_function
|
143 |
+
|
144 |
+
function_definition = f"""
|
145 |
+
def {name}(**kwargs):
|
146 |
+
result = tools.{f.name}(**kwargs)
|
147 |
+
function_call_list.append(({name}_description_function(**kwargs), {name}_explanation_function(result)))
|
148 |
+
return result
|
149 |
+
"""
|
150 |
+
exec(function_definition, locals_to_pass)
|
151 |
+
|
152 |
+
calls = [c.strip() for c in function_call_str.split(";") if c.strip()]
|
153 |
+
for call in calls:
|
154 |
+
locals_to_pass["function_call_list"] = function_call_list = []
|
155 |
+
result = eval(call, locals_to_pass)
|
156 |
+
yield result, function_call_list
|
157 |
+
|
158 |
+
|
159 |
+
class RavenDemo(gr.Blocks):
|
160 |
+
def __init__(self, config: DemoConfig) -> None:
|
161 |
+
super().__init__(theme=gr.themes.Soft(), css=CSS, title="NexusRaven V2 Demo")
|
162 |
+
|
163 |
+
self.config = config
|
164 |
+
self.tools = Tools(config)
|
165 |
+
self.functions_helper = FunctionsHelper(self.tools)
|
166 |
+
|
167 |
+
self.raven_client = InferenceClient(
|
168 |
+
model=config.raven_endpoint, token=config.hf_token
|
169 |
+
)
|
170 |
+
self.summary_model_client = InferenceClient(config.summary_model_endpoint)
|
171 |
+
|
172 |
+
self.max_num_steps = 20
|
173 |
+
|
174 |
+
with self:
|
175 |
+
gr.HTML(HEADER_HTML)
|
176 |
+
with gr.Row():
|
177 |
+
gr.Image(
|
178 |
+
"NexusRaven.png",
|
179 |
+
show_label=False,
|
180 |
+
show_share_button=True,
|
181 |
+
min_width=200,
|
182 |
+
scale=1,
|
183 |
+
)
|
184 |
+
with gr.Column(scale=4, min_width=800):
|
185 |
+
gr.Markdown(INTRO_TEXT, elem_classes="inner-large-font")
|
186 |
+
with gr.Row():
|
187 |
+
examples = [
|
188 |
+
gr.Button(query_name) for query_name in EXAMPLE_QUERIES
|
189 |
+
]
|
190 |
+
|
191 |
+
user_input = gr.Textbox(
|
192 |
+
placeholder="Ask me anything!",
|
193 |
+
show_label=False,
|
194 |
+
autofocus=True,
|
195 |
+
)
|
196 |
+
|
197 |
+
raven_function_call = gr.Code(
|
198 |
+
label="π¦ββ¬ NexusRaven V2 13B generated function call",
|
199 |
+
language="python",
|
200 |
+
interactive=False,
|
201 |
+
lines=10,
|
202 |
+
)
|
203 |
+
with gr.Accordion(
|
204 |
+
"Executing plan generated by π¦ββ¬ NexusRaven V2 13B", open=True
|
205 |
+
) as steps_accordion:
|
206 |
+
steps = [
|
207 |
+
gr.Textbox(visible=False, show_label=False)
|
208 |
+
for _ in range(self.max_num_steps)
|
209 |
+
]
|
210 |
+
|
211 |
+
with gr.Column():
|
212 |
+
initial_relevant_places = self.get_relevant_places([])
|
213 |
+
relevant_places = gr.State(initial_relevant_places)
|
214 |
+
place_dropdown_choices = self.get_place_dropdown_choices(
|
215 |
+
initial_relevant_places
|
216 |
+
)
|
217 |
+
places_dropdown = gr.Dropdown(
|
218 |
+
choices=place_dropdown_choices,
|
219 |
+
value=place_dropdown_choices[0],
|
220 |
+
label="Relevant places",
|
221 |
+
)
|
222 |
+
gmaps_html = gr.HTML(self.get_gmaps_html(initial_relevant_places[0]))
|
223 |
+
|
224 |
+
summary_model_summary = gr.Textbox(
|
225 |
+
label="Chat summary",
|
226 |
+
interactive=False,
|
227 |
+
show_copy_button=True,
|
228 |
+
lines=10,
|
229 |
+
max_lines=1000,
|
230 |
+
autoscroll=False,
|
231 |
+
elem_classes="inner-large-font",
|
232 |
+
)
|
233 |
+
|
234 |
+
with gr.Accordion("Raven inputs", open=False):
|
235 |
+
gr.Textbox(
|
236 |
+
label="Available functions",
|
237 |
+
value="`" + "`, `".join(f.name for f in FUNCTIONS) + "`",
|
238 |
+
interactive=False,
|
239 |
+
show_copy_button=True,
|
240 |
+
)
|
241 |
+
gr.Textbox(
|
242 |
+
label="Raven prompt",
|
243 |
+
value=self.functions_helper.get_prompt("{query}"),
|
244 |
+
interactive=False,
|
245 |
+
show_copy_button=True,
|
246 |
+
lines=20,
|
247 |
+
)
|
248 |
+
|
249 |
+
user_input.submit(
|
250 |
+
fn=self.on_submit,
|
251 |
+
inputs=[user_input],
|
252 |
+
outputs=[
|
253 |
+
user_input,
|
254 |
+
raven_function_call,
|
255 |
+
summary_model_summary,
|
256 |
+
relevant_places,
|
257 |
+
places_dropdown,
|
258 |
+
gmaps_html,
|
259 |
+
steps_accordion,
|
260 |
+
*steps,
|
261 |
+
],
|
262 |
+
concurrency_limit=20, # not a hyperparameter
|
263 |
+
api_name=False,
|
264 |
+
)
|
265 |
+
|
266 |
+
for i, button in enumerate(examples):
|
267 |
+
button.click(
|
268 |
+
fn=EXAMPLE_QUERIES.get,
|
269 |
+
inputs=button,
|
270 |
+
outputs=user_input,
|
271 |
+
api_name=f"button_click_{i}",
|
272 |
+
)
|
273 |
+
|
274 |
+
places_dropdown.input(
|
275 |
+
fn=self.get_gmaps_html_from_dropdown,
|
276 |
+
inputs=[places_dropdown, relevant_places],
|
277 |
+
outputs=gmaps_html,
|
278 |
+
)
|
279 |
+
|
280 |
+
def on_submit(self, query: str, request: gr.Request):
|
281 |
+
def get_returns():
|
282 |
+
return (
|
283 |
+
user_input,
|
284 |
+
raven_function_call,
|
285 |
+
summary_model_summary,
|
286 |
+
relevant_places,
|
287 |
+
places_dropdown,
|
288 |
+
gmaps_html,
|
289 |
+
steps_accordion,
|
290 |
+
*steps,
|
291 |
+
)
|
292 |
+
|
293 |
+
user_input = gr.Textbox(interactive=False)
|
294 |
+
raven_function_call = ""
|
295 |
+
summary_model_summary = ""
|
296 |
+
relevant_places = []
|
297 |
+
places_dropdown = ""
|
298 |
+
gmaps_html = ""
|
299 |
+
steps_accordion = gr.Accordion(open=True)
|
300 |
+
steps = [gr.Textbox(value="", visible=False) for _ in range(self.max_num_steps)]
|
301 |
+
yield get_returns()
|
302 |
+
|
303 |
+
raven_prompt = self.functions_helper.get_prompt(query)
|
304 |
+
print(f"{'-' * 80}\nPrompt sent to Raven\n\n{raven_prompt}\n\n{'-' * 80}\n")
|
305 |
+
stream = self.raven_client.text_generation(
|
306 |
+
raven_prompt, **RAVEN_GENERATION_KWARGS
|
307 |
+
)
|
308 |
+
for s in stream:
|
309 |
+
for c in s:
|
310 |
+
raven_function_call += c
|
311 |
+
raven_function_call = raven_function_call.removesuffix("<bot_end>")
|
312 |
+
yield get_returns()
|
313 |
+
|
314 |
+
r_calls = [c.strip() for c in raven_function_call.split(";") if c.strip()]
|
315 |
+
f_r_calls = []
|
316 |
+
for r_c in r_calls:
|
317 |
+
f_r_call = format_str(r_c.strip(), mode=Mode())
|
318 |
+
f_r_calls.append(f_r_call)
|
319 |
+
|
320 |
+
raven_function_call = "; ".join(f_r_calls)
|
321 |
+
|
322 |
+
yield get_returns()
|
323 |
+
|
324 |
+
self._set_client_ip(request)
|
325 |
+
function_call_plan = self.functions_helper.get_function_call_plan(
|
326 |
+
raven_function_call
|
327 |
+
)
|
328 |
+
for i, v in enumerate(function_call_plan):
|
329 |
+
steps[i] = gr.Textbox(value=f"{i+1}. {v}", visible=True)
|
330 |
+
yield get_returns()
|
331 |
+
sleep(0.1)
|
332 |
+
|
333 |
+
results_gen = self.functions_helper.run_function_call(raven_function_call)
|
334 |
+
results = []
|
335 |
+
previous_num_calls = 0
|
336 |
+
for result, function_call_list in results_gen:
|
337 |
+
results.extend(result)
|
338 |
+
for i, (description, explanation) in enumerate(function_call_list):
|
339 |
+
i = i + previous_num_calls
|
340 |
+
to_stream = f"{i+1}. {description} ..."
|
341 |
+
steps[i] = ""
|
342 |
+
for c in to_stream:
|
343 |
+
steps[i] += c
|
344 |
+
sleep(0.005)
|
345 |
+
yield get_returns()
|
346 |
+
|
347 |
+
to_stream = "." * randint(0, 5)
|
348 |
+
for c in to_stream:
|
349 |
+
steps[i] += c
|
350 |
+
sleep(0.2)
|
351 |
+
yield get_returns()
|
352 |
+
|
353 |
+
to_stream = f" {explanation}"
|
354 |
+
for c in to_stream:
|
355 |
+
steps[i] += c
|
356 |
+
sleep(0.005)
|
357 |
+
yield get_returns()
|
358 |
+
|
359 |
+
previous_num_calls += len(function_call_list)
|
360 |
+
|
361 |
+
relevant_places = self.get_relevant_places(results)
|
362 |
+
gmaps_html = self.get_gmaps_html(relevant_places[0])
|
363 |
+
places_dropdown_choices = self.get_place_dropdown_choices(relevant_places)
|
364 |
+
places_dropdown = gr.Dropdown(
|
365 |
+
choices=places_dropdown_choices, value=places_dropdown_choices[0]
|
366 |
+
)
|
367 |
+
steps_accordion = gr.Accordion(open=False)
|
368 |
+
yield get_returns()
|
369 |
+
|
370 |
+
while True:
|
371 |
+
try:
|
372 |
+
summary_model_prompt = self.get_summary_model_prompt(results, query)
|
373 |
+
print(
|
374 |
+
f"{'-' * 80}\nPrompt sent to summary model\n\n{summary_model_prompt}\n\n{'-' * 80}\n"
|
375 |
+
)
|
376 |
+
stream = self.summary_model_client.text_generation(
|
377 |
+
summary_model_prompt, **SUMMARY_MODEL_GENERATION_KWARGS
|
378 |
+
)
|
379 |
+
for s in stream:
|
380 |
+
for c in s:
|
381 |
+
summary_model_summary += c
|
382 |
+
summary_model_summary = summary_model_summary.lstrip().removesuffix(
|
383 |
+
"<|end_of_turn|>"
|
384 |
+
)
|
385 |
+
yield get_returns()
|
386 |
+
except huggingface_hub.inference._text_generation.ValidationError:
|
387 |
+
if len(results) > 1:
|
388 |
+
new_length = (3*len(results)) // 4
|
389 |
+
results = results[:new_length]
|
390 |
+
continue
|
391 |
+
else:
|
392 |
+
break
|
393 |
+
|
394 |
+
break
|
395 |
+
|
396 |
+
user_input = gr.Textbox(interactive=True)
|
397 |
+
yield get_returns()
|
398 |
+
|
399 |
+
def get_summary_model_prompt(self, results: List, query: str) -> None:
|
400 |
+
# TODO check what outputs are returned and return them properly
|
401 |
+
ALLOWED_KEYS = [
|
402 |
+
"author_name",
|
403 |
+
"text",
|
404 |
+
"for_location",
|
405 |
+
"time",
|
406 |
+
"author_url",
|
407 |
+
"language",
|
408 |
+
"original_language",
|
409 |
+
"name",
|
410 |
+
"opening_hours",
|
411 |
+
"rating",
|
412 |
+
"user_ratings_total",
|
413 |
+
"vicinity",
|
414 |
+
"distance",
|
415 |
+
"formatted_address",
|
416 |
+
"price_level",
|
417 |
+
"types",
|
418 |
+
]
|
419 |
+
ALLOWED_KEYS = set(ALLOWED_KEYS)
|
420 |
+
|
421 |
+
results_str = ""
|
422 |
+
for idx, res in enumerate(results):
|
423 |
+
if isinstance(res, str):
|
424 |
+
results_str += f"{res}\n"
|
425 |
+
continue
|
426 |
+
|
427 |
+
assert isinstance(res, dict)
|
428 |
+
|
429 |
+
item_str = ""
|
430 |
+
for key, value in res.items():
|
431 |
+
if key not in ALLOWED_KEYS:
|
432 |
+
continue
|
433 |
+
|
434 |
+
key = key.replace("_", " ").capitalize()
|
435 |
+
item_str += f"\t{key}: {value}\n"
|
436 |
+
|
437 |
+
results_str += f"Result {idx + 1}\n{item_str}\n"
|
438 |
+
|
439 |
+
current_time = datetime.now().strftime("%b %d, %Y %H:%M:%S")
|
440 |
+
current_location = self.tools.get_current_location()
|
441 |
+
|
442 |
+
prompt = SUMMARY_MODEL_PROMPT.format(
|
443 |
+
current_location=current_location,
|
444 |
+
current_time=current_time,
|
445 |
+
results=results_str,
|
446 |
+
query=query,
|
447 |
+
)
|
448 |
+
return prompt
|
449 |
+
|
450 |
+
def get_relevant_places(self, results: List) -> List[Tuple[str, str]]:
|
451 |
+
"""
|
452 |
+
Returns
|
453 |
+
-------
|
454 |
+
relevant_places: List[Tuple[str, str]]
|
455 |
+
A list of tuples, where each tuple is (address, name)
|
456 |
+
|
457 |
+
"""
|
458 |
+
# We use a dict to preserve ordering, while enforcing uniqueness
|
459 |
+
relevant_places = dict()
|
460 |
+
for result in results:
|
461 |
+
if "formatted_address" in result and "name" in result:
|
462 |
+
relevant_places[(result["formatted_address"], result["name"])] = None
|
463 |
+
elif "formatted_address" in result and "for_location" in result:
|
464 |
+
relevant_places[
|
465 |
+
(result["formatted_address"], result["for_location"])
|
466 |
+
] = None
|
467 |
+
|
468 |
+
relevant_places = list(relevant_places.keys())
|
469 |
+
|
470 |
+
if not relevant_places:
|
471 |
+
current_location = self.tools.get_current_location()
|
472 |
+
relevant_places.append((current_location, current_location))
|
473 |
+
|
474 |
+
return relevant_places
|
475 |
+
|
476 |
+
def get_place_dropdown_choices(
|
477 |
+
self, relevant_places: List[Tuple[str, str]]
|
478 |
+
) -> List[str]:
|
479 |
+
return [p[1] for p in relevant_places]
|
480 |
+
|
481 |
+
def get_gmaps_html(self, relevant_place: Tuple[str, str]) -> str:
|
482 |
+
address, name = relevant_place
|
483 |
+
return GMAPS_EMBED_HTML_TEMPLATE.format(
|
484 |
+
address=quote(address), location=quote(name)
|
485 |
+
)
|
486 |
+
|
487 |
+
def get_gmaps_html_from_dropdown(
|
488 |
+
self, place_name: str, relevant_places: List[Tuple[str, str]]
|
489 |
+
) -> str:
|
490 |
+
relevant_place = [p for p in relevant_places if p[1] == place_name][0]
|
491 |
+
return self.get_gmaps_html(relevant_place)
|
492 |
+
|
493 |
+
def _set_client_ip(self, request: gr.Request) -> None:
|
494 |
+
client_ip = request.client.host
|
495 |
+
if (
|
496 |
+
"headers" in request.kwargs
|
497 |
+
and "x-forwarded-for" in request.kwargs["headers"]
|
498 |
+
):
|
499 |
+
x_forwarded_for = request.kwargs["headers"]["x-forwarded-for"]
|
500 |
+
else:
|
501 |
+
x_forwarded_for = request.headers.get("x-forwarded-for", None)
|
502 |
+
if x_forwarded_for:
|
503 |
+
client_ip = x_forwarded_for.split(",")[0].strip()
|
504 |
+
|
505 |
+
self.tools.client_ip = client_ip
|
506 |
+
|
507 |
+
|
508 |
+
demo = RavenDemo(DemoConfig.load_from_env())
|
509 |
+
|
510 |
+
if __name__ == "__main__":
|
511 |
+
demo.launch(
|
512 |
+
share=True,
|
513 |
+
allowed_paths=["logo.png", "NexusRaven.png"],
|
514 |
+
favicon_path="logo.png",
|
515 |
+
)
|
requirements.txt
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
gradio==4.2.0
|
2 |
+
googlemaps==4.10.0
|
3 |
+
transformers
|
tools.py
ADDED
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
from typing import Dict, List, Union
|
7 |
+
|
8 |
+
from math import radians, cos, sin, asin, sqrt
|
9 |
+
|
10 |
+
import random
|
11 |
+
|
12 |
+
import requests
|
13 |
+
|
14 |
+
from googlemaps import Client
|
15 |
+
|
16 |
+
from config import DemoConfig
|
17 |
+
|
18 |
+
|
19 |
+
class Tools:
|
20 |
+
def __init__(self, config: DemoConfig) -> None:
|
21 |
+
self.config = config
|
22 |
+
|
23 |
+
self.gmaps = Client(config.gmaps_client_key)
|
24 |
+
self.client_ip: str | None = None
|
25 |
+
|
26 |
+
def haversine(self, lon1, lat1, lon2, lat2) -> float:
|
27 |
+
"""
|
28 |
+
Calculate the great circle distance in kilometers between two points on the earth (specified in decimal degrees).
|
29 |
+
"""
|
30 |
+
# convert decimal degrees to radians
|
31 |
+
lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])
|
32 |
+
|
33 |
+
# haversine formula
|
34 |
+
dlon = lon2 - lon1
|
35 |
+
dlat = lat2 - lat1
|
36 |
+
a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
|
37 |
+
c = 2 * asin(sqrt(a))
|
38 |
+
r = 6371 # Radius of Earth in kilometers. Use 3956 for miles
|
39 |
+
return round(c * r, 2)
|
40 |
+
|
41 |
+
def get_current_location(self) -> str:
|
42 |
+
"""
|
43 |
+
Returns the current location. ONLY use this if the user has not provided an explicit location in the query.
|
44 |
+
"""
|
45 |
+
try:
|
46 |
+
response = requests.get(f"http://ip-api.com/json/{self.client_ip}")
|
47 |
+
location_data = response.json()
|
48 |
+
city = location_data["city"]
|
49 |
+
region = location_data["regionName"]
|
50 |
+
country = location_data["countryCode"]
|
51 |
+
return f"{city}, {region}, {country}"
|
52 |
+
except:
|
53 |
+
return "San Francisco, California, US"
|
54 |
+
|
55 |
+
def sort_results(self, places : list, sort: str, descending: bool = True, first_n : int = None) -> List:
|
56 |
+
"""
|
57 |
+
Sorts the results by either 'distance', 'rating' or 'price'.
|
58 |
+
|
59 |
+
- places (list): The output list from the recommendations.
|
60 |
+
- sort (str): If set, sorts by either 'distance' or 'rating' or 'price'. ONLY supports 'distance' or 'rating' or 'price'.
|
61 |
+
- descending (bool): If descending is set, setting this boolean to true will sort the results such that the highest values are first.
|
62 |
+
- first_n (int): If provided, only retains the first n items in the final sorted list.
|
63 |
+
|
64 |
+
When people ask for 'closest' or 'nearest', sort by 'distance'.
|
65 |
+
When people ask for 'cheapest' or 'most expensive', sort by 'price'.
|
66 |
+
When people ask for 'best' or 'highest rated', sort by rating.
|
67 |
+
"""
|
68 |
+
|
69 |
+
if not sort:
|
70 |
+
return places
|
71 |
+
|
72 |
+
if sort == "price":
|
73 |
+
sort = "price_level"
|
74 |
+
|
75 |
+
items = sorted(
|
76 |
+
places,
|
77 |
+
key=lambda x: x.get(sort, float("inf")),
|
78 |
+
reverse=descending,
|
79 |
+
)
|
80 |
+
|
81 |
+
if first_n:
|
82 |
+
items = items[:first_n]
|
83 |
+
return items
|
84 |
+
|
85 |
+
def get_latitude_longitude(self, location: str) -> List:
|
86 |
+
"""
|
87 |
+
Given a city name, this function provides the latitude and longitude of the specific location.
|
88 |
+
|
89 |
+
- location: This can be a city like 'Austin', or a place like 'Austin Airport', etc.
|
90 |
+
"""
|
91 |
+
return self.gmaps.geocode(location)
|
92 |
+
|
93 |
+
def get_distance(self, place_1: str, place_2: str):
|
94 |
+
"""
|
95 |
+
Provides distance between two locations. Do NOT provide latitude longitude, but rather, provide the string descriptions.
|
96 |
+
|
97 |
+
Allows you to provide output from the get_recommendations API.
|
98 |
+
|
99 |
+
- place_1: The first location.
|
100 |
+
- place_2: The second location.
|
101 |
+
"""
|
102 |
+
if isinstance(place_1, list) and len(place_1) > 0:
|
103 |
+
place_1 = place_1[0]
|
104 |
+
|
105 |
+
if isinstance(place_2, list) and len(place_2) > 0:
|
106 |
+
place_2 = place_2[0]
|
107 |
+
|
108 |
+
latlong_1 = self.get_latitude_longitude(place_1)
|
109 |
+
latlong_2 = self.get_latitude_longitude(place_2)
|
110 |
+
|
111 |
+
if isinstance(place_1, dict):
|
112 |
+
place_1 = place_1["name"]
|
113 |
+
if isinstance(place_2, dict):
|
114 |
+
place_2 = place_2["name"]
|
115 |
+
|
116 |
+
if len(latlong_1) == 0 or len(latlong_2) == 0:
|
117 |
+
raise ValueError
|
118 |
+
|
119 |
+
latlong1 = latlong_1[0]["geometry"]["location"]
|
120 |
+
latlong2 = latlong_2[0]["geometry"]["location"]
|
121 |
+
|
122 |
+
dist = self.haversine(
|
123 |
+
latlong1["lng"], latlong1["lat"], latlong2["lng"], latlong2["lat"]
|
124 |
+
)
|
125 |
+
dist = dist * 0.621371
|
126 |
+
|
127 |
+
return [f"The distance between {place_1} and {place_2} is {dist:.3f} miles"]
|
128 |
+
|
129 |
+
def get_recommendations(self, topics: list, lat_long: tuple):
|
130 |
+
"""
|
131 |
+
Returns the recommendations for a specific topic that is of interest. Remember, a topic IS NOT an establishment. For establishments, please use another function.
|
132 |
+
|
133 |
+
- topics (list): A list of topics of interest to pull recommendations for. Can be multiple words.
|
134 |
+
- lat_long (tuple): The lat_long of interest.
|
135 |
+
"""
|
136 |
+
if len(lat_long) == 0:
|
137 |
+
return []
|
138 |
+
|
139 |
+
topic = " ".join(topics)
|
140 |
+
latlong = lat_long[0]["geometry"]["location"]
|
141 |
+
results = self.gmaps.places(
|
142 |
+
query=topic,
|
143 |
+
location=latlong,
|
144 |
+
)
|
145 |
+
return results["results"]
|
146 |
+
|
147 |
+
def find_places_near_location(
|
148 |
+
self, type_of_place: list, location: str, radius_miles: int = 50
|
149 |
+
) -> List[Dict]:
|
150 |
+
"""
|
151 |
+
Find places close to a very defined location.
|
152 |
+
|
153 |
+
- 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.
|
154 |
+
- location (str): The location for the search. This can be a city's name, region, or anything that specifies the location.
|
155 |
+
- radius_miles (int): Optional. The max distance from the described location to limit the search. Distance is specified in miles.
|
156 |
+
"""
|
157 |
+
# Get latitude and longitude for the location
|
158 |
+
verb_location = location
|
159 |
+
geocode_result = self.gmaps.geocode(location)
|
160 |
+
if geocode_result:
|
161 |
+
latlong = geocode_result[0]["geometry"]["location"]
|
162 |
+
location = (latlong["lat"], latlong["lng"])
|
163 |
+
else:
|
164 |
+
raise ValueError("Could not geocode the provided location.")
|
165 |
+
|
166 |
+
type_of_place = " ".join(type_of_place)
|
167 |
+
# Perform the search using Google Places API
|
168 |
+
places_result = self.gmaps.places_nearby(
|
169 |
+
location=location, keyword=type_of_place, radius=radius_miles * 1609.34
|
170 |
+
)
|
171 |
+
places = places_result.get("results", [])
|
172 |
+
new_places = []
|
173 |
+
for place in places:
|
174 |
+
place_location = place["geometry"]["location"]
|
175 |
+
distance = self.haversine(
|
176 |
+
latlong["lng"],
|
177 |
+
latlong["lat"],
|
178 |
+
place_location["lng"],
|
179 |
+
place_location["lat"],
|
180 |
+
)
|
181 |
+
if distance == 0.0:
|
182 |
+
continue
|
183 |
+
|
184 |
+
place["distance"] = f"{distance} kilometers from {verb_location}"
|
185 |
+
new_places.append(place)
|
186 |
+
|
187 |
+
places = new_places
|
188 |
+
if len(places) == 0:
|
189 |
+
return []
|
190 |
+
|
191 |
+
return self.sort_results(places, sort="distance", descending=False)
|
192 |
+
|
193 |
+
def get_some_reviews(self, place_names: list, location: str = None):
|
194 |
+
"""
|
195 |
+
Given an establishment (or place) name, return reviews about the establishment.
|
196 |
+
|
197 |
+
- place_names (list): The name of the establishment. This should be a physical location name. You can provide multiple inputs.
|
198 |
+
- location (str) : The location where the restaurant is located. Optional argument.
|
199 |
+
"""
|
200 |
+
all_reviews = []
|
201 |
+
for place_name in place_names:
|
202 |
+
if isinstance(place_name, str):
|
203 |
+
if location:
|
204 |
+
place_name += " , " + location
|
205 |
+
elif isinstance(place_name, dict) and "results" in place_name and "name" in place_name["results"]:
|
206 |
+
place_name = place_name["results"]["name"]
|
207 |
+
elif isinstance(place_name, dict) and "name" in place_name:
|
208 |
+
place_name = place_name["name"]
|
209 |
+
|
210 |
+
search_results = self.gmaps.places(place_name)
|
211 |
+
|
212 |
+
if not search_results.get("results"):
|
213 |
+
return []
|
214 |
+
|
215 |
+
# Assuming the first result is the most relevant
|
216 |
+
place_id = search_results["results"][0]["place_id"]
|
217 |
+
place_details = self.gmaps.place(place_id=place_id)
|
218 |
+
reviews = place_details["result"].get("reviews", [])
|
219 |
+
|
220 |
+
for review in reviews:
|
221 |
+
review["for_location"] = place_name
|
222 |
+
review["formatted_address"] = place_details["result"][
|
223 |
+
"formatted_address"
|
224 |
+
]
|
225 |
+
|
226 |
+
all_reviews.extend(reviews)
|
227 |
+
|
228 |
+
random.shuffle(all_reviews)
|
229 |
+
|
230 |
+
return all_reviews
|