razaAhmed commited on
Commit
5087ddc
Β·
1 Parent(s): 20b22f0

Upload 4 files

Browse files
Files changed (4) hide show
  1. README.md +12 -4
  2. requirements.txt +4 -0
  3. safarnama.py +337 -0
  4. style.css +80 -0
README.md CHANGED
@@ -1,10 +1,18 @@
1
  ---
2
  title: Safarnama
3
- emoji: πŸ’»
4
- colorFrom: green
5
- colorTo: indigo
6
  sdk: docker
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
1
  ---
2
  title: Safarnama
3
+ emoji: πŸš€
4
+ colorFrom: red
5
+ colorTo: pink
6
  sdk: docker
7
  pinned: false
8
+ license: mit
9
+ app_port: 7860
10
  ---
11
 
12
+ OpenAI Safarnama application with [Solara](https://solara.dev)
13
+ * [Solara GitHub](https://github.com/widgetti/solara/).
14
+ * [Deployed app](https://huggingface.co/spaces/solara-dev/wanderlust)
15
+
16
+
17
+ https://github.com/widgetti/wanderlust/assets/1765949/fe3db611-4f46-4ca3-b4c2-ace6d2b1493b
18
+
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # solara==1.22
2
+ solara @ https://github.com/widgetti/solara/archive/refs/heads/master.zip
3
+ openai
4
+ ipyleaflet==0.17.2
safarnama.py ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import time
4
+ from pathlib import Path
5
+
6
+ import ipyleaflet
7
+ from openai import NotFoundError, OpenAI
8
+ from openai.types.beta import Thread
9
+
10
+ import solara
11
+
12
+ HERE = Path(__file__).parent
13
+
14
+ center_default = (0, 0)
15
+ zoom_default = 2
16
+
17
+ messages = solara.reactive([])
18
+ zoom_level = solara.reactive(zoom_default)
19
+ center = solara.reactive(center_default)
20
+ markers = solara.reactive([])
21
+
22
+ url = ipyleaflet.basemaps.OpenStreetMap.Mapnik.build_url()
23
+ openai = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
24
+ model = "gpt-4-1106-preview"
25
+ app_style = (HERE / "style.css").read_text()
26
+
27
+
28
+ # Declare tools for openai assistant to use
29
+ tools = [
30
+ {
31
+ "type": "function",
32
+ "function": {
33
+ "name": "update_map",
34
+ "description": "Update map to center on a particular location",
35
+ "parameters": {
36
+ "type": "object",
37
+ "properties": {
38
+ "longitude": {
39
+ "type": "number",
40
+ "description": "Longitude of the location to center the map on",
41
+ },
42
+ "latitude": {
43
+ "type": "number",
44
+ "description": "Latitude of the location to center the map on",
45
+ },
46
+ "zoom": {
47
+ "type": "integer",
48
+ "description": "Zoom level of the map",
49
+ },
50
+ },
51
+ "required": ["longitude", "latitude", "zoom"],
52
+ },
53
+ },
54
+ },
55
+ {
56
+ "type": "function",
57
+ "function": {
58
+ "name": "add_marker",
59
+ "description": "Add marker to the map",
60
+ "parameters": {
61
+ "type": "object",
62
+ "properties": {
63
+ "longitude": {
64
+ "type": "number",
65
+ "description": "Longitude of the location to the marker",
66
+ },
67
+ "latitude": {
68
+ "type": "number",
69
+ "description": "Latitude of the location to the marker",
70
+ },
71
+ "label": {
72
+ "type": "string",
73
+ "description": "Text to display on the marker",
74
+ },
75
+ },
76
+ "required": ["longitude", "latitude", "label"],
77
+ },
78
+ },
79
+ },
80
+ ]
81
+
82
+
83
+ def update_map(longitude, latitude, zoom):
84
+ center.set((latitude, longitude))
85
+ zoom_level.set(zoom)
86
+ return "Map updated"
87
+
88
+
89
+ def add_marker(longitude, latitude, label):
90
+ markers.set(markers.value + [{"location": (latitude, longitude), "label": label}])
91
+ return "Marker added"
92
+
93
+
94
+ functions = {
95
+ "update_map": update_map,
96
+ "add_marker": add_marker,
97
+ }
98
+
99
+
100
+ def assistant_tool_call(tool_call):
101
+ # actually executes the tool call the OpenAI assistant wants to perform
102
+ function = tool_call.function
103
+ name = function.name
104
+ arguments = json.loads(function.arguments)
105
+ return_value = functions[name](**arguments)
106
+ tool_outputs = {
107
+ "tool_call_id": tool_call.id,
108
+ "output": return_value,
109
+ }
110
+ return tool_outputs
111
+
112
+
113
+ @solara.component
114
+ def Map():
115
+ ipyleaflet.Map.element( # type: ignore
116
+ zoom=zoom_level.value,
117
+ center=center.value,
118
+ scroll_wheel_zoom=True,
119
+ layers=[
120
+ ipyleaflet.TileLayer.element(url=url),
121
+ *[
122
+ ipyleaflet.Marker.element(location=k["location"], draggable=False)
123
+ for k in markers.value
124
+ ],
125
+ ],
126
+ )
127
+
128
+
129
+ @solara.component
130
+ def ChatMessage(message):
131
+ with solara.Row(style={"align-items": "flex-start"}):
132
+ # Catch "messages" that are actually tool calls
133
+ if isinstance(message, dict):
134
+ icon = "mdi-map" if message["output"] == "Map updated" else "mdi-map-marker"
135
+ solara.v.Icon(children=[icon], style_="padding-top: 10px;")
136
+ solara.Markdown(message["output"])
137
+ elif message.role == "user":
138
+ solara.Text(message.content[0].text.value, style={"font-weight": "bold;"})
139
+ elif message.role == "assistant":
140
+ if message.content[0].text.value:
141
+ solara.v.Icon(
142
+ children=["mdi-compass-outline"], style_="padding-top: 10px;"
143
+ )
144
+ solara.Markdown(message.content[0].text.value)
145
+ elif message.content.tool_calls:
146
+ solara.v.Icon(children=["mdi-map"], style_="padding-top: 10px;")
147
+ solara.Markdown("*Calling map functions*")
148
+ else:
149
+ solara.v.Icon(
150
+ children=["mdi-compass-outline"], style_="padding-top: 10px;"
151
+ )
152
+ solara.Preformatted(repr(message))
153
+ else:
154
+ solara.v.Icon(children=["mdi-compass-outline"], style_="padding-top: 10px;")
155
+ solara.Preformatted(repr(message))
156
+
157
+
158
+ @solara.component
159
+ def ChatBox(children=[]):
160
+ # this uses a flexbox with column-reverse to reverse the order of the messages
161
+ # if we now also reverse the order of the messages, we get the correct order
162
+ # but the scroll position is at the bottom of the container automatically
163
+ with solara.Column(style={"flex-grow": "1"}):
164
+ solara.Style(
165
+ """
166
+ .chat-box > :last-child{
167
+ padding-top: 7.5vh;
168
+ }
169
+ """
170
+ )
171
+ # The height works effectively as `min-height`, since flex will grow the container to fill the available space
172
+ solara.Column(
173
+ style={
174
+ "flex-grow": "1",
175
+ "overflow-y": "auto",
176
+ "height": "100px",
177
+ "flex-direction": "column-reverse",
178
+ },
179
+ classes=["chat-box"],
180
+ children=list(reversed(children)),
181
+ )
182
+
183
+
184
+ @solara.component
185
+ def ChatInterface():
186
+ prompt = solara.use_reactive("")
187
+ run_id: solara.Reactive[str] = solara.use_reactive(None)
188
+
189
+ # Create a thread to hold the conversation only once when this component is created
190
+ thread: Thread = solara.use_memo(openai.beta.threads.create, dependencies=[])
191
+
192
+ def add_message(value: str):
193
+ if value == "":
194
+ return
195
+ prompt.set("")
196
+ new_message = openai.beta.threads.messages.create(
197
+ thread_id=thread.id, content=value, role="user"
198
+ )
199
+ messages.set([*messages.value, new_message])
200
+ # this creates a new run for the thread
201
+ # also also triggers a rerender (since run_id.value changes)
202
+ # which will trigger the poll function blow to start in a thread
203
+ run_id.value = openai.beta.threads.runs.create(
204
+ thread_id=thread.id,
205
+ assistant_id="asst_S4GeTGmtyZZCLH1u6cbCGkKe",
206
+ tools=tools,
207
+ ).id
208
+
209
+ def poll():
210
+ if not run_id.value:
211
+ return
212
+ completed = False
213
+ while not completed:
214
+ try:
215
+ run = openai.beta.threads.runs.retrieve(
216
+ run_id.value, thread_id=thread.id
217
+ )
218
+ # Above will raise NotFoundError when run creation is still in progress
219
+ except NotFoundError:
220
+ continue
221
+ if run.status == "requires_action":
222
+ tool_outputs = []
223
+ for tool_call in run.required_action.submit_tool_outputs.tool_calls:
224
+ tool_output = assistant_tool_call(tool_call)
225
+ tool_outputs.append(tool_output)
226
+ messages.set([*messages.value, tool_output])
227
+ openai.beta.threads.runs.submit_tool_outputs(
228
+ thread_id=thread.id,
229
+ run_id=run_id.value,
230
+ tool_outputs=tool_outputs,
231
+ )
232
+ if run.status == "completed":
233
+ messages.set(
234
+ [
235
+ *messages.value,
236
+ openai.beta.threads.messages.list(thread.id).data[0],
237
+ ]
238
+ )
239
+ run_id.set(None)
240
+ completed = True
241
+ time.sleep(0.1)
242
+
243
+ # run/restart a thread any time the run_id changes
244
+ result = solara.use_thread(poll, dependencies=[run_id.value])
245
+
246
+ # Create DOM for chat interface
247
+ with solara.Column(classes=["chat-interface"]):
248
+ if len(messages.value) > 0:
249
+ with ChatBox():
250
+ for message in messages.value:
251
+ ChatMessage(message)
252
+
253
+ with solara.Column():
254
+ solara.InputText(
255
+ label="Where do you want to go?"
256
+ if len(messages.value) == 0
257
+ else "Ask more question here",
258
+ value=prompt,
259
+ style={"flex-grow": "1"},
260
+ on_value=add_message,
261
+ disabled=result.state == solara.ResultState.RUNNING,
262
+ )
263
+ solara.ProgressLinear(result.state == solara.ResultState.RUNNING)
264
+ if result.state == solara.ResultState.ERROR:
265
+ solara.Error(repr(result.error))
266
+
267
+
268
+ @solara.component
269
+ def Page():
270
+ with solara.Column(
271
+ classes=["ui-container"],
272
+ gap="5vh",
273
+ ):
274
+ with solara.Row(justify="space-between"):
275
+ with solara.Row(gap="10px", style={"align-items": "center"}):
276
+ solara.v.Icon(children=["mdi-compass-rose"], size="36px")
277
+ solara.HTML(
278
+ tag="h2",
279
+ unsafe_innerHTML="SafarNama",
280
+ style={"display": "inline-block"},
281
+ )
282
+ with solara.Row(
283
+ gap="30px",
284
+ style={"align-items": "center"},
285
+ classes=["link-container"],
286
+ justify="end",
287
+ ):
288
+ with solara.Row(gap="5px", style={"align-items": "center"}):
289
+ solara.Text("Source Code:", style="font-weight: bold;")
290
+ # target="_blank" links are still easiest to do via ipyvuetify
291
+ with solara.v.Btn(
292
+ icon=True,
293
+ tag="a",
294
+ attributes={
295
+ "href": "https://github.com/widgetti/wanderlust",
296
+ "title": "Wanderlust Source Code",
297
+ "target": "_blank",
298
+ },
299
+ ):
300
+ solara.v.Icon(children=["mdi-github-circle"])
301
+ with solara.Row(gap="5px", style={"align-items": "center"}):
302
+ solara.Text("Powered by Solara:", style="font-weight: bold;")
303
+ with solara.v.Btn(
304
+ icon=True,
305
+ tag="a",
306
+ attributes={
307
+ "href": "https://solara.dev/",
308
+ "title": "Solara",
309
+ "target": "_blank",
310
+ },
311
+ ):
312
+ solara.HTML(
313
+ tag="img",
314
+ attributes={
315
+ "src": "https://solara.dev/static/public/logo.svg",
316
+ "width": "24px",
317
+ },
318
+ )
319
+ with solara.v.Btn(
320
+ icon=True,
321
+ tag="a",
322
+ attributes={
323
+ "href": "https://github.com/widgetti/solara",
324
+ "title": "Solara Source Code",
325
+ "target": "_blank",
326
+ },
327
+ ):
328
+ solara.v.Icon(children=["mdi-github-circle"])
329
+
330
+ with solara.Row(
331
+ justify="space-between", style={"flex-grow": "1"}, classes=["container-row"]
332
+ ):
333
+ ChatInterface()
334
+ with solara.Column(classes=["map-container"]):
335
+ Map()
336
+
337
+ solara.Style(app_style)
style.css ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .jupyter-widgets.leaflet-widgets {
2
+ height: 100%;
3
+ border-radius: 20px;
4
+ }
5
+
6
+ .solara-autorouter-content {
7
+ display: flex;
8
+ flex-direction: column;
9
+ justify-content: stretch;
10
+ }
11
+
12
+ .v-toolbar__title {
13
+ display: flex;
14
+ align-items: center;
15
+ column-gap: 0.5rem;
16
+ }
17
+
18
+ .ui-container {
19
+ height: 95vh;
20
+ justify-content: center;
21
+ padding: 45px 50px 75px 50px;
22
+ }
23
+
24
+ .chat-interface {
25
+ height: 100%;
26
+ width: 38vw;
27
+ justify-content: center;
28
+ position: relative;
29
+ }
30
+
31
+ .chat-interface:after {
32
+ content: "";
33
+ position: absolute;
34
+ z-index: 1;
35
+ top: 0;
36
+ left: 0;
37
+ pointer-events: none;
38
+ background-image: linear-gradient(to top, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1) 100%);
39
+ width: 100%;
40
+ height: 15%;
41
+ }
42
+
43
+ .map-container {
44
+ width: 50vw;
45
+ height: 100%;
46
+ justify-content: center;
47
+ }
48
+
49
+ .v-application--wrap > div:nth-child(2) > div:nth-child(2){
50
+ display: none !important;
51
+ }
52
+
53
+ @media screen and (max-aspect-ratio: 1/1) {
54
+ .ui-container {
55
+ padding: 30px;
56
+ height: 100vh;
57
+ }
58
+
59
+ .container-row {
60
+ flex-direction: column-reverse !important;
61
+ width: 100% !important;
62
+ }
63
+
64
+ .chat-interface {
65
+ width: unset;
66
+ justify-content: flex-end;
67
+ }
68
+
69
+ .map-container {
70
+ width: unset;
71
+ }
72
+
73
+ .link-container{
74
+ position: absolute;
75
+ bottom: 0;
76
+ left: 0;
77
+ width: 100%;
78
+ padding: 0 30px;
79
+ }
80
+ }