%load_ext dotenv
%dotenv
The dotenv extension is already loaded. To reload it, use:
%reload_ext dotenv
Hi, I’m Soma! You can find me on email at jonathan.soma@gmail.com, on Twitter at @dangerscarf, or maybe even on this newsletter I’ve never sent.
The top track or two for practically any band on Spotify is a slow one. The Cars have Drive, Green Day has Boulevard of Broken Dreams, Blink 182 has I Miss You. They put me to sleep! Instead of screaming at Alexa to “play The Cars” I want to say “play The Cars but none of those boring slow songs.”
Spotify actually knows the diference between a slow song and an upbeat one, although somewhat secretly – they don’t reveal it in the app, but their API includes data for songs like energy level and danceability. Can we combine this information with LangChain and GPT to talk to Spotify through natural language?
Trying to get this to work, you hit a wall pretty quickly: while LangChain supports APIs, the Spotify API is an awful complex OAuth2 beast that doesn’t fit the existing examples. The Spotipy library is a lot easier to use, but as of this moment it isn’t super-simple to run arbitrary code through LangChain.
But let’s make it happen anyway! In this walkthrough, we’ll look at:
APIChain
(API access) and PALChain
(Python execution) chains are builtIf you’re a LangChain pro and are just looking for code to run, you can can skip all these words and just visit the GitHub repo
If you are a less technical person, just relax while you’re reading the next few sections! They’re the “why” for how we end up tackling our problem.
To get the disappointment and/or excitement out of the way early on, here’s the playlist we wind up with at the end:
The prompt was “Give me a list of Cars, Blink 182 and Sum 41 songs that are upbeat, loud and fun. Make sure the songs are popular enough for me to have heard of them.”
Speaking of The Rock Show, but there’s nothing better than listening to it while rolling up to the NJ Mineral, Fossil, Gem & Jewelry Show. It’s coming up!!
Both GPT and Spotify require me to prove my identity using API keys. If you had my keys you’d be able to impersonate me, talk to my chatbots, and make a bunch of awful playlists – we don’t want any of those happening. Instead of putting the API keys in my notebook, I’m using dotenv-python to keep them nice and secret. I recommend it!
%load_ext dotenv
%dotenv
The dotenv extension is already loaded. To reload it, use:
%reload_ext dotenv
To access GPT-3.5-turbo, we’re use to use a LangChain chain.
It’s a little more complicated than when we were talking to fairy tales, but the former method of using a plain OpenAI
object is being deprecated in favor of ChatOpenAI
.
from langchain.chat_models import ChatOpenAI
from langchain.prompts import PromptTemplate
from langchain import LLMChain
= ChatOpenAI(model_name='gpt-3.5-turbo') llm
Let’s test it out with a sample PromptTemplate
about energetic songs.
= PromptTemplate(
prompt =["artist"],
input_variables="What are some of the more energetic songs by {artist}?",
template
)
= LLMChain(llm=llm, prompt=prompt) chain
Now that it’s assembled, let’s use it.
= chain.run("The Promise Ring")
response print(response)
Some of the more energetic songs by The Promise Ring include:
1. "Is This Thing On?"
2. "Emergency! Emergency!"
3. "Red & Blue Jeans"
4. "Happiness is All the Rage"
5. "Make Me a Chevy"
6. "Jersey Shore"
7. "B Is for Bethlehem"
8. "Why Did We Ever Meet?"
9. "Stop Playing Guitar"
10. "Pink Chimneys"
Yes, that’s an answer, but it isn’t good enough. Asking GPT for energetic songs works, I want to at least pretend that we’re basing this on science! You never know if record labels from the late 90’s are funnelling money to OpenAI to bias the results.
Luckily, Spotify has that information: their API includes access a track’s audio features including loudness, danceability and energy! With that in mind, our goal is now to create a way for GPT and Spotify to interface so that we can leverage that information when building our playlist.
We’re going to use the Spotipy Python library to access Spotify. It handles all of the OAuth login, the refreshing of tokens (they’re only good for 5 minutes!), and everything of that ilk.
import os
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
= SpotifyClientCredentials(
auth =os.environ['SPOTIPY_CLIENT_ID'],
client_id=os.environ['SPOTIPY_CLIENT_SECRET']
client_secret
)= spotipy.Spotify(auth_manager=auth) sp
If we want songs by the band Wet Leg, we can’t just say “give me Wet Legs top tracks.” Instead, we need to find Wet Leg’s URI
- uniform resource indicator, Spotify’s cataloguing ID – and then use that to get the top tracks. It’s important to note that it’s almost always a multi-step process to get anything useful from Spotify.
For example, let’s say we want to filter for energetic songs, which means we want a track’s audio features. If you wanted the audio features for a track but only know the band name, the process looks like what is outlined below:
We’ll use Spotify’s search to try to find the artist Wet Leg, and then assume the first result is the right one.
= sp.search('Wet Leg', type='artist')
response = response['artists']['items'][0]['uri']
uri uri
'spotify:artist:2TwOrUcYnAlIiKmVQkkoSZ'
We’ll then use the artist’s URI to find some top tracks from that artist (there’s actually an endpoint for top tracks!).
= sp.artist_top_tracks(uri)
response = response['tracks'][:5]
top_five
for track in top_five:
print(track['popularity'], track['name'], track['uri'])
70 Wet Dream spotify:track:260Ub1Yuj4CobdISTOBvM9
66 Chaise Longue spotify:track:0nys6GusuHnjSYLW0PYYb7
63 Being In Love spotify:track:4VBE0mwU8Nmm8hiqfCe4Ve
62 Angelica spotify:track:3EwTIu5qka2l5ZekB0b6QC
60 Ur Mum spotify:track:4ug5wsIcbAPBun8TCKn2t6
Instead of coming with the track results, the danceability and all of those scores are in a completely different endpoint! So we’ll now use the track URIs to access the audio features.
import pandas as pd
= [track['uri'] for track in top_five]
uris = sp.audio_features(uris)
audio_features pd.DataFrame(audio_features)
danceability | energy | key | loudness | mode | speechiness | acousticness | instrumentalness | liveness | valence | tempo | type | id | uri | track_href | analysis_url | duration_ms | time_signature | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0.721 | 0.701 | 2 | -5.941 | 1 | 0.0306 | 0.000927 | 0.026000 | 0.234 | 0.892 | 130.091 | audio_features | 260Ub1Yuj4CobdISTOBvM9 | spotify:track:260Ub1Yuj4CobdISTOBvM9 | https://api.spotify.com/v1/tracks/260Ub1Yuj4Co... | https://api.spotify.com/v1/audio-analysis/260U... | 140080 | 3 |
1 | 0.684 | 0.749 | 7 | -6.565 | 1 | 0.0600 | 0.001350 | 0.111000 | 0.141 | 0.935 | 160.021 | audio_features | 0nys6GusuHnjSYLW0PYYb7 | spotify:track:0nys6GusuHnjSYLW0PYYb7 | https://api.spotify.com/v1/tracks/0nys6GusuHnj... | https://api.spotify.com/v1/audio-analysis/0nys... | 196905 | 4 |
2 | 0.716 | 0.687 | 9 | -4.940 | 0 | 0.0342 | 0.009220 | 0.110000 | 0.123 | 0.342 | 126.030 | audio_features | 4VBE0mwU8Nmm8hiqfCe4Ve | spotify:track:4VBE0mwU8Nmm8hiqfCe4Ve | https://api.spotify.com/v1/tracks/4VBE0mwU8Nmm... | https://api.spotify.com/v1/audio-analysis/4VBE... | 122467 | 4 |
3 | 0.491 | 0.870 | 0 | -5.138 | 1 | 0.0393 | 0.000141 | 0.000729 | 0.368 | 0.314 | 131.989 | audio_features | 3EwTIu5qka2l5ZekB0b6QC | spotify:track:3EwTIu5qka2l5ZekB0b6QC | https://api.spotify.com/v1/tracks/3EwTIu5qka2l... | https://api.spotify.com/v1/audio-analysis/3EwT... | 232320 | 4 |
4 | 0.685 | 0.720 | 4 | -5.553 | 1 | 0.0280 | 0.007020 | 0.275000 | 0.425 | 0.554 | 133.016 | audio_features | 4ug5wsIcbAPBun8TCKn2t6 | spotify:track:4ug5wsIcbAPBun8TCKn2t6 | https://api.spotify.com/v1/tracks/4ug5wsIcbAPB... | https://api.spotify.com/v1/audio-analysis/4ug5... | 201253 | 4 |
So what I’m saying is: we can’t just hit one endpoint and run away. This is a lot of work!
When attempting to talk to the Spotify API through Spotipy, there are two obvious answers from the LangChain documentation that might come to mind:
APIChain
, which is used for talking to APIsPALChain
, which is used for running Python codeNow we’ll look at the shortcomings of each and why we need to create our own custom chain.
An APIChain can be used to access an API! This is a slightly adapted version of the APIChain example from the docs.
from langchain.chains import APIChain
from langchain.chains.api import open_meteo_docs
= APIChain.from_llm_and_api_docs(llm, open_meteo_docs.OPEN_METEO_DOCS, verbose=True)
chain_new 'What is the weather like right now in Munich, Germany in degrees Farenheit? Do not include a forecast.') chain_new.run(
> Entering new APIChain chain...
https://api.open-meteo.com/v1/forecast?latitude=48.137154&longitude=11.576124¤t_weather=true&temperature_unit=fahrenheit
{"latitude":48.14,"longitude":11.58,"generationtime_ms":0.1989603042602539,"utc_offset_seconds":0,"timezone":"GMT","timezone_abbreviation":"GMT","elevation":526.0,"current_weather":{"temperature":50.0,"windspeed":16.1,"winddirection":254.0,"weathercode":3,"time":"2023-03-26T16:00"}}
> Finished chain.
'The weather in Munich, Germany right now is 50 degrees Fahrenheit.'
An important thing to take note of here is open_meteo_docs.OPEN_METEO_DOCS
: along with our prompt and an llm, we’re also sending the documentation for the Open-Meteo API. It looks like this:
print(open_meteo_docs.OPEN_METEO_DOCS[:1000])
BASE URL: https://api.open-meteo.com/
API Documentation
The API endpoint /v1/forecast accepts a geographical coordinate, a list of weather variables and responds with a JSON hourly weather forecast for 7 days. Time always starts at 0:00 today and contains 168 hours. All URL parameters are listed below:
Parameter Format Required Default Description
latitude, longitude Floating point Yes Geographical WGS84 coordinate of the location
hourly String array No A list of weather variables which should be returned. Values can be comma separated, or multiple &hourly= parameter in the URL can be used.
daily String array No A list of daily weather variable aggregations which should be returned. Values can be comma separated, or multiple &daily= parameter in the URL can be used. If daily weather variables are specified, parameter timezone is required.
current_weather Bool No false Include current weather conditions in the JSON output.
temperature_unit String No celsius If fahrenheit is set, al
But what is the chain doing with the Open-Meteo docs? If we dig around in the source code we can find a few lines of code that get into the details:
= LLMChain(llm=llm, prompt=api_url_prompt)
get_request_chain = RequestsWrapper(headers=headers)
requests_wrapper = LLMChain(llm=llm, prompt=api_response_prompt) get_answer_chain
These are used in a three-step process:
The first step builds an LLMChain
to talk to GPT. LangChain then provides the API documentation to GPT, and asks it to determine the API endpoint to visit.
"""You are given the below API Documentation:
{api_docs}
Using this documentation, generate the full API url to call for answering the user question.
You should build the API url in order to get a response that is as short as possible, while still getting the necessary information to answer the question. Pay attention to deliberately exclude any unnecessary pieces of data in the API call.
Question:{question}
API url:"""
The second step builds a RequestsWrapper
to access the API URL and returns the response. But it isn’t a human-readable response to our question yet, it’s almost always going to be a bunch of JSON.
The final step uses another LLMChain
to talk to GPT again: LangChain sends the API response to GPT and asks for a human-readable summary to answer the question.
"""Here is the response from the API:
{api_response}
Summarize this response to answer the original question.
Summary:"""
Even though we want to talk to an API, we want to talk to an API through the Spotipy Python library, not a series of URLs. Since the APIChain
is based around making an actual request to somewhere on the internet, this isn’t going to work for us.
If the Spotify API were a nice simple REST API we could just feed APIChain
the documentation, but that isn’t the case.
A PALChain can be used to create and run arbitrary Python code! This is the PALChain example from the docs.
from langchain.chains import PALChain
= PALChain.from_math_prompt(llm, verbose=True)
pal_chain = "Jan has three times the number of pets as Marcia. Marcia has two more pets than Cindy. If Cindy has four pets, how many total pets do the three have?"
question pal_chain.run(question)
> Entering new PALChain chain...
def solution():
"""Jan has three times the number of pets as Marcia. Marcia has two more pets than Cindy. If Cindy has four pets, how many total pets do the three have?"""
cindy_pets = 4
marcia_pets = cindy_pets + 2
jan_pets = marcia_pets * 3
total_pets = cindy_pets + marcia_pets + jan_pets
result = total_pets
return result
> Finished chain.
'28'
If we look at the code for the math chain’s prompt it’s very long. Here’s a portion of it:
from langchain.prompts.prompt import PromptTemplate
= (
template '''
Q: Olivia has $23. She bought five bagels for $3 each. How much money does she have left?
# solution in Python:
def solution():
"""Olivia has $23. She bought five bagels for $3 each. How much money does she have left?"""
money_initial = 23
bagels = 5
bagel_cost = 3
money_spent = bagels * bagel_cost
money_left = money_initial - money_spent
result = money_left
return result
Q: There are 15 trees in the grove. Grove workers will plant trees in the grove today. After they are done, there will be 21 trees. How many trees did the grove workers plant today?
# solution in Python:
def solution():
"""There are 15 trees in the grove. Grove workers will plant trees in the grove today. After they are done, there will be 21 trees. How many trees did the grove workers plant today?"""
trees_initial = 15
trees_after = 21
trees_added = trees_after - trees_initial
result = trees_added
return result
Q: {question}
# solution in Python:
'''.strip()
+ "\n\n\n"
)= PromptTemplate(input_variables=["question"], template=template) MATH_PROMPT
That prompt only gives you Python code, though, not the actual result! To see what happens with the result we need to check the PALChain code itself, lightly edited for clarity:
def _call(self, inputs: Dict[str, str]) -> Dict[str, str]:
= LLMChain(llm=self.llm, prompt=self.prompt)
llm_chain = llm_chain.predict(stop=[self.stop], **inputs)
code = PythonREPL(_globals=self.python_globals, _locals=self.python_locals)
repl = repl.run(code + f"\n{self.get_answer_expr}")
res = {self.output_key: res.strip()}
output return output
The code looks a little wild, but the process is pretty simple:
self.get_answer_expr
to get the result of the solution.In this case, the get_answer_expr
is print(solution())
. Our prompt insists the answer be put in a function called solution
, so this is how we call the function and obtain the result.
With a little work, we actually can make it work! It just needs a little extra effort and inspiration from APIChain
.
Our process is going to take two steps:
PALChain
to write and run the Spotipy codeLLMChain
to clean it up and provide an answerThe separte steps of “data acquisition first, then clean up for the presentation” is inspired by our friend APIChain
. In theory we might even be able to split this into three steps – develop the code, run the code, analyze the results – but let’s keep it to two for now.
How do we get GPT to write Spotipy code for us? It’s similar to the API example – giving the documentation to GPT along with our question – but in this case we wouldn’t use Spotify’s API documentation, we’d use documentation for the Spotipy library.
While we could try feeding GPT the entire documentation page for Spotipy, it’s too long to do it all at once. We could chunk it and feed it into a reference database that is selectively queried for relevant content… but that’s just too much work. We want something simple.
Instead, we’re going to go the lazy route: the Spotify library has been around for ages and GPT already knows how it works, so we’ll just rely on its in-built knowledge. We just need to provide a few examples of how we like to work with the library and what we need returned, and GPT will follow our lead.
Here’s our prompt for generating Spotipy code to access the Spotify API:
from langchain.prompts.prompt import PromptTemplate
= (
SPOTIPY_PROMPT_TEMPLATE '''
API LIMITATIONS TO NOTE
* When requesting track information, the limit is 50 at a time
* When requesting audio features, the limit is 100 at a time
* When selecting multiple artists, the limit is 50 at a time
* When asking for recommendations, the limit is 100 at a time
=====
Q: What albums has the band Green Day made?
# solution in Python:
def solution():
"""What albums has the band Green Day made?"""
search_results = sp.search(q='Green Day', type='artist')
uri = search_results['artists']['items'][0]['uri']
albums = sp.artist_albums(green_day_uri, album_type='album')
return albums
Q: Who are some musicians similar to Fiona Apple?
# solution in Python:
def solution():
"""Who are some musicians similar to Fiona Apple?"""
search_results = sp.search(q='Fiona Apple', type='artist')
uri = search_results['artists']['items'][0].get('uri')
artists = sp.artist_related_artists(uri)
return artists
Q: Tell me what songs by The Promise Ring sound like
# solution in Python:
def solution():
"""Tell me what songs by The Promise Ring sound like?"""
search_results = sp.search(q='The Promise Ring', type='artist')
uri = search_results['artists']['items'][0].get('uri')
tracks = sp.artist_top_tracks(uri)
track_uris = [track.get('uri') for track in tracks['tracks']]
audio_details = sp.audio_features(track_uris)
return audio_details
Q: Get me the URI for the album The Colour And The Shape
# solution in Python:
def solution():
"""Get me the URI for the album The Colour And The Shape"""
search_results = sp.search(q='The Colour And The Shape', type='album')
uri = search_results['albums']['items'][0].get('uri')
return uri
Q: What are the first three songs on Diet Cig's Over Easy?
# solution in Python:
def solution():
"""What are the first three songs on Diet Cig's Over Easy?"""
# Get the URI for the album
search_results = sp.search(q='Diet Cig Over Easy', type='album')
album = search_results['albums']['items'][0]
album_uri = album['uri']
# Get the album tracks
album_tracks = sp.album_tracks(album_uri)['items']
# Sort the tracks by duration
first_three = album_tracks[:3]
tracks = []
# Only include relevant fields
for i, track in enumerate(first_three):
# track['album'] does NOT work with sp.album_tracks
# you need to use album['name'] instead
tracks.append({{
'position': i+1,
'song_name': track.get('name'),
'song_uri': track['artists'][0].get('uri'),
'artist_uri': track['artists'][0].get('uri'),
'album_uri': album.get('uri'),
'album_name': album.get('name')
}})
return tracks
Q: What are the thirty most danceable songs by Metallica?
# solution in Python:
def solution():
"""What are most danceable songs by Metallica?"""
search_results = sp.search(q='Metallica', type='artist')
uri = search_results['artists']['items'][0]['uri']
albums = sp.artist_albums(uri, album_type='album')
album_uris = [album['uri'] for album in albums['items']]
tracks = []
for album_uri in album_uris:
album_tracks = sp.album_tracks(album_uri)
tracks.extend(album_tracks['items'])
track_uris = [track['uri'] for track in tracks]
danceable_tracks = []
# You can only have 100 at a time
for i in range(0, len(track_uris), 100):
subset_track_uris = track_uris[i:i+100]
audio_details = sp.audio_features(subset_track_uris)
for j, details in enumerate(audio_details):
if details['danceability'] > 0.7:
track = tracks[i+j]
danceable_tracks.append({{
'song': track.get('name')
'album': track.get('album').get('name')
'danceability': details.get('danceability'),
'tempo': details.get('tempo'),
}})
# Be sure to add the audio details to the track
danceable_tracks.append(track)
return danceable_tracks
Q: {question}. Return a list or dictionary, only including the fields necessary to answer the question, including relevant scores and the uris to the albums/songs/artists mentioned. Only return the data – if the prompt asks for a format such as markdown or a simple string, ignore it: you are only meant to provide the information, not the formatting. A later step in the process will convert the data into the new format (table, sentence, etc).
# solution in Python:
'''.strip()
+ "\n\n\n"
)
= PromptTemplate(input_variables=["question"], template=SPOTIPY_PROMPT_TEMPLATE) SPOTIPY_PROMPT
The PALChain example from the docs makes it look so simple:
= PALChain.from_math_prompt(llm, verbose=True)
pal_chain = "Jan has three times the number of pets as Marcia. Marcia has two more pets than Cindy. If Cindy has four pets, how many total pets do the three have?"
question pal_chain.run(question)
But a lot of work is happening behind the scenes! In our case, we’re going to be building a PALChain from scratch instead of relying on a constructor.
There are a couple important additions we make as we initialize the PALChain. First, we need to provide our initialized and authenticated Spotipy sp
instance so the PythonREPL can access Spotipy. We’ll do this using python_globals=
.
={
python_globals'sp': sp
},
The next step is wrangling our data. Results from chains come as strings, but the Spotify API returns JSON (or more specifically, a Python dictionary). To nicely convert our dictionary into a string we’ll be using json.dumps
. The json
module isn’t included by default, so this requires importing hte json library before we do the conversion.
Both of these steps are squished into the get_answer_expr
parameter. It’s a bit garish but it works!
="import json; print(json.dumps(solution()))", get_answer_expr
Finally, we’re also adding return_intermediate_steps=True
to make sure it returns the result of the code running and the code it ran.
This is what it looks like all put together:
from langchain.chains import PALChain
= PALChain(
spotify_chain =llm,
llm=SPOTIPY_PROMPT,
prompt={
python_globals'sp': sp
},='\n\n\n',
stop=True,
verbose=True,
return_intermediate_steps="import json; print(json.dumps(solution()))",
get_answer_expr )
It’s complicated enough, but does it work?
= spotify_chain({'question': "What are the most popular Bouncing Souls songs?"})
spotify_response 'result'] spotify_response[
> Entering new PALChain chain...
def solution():
"""What are the most popular Bouncing Souls songs?"""
search_results = sp.search(q='Bouncing Souls', type='artist')
uri = search_results['artists']['items'][0].get('uri')
top_tracks = sp.artist_top_tracks(uri)
top_track_uris = [track.get('uri') for track in top_tracks['tracks']]
audio_details = sp.audio_features(top_track_uris)
popular_songs = []
for i, track in enumerate(top_tracks['tracks']):
details = audio_details[i]
popular_songs.append({
'song_name': track.get('name'),
'song_uri': track.get('uri'),
'artist_name': track.get('artists')[0].get('name'),
'artist_uri': track.get('artists')[0].get('uri'),
'album_name': track.get('album').get('name'),
'album_uri': track.get('album').get('uri'),
'popularity': track.get('popularity'),
'danceability': details.get('danceability'),
'energy': details.get('energy'),
'key': details.get('key'),
'loudness': details.get('loudness'),
'mode': details.get('mode'),
'speechiness': details.get('speechiness'),
'acousticness': details.get('acousticness'),
'instrumentalness': details.get('instrumentalness'),
'liveness': details.get('liveness'),
'valence': details.get('valence'),
'tempo': details.get('tempo'),
})
return popular_songs[:10] # Return top 10 songs
> Finished chain.
'[{"song_name": "True Believers", "song_uri": "spotify:track:4fRmFVMd0c1SGfzazBJIM8", "artist_name": "The Bouncing Souls", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_name": "How I Spent My Summer Vacation", "album_uri": "spotify:album:64zbLX1ze8N3kcAMX0qq7G", "popularity": 55, "danceability": 0.237, "energy": 0.981, "key": 0, "loudness": -4.32, "mode": 1, "speechiness": 0.0989, "acousticness": 0.000296, "instrumentalness": 3.81e-05, "liveness": 0.202, "valence": 0.475, "tempo": 98.181}, {"song_name": "Lean On Sheena", "song_uri": "spotify:track:7IR7GUO0dUyUsBp7BfQ3vJ", "artist_name": "The Bouncing Souls", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_name": "The Gold Record", "album_uri": "spotify:album:3MF7PvmrMjEXGvA8fP3L6l", "popularity": 51, "danceability": 0.491, "energy": 0.866, "key": 11, "loudness": -4.431, "mode": 1, "speechiness": 0.0583, "acousticness": 0.16, "instrumentalness": 0.000211, "liveness": 0.13, "valence": 0.694, "tempo": 175.969}, {"song_name": "Hopeless Romantic", "song_uri": "spotify:track:180mXjN61yhrKhbY2yQc0E", "artist_name": "The Bouncing Souls", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_name": "Hopeless Romantic", "album_uri": "spotify:album:56CbFyDsG65LI1Eoh7hsOT", "popularity": 49, "danceability": 0.243, "energy": 0.981, "key": 4, "loudness": -5.251, "mode": 1, "speechiness": 0.074, "acousticness": 0.000164, "instrumentalness": 1.11e-05, "liveness": 0.207, "valence": 0.216, "tempo": 105.022}, {"song_name": "Manthem", "song_uri": "spotify:track:5pSjxUAwOol5e0TWp1ecHC", "artist_name": "The Bouncing Souls", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_name": "How I Spent My Summer Vacation", "album_uri": "spotify:album:64zbLX1ze8N3kcAMX0qq7G", "popularity": 46, "danceability": 0.524, "energy": 0.986, "key": 2, "loudness": -2.865, "mode": 1, "speechiness": 0.0634, "acousticness": 9.44e-05, "instrumentalness": 0.000214, "liveness": 0.0772, "valence": 0.724, "tempo": 94.348}, {"song_name": "Sing Along Forever", "song_uri": "spotify:track:5feYKXxg4HL2APTQGCfAav", "artist_name": "The Bouncing Souls", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_name": "Anchors Aweigh", "album_uri": "spotify:album:1xgfRXjCoynPLqtdNu50pR", "popularity": 46, "danceability": 0.592, "energy": 0.964, "key": 0, "loudness": -3.672, "mode": 1, "speechiness": 0.0817, "acousticness": 0.00912, "instrumentalness": 0, "liveness": 0.27, "valence": 0.585, "tempo": 101.252}, {"song_name": "Say Anything", "song_uri": "spotify:track:06peZfvxR5721oGqHwogha", "artist_name": "The Bouncing Souls", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_name": "The Bouncing Souls", "album_uri": "spotify:album:7LgICzKkhaLV9Gttn8xM7a", "popularity": 45, "danceability": 0.448, "energy": 0.995, "key": 6, "loudness": -3.111, "mode": 1, "speechiness": 0.0539, "acousticness": 0.00275, "instrumentalness": 0, "liveness": 0.297, "valence": 0.643, "tempo": 101.405}, {"song_name": "Ole", "song_uri": "spotify:track:2McQQA5nCLVL0XvzcxWhFC", "artist_name": "The Bouncing Souls", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_name": "Hopeless Romantic", "album_uri": "spotify:album:56CbFyDsG65LI1Eoh7hsOT", "popularity": 43, "danceability": 0.33, "energy": 0.833, "key": 7, "loudness": -6.507, "mode": 1, "speechiness": 0.0768, "acousticness": 0.0491, "instrumentalness": 0, "liveness": 0.687, "valence": 0.553, "tempo": 128.329}, {"song_name": "Ten Stories High", "song_uri": "spotify:track:1t9Y1HGwikUCCo5xCupAnT", "artist_name": "The Bouncing Souls", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_name": "Ten Stories High", "album_uri": "spotify:album:0wdbr46ndnwB1cgZoNzT48", "popularity": 30, "danceability": 0.361, "energy": 0.984, "key": 5, "loudness": -1.913, "mode": 1, "speechiness": 0.0883, "acousticness": 0.000322, "instrumentalness": 0.000873, "liveness": 0.329, "valence": 0.491, "tempo": 198.064}, {"song_name": "Kids and Heroes", "song_uri": "spotify:track:7ru4QA7k7ViuLS9oDtdRBI", "artist_name": "The Bouncing Souls", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_name": "Anchors Aweigh", "album_uri": "spotify:album:1xgfRXjCoynPLqtdNu50pR", "popularity": 43, "danceability": 0.405, "energy": 0.963, "key": 0, "loudness": -5.216, "mode": 1, "speechiness": 0.0697, "acousticness": 0.0127, "instrumentalness": 0.000256, "liveness": 0.289, "valence": 0.198, "tempo": 101.759}, {"song_name": "Kate Is Great", "song_uri": "spotify:track:1VT2wLreLu0l7E4T0JDedh", "artist_name": "The Bouncing Souls", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_name": "The Bouncing Souls", "album_uri": "spotify:album:7LgICzKkhaLV9Gttn8xM7a", "popularity": 42, "danceability": 0.358, "energy": 0.93, "key": 2, "loudness": -4.726, "mode": 1, "speechiness": 0.164, "acousticness": 0.0822, "instrumentalness": 0, "liveness": 0.0699, "valence": 0.809, "tempo": 175.011}]'
Let’s look at the three separate keys the PALChain
response gives us.
First, the question:
'question'] spotify_response[
'What are the most popular Bouncing Souls songs?'
Second, the intermediate steps (the code that it ran):
print(spotify_response['intermediate_steps'])
def solution():
"""What are the most popular Bouncing Souls songs?"""
search_results = sp.search(q='Bouncing Souls', type='artist')
uri = search_results['artists']['items'][0].get('uri')
top_tracks = sp.artist_top_tracks(uri)
top_track_uris = [track.get('uri') for track in top_tracks['tracks']]
audio_details = sp.audio_features(top_track_uris)
popular_songs = []
for i, track in enumerate(top_tracks['tracks']):
details = audio_details[i]
popular_songs.append({
'song_name': track.get('name'),
'song_uri': track.get('uri'),
'artist_name': track.get('artists')[0].get('name'),
'artist_uri': track.get('artists')[0].get('uri'),
'album_name': track.get('album').get('name'),
'album_uri': track.get('album').get('uri'),
'popularity': track.get('popularity'),
'danceability': details.get('danceability'),
'energy': details.get('energy'),
'key': details.get('key'),
'loudness': details.get('loudness'),
'mode': details.get('mode'),
'speechiness': details.get('speechiness'),
'acousticness': details.get('acousticness'),
'instrumentalness': details.get('instrumentalness'),
'liveness': details.get('liveness'),
'valence': details.get('valence'),
'tempo': details.get('tempo'),
})
return popular_songs[:10] # Return top 10 songs
Finally, the actual response. In the PALChain
examples it’s mostly the result of a quick calculation, but this time it’s a whole big mess of JSON:
'result'] spotify_response[
'[{"song_name": "True Believers", "song_uri": "spotify:track:4fRmFVMd0c1SGfzazBJIM8", "artist_name": "The Bouncing Souls", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_name": "How I Spent My Summer Vacation", "album_uri": "spotify:album:64zbLX1ze8N3kcAMX0qq7G", "popularity": 55, "danceability": 0.237, "energy": 0.981, "key": 0, "loudness": -4.32, "mode": 1, "speechiness": 0.0989, "acousticness": 0.000296, "instrumentalness": 3.81e-05, "liveness": 0.202, "valence": 0.475, "tempo": 98.181}, {"song_name": "Lean On Sheena", "song_uri": "spotify:track:7IR7GUO0dUyUsBp7BfQ3vJ", "artist_name": "The Bouncing Souls", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_name": "The Gold Record", "album_uri": "spotify:album:3MF7PvmrMjEXGvA8fP3L6l", "popularity": 51, "danceability": 0.491, "energy": 0.866, "key": 11, "loudness": -4.431, "mode": 1, "speechiness": 0.0583, "acousticness": 0.16, "instrumentalness": 0.000211, "liveness": 0.13, "valence": 0.694, "tempo": 175.969}, {"song_name": "Hopeless Romantic", "song_uri": "spotify:track:180mXjN61yhrKhbY2yQc0E", "artist_name": "The Bouncing Souls", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_name": "Hopeless Romantic", "album_uri": "spotify:album:56CbFyDsG65LI1Eoh7hsOT", "popularity": 49, "danceability": 0.243, "energy": 0.981, "key": 4, "loudness": -5.251, "mode": 1, "speechiness": 0.074, "acousticness": 0.000164, "instrumentalness": 1.11e-05, "liveness": 0.207, "valence": 0.216, "tempo": 105.022}, {"song_name": "Manthem", "song_uri": "spotify:track:5pSjxUAwOol5e0TWp1ecHC", "artist_name": "The Bouncing Souls", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_name": "How I Spent My Summer Vacation", "album_uri": "spotify:album:64zbLX1ze8N3kcAMX0qq7G", "popularity": 46, "danceability": 0.524, "energy": 0.986, "key": 2, "loudness": -2.865, "mode": 1, "speechiness": 0.0634, "acousticness": 9.44e-05, "instrumentalness": 0.000214, "liveness": 0.0772, "valence": 0.724, "tempo": 94.348}, {"song_name": "Sing Along Forever", "song_uri": "spotify:track:5feYKXxg4HL2APTQGCfAav", "artist_name": "The Bouncing Souls", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_name": "Anchors Aweigh", "album_uri": "spotify:album:1xgfRXjCoynPLqtdNu50pR", "popularity": 46, "danceability": 0.592, "energy": 0.964, "key": 0, "loudness": -3.672, "mode": 1, "speechiness": 0.0817, "acousticness": 0.00912, "instrumentalness": 0, "liveness": 0.27, "valence": 0.585, "tempo": 101.252}, {"song_name": "Say Anything", "song_uri": "spotify:track:06peZfvxR5721oGqHwogha", "artist_name": "The Bouncing Souls", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_name": "The Bouncing Souls", "album_uri": "spotify:album:7LgICzKkhaLV9Gttn8xM7a", "popularity": 45, "danceability": 0.448, "energy": 0.995, "key": 6, "loudness": -3.111, "mode": 1, "speechiness": 0.0539, "acousticness": 0.00275, "instrumentalness": 0, "liveness": 0.297, "valence": 0.643, "tempo": 101.405}, {"song_name": "Ole", "song_uri": "spotify:track:2McQQA5nCLVL0XvzcxWhFC", "artist_name": "The Bouncing Souls", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_name": "Hopeless Romantic", "album_uri": "spotify:album:56CbFyDsG65LI1Eoh7hsOT", "popularity": 43, "danceability": 0.33, "energy": 0.833, "key": 7, "loudness": -6.507, "mode": 1, "speechiness": 0.0768, "acousticness": 0.0491, "instrumentalness": 0, "liveness": 0.687, "valence": 0.553, "tempo": 128.329}, {"song_name": "Ten Stories High", "song_uri": "spotify:track:1t9Y1HGwikUCCo5xCupAnT", "artist_name": "The Bouncing Souls", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_name": "Ten Stories High", "album_uri": "spotify:album:0wdbr46ndnwB1cgZoNzT48", "popularity": 30, "danceability": 0.361, "energy": 0.984, "key": 5, "loudness": -1.913, "mode": 1, "speechiness": 0.0883, "acousticness": 0.000322, "instrumentalness": 0.000873, "liveness": 0.329, "valence": 0.491, "tempo": 198.064}, {"song_name": "Kids and Heroes", "song_uri": "spotify:track:7ru4QA7k7ViuLS9oDtdRBI", "artist_name": "The Bouncing Souls", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_name": "Anchors Aweigh", "album_uri": "spotify:album:1xgfRXjCoynPLqtdNu50pR", "popularity": 43, "danceability": 0.405, "energy": 0.963, "key": 0, "loudness": -5.216, "mode": 1, "speechiness": 0.0697, "acousticness": 0.0127, "instrumentalness": 0.000256, "liveness": 0.289, "valence": 0.198, "tempo": 101.759}, {"song_name": "Kate Is Great", "song_uri": "spotify:track:1VT2wLreLu0l7E4T0JDedh", "artist_name": "The Bouncing Souls", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_name": "The Bouncing Souls", "album_uri": "spotify:album:7LgICzKkhaLV9Gttn8xM7a", "popularity": 42, "danceability": 0.358, "energy": 0.93, "key": 2, "loudness": -4.726, "mode": 1, "speechiness": 0.164, "acousticness": 0.0822, "instrumentalness": 0, "liveness": 0.0699, "valence": 0.809, "tempo": 175.011}]'
Looks great! These don’t answer our question, they only provides the data, so we’ll need one more step.
This is similar to what happens in the APIChain
: we have an API response, but we want something a little more human. We’ll use an LLMChain
to send the JSON to GPT along with our question, then get back a readable response.
= ("""
RESPONSE_CLEANUP_PROMPT_TEMPLATE Using this code:
```python
{intermediate_steps}
```
We got the following output from the Spotify API:
```json
{result}
```
Using the output above as your data source, answer the question {question}. Don't describe the code or process, just answer the question.
Answer:"""
)
= PromptTemplate(
RESPONSE_CLEANUP_PROMPT =["question", "intermediate_steps", "result"],
input_variables=RESPONSE_CLEANUP_PROMPT_TEMPLATE,
template )
In the prompt above, we’re providing three things to the prompt:
PALChain
createdPALChain
(aka the JSON)We can now use this prompt with an LLMChain
to turn the JSON into an actual answer.
= LLMChain(
explainer_chain =llm,
llm=RESPONSE_CLEANUP_PROMPT,
prompt=True,
verbose='answer'
output_key )
Now that we’ve built the structure of the explainer, let’s feed it the previous Spotify response and see what happens.
= explainer_chain(spotify_response) explainer_response
> Entering new LLMChain chain...
Prompt after formatting:
Using this code:
```python
def solution():
"""What are the most popular Bouncing Souls songs?"""
search_results = sp.search(q='Bouncing Souls', type='artist')
uri = search_results['artists']['items'][0]['uri']
top_tracks = sp.artist_top_tracks(uri)
tracks = []
for i, track in enumerate(top_tracks['tracks']):
# Only include relevant fields
tracks.append({
'position': i+1,
'song_name': track.get('name'),
'song_uri': track.get('uri'),
'artist_uri': uri,
'album_uri': track.get('album').get('uri'),
'album_name': track.get('album').get('name'),
'popularity': track.get('popularity')
})
return tracks
```
We got the following output from the Spotify API:
```json
[{"position": 1, "song_name": "True Believers", "song_uri": "spotify:track:4fRmFVMd0c1SGfzazBJIM8", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_uri": "spotify:album:64zbLX1ze8N3kcAMX0qq7G", "album_name": "How I Spent My Summer Vacation", "popularity": 54}, {"position": 2, "song_name": "Lean On Sheena", "song_uri": "spotify:track:7IR7GUO0dUyUsBp7BfQ3vJ", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_uri": "spotify:album:3MF7PvmrMjEXGvA8fP3L6l", "album_name": "The Gold Record", "popularity": 51}, {"position": 3, "song_name": "Hopeless Romantic", "song_uri": "spotify:track:180mXjN61yhrKhbY2yQc0E", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_uri": "spotify:album:56CbFyDsG65LI1Eoh7hsOT", "album_name": "Hopeless Romantic", "popularity": 49}, {"position": 4, "song_name": "Manthem", "song_uri": "spotify:track:5pSjxUAwOol5e0TWp1ecHC", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_uri": "spotify:album:64zbLX1ze8N3kcAMX0qq7G", "album_name": "How I Spent My Summer Vacation", "popularity": 46}, {"position": 5, "song_name": "Sing Along Forever", "song_uri": "spotify:track:5feYKXxg4HL2APTQGCfAav", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_uri": "spotify:album:1xgfRXjCoynPLqtdNu50pR", "album_name": "Anchors Aweigh", "popularity": 46}, {"position": 6, "song_name": "Say Anything", "song_uri": "spotify:track:06peZfvxR5721oGqHwogha", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_uri": "spotify:album:7LgICzKkhaLV9Gttn8xM7a", "album_name": "The Bouncing Souls", "popularity": 45}, {"position": 7, "song_name": "Ole", "song_uri": "spotify:track:2McQQA5nCLVL0XvzcxWhFC", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_uri": "spotify:album:56CbFyDsG65LI1Eoh7hsOT", "album_name": "Hopeless Romantic", "popularity": 43}, {"position": 8, "song_name": "Kids and Heroes", "song_uri": "spotify:track:7ru4QA7k7ViuLS9oDtdRBI", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_uri": "spotify:album:1xgfRXjCoynPLqtdNu50pR", "album_name": "Anchors Aweigh", "popularity": 42}, {"position": 9, "song_name": "Kate Is Great", "song_uri": "spotify:track:1VT2wLreLu0l7E4T0JDedh", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_uri": "spotify:album:7LgICzKkhaLV9Gttn8xM7a", "album_name": "The Bouncing Souls", "popularity": 41}, {"position": 10, "song_name": "Ten Stories High", "song_uri": "spotify:track:0Wz9RJySVFtUTFQk8sjRBv", "artist_uri": "spotify:artist:3mvTAjG7rcyk7DQzLwauzV", "album_uri": "spotify:album:5xEwUAv3WJiDtHScEPliQl", "album_name": "Ten Stories High", "popularity": 40}]
```
Using the output above as your data source, answer the question What are the most popular Bouncing Souls songs?. Don't describe the code or process, just answer the question.
Answer:
> Finished chain.
print(explainer_response['answer'])
The most popular Bouncing Souls songs, based on the provided data, are:
1. True Believers
2. Lean On Sheena
3. Hopeless Romantic
4. Manthem
5. Sing Along Forever
6. Say Anything
7. Ole
8. Kids and Heroes
9. Kate Is Great
10. Ten Stories High
I completely disagree with everyone’s taste in music, but it’s a perfect response!
Now that we have a working PALChain
to access the API and an LLMChain
to process the resulting JSON, let’s connect them together so this no longer takes two steps.
We’re going to use a SequentialChain
instead of a SimpleSequentialChain
because the explainer chain needs the question, code and output from the API chain. The SSC only supports one thing being passed along, which would restrict its ability to get all of the necessary information.
from langchain.chains import SequentialChain
= SequentialChain(
overall_chain =[spotify_chain, explainer_chain],
chains=['question'],
input_variables=True
verbose )
= overall_chain.run("Give me a list of Cars, Blink 182 and Sum 41 songs that are upbeat, loud and fun. Make sure the songs are popular enough for me to have heard of them.") overall_response
> Entering new SequentialChain chain...
> Entering new PALChain chain...
def solution():
"""Give me a list of Cars, Blink 182 and Sum 41 songs that are upbeat, loud and fun."""
# Get the URIs for the artists
artists = ['The Cars', 'Blink-182', 'Sum 41']
artist_uris = []
for artist in artists:
search_results = sp.search(q=artist, type='artist', limit=1)
if search_results['artists']['total'] > 0:
artist_uri = search_results['artists']['items'][0]['uri']
artist_uris.append(artist_uri)
# Get the top tracks for each artist
tracks = []
for artist_uri in artist_uris:
artist_tracks = sp.artist_top_tracks(artist_uri)
tracks.extend(artist_tracks['tracks'])
track_uris = [track['uri'] for track in tracks]
# Only keep tracks that are upbeat, loud and fun
upbeat_tracks = []
for i in range(0, len(track_uris), 100):
subset_track_uris = track_uris[i:i+100]
audio_details = sp.audio_features(subset_track_uris)
for j, details in enumerate(audio_details):
if details['valence'] > 0.7 and details['energy'] > 0.7 and details['loudness'] > -7:
track = tracks[i+j]
upbeat_tracks.append({
'song': track.get('name'),
'artist': track.get('artists')[0].get('name'),
'album': track.get('album').get('name'),
'valence': details.get('valence'),
'energy': details.get('energy'),
'loudness': details.get('loudness'),
'song_uri': track.get('uri'),
'artist_uri': track.get('artists')[0].get('uri'),
'album_uri': track.get('album').get('uri')
})
return upbeat_tracks[:30]
> Finished chain.
> Entering new LLMChain chain...
Prompt after formatting:
Using this code:
```python
def solution():
"""Give me a list of Cars, Blink 182 and Sum 41 songs that are upbeat, loud and fun."""
# Get the URIs for the artists
artists = ['The Cars', 'Blink-182', 'Sum 41']
artist_uris = []
for artist in artists:
search_results = sp.search(q=artist, type='artist', limit=1)
if search_results['artists']['total'] > 0:
artist_uri = search_results['artists']['items'][0]['uri']
artist_uris.append(artist_uri)
# Get the top tracks for each artist
tracks = []
for artist_uri in artist_uris:
artist_tracks = sp.artist_top_tracks(artist_uri)
tracks.extend(artist_tracks['tracks'])
track_uris = [track['uri'] for track in tracks]
# Only keep tracks that are upbeat, loud and fun
upbeat_tracks = []
for i in range(0, len(track_uris), 100):
subset_track_uris = track_uris[i:i+100]
audio_details = sp.audio_features(subset_track_uris)
for j, details in enumerate(audio_details):
if details['valence'] > 0.7 and details['energy'] > 0.7 and details['loudness'] > -7:
track = tracks[i+j]
upbeat_tracks.append({
'song': track.get('name'),
'artist': track.get('artists')[0].get('name'),
'album': track.get('album').get('name'),
'valence': details.get('valence'),
'energy': details.get('energy'),
'loudness': details.get('loudness'),
'song_uri': track.get('uri'),
'artist_uri': track.get('artists')[0].get('uri'),
'album_uri': track.get('album').get('uri')
})
return upbeat_tracks[:30]
```
We got the following output from the Spotify API:
```json
[{"song": "You Might Think", "artist": "The Cars", "album": "Heartbeat City (Expanded Edition)", "valence": 0.971, "energy": 0.855, "loudness": -6.031, "song_uri": "spotify:track:35wVRTJlUu2kDkqXFegOKt", "artist_uri": "spotify:artist:6DCIj8jNaNpBz8e5oKFPtp", "album_uri": "spotify:album:7LPfdVDw4uXf9Bw5LQDESf"}, {"song": "Magic - 2017 Remaster", "artist": "The Cars", "album": "Heartbeat City (Expanded Edition)", "valence": 0.964, "energy": 0.794, "loudness": -6.381, "song_uri": "spotify:track:0SAbkr0dS7WK3yJSjaZSZl", "artist_uri": "spotify:artist:6DCIj8jNaNpBz8e5oKFPtp", "album_uri": "spotify:album:7LPfdVDw4uXf9Bw5LQDESf"}, {"song": "Shake It Up - 2017 Remaster", "artist": "The Cars", "album": "Shake It Up (Expanded Edition)", "valence": 0.854, "energy": 0.827, "loudness": -5.738, "song_uri": "spotify:track:3ZBzJbqwV2gQUAe4ofghOo", "artist_uri": "spotify:artist:6DCIj8jNaNpBz8e5oKFPtp", "album_uri": "spotify:album:7KpSpJbHn3SYZIMHKkdO6V"}, {"song": "First Date", "artist": "blink-182", "album": "Take Off Your Pants And Jacket", "valence": 0.882, "energy": 0.928, "loudness": -4.344, "song_uri": "spotify:track:1fJFuvU2ldmeAm5nFIHcPP", "artist_uri": "spotify:artist:6FBDaR13swtiWwGhX1WQsP", "album_uri": "spotify:album:3nHpBmW5wJXGeC3ojBkpey"}, {"song": "The Rock Show", "artist": "blink-182", "album": "Take Off Your Pants And Jacket", "valence": 0.83, "energy": 0.959, "loudness": -4.563, "song_uri": "spotify:track:2ydUT1pFhuLDnouelIv4WH", "artist_uri": "spotify:artist:6FBDaR13swtiWwGhX1WQsP", "album_uri": "spotify:album:3nHpBmW5wJXGeC3ojBkpey"}, {"song": "EDGING", "artist": "blink-182", "album": "EDGING", "valence": 0.706, "energy": 0.905, "loudness": -3.117, "song_uri": "spotify:track:2wVWGFVkL5I3JGsoWBx2AZ", "artist_uri": "spotify:artist:6FBDaR13swtiWwGhX1WQsP", "album_uri": "spotify:album:0EspGdWdoWAxa5mBdQ5z55"}, {"song": "In Too Deep", "artist": "Sum 41", "album": "All Killer, No Filler", "valence": 0.766, "energy": 0.844, "loudness": -5.875, "song_uri": "spotify:track:1HNE2PX70ztbEl6MLxrpNL", "artist_uri": "spotify:artist:0qT79UgT5tY4yudH9VfsdT", "album_uri": "spotify:album:2UCWsnmZEVg9HhnMeKTsim"}, {"song": "Over My Head (Better Off Dead)", "artist": "Sum 41", "album": "Does This Look Infected?", "valence": 0.773, "energy": 0.919, "loudness": -4.462, "song_uri": "spotify:track:3SO0vfryYv381w1ImgWONG", "artist_uri": "spotify:artist:0qT79UgT5tY4yudH9VfsdT", "album_uri": "spotify:album:2kLmv0O8blKeM5HKxLtQrC"}, {"song": "Underclass Hero", "artist": "Sum 41", "album": "Underclass Hero", "valence": 0.765, "energy": 0.988, "loudness": -2.979, "song_uri": "spotify:track:6dXizHF3KbmdvOgvMAhnQC", "artist_uri": "spotify:artist:0qT79UgT5tY4yudH9VfsdT", "album_uri": "spotify:album:4fc73QNw5EjIorFfZ6n6YG"}]
```
Using the output above as your data source, answer the question Give me a list of Cars, Blink 182 and Sum 41 songs that are upbeat, loud and fun. Make sure the songs are popular enough for me to have heard of them.. Don't describe the code or process, just answer the question.
Answer:
> Finished chain.
> Finished chain.
print(overall_response)
- You Might Think by The Cars
- Magic by The Cars
- Shake It Up by The Cars
- First Date by blink-182
- The Rock Show by blink-182
- In Too Deep by Sum 41
- Over My Head (Better Off Dead) by Sum 41
- Underclass Hero by Sum 41
Tada! There we go! That’s it! That’s… it? Guess the magic is in the process.
Let’s see how it approached each aspect of my prompt:
Give me a list of Cars, Blink 182 and Sum 41 songs
Made a list of the bands, looped through each band to search for them.
= ['The Cars', 'Blink-182', 'Sum 41']
artists = []
artist_uris for artist in artists:
= sp.search(q=artist, type='artist', limit=1) search_results
Make sure the songs are popular enough for me to have heard of them.
GPT only pulled the top tracks from each artist, which implies popularity.
for artist_uri in artist_uris:
= sp.artist_top_tracks(artist_uri)
artist_tracks 'tracks']) tracks.extend(artist_tracks[
that are upbeat, loud and fun.
Filtering is done based on scores of valence, energy and loudness. Valence is “musical positiveness,” so that plus energy and loudness seems like a reasonable decision.
= sp.audio_features(subset_track_uris)
audio_details for j, details in enumerate(audio_details):
if details['valence'] > 0.7 and details['energy'] > 0.7 and details['loudness'] > -7:
= tracks[i+j] track
One of the redeeming parts of running this code again and again was seeing how GPT translated the qualitative characteristics like “loud” or “fun” into actual quantitative numbers. Almost every time was a little bit different!
Mission: accomplished?
This was not as easy as I thought, and the end result isn’t as nice as I’d like! But the path was really worth it in terms of exploration and understanding. Let’s talk about a few difficulties I ran into:
I resisted reading the LangChain source early on, and trying to make do with only the examples really slowed me down. Just open the code!!!! It’s plenty readable and concepts that might have been fuzzy clear up when you really dig in and see how the pieces fit together.
Reminding GPT of gotchas with the Spotify API was difficult. For example, tracks don’t have track['album']['name']
if they’re from the sp.album_tracks
endpoint while they do from sp.artist_top_tracks
, different endpoints have different limits, etc etc. Carefully crafting examples while trying to not have ten thousand of them was not fun.
Convincing the PALChain to not do too much work was almost impossible. Since a Python-executing PALChain
can do pretty much anything, it would often end up giving me Markdown tables or other formatting situations that I was hoping to instead get from the later LLMChain
output. The fix as a combination of fine-tuning the prompt – “you are ONLY providing DATA” – and adjusting my queries into a format closer to “find this data, then display it this way.”
All in all, maybe this would probably be better broken up into pieces via ReAct tools? I just didn’t think of that until I was like halfway through, but we at least survived.
As much as I’m proud of my little LangChain child here, I was not about to give this bad boy access to my Spotify playlists. That also would have simplified the process - instead of going through the second LLMChain
, we could have simply stopped at the PALChain
part!
It’s also slightly more complicated auth flow with Spotify, which I didn’t want to step through. Maybe next time!
Feel free to check the GitHub repo if you’d like a simple notebook that has the code pre-arranged for you.