brian-yu-nexusflow commited on
Commit
5321b2e
β€’
1 Parent(s): f85ccfd

Upload 7 files

Browse files
Files changed (8) hide show
  1. .gitattributes +1 -0
  2. NexusRaven.png +3 -0
  3. config.py +22 -0
  4. constants.py +73 -0
  5. logo.png +0 -0
  6. raven_demo.py +515 -0
  7. requirements.txt +3 -0
  8. 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

  • SHA256: 5a1863ef0e85b87c0dc177ffb2bc419757263f8ee0358dc08e6d04f8aea097e6
  • Pointer size: 132 Bytes
  • Size of remote file: 1.71 MB
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&amp;height=600&amp;hl=en&amp;q={location}+{address}&amp;t=&amp;z=18&amp;ie=UTF8&amp;iwloc=B&amp;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