├── README.md └── spotify-langchain-gpt.ipynb /README.md: -------------------------------------------------------------------------------- 1 | # Building Spotify playlists based on vibes using LangChain and GPT 2 | 3 | ### How to run arbitrary libraries with LangChain to integrate Spotify with GPT. 4 | 5 | [Full writeup here](https://jonathansoma.com/words/custom-execution-chain.html), which includes a nice introduction to APIChain, PALChain and SequentialChain. You can also just look at [an example notebook](spotify-langchain-gpt.ipynb) -------------------------------------------------------------------------------- /spotify-langchain-gpt.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "f05f952a", 6 | "metadata": {}, 7 | "source": [ 8 | "# Building Spotify playlists based on vibes using LangChain and GPT\n", 9 | "\n", 10 | "## How to run arbitrary libraries with LangChain to integrate Spotify with GPT.\n", 11 | "\n", 12 | "Full writeup at [https://jonathansoma.com/words/custom-execution-chain.html](https://jonathansoma.com/words/custom-execution-chain.html), which includes a nice introduction to APIChain, PALChain and SequentialChain." 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 2, 18 | "id": "a111f7bd", 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "%load_ext dotenv\n", 23 | "%dotenv" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 12, 29 | "id": "dbed579b", 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "# LangChain\n", 34 | "from langchain.chains import PALChain\n", 35 | "from langchain.chains import LLMChain\n", 36 | "from langchain.chains import SequentialChain\n", 37 | "from langchain.prompts import PromptTemplate\n", 38 | "from langchain.chat_models import ChatOpenAI\n", 39 | "from langchain.prompts.prompt import PromptTemplate\n", 40 | "\n", 41 | "# Spotipy\n", 42 | "import spotipy\n", 43 | "from spotipy.oauth2 import SpotifyClientCredentials\n", 44 | "\n", 45 | "# Etc\n", 46 | "import os" 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "id": "75a84e6b", 52 | "metadata": {}, 53 | "source": [ 54 | "# Connect to GPT" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 13, 60 | "id": "e8f5ba4d", 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "llm = ChatOpenAI(model_name='gpt-3.5-turbo')" 65 | ] 66 | }, 67 | { 68 | "cell_type": "markdown", 69 | "id": "7d68d884", 70 | "metadata": {}, 71 | "source": [ 72 | "# Connect to Spotify\n", 73 | "\n", 74 | "We're using [Spotipy](https://spotipy.readthedocs.io/en/2.22.1/) to do this." 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": 14, 80 | "id": "32facb60", 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [ 84 | "auth = SpotifyClientCredentials(\n", 85 | " client_id=os.environ['SPOTIPY_CLIENT_ID'],\n", 86 | " client_secret=os.environ['SPOTIPY_CLIENT_SECRET']\n", 87 | ")\n", 88 | "sp = spotipy.Spotify(auth_manager=auth)" 89 | ] 90 | }, 91 | { 92 | "cell_type": "markdown", 93 | "id": "5158ec54", 94 | "metadata": {}, 95 | "source": [ 96 | "# PALChain\n", 97 | "\n", 98 | "## The prompt\n", 99 | "\n", 100 | "We'll show GPT some examples of how to use Spotipy to access information from Spotify. **Note that his prompt is far from perfect!**" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": 16, 106 | "id": "97addd2b", 107 | "metadata": {}, 108 | "outputs": [], 109 | "source": [ 110 | "SPOTIPY_PROMPT_TEMPLATE = (\n", 111 | " '''\n", 112 | "API LIMITATIONS TO NOTE\n", 113 | "* When requesting track information, the limit is 50 at a time\n", 114 | "* When requesting audio features, the limit is 100 at a time\n", 115 | "* When selecting multiple artists, the limit is 50 at a time\n", 116 | "* When asking for recommendations, the limit is 100 at a time\n", 117 | "=====\n", 118 | "\n", 119 | "Q: What albums has the band Green Day made?\n", 120 | "\n", 121 | "# solution in Python:\n", 122 | "\n", 123 | "\n", 124 | "def solution():\n", 125 | " \"\"\"What albums has the band Green Day made?\"\"\"\n", 126 | " search_results = sp.search(q='Green Day', type='artist')\n", 127 | " uri = search_results['artists']['items'][0]['uri']\n", 128 | " albums = sp.artist_albums(green_day_uri, album_type='album')\n", 129 | " return albums\n", 130 | "\n", 131 | "\n", 132 | "\n", 133 | "\n", 134 | "Q: Who are some musicians similar to Fiona Apple?\n", 135 | "\n", 136 | "# solution in Python:\n", 137 | "\n", 138 | "\n", 139 | "def solution():\n", 140 | " \"\"\"Who are some musicians similar to Fiona Apple?\"\"\"\n", 141 | " search_results = sp.search(q='Fiona Apple', type='artist')\n", 142 | " uri = search_results['artists']['items'][0].get('uri')\n", 143 | " artists = sp.artist_related_artists(uri)\n", 144 | " return artists\n", 145 | "\n", 146 | "\n", 147 | "\n", 148 | "Q: Tell me what songs by The Promise Ring sound like\n", 149 | "\n", 150 | "# solution in Python:\n", 151 | "\n", 152 | "\n", 153 | "def solution():\n", 154 | " \"\"\"Tell me what songs by The Promise Ring sound like?\"\"\"\n", 155 | " search_results = sp.search(q='The Promise Ring', type='artist')\n", 156 | " uri = search_results['artists']['items'][0].get('uri')\n", 157 | " tracks = sp.artist_top_tracks(uri)\n", 158 | " track_uris = [track.get('uri') for track in tracks['tracks']]\n", 159 | " audio_details = sp.audio_features(track_uris)\n", 160 | " return audio_details\n", 161 | "\n", 162 | "\n", 163 | "\n", 164 | "Q: Get me the URI for the album The Colour And The Shape\n", 165 | "\n", 166 | "# solution in Python:\n", 167 | "\n", 168 | "\n", 169 | "def solution():\n", 170 | " \"\"\"Get me the URI for the album The Colour And The Shape\"\"\"\n", 171 | " search_results = sp.search(q='The Colour And The Shape', type='album')\n", 172 | " uri = search_results['albums']['items'][0].get('uri')\n", 173 | " return uri\n", 174 | "\n", 175 | "\n", 176 | "\n", 177 | "Q: What are the first three songs on Diet Cig's Over Easy?\n", 178 | "\n", 179 | "# solution in Python:\n", 180 | "\n", 181 | "\n", 182 | "def solution():\n", 183 | " \"\"\"What are the first three songs on Diet Cig's Over Easy?\"\"\"\n", 184 | " # Get the URI for the album\n", 185 | " search_results = sp.search(q='Diet Cig Over Easy', type='album')\n", 186 | " album = search_results['albums']['items'][0]\n", 187 | " album_uri = album['uri']\n", 188 | " # Get the album tracks\n", 189 | " album_tracks = sp.album_tracks(album_uri)['items']\n", 190 | " # Sort the tracks by duration\n", 191 | " first_three = album_tracks[:3]\n", 192 | " tracks = []\n", 193 | " # Only include relevant fields\n", 194 | " for i, track in enumerate(first_three):\n", 195 | " # track['album'] does NOT work with sp.album_tracks\n", 196 | " # you need to use album['name'] instead\n", 197 | " tracks.append({{\n", 198 | " 'position': i+1,\n", 199 | " 'song_name': track.get('name'),\n", 200 | " 'song_uri': track['artists'][0].get('uri'),\n", 201 | " 'artist_uri': track['artists'][0].get('uri'),\n", 202 | " 'album_uri': album.get('uri'),\n", 203 | " 'album_name': album.get('name')\n", 204 | " }})\n", 205 | " return tracks\n", 206 | "\n", 207 | "\n", 208 | "Q: What are the thirty most danceable songs by Metallica?\n", 209 | "\n", 210 | "# solution in Python:\n", 211 | "\n", 212 | "\n", 213 | "def solution():\n", 214 | " \"\"\"What are most danceable songs by Metallica?\"\"\"\n", 215 | " search_results = sp.search(q='Metallica', type='artist')\n", 216 | " uri = search_results['artists']['items'][0]['uri']\n", 217 | " albums = sp.artist_albums(uri, album_type='album')\n", 218 | " album_uris = [album['uri'] for album in albums['items']]\n", 219 | " tracks = []\n", 220 | " for album_uri in album_uris:\n", 221 | " album_tracks = sp.album_tracks(album_uri)\n", 222 | " tracks.extend(album_tracks['items'])\n", 223 | " track_uris = [track['uri'] for track in tracks]\n", 224 | " danceable_tracks = []\n", 225 | " # You can only have 100 at a time\n", 226 | " for i in range(0, len(track_uris), 100):\n", 227 | " subset_track_uris = track_uris[i:i+100]\n", 228 | " audio_details = sp.audio_features(subset_track_uris)\n", 229 | " for j, details in enumerate(audio_details):\n", 230 | " if details['danceability'] > 0.7:\n", 231 | " track = tracks[i+j]\n", 232 | " danceable_tracks.append({{\n", 233 | " 'song': track.get('name')\n", 234 | " 'album': track.get('album').get('name')\n", 235 | " 'danceability': details.get('danceability'),\n", 236 | " 'tempo': details.get('tempo'),\n", 237 | " }})\n", 238 | " # Be sure to add the audio details to the track\n", 239 | " danceable_tracks.append(track)\n", 240 | " return danceable_tracks\n", 241 | "\n", 242 | "\n", 243 | "\n", 244 | "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).\n", 245 | "\n", 246 | "# solution in Python:\n", 247 | "'''.strip()\n", 248 | " + \"\\n\\n\\n\"\n", 249 | ")\n", 250 | "\n", 251 | "SPOTIPY_PROMPT = PromptTemplate(input_variables=[\"question\"], template=SPOTIPY_PROMPT_TEMPLATE)" 252 | ] 253 | }, 254 | { 255 | "cell_type": "markdown", 256 | "id": "9234ddb1", 257 | "metadata": {}, 258 | "source": [ 259 | "## Creating our PALChain\n", 260 | "\n", 261 | "We pass our logged-in Spotipy instance to the PALChain and make a somewhat-complex `get_answer_expr` to be returned appropriate JSON." 262 | ] 263 | }, 264 | { 265 | "cell_type": "code", 266 | "execution_count": 17, 267 | "id": "24a37806", 268 | "metadata": {}, 269 | "outputs": [], 270 | "source": [ 271 | "spotify_chain = PALChain(\n", 272 | " llm=llm,\n", 273 | " prompt=SPOTIPY_PROMPT,\n", 274 | " python_globals={\n", 275 | " 'sp': sp\n", 276 | " },\n", 277 | " stop='\\n\\n\\n',\n", 278 | " verbose=True,\n", 279 | " return_intermediate_steps=True,\n", 280 | " get_answer_expr=\"import json; print(json.dumps(solution()))\",\n", 281 | ")" 282 | ] 283 | }, 284 | { 285 | "cell_type": "markdown", 286 | "id": "5dabeb9b", 287 | "metadata": {}, 288 | "source": [ 289 | "# LLMChain for cleanup\n", 290 | "\n", 291 | "The PALChain gives us JSON, this turns it into words.\n", 292 | "\n", 293 | "## The prompt" 294 | ] 295 | }, 296 | { 297 | "cell_type": "code", 298 | "execution_count": 18, 299 | "id": "d1d9eb01", 300 | "metadata": {}, 301 | "outputs": [], 302 | "source": [ 303 | "RESPONSE_CLEANUP_PROMPT_TEMPLATE = (\"\"\" \n", 304 | "Using this code:\n", 305 | "\n", 306 | "```python\n", 307 | "{intermediate_steps}\n", 308 | "```\n", 309 | "\n", 310 | "We got the following output from the Spotify API:\n", 311 | "\n", 312 | "```json\n", 313 | "{result}\n", 314 | "```\n", 315 | "\n", 316 | "Using the output above as your data source, answer the question {question}. Don't describe the code or process, just answer the question.\n", 317 | "Answer:\"\"\"\n", 318 | ")\n", 319 | "\n", 320 | "RESPONSE_CLEANUP_PROMPT = PromptTemplate(\n", 321 | " input_variables=[\"question\", \"intermediate_steps\", \"result\"],\n", 322 | " template=RESPONSE_CLEANUP_PROMPT_TEMPLATE,\n", 323 | ")" 324 | ] 325 | }, 326 | { 327 | "cell_type": "markdown", 328 | "id": "225e3aac", 329 | "metadata": {}, 330 | "source": [ 331 | "## Creating the LLMChain" 332 | ] 333 | }, 334 | { 335 | "cell_type": "code", 336 | "execution_count": 19, 337 | "id": "fd271ee0", 338 | "metadata": {}, 339 | "outputs": [], 340 | "source": [ 341 | "explainer_chain = LLMChain(\n", 342 | " llm=llm,\n", 343 | " prompt=RESPONSE_CLEANUP_PROMPT,\n", 344 | " verbose=True,\n", 345 | " output_key='answer'\n", 346 | ")" 347 | ] 348 | }, 349 | { 350 | "cell_type": "markdown", 351 | "id": "a40a0f9b", 352 | "metadata": {}, 353 | "source": [ 354 | "# Connecting the chains\n", 355 | "\n", 356 | "Now we'll plug the two chains together to get our full process." 357 | ] 358 | }, 359 | { 360 | "cell_type": "code", 361 | "execution_count": 20, 362 | "id": "e02a7353", 363 | "metadata": {}, 364 | "outputs": [], 365 | "source": [ 366 | "overall_chain = SequentialChain(\n", 367 | " chains=[spotify_chain, explainer_chain],\n", 368 | " input_variables=['question'],\n", 369 | " verbose=True\n", 370 | ")" 371 | ] 372 | }, 373 | { 374 | "cell_type": "markdown", 375 | "id": "ccf26971", 376 | "metadata": {}, 377 | "source": [ 378 | "# Using the chains\n", 379 | "\n", 380 | "Here we go!" 381 | ] 382 | }, 383 | { 384 | "cell_type": "code", 385 | "execution_count": 23, 386 | "id": "1d6c958e", 387 | "metadata": {}, 388 | "outputs": [ 389 | { 390 | "name": "stdout", 391 | "output_type": "stream", 392 | "text": [ 393 | "\n", 394 | "\n", 395 | "\u001b[1m> Entering new SequentialChain chain...\u001b[0m\n", 396 | "\n", 397 | "\n", 398 | "\u001b[1m> Entering new PALChain chain...\u001b[0m\n", 399 | "\u001b[32;1m\u001b[1;3mdef solution():\n", 400 | " \"\"\"List the 3 most downbeat songs from The Clash's Combat Rock\"\"\"\n", 401 | " # Get the URI for the album\n", 402 | " search_results = sp.search(q='The Clash Combat Rock', type='album')\n", 403 | " album = search_results['albums']['items'][0]\n", 404 | " album_uri = album['uri']\n", 405 | " # Get the album tracks\n", 406 | " album_tracks = sp.album_tracks(album_uri)['items']\n", 407 | " # Sort the tracks by valence (downbeatness)\n", 408 | " tracks = []\n", 409 | " for i, track in enumerate(album_tracks):\n", 410 | " audio_details = sp.audio_features([track['uri']])[0]\n", 411 | " tracks.append({\n", 412 | " 'position': i+1,\n", 413 | " 'song_name': track.get('name'),\n", 414 | " 'song_uri': track.get('uri'),\n", 415 | " 'artist_uri': track['artists'][0].get('uri'),\n", 416 | " 'album_uri': album.get('uri'),\n", 417 | " 'album_name': album.get('name'),\n", 418 | " 'valence': audio_details.get('valence')\n", 419 | " })\n", 420 | " sorted_tracks = sorted(tracks, key=lambda x: x['valence'])\n", 421 | " downbeat_tracks = sorted_tracks[:3]\n", 422 | " return downbeat_tracks\u001b[0m\n", 423 | "\n", 424 | "\u001b[1m> Finished chain.\u001b[0m\n", 425 | "\n", 426 | "\n", 427 | "\u001b[1m> Entering new LLMChain chain...\u001b[0m\n", 428 | "Prompt after formatting:\n", 429 | "\u001b[32;1m\u001b[1;3m \n", 430 | "Using this code:\n", 431 | "\n", 432 | "```python\n", 433 | "def solution():\n", 434 | " \"\"\"List the 3 most downbeat songs from The Clash's Combat Rock\"\"\"\n", 435 | " # Get the URI for the album\n", 436 | " search_results = sp.search(q='The Clash Combat Rock', type='album')\n", 437 | " album = search_results['albums']['items'][0]\n", 438 | " album_uri = album['uri']\n", 439 | " # Get the album tracks\n", 440 | " album_tracks = sp.album_tracks(album_uri)['items']\n", 441 | " # Sort the tracks by valence (downbeatness)\n", 442 | " tracks = []\n", 443 | " for i, track in enumerate(album_tracks):\n", 444 | " audio_details = sp.audio_features([track['uri']])[0]\n", 445 | " tracks.append({\n", 446 | " 'position': i+1,\n", 447 | " 'song_name': track.get('name'),\n", 448 | " 'song_uri': track.get('uri'),\n", 449 | " 'artist_uri': track['artists'][0].get('uri'),\n", 450 | " 'album_uri': album.get('uri'),\n", 451 | " 'album_name': album.get('name'),\n", 452 | " 'valence': audio_details.get('valence')\n", 453 | " })\n", 454 | " sorted_tracks = sorted(tracks, key=lambda x: x['valence'])\n", 455 | " downbeat_tracks = sorted_tracks[:3]\n", 456 | " return downbeat_tracks\n", 457 | "```\n", 458 | "\n", 459 | "We got the following output from the Spotify API:\n", 460 | "\n", 461 | "```json\n", 462 | "[{\"position\": 1, \"song_name\": \"Know Your Rights - Remastered\", \"song_uri\": \"spotify:track:31l6t3Jq09uywRTVGbzant\", \"artist_uri\": \"spotify:artist:3RGLhK1IP9jnYFH4BRFJBS\", \"album_uri\": \"spotify:album:1ZH5g1RDq3GY1OvyD0w0s2\", \"album_name\": \"Combat Rock (Remastered)\", \"valence\": 0.348}, {\"position\": 9, \"song_name\": \"Sean Flynn - Remastered\", \"song_uri\": \"spotify:track:4AWJmxQP5DK2mpQvKecaUB\", \"artist_uri\": \"spotify:artist:3RGLhK1IP9jnYFH4BRFJBS\", \"album_uri\": \"spotify:album:1ZH5g1RDq3GY1OvyD0w0s2\", \"album_name\": \"Combat Rock (Remastered)\", \"valence\": 0.387}, {\"position\": 12, \"song_name\": \"Death is a Star - Remastered\", \"song_uri\": \"spotify:track:4PY3mBItnv6YzN66Sq3Ci4\", \"artist_uri\": \"spotify:artist:3RGLhK1IP9jnYFH4BRFJBS\", \"album_uri\": \"spotify:album:1ZH5g1RDq3GY1OvyD0w0s2\", \"album_name\": \"Combat Rock (Remastered)\", \"valence\": 0.509}]\n", 463 | "```\n", 464 | "\n", 465 | "Using the output above as your data source, answer the question List the 3 most downbeat songs from The Clash's Combat Rock. Don't describe the code or process, just answer the question.\n", 466 | "Answer:\u001b[0m\n", 467 | "\n", 468 | "\u001b[1m> Finished chain.\u001b[0m\n", 469 | "\n", 470 | "\u001b[1m> Finished chain.\u001b[0m\n" 471 | ] 472 | } 473 | ], 474 | "source": [ 475 | "overall_response = overall_chain.run(\"List the 3 most downbeat songs from The Clash's Combat Rock\")" 476 | ] 477 | }, 478 | { 479 | "cell_type": "code", 480 | "execution_count": 27, 481 | "id": "c6e5e1a0", 482 | "metadata": {}, 483 | "outputs": [ 484 | { 485 | "name": "stdout", 486 | "output_type": "stream", 487 | "text": [ 488 | "The 3 most downbeat songs from The Clash's Combat Rock are \"Know Your Rights - Remastered\", \"Sean Flynn - Remastered\", and \"Death is a Star - Remastered\".\n" 489 | ] 490 | } 491 | ], 492 | "source": [ 493 | "# Does low valance mean downbeat? According to GPT!\n", 494 | "print(overall_response)" 495 | ] 496 | }, 497 | { 498 | "cell_type": "code", 499 | "execution_count": null, 500 | "id": "f260b7dd", 501 | "metadata": {}, 502 | "outputs": [], 503 | "source": [] 504 | } 505 | ], 506 | "metadata": { 507 | "kernelspec": { 508 | "display_name": "Python 3 (ipykernel)", 509 | "language": "python", 510 | "name": "python3" 511 | }, 512 | "language_info": { 513 | "codemirror_mode": { 514 | "name": "ipython", 515 | "version": 3 516 | }, 517 | "file_extension": ".py", 518 | "mimetype": "text/x-python", 519 | "name": "python", 520 | "nbconvert_exporter": "python", 521 | "pygments_lexer": "ipython3", 522 | "version": "3.10.3" 523 | } 524 | }, 525 | "nbformat": 4, 526 | "nbformat_minor": 5 527 | } 528 | --------------------------------------------------------------------------------