sjw commited on
Commit
897bfdc
·
1 Parent(s): 83cbb27

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +846 -0
app.py ADDED
@@ -0,0 +1,846 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Standard Library Imports
2
+ import os
3
+ import random
4
+ import re
5
+ import time
6
+ from urllib.parse import urlparse, parse_qs
7
+
8
+ # Third-Party Imports
9
+ import gradio as gr
10
+ import lyricsgenius
11
+ import requests
12
+ import spotipy
13
+ from bs4 import BeautifulSoup
14
+ from dotenv import load_dotenv
15
+ from fuzzywuzzy import fuzz
16
+ from pydantic import BaseModel, Field
17
+ from requests.exceptions import Timeout
18
+ from sentence_transformers import SentenceTransformer
19
+ from sklearn.metrics.pairwise import cosine_similarity
20
+ from spotipy.exceptions import SpotifyException
21
+
22
+ # Local Application/Library Specific Imports
23
+ from langchain.agents import OpenAIFunctionsAgent, AgentExecutor, tool
24
+ from langchain.chat_models import ChatOpenAI
25
+ from langchain.memory import ConversationBufferMemory
26
+ from langchain.prompts import MessagesPlaceholder
27
+ from langchain.schema import SystemMessage, HumanMessage
28
+ from messages import SYSTEM_MESSAGE, GENRE_LIST
29
+
30
+ from dotenv import load_dotenv
31
+ load_dotenv()
32
+
33
+
34
+ # ------------------------------
35
+ # Section: Global Vars
36
+ # ------------------------------
37
+
38
+
39
+ GENIUS_TOKEN = os.getenv("GENIUS_ACCESS_TOKEN")
40
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
41
+
42
+ DEBUG_MODE = True
43
+ def debug_print(*args, **kwargs):
44
+ if DEBUG_MODE:
45
+ print(*args, **kwargs)
46
+
47
+ THEME = gr.themes.Default(
48
+ primary_hue=gr.themes.colors.red,
49
+ secondary_hue=gr.themes.colors.pink,
50
+ font=[gr.themes.GoogleFont("Inconsolata"), "Arial", "sans-serif"],
51
+ spacing_size=gr.themes.sizes.spacing_sm,
52
+ radius_size=gr.themes.sizes.radius_sm
53
+ )#.set(body_background_fill="#FFFFFF")
54
+
55
+ # TODO: switch to personal website
56
+ REDIRECT_URI = "https://huggingface.co/sjw"
57
+
58
+ # Spotify functions
59
+ SCOPE = [
60
+ 'user-library-read',
61
+ 'user-read-playback-state',
62
+ 'user-modify-playback-state',
63
+ 'playlist-modify-public',
64
+ 'user-top-read'
65
+ ]
66
+
67
+ MOOD_SETTINGS = {
68
+ "happy": {"max_instrumentalness": 0.001, "min_valence": 0.6},
69
+ "sad": {"max_danceability": 0.65, "max_valence": 0.4},
70
+ "energetic": {"min_tempo": 120, "min_danceability": 0.75},
71
+ "calm": {"max_energy": 0.65, "max_tempo": 130}
72
+ }
73
+
74
+ # genre + mood function
75
+ NUM_ARTISTS = 20 # artists to retrieve from user's top artists
76
+ TIME_RANGE = "medium_term" # short, medium, long
77
+ NUM_TRACKS = 10 # tracks to add to playback
78
+ MAX_ARTISTS = 4 # sp.recommendations() seeds: 4/5 artists, 1/5 genre
79
+
80
+ # artist + mood function
81
+ NUM_ALBUMS = 20 # maximum number of albums to retrieve from an artist
82
+ MAX_TRACKS = 10 # tracks to randomly select from an artist
83
+
84
+ # matching playlists + moods
85
+ MODEL = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2') # smaller BERT
86
+ os.environ["TOKENIZERS_PARALLELISM"] = "false" # warning
87
+ MOOD_LIST = ["happy", "sad", "energetic", "calm"]
88
+ MOOD_EMBEDDINGS = MODEL.encode(MOOD_LIST)
89
+ GENRE_EMBEDDINGS = MODEL.encode(GENRE_LIST)
90
+
91
+ # agent tools
92
+ RETURN_DIRECT = True
93
+
94
+ MODEL = "gpt-3.5-turbo-0613"
95
+
96
+ THEMES = ["Epic", "Hypnotic", "Dreamy", "Legendary", "Majestic",
97
+ "Enchanting", "Ethereal", "Super Lit", "Harmonious", "Heroic"]
98
+
99
+
100
+ with gr.Blocks(theme=THEME) as auth_page:
101
+
102
+ # ------------------------------
103
+ # Section: Spotify Authentication
104
+ # ------------------------------
105
+
106
+
107
+ ACCESS_TOKEN_VAR = gr.State()
108
+ AGENT_EXECUTOR_VAR = gr.State()
109
+
110
+
111
+ client_id = gr.Textbox(placeholder="5. Paste Spotify Client ID here, then click the button below", container=False, text_align="center")
112
+ generate_link = gr.Button("6. Get Authentication Link")
113
+ display_link = gr.Markdown()
114
+ url = gr.Textbox(placeholder="7. Paste entire URL here, then click the button below", container=False, text_align="center")
115
+ authorize_url = gr.Button("8. Authorize URL")
116
+ auth_result = gr.Markdown()
117
+
118
+
119
+ def spotify_auth(client_id, url=None, access_tokens=None):
120
+ """
121
+ Authenticate Spotify with the provided client_id and url.
122
+ """
123
+ if url:
124
+ parsed_url = urlparse(url)
125
+ fragment = parsed_url.fragment
126
+ access_token = parse_qs(fragment)['access_token'][0]
127
+ print(access_token)
128
+
129
+ return access_token, """<span style="font-size:18px;">Authentication Success.</span>"""
130
+
131
+ else:
132
+ auth_url = (
133
+ f"https://accounts.spotify.com/authorize?response_type=token&client_id={client_id}"
134
+ f"&scope={'%20'.join(SCOPE)}&redirect_uri={REDIRECT_URI}"
135
+ )
136
+
137
+ return {
138
+ display_link: ("""<span style="font-size:18px;">Authorize by clicking <strong><a href='""" + f"{auth_url}" +
139
+ """' target="_blank">here</a></strong> and copy the '<strong>entire URL</strong>' you are redirected to</span>""")
140
+ }
141
+
142
+
143
+ generate_link.click(spotify_auth, inputs=[client_id], outputs=display_link)
144
+ authorize_url.click(spotify_auth, inputs=[client_id, url, ACCESS_TOKEN_VAR], outputs=[ACCESS_TOKEN_VAR, auth_result])
145
+
146
+ create_agent_button = gr.Button("Create Apollo")
147
+
148
+ def create_agent(access_token):
149
+
150
+
151
+ llm = ChatOpenAI(openai_api_key=OPENAI_API_KEY, max_retries=3, temperature=0, model=MODEL)
152
+
153
+
154
+ # ------------------------------
155
+ # Section: Spotify Functions
156
+ # ------------------------------
157
+
158
+
159
+ sp = spotipy.Spotify(auth=access_token)
160
+ device_id = sp.devices()['devices'][0]['id']
161
+
162
+
163
+ def find_track_by_name(track_name):
164
+ """
165
+ Finds the Spotify track URI given the track name.
166
+ """
167
+ results = sp.search(q=track_name, type='track')
168
+ track_uri = results['tracks']['items'][0]['uri']
169
+ return track_uri
170
+
171
+
172
+ def play_track_by_name(track_name):
173
+ """
174
+ Plays a track given its name. Uses the above function.
175
+ """
176
+ track_uri = find_track_by_name(track_name)
177
+ track_name = sp.track(track_uri)["name"]
178
+ artist_name = sp.track(track_uri)['artists'][0]['name']
179
+
180
+ try:
181
+ sp.start_playback(device_id=device_id, uris=[track_uri])
182
+ return f"♫ Now playing {track_name} by {artist_name} ♫"
183
+ except SpotifyException as e:
184
+ return f"An error occurred with Spotify: {e}. \n\n**Remember to wake up Spotify.**"
185
+ except Exception as e:
186
+ return f"An unexpected error occurred: {e}."
187
+
188
+
189
+ def queue_track_by_name(track_name):
190
+ """
191
+ Queues track given its name.
192
+ """
193
+ track_uri = find_track_by_name(track_name)
194
+ track_name = sp.track(track_uri)["name"]
195
+ sp.add_to_queue(uri=track_uri, device_id=device_id)
196
+ return f"♫ Added {track_name} to your queue ♫"
197
+
198
+
199
+ def pause_track():
200
+ """
201
+ Pauses the current playback.
202
+ """
203
+ sp.pause_playback(device_id=device_id)
204
+ return "♫ Playback paused ♫"
205
+
206
+
207
+ def resume_track():
208
+ """
209
+ Resumes the current playback.
210
+ """
211
+ sp.start_playback(device_id=device_id)
212
+ return "♫ Playback started ♫"
213
+
214
+
215
+ def skip_track():
216
+ """
217
+ Skips the current playback.
218
+ """
219
+ sp.next_track(device_id=device_id)
220
+ return "♫ Skipped to your next track ♫"
221
+
222
+
223
+ ### ### ### More Elaborate Functions ### ### ###
224
+
225
+
226
+ def play_album_by_name_and_artist(album_name, artist_name):
227
+ """
228
+ Plays an album given its name and the artist.
229
+ context_uri (provide a context_uri to start playback of an album, artist, or playlist) expects a string.
230
+ """
231
+ results = sp.search(q=f'{album_name} {artist_name}', type='album')
232
+ album_id = results['albums']['items'][0]['id']
233
+ album_info = sp.album(album_id)
234
+ album_name = album_info['name']
235
+ artist_name = album_info['artists'][0]['name']
236
+
237
+ try:
238
+ sp.start_playback(device_id=device_id, context_uri=f'spotify:album:{album_id}')
239
+ return f"♫ Now playing {album_name} by {artist_name} ♫"
240
+ except spotipy.SpotifyException as e:
241
+ return f"An error occurred with Spotify: {e}. \n\n**Remember to wake up Spotify.**"
242
+ except Timeout:
243
+ return f"An unexpected error occurred: {e}."
244
+
245
+
246
+ def play_playlist_by_name(playlist_name):
247
+ """
248
+ Plays an existing playlist in the user's library given its name.
249
+ """
250
+ playlists = sp.current_user_playlists()
251
+ playlist_dict = {playlist['name']: (playlist['id'], playlist['owner']['display_name']) for playlist in playlists['items']}
252
+ playlist_names = [key for key in playlist_dict.keys()]
253
+
254
+ # defined inside to capture user-specific playlists
255
+ playlist_name_embeddings = MODEL.encode(playlist_names)
256
+ user_playlist_embedding = MODEL.encode([playlist_name])
257
+
258
+ # compares (embedded) given name to (embedded) playlist library and outputs the closest match
259
+ similarity_scores = cosine_similarity(user_playlist_embedding, playlist_name_embeddings)
260
+ most_similar_index = similarity_scores.argmax()
261
+ playlist_name = playlist_names[most_similar_index]
262
+
263
+ try:
264
+ playlist_id, creator_name = playlist_dict[playlist_name]
265
+ sp.start_playback(device_id=device_id, context_uri=f'spotify:playlist:{playlist_id}')
266
+ return f'♫ Now playing {playlist_name} by {creator_name} ♫'
267
+ except:
268
+ return "Unable to find playlist. Please try again."
269
+
270
+
271
+ def get_track_info():
272
+ """
273
+ Harvests information for explain_track() using Genius' API and basic webscraping.
274
+ """
275
+ current_track_item = sp.current_user_playing_track()['item']
276
+ track_name = current_track_item['name']
277
+ artist_name = current_track_item['artists'][0]['name']
278
+ album_name = current_track_item['album']['name']
279
+ release_date = current_track_item['album']['release_date']
280
+ basic_info = {
281
+ 'track_name': track_name,
282
+ 'artist_name': artist_name,
283
+ 'album_name': album_name,
284
+ 'release_date': release_date,
285
+ }
286
+
287
+ # define inside to avoid user conflicts (simultaneously query Genius)
288
+ genius = lyricsgenius.Genius(GENIUS_TOKEN)
289
+ # removing feature information from song titles to avoid scewing search
290
+ track_name = re.split(' \(with | \(feat\. ', track_name)[0]
291
+ result = genius.search_song(track_name, artist_name)
292
+
293
+ # if no Genius page exists
294
+ if result is not None and hasattr(result, 'artist'):
295
+ genius_artist = result.artist.lower().replace(" ", "")
296
+ spotify_artist = artist_name.lower().replace(" ", "")
297
+ debug_print(spotify_artist)
298
+ debug_print(genius_artist)
299
+ if spotify_artist not in genius_artist:
300
+ return basic_info, None, None, None
301
+ else:
302
+ genius_artist = None
303
+ return basic_info, None, None, None
304
+
305
+ # if Genius page exists
306
+ lyrics = result.lyrics
307
+ url = result.url
308
+ response = requests.get(url)
309
+
310
+ # parsing the webpage and locating 'About' section
311
+ soup = BeautifulSoup(response.text, 'html.parser')
312
+ # universal 'About' section element across all Genius song lyrics pages
313
+ about_section = soup.select_one('div[class^="RichText__Container-oz284w-0"]')
314
+
315
+ # if no 'About' section exists
316
+ if not about_section:
317
+ return basic_info, None, lyrics, url
318
+
319
+ # if 'About' section exists
320
+ else:
321
+ about_section = about_section.get_text(separator='\n')
322
+ return basic_info, about_section, lyrics, url
323
+
324
+
325
+ def explain_track():
326
+ """
327
+ Displays track information in an organized, informational, and compelling manner.
328
+ Uses the above function.
329
+ """
330
+
331
+
332
+ basic_info, about_section, lyrics, url = get_track_info()
333
+ debug_print(basic_info, about_section, lyrics, url)
334
+
335
+ if lyrics: # if Genius page exists
336
+ system_message_content = """
337
+ Your task is to create an engaging summary for a track using the available details
338
+ about the track and its lyrics. If there's insufficient or no additional information
339
+ besides the lyrics, craft the entire summary based solely on the lyrical content."
340
+ """
341
+ human_message_content = f"{about_section}\n\n{lyrics}"
342
+ messages = [
343
+ SystemMessage(content=system_message_content),
344
+ HumanMessage(content=human_message_content)
345
+ ]
346
+ ai_response = llm(messages).content
347
+ summary = f"""
348
+ **Name:** <span style="color: red; font-weight: bold; font-style: italic;">{basic_info["track_name"]}</span>
349
+ **Artist:** {basic_info["artist_name"]}
350
+ **Album:** {basic_info["album_name"]}
351
+ **Release:** {basic_info["release_date"]}
352
+
353
+ **About:**
354
+ {ai_response}
355
+
356
+ <a href='{url}'>Click here for more information on Genius!</a>
357
+ """
358
+ return summary
359
+
360
+ else: # if no Genius page exists
361
+ url = "https://genius.com/Genius-how-to-add-songs-to-genius-annotated"
362
+ summary = f"""
363
+ **Name:** <span style="color: red; font-weight: bold; font-style: italic;">{basic_info["track_name"]}</span>
364
+ **Artist:** {basic_info["artist_name"]}
365
+ **Album:** {basic_info["album_name"]}
366
+ **Release:** {basic_info["release_date"]}
367
+
368
+ **About:**
369
+ Unfortunately, this track has not been uploaded to Genius.com
370
+
371
+ <a href='{url}'>Be the first to change that!</a>
372
+ """
373
+ return summary
374
+
375
+
376
+ ### ### ### Genre + Mood ### ### ###
377
+
378
+
379
+ def get_user_mood(user_mood):
380
+ """
381
+ Categorizes the user's mood as either 'happy', 'sad', 'energetic', or 'calm'.
382
+ Uses same cosine similarity/embedding concepts as with determining playlist names.
383
+ """
384
+ if user_mood.lower() in MOOD_LIST:
385
+ user_mood = user_mood.lower()
386
+ return user_mood
387
+ else:
388
+ user_mood_embedding = MODEL.encode([user_mood.lower()])
389
+ similarity_scores = cosine_similarity(user_mood_embedding, MOOD_EMBEDDINGS)
390
+ most_similar_index = similarity_scores.argmax()
391
+ user_mood = MOOD_LIST[most_similar_index]
392
+ return user_mood
393
+
394
+
395
+ def get_genre_by_name(genre_name):
396
+ """
397
+ Matches user's desired genre to closest (most similar) existing genre in the list of genres.
398
+ recommendations() only accepts genres from this list.
399
+ """
400
+ if genre_name.lower() in GENRE_LIST:
401
+ genre_name = genre_name.lower()
402
+ return genre_name
403
+ else:
404
+ genre_name_embedding = MODEL.encode([genre_name.lower()])
405
+ similarity_scores = cosine_similarity(genre_name_embedding, GENRE_EMBEDDINGS)
406
+ most_similar_index = similarity_scores.argmax()
407
+ genre_name = GENRE_LIST[most_similar_index]
408
+ return genre_name
409
+
410
+
411
+ def is_genre_match(genre1, genre2, threshold=75):
412
+ """
413
+ Determines if two genres are semantically similar.
414
+ token_set_ratio() - for quantifying semantic similarity - and
415
+ threshold of 75 (out of 100) were were arbitrarily determined through basic testing.
416
+ """
417
+ score = fuzz.token_set_ratio(genre1, genre2)
418
+ debug_print(score)
419
+ return score >= threshold
420
+
421
+
422
+ def create_track_list_str(track_uris):
423
+ """
424
+ Creates an organized list of track names.
425
+ Used in final return statements by functions below.
426
+ """
427
+ track_details = sp.tracks(track_uris)
428
+ track_names_with_artists = [f"{track['name']} by {track['artists'][0]['name']}" for track in track_details['tracks']]
429
+ track_list_str = "<br>".join(track_names_with_artists)
430
+ return track_list_str
431
+
432
+
433
+ def play_genre_by_name_and_mood(genre_name, user_mood):
434
+ """
435
+ 1. Retrieves user's desired genre and current mood.
436
+ 2. Matches genre and mood to existing options.
437
+ 3. Gets 4 of user's top artists that align with genre.
438
+ 4. Conducts personalized recommendations() search.
439
+ 5. Plays selected track, clears the queue, and adds the rest to the now-empty queue.
440
+ """
441
+ genre_name = get_genre_by_name(genre_name)
442
+ user_mood = get_user_mood(user_mood).lower()
443
+ debug_print(genre_name)
444
+ debug_print(user_mood)
445
+
446
+ # increased personalization
447
+ user_top_artists = sp.current_user_top_artists(limit=NUM_ARTISTS, time_range=TIME_RANGE)
448
+ matching_artists_ids = []
449
+
450
+ for artist in user_top_artists['items']:
451
+ debug_print(artist['genres'])
452
+ for artist_genre in artist['genres']:
453
+ if is_genre_match(genre_name, artist_genre):
454
+ matching_artists_ids.append(artist['id'])
455
+ break # don't waste time cycling artist genres after match
456
+ if len(matching_artists_ids) == MAX_ARTISTS:
457
+ break
458
+
459
+ if not matching_artists_ids:
460
+ matching_artists_ids = None
461
+ else:
462
+ artist_names = [artist['name'] for artist in sp.artists(matching_artists_ids)['artists']]
463
+ debug_print(artist_names)
464
+ debug_print(matching_artists_ids)
465
+
466
+ recommendations = sp.recommendations( # accepts maximum {genre + artists} = 5 seeds
467
+ seed_artists=matching_artists_ids,
468
+ seed_genres=[genre_name],
469
+ seed_tracks=None,
470
+ limit=NUM_TRACKS, # number of tracks to return
471
+ country=None,
472
+ **MOOD_SETTINGS[user_mood]) # maps to mood settings dictionary
473
+
474
+ track_uris = [track['uri'] for track in recommendations['tracks']]
475
+ track_list_str = create_track_list_str(track_uris)
476
+ sp.start_playback(device_id=device_id, uris=track_uris)
477
+
478
+ return f"""
479
+ **♫ Now Playing:** <span style="color: red; font-weight: bold; font-style: italic;">{genre_name}</span> ♫
480
+
481
+ **Selected Tracks:**
482
+ {track_list_str}
483
+ """
484
+
485
+
486
+ ### ### ### Artist + Mood ### ### ###
487
+
488
+
489
+ def play_artist_by_name_and_mood(artist_name, user_mood):
490
+ """
491
+ Plays tracks (randomly selected) by a given artist that matches the user's mood.
492
+ """
493
+ user_mood = get_user_mood(user_mood).lower()
494
+ debug_print(user_mood)
495
+
496
+ # retrieving and shuffling all artist's tracks
497
+ first_name = artist_name.split(',')[0].strip()
498
+ results = sp.search(q=first_name, type='artist')
499
+ artist_id = results['artists']['items'][0]['id']
500
+ # most recent albums retrieved first
501
+ artist_albums = sp.artist_albums(artist_id, album_type='album', limit=NUM_ALBUMS)
502
+ artist_tracks = []
503
+ for album in artist_albums['items']:
504
+ album_tracks = sp.album_tracks(album['id'])['items']
505
+ artist_tracks.extend(album_tracks)
506
+ random.shuffle(artist_tracks)
507
+
508
+ # filtering until we find enough (MAX_TRACKS) tracks that match user's mood
509
+ selected_tracks = []
510
+ for track in artist_tracks:
511
+ if len(selected_tracks) == MAX_TRACKS:
512
+ break
513
+ features = sp.audio_features([track['id']])[0]
514
+ mood_criteria = MOOD_SETTINGS[user_mood]
515
+
516
+ match = True
517
+ for criteria, threshold in mood_criteria.items():
518
+ if "min_" in criteria and features[criteria[4:]] < threshold:
519
+ match = False
520
+ break
521
+ elif "max_" in criteria and features[criteria[4:]] > threshold:
522
+ match = False
523
+ break
524
+ if match:
525
+ debug_print(f"{features}\n{mood_criteria}\n\n")
526
+ selected_tracks.append(track)
527
+
528
+ track_names = [track['name'] for track in selected_tracks]
529
+ track_list_str = "<br>".join(track_names) # using HTML line breaks for each track name
530
+ debug_print(track_list_str)
531
+ track_uris = [track['uri'] for track in selected_tracks]
532
+ sp.start_playback(device_id=device_id, uris=track_uris)
533
+
534
+ return f"""
535
+ **♫ Now Playing:** <span style="color: red; font-weight: bold; font-style: italic;">{artist_name}</span> ♫
536
+
537
+ **Selected Tracks:**
538
+ {track_list_str}
539
+ """
540
+
541
+
542
+ ### ### ### Recommendations ### ### ###
543
+
544
+
545
+ def recommend_tracks(genre_name=None, artist_name=None, track_name=None, user_mood=None):
546
+ """
547
+ 1. Retrieves user's preferences based on artist_name, track_name, genre_name, and/or user_mood.
548
+ 2. Uses these parameters to conduct personalized recommendations() search.
549
+ 3. Returns the track URIs of (NUM_TRACKS) recommendation tracks.
550
+ """
551
+ user_mood = get_user_mood(user_mood).lower() if user_mood else None
552
+ debug_print(user_mood)
553
+
554
+ seed_genre, seed_artist, seed_track = None, None, None
555
+
556
+ if genre_name:
557
+ first_name = genre_name.split(',')[0].strip()
558
+ genre_name = get_genre_by_name(first_name)
559
+ seed_genre = [genre_name]
560
+ debug_print(seed_genre)
561
+
562
+ if artist_name:
563
+ first_name = artist_name.split(',')[0].strip() # if user provides multiple artists, use the first
564
+ results = sp.search(q='artist:' + first_name, type='artist')
565
+ seed_artist = [results['artists']['items'][0]['id']]
566
+
567
+ if track_name:
568
+ first_name = track_name.split(',')[0].strip()
569
+ results = sp.search(q='track:' + first_name, type='track')
570
+ seed_track = [results['tracks']['items'][0]['id']]
571
+
572
+ # if user requests recommendations without specifying anything but their mood
573
+ # this is because recommendations() requires at least one seed
574
+ if seed_genre is None and seed_artist is None and seed_track is None:
575
+ raise ValueError("At least one genre, artist, or track must be provided.")
576
+
577
+ recommendations = sp.recommendations( # passing in 3 seeds
578
+ seed_artists=seed_artist,
579
+ seed_genres=seed_genre,
580
+ seed_tracks=seed_track,
581
+ limit=NUM_TRACKS,
582
+ country=None,
583
+ **MOOD_SETTINGS[user_mood] if user_mood else {})
584
+
585
+ track_uris = [track['uri'] for track in recommendations['tracks']]
586
+ return track_uris
587
+
588
+
589
+ def play_recommended_tracks(genre_name=None, artist_name=None, track_name=None, user_mood=None):
590
+ """
591
+ Plays the track_uris returned by recommend_tracks().
592
+ """
593
+ try:
594
+ track_uris = recommend_tracks(genre_name, artist_name, track_name, user_mood)
595
+ track_list_str = create_track_list_str(track_uris)
596
+ sp.start_playback(device_id=device_id, uris=track_uris)
597
+
598
+ return f"""
599
+ **♫ Now Playing Recommendations Based On:** <span style="color: red; font-weight: bold; font-style: italic;">
600
+ {', '.join(filter(None, [genre_name, artist_name, track_name, "Your Mood"]))}</span> ♫
601
+
602
+ **Selected Tracks:**
603
+ {track_list_str}
604
+ """
605
+ except ValueError as e:
606
+ return str(e)
607
+
608
+
609
+ def create_playlist_from_recommendations(genre_name=None, artist_name=None, track_name=None, user_mood=None):
610
+ """
611
+ Creates a playlist from recommend_tracks().
612
+ """
613
+ user = sp.current_user()
614
+ user_id = user['id']
615
+ user_name = user["display_name"]
616
+
617
+ playlists = sp.current_user_playlists()
618
+ playlist_names = [playlist['name'] for playlist in playlists["items"]]
619
+ chosen_theme = random.choice(THEMES)
620
+ playlist_name = f"{user_name}'s {chosen_theme} Playlist"
621
+ # ensuring the use of new adjective each time
622
+ while playlist_name in playlist_names:
623
+ chosen_theme = random.choice(THEMES)
624
+ playlist_name = f"{user_name}'s {chosen_theme} Playlist"
625
+
626
+ playlist_description=f"Apollo AI's personalized playlist for {user_name}. Get yours here: (add link)." # TODO: add link to project
627
+ new_playlist = sp.user_playlist_create(user=user_id, name=playlist_name,
628
+ public=True, collaborative=False, description=playlist_description)
629
+
630
+ track_uris = recommend_tracks(genre_name, artist_name, track_name, user_mood)
631
+ track_list_str = create_track_list_str(track_uris)
632
+ sp.user_playlist_add_tracks(user=user_id, playlist_id=new_playlist['id'], tracks=track_uris, position=None)
633
+ playlist_url = f"https://open.spotify.com/playlist/{new_playlist['id']}"
634
+
635
+ return f"""
636
+ ♫ Created *{playlist_name}* Based On: <span style='color: red; font-weight: bold; font-style: italic;'>
637
+ {', '.join(filter(None, [genre_name, artist_name, track_name, 'Your Mood']))}</span> ♫
638
+
639
+ **Selected Tracks:**
640
+ {track_list_str}
641
+
642
+ <a href='{playlist_url}'>Click here to listen to the playlist on Spotify!</a>
643
+ """
644
+
645
+
646
+ # ------------------------------
647
+ # Section: Agent Tools
648
+ # ------------------------------
649
+
650
+
651
+ class TrackNameInput(BaseModel):
652
+ track_name: str = Field(description="Track name in the user's request.")
653
+
654
+
655
+ class AlbumNameAndArtistNameInput(BaseModel):
656
+ album_name: str = Field(description="Album name in the user's request.")
657
+ artist_name: str = Field(description="Artist name in the user's request.")
658
+
659
+
660
+ class PlaylistNameInput(BaseModel):
661
+ playlist_name: str = Field(description="Playlist name in the user's request.")
662
+
663
+
664
+ class GenreNameAndUserMoodInput(BaseModel):
665
+ genre_name: str = Field(description="Genre name in the user's request.")
666
+ user_mood: str = Field(description="User's current mood/state-of-being.")
667
+
668
+
669
+ class ArtistNameAndUserMoodInput(BaseModel):
670
+ artist_name: str = Field(description="Artist name in the user's request.")
671
+ user_mood: str = Field(description="User's current mood/state-of-being.")
672
+
673
+
674
+ class RecommendationsInput(BaseModel):
675
+ genre_name: str = Field(description="Genre name in the user's request.")
676
+ artist_name: str = Field(description="Artist name in the user's request.")
677
+ track_name: str = Field(description="Track name in the user's request.")
678
+ user_mood: str = Field(description="User's current mood/state-of-being.")
679
+
680
+
681
+ @tool("play_track_by_name", return_direct=RETURN_DIRECT, args_schema=TrackNameInput)
682
+ def tool_play_track_by_name(track_name: str) -> str:
683
+ """
684
+ Use this tool when a user wants to play a particular track by its name.
685
+ You will need to identify the track name from the user's request.
686
+ Usually, the requests will look like 'play {track name}'.
687
+ This tool is specifically designed for clear and accurate track requests.
688
+ """
689
+ return play_track_by_name(track_name)
690
+
691
+
692
+ @tool("queue_track_by_name", return_direct=RETURN_DIRECT, args_schema=TrackNameInput)
693
+ def tool_queue_track_by_name(track_name: str) -> str:
694
+ """
695
+ Always use this tool when a user says "queue" in their request.
696
+ """
697
+ return queue_track_by_name(track_name)
698
+
699
+
700
+ @tool("pause_track", return_direct=RETURN_DIRECT)
701
+ def tool_pause_track(query: str) -> str:
702
+ """
703
+ Always use this tool when a user says "pause" or "stop" in their request.
704
+ """
705
+ return pause_track()
706
+
707
+
708
+ @tool("resume_track", return_direct=RETURN_DIRECT)
709
+ def tool_resume_track(query: str) -> str:
710
+ """
711
+ Always use this tool when a user says "resume" or "unpause" in their request.
712
+ """
713
+ return resume_track()
714
+
715
+
716
+ @tool("skip_track", return_direct=RETURN_DIRECT)
717
+ def tool_skip_track(query: str) -> str:
718
+ """
719
+ Always use this tool when a user says "skip" or "next" in their request.
720
+ """
721
+ return skip_track()
722
+
723
+
724
+ @tool("play_album_by_name_and_artist", return_direct=RETURN_DIRECT, args_schema=AlbumNameAndArtistNameInput)
725
+ def tool_play_album_by_name_and_artist(album_name: str, artist_name: str) -> str:
726
+ """
727
+ Use this tool when a user wants to play an album.
728
+ You will need to identify both the album name and artist name from the user's request.
729
+ Usually, the requests will look like 'play the album {album_name} by {artist_name}'.
730
+ """
731
+ return play_album_by_name_and_artist(album_name, artist_name)
732
+
733
+
734
+ @tool("play_playlist_by_name", return_direct=RETURN_DIRECT, args_schema=PlaylistNameInput)
735
+ def tool_play_playlist_by_name(playlist_name: str) -> str:
736
+ """
737
+ Use this tool when a user wants to play one of their playlists.
738
+ You will need to identify the playlist name from the user's request.
739
+ """
740
+ return play_playlist_by_name(playlist_name)
741
+
742
+
743
+ @tool("explain_track", return_direct=RETURN_DIRECT)
744
+ def tool_explain_track(query: str) -> str:
745
+ """
746
+ Use this tool when a user wants to know about the currently playing track.
747
+ """
748
+ return explain_track()
749
+
750
+
751
+ @tool("play_genre_by_name_and_mood", return_direct=RETURN_DIRECT, args_schema=GenreNameAndUserMoodInput)
752
+ def tool_play_genre_by_name_and_mood(genre_name: str, user_mood: str) -> str:
753
+ """
754
+ Use this tool when a user wants to play a genre.
755
+ You will need to identify both the genre name from the user's request,
756
+ and also their current mood, which you should always be monitoring.
757
+ """
758
+ return play_genre_by_name_and_mood(genre_name, user_mood)
759
+
760
+
761
+ @tool("play_artist_by_name_and_mood", return_direct=RETURN_DIRECT, args_schema=ArtistNameAndUserMoodInput)
762
+ def tool_play_artist_by_name_and_mood(artist_name: str, user_mood: str) -> str:
763
+ """
764
+ Use this tool when a user wants to play an artist.
765
+ You will need to identify both the artist name from the user's request,
766
+ and also their current mood, which you should always be monitoring.
767
+ If you don't know the user's mood, ask them before using this tool.
768
+ """
769
+ return play_artist_by_name_and_mood(artist_name, user_mood)
770
+
771
+
772
+ @tool("play_recommended_tracks", return_direct=RETURN_DIRECT, args_schema=RecommendationsInput)
773
+ def tool_play_recommended_tracks(genre_name: str, artist_name: str, track_name: str, user_mood: str) -> str:
774
+ """
775
+ Use this tool when a user wants track recommendations.
776
+ You will need to identify the genre name, artist name, and/or track name
777
+ from the user's request... and also their current mood, which you should always be monitoring.
778
+ The user must provide at least genre, artist, or track.
779
+ """
780
+ return play_recommended_tracks(genre_name, artist_name, track_name, user_mood)
781
+
782
+
783
+ @tool("create_playlist_from_recommendations", return_direct=RETURN_DIRECT, args_schema=RecommendationsInput)
784
+ def tool_create_playlist_from_recommendations(genre_name: str, artist_name: str, track_name: str, user_mood: str) -> str:
785
+ """
786
+ Use this tool when a user wants a playlist created (from recommended tracks).
787
+ You will need to identify the genre name, artist name, and/or track name
788
+ from the user's request... and also their current mood, which you should always be monitoring.
789
+ The user must provide at least genre, artist, or track.
790
+ """
791
+ return create_playlist_from_recommendations(genre_name, artist_name, track_name, user_mood)
792
+
793
+
794
+ CUSTOM_TOOLS =[
795
+ tool_play_track_by_name,
796
+ tool_queue_track_by_name,
797
+ tool_pause_track,
798
+ tool_resume_track,
799
+ tool_skip_track,
800
+ tool_play_album_by_name_and_artist,
801
+ tool_play_playlist_by_name,
802
+ tool_explain_track,
803
+ tool_play_genre_by_name_and_mood,
804
+ tool_play_artist_by_name_and_mood,
805
+ tool_play_recommended_tracks,
806
+ tool_create_playlist_from_recommendations
807
+ ]
808
+
809
+
810
+ # ------------------------------
811
+ # Section: Chatbot
812
+ # ------------------------------
813
+
814
+
815
+ system_message = SystemMessage(content=SYSTEM_MESSAGE)
816
+ MEMORY_KEY = "chat_history"
817
+ prompt = OpenAIFunctionsAgent.create_prompt(
818
+ system_message=system_message,
819
+ extra_prompt_messages=[MessagesPlaceholder(variable_name=MEMORY_KEY)]
820
+ )
821
+ memory = ConversationBufferMemory(memory_key=MEMORY_KEY, return_messages=True)
822
+ # NOTE: llm defined above to power explain_track() function
823
+ agent = OpenAIFunctionsAgent(llm=llm, tools=CUSTOM_TOOLS, prompt=prompt)
824
+ agent_executor = AgentExecutor(agent=agent, tools=CUSTOM_TOOLS, memory=memory, verbose=True)
825
+
826
+ return agent_executor
827
+
828
+ create_agent_button.click(create_agent, inputs=[ACCESS_TOKEN_VAR], outputs=[AGENT_EXECUTOR_VAR])
829
+
830
+
831
+ # ------------------------------
832
+ # Section: Chat Interface
833
+ # ------------------------------
834
+
835
+ chatbot = gr.Chatbot()
836
+ msg = gr.Textbox()
837
+
838
+ def respond(user_message, chat_history, agent_executor):
839
+ bot_message = agent_executor.run(user_message)
840
+ chat_history.append((user_message, bot_message))
841
+ time.sleep(2)
842
+ return "", chat_history
843
+
844
+ msg.submit(respond, inputs=[msg, chatbot, AGENT_EXECUTOR_VAR], outputs=[msg, chatbot])
845
+
846
+ auth_page.launch(share=True)