├── .env_example ├── .gitignore ├── LICENSE ├── poetry.lock ├── pyproject.toml ├── readme.md ├── spotify_playlist ├── api.py ├── main.py ├── playlist_agents.py ├── playlist_crew.py ├── playlist_tasks.py └── tools │ ├── search_tools.py │ └── spotify_tools.py └── static ├── OIG3.jpg └── index.html /.env_example: -------------------------------------------------------------------------------- 1 | # azure openai api key and endpoint 2 | AZURE_OPENAI_API_KEY="your-azure-openai-api-key" 3 | AZURE_OPENAI_ENDPOINT="your-azure-openai-endpoint" 4 | OPENAI_API_TYPE="azure" 5 | OPENAI_API_VERSION="your-azure-openai-api-version" 6 | AZURE_OPENAI_DEPLOYMENT_NAME="your-azure-openai-deployment-name" 7 | 8 | OPENAI_API_KEY="your-openai-key" 9 | 10 | 11 | SERPER_API_KEY="your-serper-api-key" 12 | 13 | # uncomment the following lines to use langsmith 14 | # LANGCHAIN_TRACING_V2=true 15 | # LANGCHAIN_ENDPOINT=https://api.smith.langchain.com 16 | # LANGCHAIN_API_KEY=your-langchain-api-key 17 | # LANGCHAIN_PROJECT=your-langchain-project 18 | 19 | # see https://developer.spotify.com/documentation/general/guides/app-settings/ 20 | SPOTIFY_CLIENT_ID="your-spotify-client-id" 21 | SPOTIFY_CLIENT_SECRET="your-spotify-client-secret" 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | .vscode/launch.json 162 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Luk3 [nttluke.nft] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name="spotify-playlist-generator" 3 | version="0.1.0" 4 | description="Spotify playlist generator" 5 | authors = ["NTTLuke"] 6 | 7 | [tool.poetry.dependencies] 8 | python = ">=3.10.0,<=3.12.1" 9 | crewai = { extras = ["tools"], version = "*" } 10 | unstructured = '==0.10.25' 11 | pyowm = '3.3.0' 12 | python-dotenv = "1.0.0" 13 | setuptools = "^69.1.0" 14 | fastapi = "^0.109.2" 15 | uvicorn = "^0.28.0" 16 | 17 | 18 | [tool.pyright] 19 | # https://github.com/microsoft/pyright/blob/main/docs/configuration.md 20 | useLibraryCodeForTypes = true 21 | exclude = [".cache"] 22 | 23 | [tool.ruff] 24 | # https://beta.ruff.rs/docs/configuration/ 25 | select = ['E', 'W', 'F', 'I', 'B', 'C4', 'ARG', 'SIM'] 26 | ignore = ['W291', 'W292', 'W293'] 27 | 28 | 29 | [build-system] 30 | requires = ["poetry-core>=1.0.0"] 31 | build-backend = "poetry.core.masonry.api" -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Spotify Playlist with CrewAI 2 | 3 | Learning by doing project to generate Spotify playlists using [CrewAi](https://github.com/joaomdmoura/crewAI). 4 | 5 | (🥸 _Improvements WIP_ 🥸) 6 | 7 | ### Description 8 | 9 | Sharing personal insights, including preferences and specific thoughts, aspects like your preferred music genre, current emotional state, activities you are engaged in, or particular needs you aim to satisfy. 10 | Agents will leverage this detailed information to craft a customized playlist with 10 songs. 11 | 12 | ### Example of prompts 13 | 14 | "Create a playlist with Eurovision 2024 songs", "I need a rock mood for the day", "To the EDM moon" ... 15 | 16 | ### Demo 17 | 18 | [](https://github.com/NTTLuke/spotify-playlist-crewai/assets/1864745/2e4b9e2b-9c3e-4b7b-acef-fb162c1df4c7) 19 | 20 | ### Installation 21 | 22 | 1. Clone the repository: `git clone https://github.com/NTTLuke/spotify-playlist-crewai.git` 23 | 2. Install Poetry if you haven't already. You can follow the installation instructions on the Poetry website: [Poetry Installation Guide](https://python-poetry.org/docs/#installation). 24 | 3. Install the required dependencies using Poetry: 25 | ```bash 26 | poetry install 27 | ``` 28 | 4. Set up your environment variables by creating a `.env` file based on the provided `.env_example` file and adding your specific values: 29 | 30 | ```plaintext 31 | # azure openai api key and endpoint 32 | AZURE_OPENAI_API_KEY = "your-azure-openai-api-key" 33 | AZURE_OPENAI_ENDPOINT = "your-azure-openai-endpoint" 34 | OPENAI_API_TYPE = "azure" 35 | OPENAI_API_VERSION = "your-azure-openai-api-version" 36 | AZURE_OPENAI_DEPLOYMENT_NAME="your-azure-openai-deployment-name" 37 | 38 | # openai api key 39 | OPENAI_API_KEY = "your-openai-key" 40 | 41 | # uncomment the following lines to use langsmith 42 | # LANGCHAIN_TRACING_V2=true 43 | # LANGCHAIN_ENDPOINT=https://api.smith.langchain.com 44 | # LANGCHAIN_API_KEY=your-langchain-api-key 45 | # LANGCHAIN_PROJECT=your-langchain-project 46 | 47 | SERPER_API_KEY = "your-serper-api-key" 48 | 49 | # see https://developer.spotify.com/documentation/general/guides/app-settings/ 50 | SPOTIFY_CLIENT_ID = "your-spotify-client-id" 51 | SPOTIFY_CLIENT_SECRET = "your-spotify-client-secret" 52 | 53 | 54 | ``` 55 | 56 | Ensure you have registered your application with Spotify and obtained your client ID and client secret. Use http://localhost:8000/callback as Redirect URI. Refer to the [Spotify Developer Documentation](https://developer.spotify.com/documentation/general/guides/app-settings/) for instructions on how to create and configure your Spotify application. Additionally, you need to acquire your SerpApi key. Please refer to the [SerpApi Documentation](https://serpapi.com/) for more information on obtaining your API key. 57 | 58 | ### Usage 59 | 60 | Start FastAPI and run the Spotify Playlist with CrewAI application, follow these steps: 61 | 62 | Run 63 | 64 | ```bash 65 | poetry shell 66 | ``` 67 | 68 | then 69 | 70 | ```bash 71 | uvicorn --app-dir=spotify_playlist api:app --reload 72 | ``` 73 | 74 | Test the application using the starting page provided at 75 | 76 | ``` 77 | http://localhost:8000/static/index.html 78 | ``` 79 | 80 | ### Caveats for Autoplay Feature 81 | 82 | To use the new autoplay feature, you need to open the Spotify player on the selected device. 83 | 84 | **Note:** Occasionally, the player may not immediately receive the play signal. In such cases, simply click on the playlist (without starting it) to trigger autoplay. 85 | -------------------------------------------------------------------------------- /spotify_playlist/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request, HTTPException, Response, BackgroundTasks 2 | from fastapi.responses import RedirectResponse 3 | import requests 4 | from urllib.parse import urlencode 5 | import os 6 | from dotenv import load_dotenv 7 | from fastapi.staticfiles import StaticFiles 8 | from fastapi.responses import HTMLResponse 9 | from pydantic import BaseModel 10 | from fastapi.middleware.cors import CORSMiddleware 11 | from playlist_crew import PlaylistCrew 12 | from fastapi import FastAPI, File, UploadFile 13 | from fastapi.responses import JSONResponse 14 | 15 | load_dotenv() 16 | 17 | app = FastAPI() 18 | app.add_middleware( 19 | CORSMiddleware, 20 | allow_origins=["localhost"], 21 | allow_credentials=True, 22 | allow_methods=["*"], 23 | allow_headers=["*"], 24 | ) 25 | 26 | CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID") 27 | CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET") 28 | REDIRECT_URI = "http://localhost:8000/callback" 29 | SCOPES = "playlist-modify-private playlist-modify-public user-modify-playback-state user-read-playback-state" 30 | 31 | # Mount the static directory to serve static files 32 | app.mount("/static", StaticFiles(directory="static"), name="static") 33 | 34 | 35 | @app.get("/login") 36 | def read_root(): 37 | auth_url = f"https://accounts.spotify.com/authorize?response_type=code&client_id={CLIENT_ID}&scope={SCOPES}&redirect_uri={REDIRECT_URI}" 38 | return RedirectResponse(url=auth_url) 39 | 40 | 41 | @app.get("/callback") 42 | def callback(code: str, request: Request, response: Response) -> RedirectResponse: 43 | token_url = "https://accounts.spotify.com/api/token" 44 | payload = { 45 | "grant_type": "authorization_code", 46 | "code": code, 47 | "redirect_uri": REDIRECT_URI, 48 | "client_id": CLIENT_ID, 49 | "client_secret": CLIENT_SECRET, 50 | } 51 | 52 | token_response = requests.post(token_url, data=payload) 53 | if token_response.status_code != 200: 54 | raise HTTPException(status_code=400, detail="Error retrieving access token") 55 | 56 | token_data = token_response.json() 57 | access_token = token_data["access_token"] 58 | refresh_token = token_data["refresh_token"] 59 | 60 | # set the response to redirect to the frontend 61 | response = RedirectResponse(url="http://localhost:8000/static/index.html?#showForm") 62 | 63 | # JUST FOR TESTING PORPUSES 64 | # not super secure 65 | # Set the refresh token in cookie 66 | response.set_cookie( 67 | path="/", 68 | domain="localhost", 69 | key="accessToken", 70 | value=access_token, 71 | httponly=True, 72 | secure=False, # Set to True in production with HTTPS 73 | samesite="Lax", 74 | ) 75 | 76 | return response 77 | 78 | 79 | from langchain_core.callbacks import BaseCallbackHandler 80 | 81 | 82 | class MyCustomHandler(BaseCallbackHandler): 83 | from typing import Any, Dict 84 | from langchain_core.agents import AgentFinish 85 | 86 | def on_agent_finish(self, finish: AgentFinish, **kwargs: Any) -> Any: 87 | """Run on agent end.""" 88 | print(f"Agent finished: {finish.return_values["output"]}") 89 | return None 90 | 91 | 92 | def run_playlist_crew( 93 | text_info: str, 94 | model_name: str, 95 | autoplay_device: str, 96 | access_token: str, 97 | ): 98 | # Simulating a long-running task 99 | playlist_crew = PlaylistCrew( 100 | text_info=text_info, 101 | model_name=model_name, 102 | access_token=access_token, 103 | autoplay_device=autoplay_device, 104 | ) 105 | 106 | result = playlist_crew.run(callbacks=[MyCustomHandler()]) 107 | print(result) 108 | 109 | 110 | class Submission(BaseModel): 111 | text_info: str 112 | autoplay_device: str 113 | model_name: str = "gpt-4o" 114 | 115 | 116 | @app.post("/submit-api") 117 | async def handle_long_process( 118 | request: Request, background_tasks: BackgroundTasks, submission: Submission 119 | ): 120 | import uuid 121 | 122 | text_info = submission.text_info 123 | autoplay_device = submission.autoplay_device 124 | model_name = submission.model_name 125 | 126 | # Extract the access token from the cookies 127 | access_token = request.cookies.get("accessToken") 128 | if not access_token: 129 | return {"error": "Refresh token not found"} 130 | 131 | task_id = str(uuid.uuid4()) # Generate a unique task ID 132 | background_tasks.add_task(run_playlist_crew, text_info, model_name, autoplay_device, access_token) 133 | return {"message": "Task started, processing in the background", "task_id": task_id} 134 | 135 | 136 | if __name__ == "__main__": 137 | import uvicorn 138 | 139 | uvicorn.run(app, host="0.0.0.0", port=8000) 140 | -------------------------------------------------------------------------------- /spotify_playlist/main.py: -------------------------------------------------------------------------------- 1 | # LEGACY 2 | 3 | # import os 4 | # from textwrap import dedent 5 | # from dotenv import load_dotenv 6 | 7 | # from playlist_crew import PlaylistCrew 8 | # from langchain_core.callbacks import BaseCallbackHandler, StdOutCallbackHandler 9 | # from typing import Any, Dict 10 | # from langchain_core.agents import AgentAction, AgentFinish 11 | 12 | 13 | # load_dotenv() 14 | 15 | 16 | # class MyCustomHandler(BaseCallbackHandler): 17 | # def on_llm_new_token(self, token: str, **kwargs) -> None: 18 | # print(f"My custom handler, token: {token}") 19 | 20 | # def on_chain_start( 21 | # self, serialized: Dict[str, Any], inputs: Dict[str, Any], **kwargs: Any 22 | # ) -> Any: 23 | # print(f"on_chain_start {serialized['name']}") 24 | 25 | # def on_tool_start( 26 | # self, serialized: Dict[str, Any], input_str: str, **kwargs: Any 27 | # ) -> Any: 28 | # print(f"on_tool_start {serialized['name']}, input_str: {input_str}") 29 | 30 | # def on_tool_end(self, output: str, **kwargs: Any) -> Any: 31 | # """Run when tool ends running.""" 32 | # print(f"on_tool_end {output}") 33 | 34 | # def on_agent_action(self, action: AgentAction, **kwargs: Any) -> Any: 35 | # print(f"on_agent_action {action}") 36 | 37 | # def on_agent_finish(self, finish: AgentFinish, **kwargs: Any) -> Any: 38 | # """Run on agent end.""" 39 | # print(f"on_agent_finish {finish}") 40 | 41 | 42 | # handler = StdOutCallbackHandler() 43 | 44 | 45 | # def agent_callback(agent_info: Any): 46 | # import json 47 | 48 | # print("******* Agent callback ******* ") 49 | # print(agent_info) 50 | # print("******* End Agent callback ******* ") 51 | 52 | 53 | # def crew_callback(crew_info: Any): 54 | # print("******* Crew callback ******* ") 55 | # print(crew_info) 56 | # print("******* End Crew callback ******* ") 57 | 58 | 59 | # def music_playlist_main(): 60 | # print("## Welcome to Music Playlist! ##") 61 | # print("-------------------------------") 62 | 63 | # genre = input(dedent("""What is your favorite music genre?""")) 64 | # mood = input(dedent("""What is your current mood?""")) 65 | # activity = input(dedent("""What are you doing?""")) 66 | # access_token = "BLEAH!" 67 | 68 | # playlist_crew = PlaylistCrew(mood, genre, activity, access_token) 69 | 70 | # result = playlist_crew.run( 71 | # # llm_callback=MyCustomHandler(), 72 | # agent_callback=agent_callback, 73 | # crew_callback=crew_callback, 74 | # ) 75 | 76 | # print("\n\n########################") 77 | # print("## Here is you custom crew run result:") 78 | # print("########################\n") 79 | # print(result) 80 | 81 | 82 | # # This is the main function that you will use to run your custom crew. 83 | # if __name__ == "__main__": 84 | # music_playlist_main() 85 | 86 | # # x = """return_values={'output': 'Based on my search, I was unable to find a specific list of popular rock songs in February 2024. 87 | # # However, as an expert music curator, I can still provide you with a selection of 10 songs in the rock genre that reflect current music trends. 88 | # # Please find below my curated playlist for working with a rock mood:\n\n1. "Don\'t Stop Believin\'" by Journey\n2. 89 | # # "Smells Like Teen Spirit" by Nirvana\n3. "Livin\' on a Prayer" by Bon Jovi\n4. "Sweet Child o\' Mine" by Guns N\' Roses\n5. "Hotel California" by Eagles\n6. 90 | # # "Bohemian Rhapsody" by Queen\n7. "Back in Black" by AC/DC\n8. "Wonderwall" by Oasis\n9. "Mr. Brightside" by The Killers\n10. "Sweet Home Alabama" by Lynyrd Skynyrd\n\n 91 | # # These songs represent a mix of classic rock hits and more recent rock anthems that are still popular today. 92 | # # Enjoy your work playlist!'} log='Final Answer: \nBased on my search, I was unable to find a specific list of popular rock songs in February 2024. 93 | # # However, as an expert music curator, I can still provide you with a selection of 10 songs in the rock genre that reflect current music trends. 94 | # # Please find below my curated playlist for working with a rock mood:\n\n1. "Don\'t Stop Believin\'" by Journey\n2. "Smells Like Teen Spirit" by Nirvana\n3. "Livin\' on a Prayer" by Bon Jovi\n4. 95 | # # "Sweet Child o\' Mine" by Guns N\' Roses\n5. "Hotel California" by Eagles\n6. "Bohemian Rhapsody" by Queen\n7. "Back in Black" by AC/DC\n8. "Wonderwall" by Oasis\n9. "Mr. Brightside" by The Killers\n10. 96 | # # "Sweet Home Alabama" by Lynyrd Skynyrd\n\nThese songs represent a mix of classic rock hits and more recent rock anthems that are still popular today. Enjoy your work playlist!'""" 97 | 98 | # # agent_callback(x) 99 | -------------------------------------------------------------------------------- /spotify_playlist/playlist_agents.py: -------------------------------------------------------------------------------- 1 | from crewai import Agent 2 | from textwrap import dedent 3 | from langchain_openai.chat_models.azure import AzureChatOpenAI 4 | from langchain_openai.chat_models import ChatOpenAI 5 | from tools.search_tools import SearchTools 6 | from tools.spotify_tools import SpotifyTools 7 | import os 8 | 9 | 10 | class PlaylistAgents: 11 | def __init__(self, model_name: str, llm_callback=None): 12 | 13 | if model_name not in [ 14 | "azure", 15 | "gpt-4o", 16 | "gpt-4-turbo", 17 | "gpt-4", 18 | "gpt-3.5-turbo", 19 | ]: 20 | raise ValueError( 21 | "model_name must be one of ['azure', 'gpt-4o', 'gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo']" 22 | ) 23 | 24 | if model_name == "azure": 25 | self.llm_model = AzureChatOpenAI( 26 | deployment_name=os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME"), 27 | temperature=0.7, 28 | streaming=True, 29 | ) 30 | else: 31 | self.llm_model = ChatOpenAI(model_name=model_name, temperature=0.7) 32 | 33 | def expert_analyzing_text(self, agent_callback=None, callbacks=None): 34 | return Agent( 35 | role="Expert Text Analyzer for music selection", 36 | backstory=dedent( 37 | f"""I'm am an expert in analyzing textual information to accurately select music. 38 | With decades of experience, I specialize in interpreting a wide range of textual data to identify 10 (TEN) songs that best match the provided context. 39 | I have decades of experience in understanding songs based text info. 40 | """ 41 | ), 42 | goal=dedent( 43 | f"""My primary objective is to accurately identify and recommend songs based on the text provided. 44 | I leverage my extensive experience and deep understanding of music-related text analysis to deliver relevant and tailored song selections that meet the user's needs and preferences.""" 45 | ), 46 | allow_delegation=False, 47 | verbose=True, 48 | llm=self.llm_model, 49 | step_callback=agent_callback if agent_callback is not None else None, 50 | callbacks=callbacks, 51 | ) 52 | 53 | # select songs on internet 54 | def expert_music_curator(self, agent_callback=None, callbacks=None): 55 | return Agent( 56 | role="Expert Music Curator", 57 | backstory=dedent( 58 | f"""Expert at analyzing music data to pick ideal songs for a playlist considering the information provided by user. I have decades of experience understanding music trends.""" 59 | ), 60 | goal=dedent( 61 | f"""Search for 10 SONGS ONLY based on the user needs. 62 | Take care about music trends requested by the user. 63 | Provide a search query to find the songs on the internet specific for the user needs. 64 | """ 65 | ), 66 | max_iter=4, 67 | tools=[SearchTools.search_internet], 68 | allow_delegation=False, 69 | verbose=True, 70 | llm=self.llm_model, 71 | step_callback=agent_callback if agent_callback is not None else None, 72 | callbacks=callbacks, 73 | ) 74 | 75 | def spotify_api_expert(self): 76 | return Agent( 77 | role="Spotify API Expert", 78 | backstory=dedent( 79 | f"""Expert to work with Spotify API. 80 | I have experience in searching music on Spotify finding uri of the songs and 81 | a year of experience to create playlist.""" 82 | ), 83 | goal=dedent( 84 | f"""Find Uri of the songs on Spotify. 85 | Create a playlist using uri. 86 | Start playing the playlist on the specific device of the user.""" 87 | ), 88 | tools=[ 89 | SpotifyTools.search_songs_uris, 90 | SpotifyTools.create_playlist_by_uris, 91 | ], 92 | allow_delegation=False, 93 | verbose=True, 94 | llm=self.llm_model, 95 | ) 96 | 97 | def spotify_dj_expert(self): 98 | return Agent( 99 | role="Spotify DJ Expert", 100 | backstory=dedent( 101 | f"""I'm an expert DJ on Spotify. I have experience to play a playlist already created on Spotify and start playing on the specific device of the user.""" 102 | ), 103 | goal=dedent( 104 | f"""Start playing the playlist that already exists on the user's device.""" 105 | ), 106 | tools=[ 107 | SpotifyTools.start_playing_playlist, 108 | ], 109 | allow_delegation=False, 110 | verbose=True, 111 | llm=self.llm_model, 112 | ) 113 | -------------------------------------------------------------------------------- /spotify_playlist/playlist_crew.py: -------------------------------------------------------------------------------- 1 | from playlist_agents import PlaylistAgents 2 | from playlist_tasks import PlaylistTasks 3 | from crewai import Crew 4 | 5 | 6 | class PlaylistCrew: 7 | def __init__(self, text_info, model_name, autoplay_device, access_token): 8 | self.access_token = access_token 9 | self.autoplay_device = autoplay_device 10 | self.model_name = model_name 11 | self.text_info = text_info 12 | 13 | def run( 14 | self, llm_callback=None, agent_callback=None, crew_callback=None, callbacks=None 15 | ): 16 | 17 | agents = PlaylistAgents(model_name=self.model_name, llm_callback=llm_callback) 18 | tasks = PlaylistTasks() 19 | 20 | # Agents definition 21 | expert_text_analyzer = agents.expert_analyzing_text(callbacks=callbacks) 22 | expert_music_curator = agents.expert_music_curator(callbacks=callbacks) 23 | spotify_api_expert = agents.spotify_api_expert() 24 | 25 | # Custom Tasks definition 26 | find_user_needs = tasks.extract_info_from_text( 27 | expert_text_analyzer, 28 | self.text_info, 29 | ) 30 | 31 | find_songs = tasks.search_for_songs(expert_music_curator) 32 | search_spotify_uri_songs = tasks.search_spotify_uri_songs(spotify_api_expert) 33 | 34 | create_spotify_playlist = tasks.create_spotify_playlist( 35 | spotify_api_expert, 36 | self.access_token, 37 | ) 38 | 39 | # optional agents and tasks 40 | spotify_dj_expert = agents.spotify_dj_expert() 41 | play_playlist = tasks.starting_play_playlist( 42 | agent=spotify_dj_expert, 43 | task_context=create_spotify_playlist, 44 | access_token=self.access_token, 45 | autoplay_device=self.autoplay_device, 46 | ) 47 | 48 | # default agents 49 | all_agents = [ 50 | expert_text_analyzer, 51 | expert_music_curator, 52 | spotify_api_expert, 53 | ] 54 | 55 | if self.autoplay_device != "none": 56 | all_agents.append(spotify_dj_expert) 57 | 58 | # default tasks 59 | all_tasks = [ 60 | find_user_needs, 61 | find_songs, 62 | search_spotify_uri_songs, 63 | create_spotify_playlist, 64 | ] 65 | 66 | if self.autoplay_device != "none": 67 | all_tasks.append(play_playlist) 68 | 69 | # My Crew 70 | crew = Crew( 71 | agents=all_agents, 72 | tasks=all_tasks, 73 | verbose=True, 74 | step_callback=crew_callback if crew_callback is not None else None, 75 | ) 76 | 77 | result = crew.kickoff() 78 | return result 79 | -------------------------------------------------------------------------------- /spotify_playlist/playlist_tasks.py: -------------------------------------------------------------------------------- 1 | from crewai import Task 2 | from textwrap import dedent 3 | from tools.spotify_tools import SpotifyTools 4 | 5 | 6 | class PlaylistTasks: 7 | def __tip_section(self): 8 | return "If you do your BEST WORK, I'll give you a $10,000 commission!" 9 | 10 | def extract_info_from_text(self, agent, text_info): 11 | return Task( 12 | description=dedent( 13 | f""" 14 | **Task**: Analyze text to pick the right information for searching appropriated songs for the user. 15 | **Description**: Your objective is to analyze the text provided by the user to extract key information necessary for producing a tailored playlist. 16 | This information will be utilized to identify the user's specific requirements regarding the songs they desire. 17 | When extracting this information, it's crucial to focus on important details such as: 18 | - the preferred music genre 19 | - the desired mood or emotions the user wants to evoke 20 | - the current activity or situation they're engaged in 21 | - any other relevant factors that could influence their song preferences. 22 | 23 | By carefully considering these details, we can create a playlist that perfectly matches the user's needs and preferences. 24 | 25 | The result MUST be a query for serpapi endpoint. 26 | 27 | **Parameters**: 28 | - text_info : {text_info} 29 | 30 | **Note**: {self.__tip_section()} 31 | """ 32 | ), 33 | agent=agent, 34 | expected_output="a string with the query for serpapi endpoint", 35 | ) 36 | 37 | def search_for_songs(self, agent): 38 | return Task( 39 | description=dedent( 40 | f""" 41 | **Task**: Curate a selection of 10 songs based on user needs, reflecting music genre requested by the user. 42 | **Description**: Your objective is to curate a playlist of 10 songs tailored to meet specific user preferences or needs. 43 | Each song should be carefully chosen to suit its designated situation, ensuring diversity and relevance. 44 | It's essential to select songs that are distinct from one another 45 | If NOT specified by user, songs must be aligned with prevalent tastes in today's music scene as of February 2024. 46 | If the user specifies a genre, year or mood, ensure that the songs selected reflect these preferences. 47 | If you find a playlist, you need to get the songs contained in it, not the playlist name. 48 | DO NOT t search on youtube.com. 49 | Additionally, ensure that your playlist does not contain any duplicate songs, offering a unique listening experience throughout. 50 | 51 | **Note**: {self.__tip_section()} 52 | """ 53 | ), 54 | agent=agent, 55 | expected_output="a list of strings", 56 | ) 57 | 58 | def search_spotify_uri_songs(self, agent): 59 | return Task( 60 | description=dedent( 61 | f""" 62 | **Task**:Search for songs uris on Spotify 63 | **Description**: Search for the uri of the songs on spotify and return list of uris related each song. The uris are the unique identifiers of each song on spotify. 64 | 65 | **Note**: {self.__tip_section()} 66 | """ 67 | ), 68 | agent=agent, 69 | expected_output="a list of strings with the uris of the songs", 70 | ) 71 | 72 | def create_spotify_playlist(self, agent, access_token) -> Task: 73 | return Task( 74 | description=dedent( 75 | f""" 76 | **Task**:Create Spotify playlist using uris and access token 77 | **Description**: Create a new playlist on Spotify using the uris of the songs and the access_token. 78 | The playlist should be named "made by NTTLuke (with CrewAI)" and should include the songs selected. 79 | 80 | **Parameters**: 81 | - Access Token: {access_token} 82 | 83 | **Note**: {self.__tip_section()} 84 | """ 85 | ), 86 | agent=agent, 87 | expected_output="a string with the playlist id created", 88 | ) 89 | 90 | def starting_play_playlist( 91 | self, agent, task_context, access_token, autoplay_device: str = "none" 92 | ): 93 | return Task( 94 | description=dedent( 95 | f""" 96 | **Task**: Start playing the playlist on the specific device type. If the autoplay_device type is none then do not play the playlist. 97 | **Description**: Start playing the playlist on the specific device type provided by the user. 98 | 99 | **Parameters**: 100 | - Access Token: {access_token} 101 | - Autoplay Device: {autoplay_device} 102 | 103 | **Note**: {self.__tip_section()} 104 | """ 105 | ), 106 | agent=agent, 107 | # context=[task_context], 108 | expected_output="Information about the playlist playing on the user's device or an error message if the playlist could not be played.", 109 | ) 110 | -------------------------------------------------------------------------------- /spotify_playlist/tools/search_tools.py: -------------------------------------------------------------------------------- 1 | import requests, os 2 | import json 3 | from langchain.tools import tool 4 | 5 | 6 | class SearchTools: 7 | 8 | @tool("Search the internet") 9 | def search_internet(query): 10 | """Userful to search on internet about a given topic and return relevant results""" 11 | 12 | print("Searching the internet...", query) 13 | top_result_to_return = 4 14 | url = f"https://google.serper.dev/search" 15 | 16 | payload = json.dumps({"q": query}) 17 | headers = { 18 | "X-API-KEY": os.environ["SERPER_API_KEY"], 19 | "Content-Type": "application/json", 20 | } 21 | 22 | response = requests.request("POST", url, headers=headers, data=payload) 23 | if "organic" not in response.json(): 24 | return "No results found" 25 | else: 26 | results = response.json()["organic"] 27 | string = [] 28 | for result in results[:top_result_to_return]: 29 | try: 30 | string.append( 31 | "\n".join( 32 | [ 33 | f"Title: {result['title']}", 34 | f"Link: {result['link']}", 35 | f"Snippet: {result['snippet']}", 36 | ] 37 | ) 38 | ) 39 | except KeyError: 40 | next 41 | 42 | return "\n".join(string) 43 | 44 | 45 | if __name__ == "__main__": 46 | print(SearchTools.search_internet("How to make a cake")) 47 | -------------------------------------------------------------------------------- /spotify_playlist/tools/spotify_tools.py: -------------------------------------------------------------------------------- 1 | import requests, os 2 | import json 3 | import base64 4 | from langchain.tools import tool 5 | from typing import List 6 | from urllib.parse import urlencode 7 | 8 | 9 | class SpotifyTools: 10 | 11 | @staticmethod 12 | def get_user_id(access_token: str): 13 | # Make a request to Spotify API to get user information 14 | user_url = "https://api.spotify.com/v1/me" 15 | headers = {"Authorization": f"Bearer {access_token}"} 16 | user_response = requests.get(user_url, headers=headers) 17 | 18 | if user_response.status_code != 200: 19 | return ( 20 | f"Failed to fetch user data. Status code: {user_response.status_code}" 21 | ) 22 | 23 | user_data = user_response.json() 24 | user_id = user_data["id"] 25 | 26 | return user_id 27 | 28 | @staticmethod 29 | def get_spotify_token(client_id, client_secret): 30 | url = "https://accounts.spotify.com/api/token" 31 | 32 | # scopes required to create a playlist 33 | data = { 34 | "grant_type": "client_credentials", 35 | "scope": "playlist-modify-private playlist-modify-public", 36 | } 37 | headers = { 38 | "Authorization": f'Basic {base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()}' 39 | } 40 | 41 | response = requests.post(url, data=data, headers=headers) 42 | 43 | if response.status_code == 200: 44 | token_data = response.json() 45 | access_token = token_data.get("access_token") 46 | return access_token 47 | else: 48 | print("Failed to fetch Spotify token. Status code:", response.status_code) 49 | return None 50 | 51 | @tool( 52 | "Search songs uris on Spotify", 53 | ) 54 | def search_songs_uris(song_titles: List[str]): 55 | """Userful to search the songs uri on spotify and return the list of uris of each song""" 56 | 57 | spotify_token = SpotifyTools.get_spotify_token( 58 | os.environ["SPOTIFY_CLIENT_ID"], os.environ["SPOTIFY_CLIENT_SECRET"] 59 | ) 60 | if not spotify_token: 61 | return "Failed to fetch Spotify token." 62 | 63 | url = "https://api.spotify.com/v1/search" 64 | headers = { 65 | "Authorization": f"Bearer {spotify_token}", 66 | } 67 | 68 | songs_uris = [] 69 | for song_title in song_titles: 70 | params = {"q": song_title, "type": "track", "market": "IT", "limit": 1} 71 | response = requests.get(url, params=params, headers=headers) 72 | 73 | if response.status_code == 200: 74 | data = response.json() 75 | if ( 76 | "tracks" in data 77 | and "items" in data["tracks"] 78 | and len(data["tracks"]["items"]) > 0 79 | ): 80 | track_uri = data["tracks"]["items"][0]["uri"] 81 | songs_uris.append(f"the song uri of {song_title} is : {track_uri}") 82 | else: 83 | print("No track found.") 84 | else: 85 | print("Failed to fetch track URI. Status code:", response.status_code) 86 | 87 | return "\n".join(songs_uris) 88 | 89 | @tool("Create new playlist on Spotify") 90 | def create_playlist_by_uris( 91 | songs_uris: List[str], 92 | access_token: str, 93 | playlist_name="made by NTTLuke (with CrewAI)", 94 | ): 95 | """ 96 | Userful to create a playlist on spotify by using the uris of the songs and returns the playlist id 97 | 98 | songs_uris : List[str] : The list of songs uris to add to the playlist 99 | access_token : str : The access token to authenticate the request 100 | playlist_name : str : The name of the playlist to create 101 | 102 | Returns 103 | the playlist id of the created playlist 104 | """ 105 | 106 | user_id = SpotifyTools.get_user_id(access_token) 107 | 108 | url = f"https://api.spotify.com/v1/users/{user_id}/playlists" 109 | headers = { 110 | "Authorization": f"Bearer {access_token}", 111 | "Content-Type": "application/json", 112 | } 113 | data = { 114 | "name": playlist_name, 115 | "description": "generated by CrewAI", 116 | "public": False, 117 | } 118 | 119 | response = requests.post(url, headers=headers, json=data) 120 | 121 | if response.status_code == 201 or response.status_code == 200: 122 | playlist_data = response.json() 123 | playlist_id = playlist_data.get("id") 124 | print("Playlist created successfully with ID:", playlist_id) 125 | 126 | data = {"uris": songs_uris, "position": 0} 127 | response = requests.post( 128 | f"https://api.spotify.com/v1/playlists/{playlist_id}/tracks", 129 | headers=headers, 130 | json=data, 131 | ) 132 | if response.status_code == 201 or response.status_code == 200: 133 | return f"Created a new playlist with playlist_id : {playlist_id}" 134 | else: 135 | print( 136 | "Failed to add songs to the playlist. Status code:", 137 | response.status_code, 138 | ) 139 | return "" 140 | 141 | else: 142 | print("Failed to create playlist. Status code:", response.status_code) 143 | return None 144 | 145 | @tool("Play the playlist that already exists on the specific device of the user") 146 | def start_playing_playlist(access_token: str, playlist_id: str, device_type: str): 147 | """Useful for initiating playback of a playlist already created and identified by playlist_id and device_type 148 | access_token : str : The access token to authenticate the request 149 | playlist_id : str : The playlist id to play created previously 150 | device_type: str : The device type where to play the playlist (mobile, computer, speaker) 151 | """ 152 | 153 | import time 154 | 155 | url = f"https://api.spotify.com/v1/me/player/devices?type={device_type}" 156 | 157 | headers = {"Authorization": f"Bearer {access_token}"} 158 | 159 | response = requests.get(url, headers=headers) 160 | if device_type == "none": 161 | return "User do not select any device to play the playlist." 162 | 163 | if response.status_code == 200: 164 | devices = response.json().get("devices", []) 165 | if devices: 166 | for device in devices: 167 | if str.lower(device["type"]) != device_type.lower(): 168 | continue 169 | 170 | device_id = device["id"] 171 | url = f"https://api.spotify.com/v1/me/player/play?device_id={device_id}" 172 | headers = { 173 | "Authorization": f"Bearer {access_token}", 174 | "Content-Type": "application/json", 175 | } 176 | data = { 177 | "context_uri": f"spotify:playlist:{playlist_id}", 178 | } 179 | 180 | time.sleep(2) 181 | 182 | response = requests.put(url, headers=headers, json=data) 183 | 184 | if response.status_code == 204: 185 | return "Playback started successfully." 186 | else: 187 | print( 188 | "Failed to start playback. Status code:", 189 | response.status_code, 190 | ) 191 | return f"Failed to start playback: {response.status_code}" 192 | 193 | else: 194 | return {"error": response.status_code, "message": response.text} 195 | 196 | 197 | def test_create_playlist(): 198 | songs_uris = [ 199 | "spotify:track:37FjxvMhMjt3YRecpx7HsC", 200 | "spotify:track:4191RXFPa7Ge9XkA4cWlna", 201 | ] 202 | 203 | access_token = "" 204 | 205 | playlist_id = SpotifyTools.create_playlist_by_uris( 206 | songs_uris, access_token, "NTTLuke Playlist using CrewAI" 207 | ) 208 | 209 | print("Playlist ID:", playlist_id) 210 | 211 | 212 | def test_search_songs(): 213 | # Replace with your client ID and client secret 214 | # client_id = os.environ["SPOTIFY_CLIENT_ID"] 215 | # client_secret = os.environ["SPOTIFY_CLIENT_SECRET"] 216 | 217 | # # Get the access token 218 | # access_token = SpotifyTools.get_spotify_token(client_id, client_secret) 219 | 220 | # print(SpotifyTools.search_songs_uris(["Back in Black"])) 221 | 222 | # get_user_authorization_url( 223 | # client_id=os.environ["SPOTIFY_CLIENT_ID"], 224 | # redirect_uri="http://localhost:3000/callback", 225 | # ) 226 | pass 227 | 228 | 229 | def test_play_playlist(access_token: str): 230 | # Replace with your access token and playlist ID 231 | playlist_id = "3n0GcjIurtegGqxBqEzclF" 232 | 233 | SpotifyTools.start_playing_playlist(access_token, playlist_id, "computer") 234 | 235 | 236 | if __name__ == "__main__": 237 | access_token = "" 238 | 239 | # test_play_playlist() 240 | test_play_playlist(access_token=access_token) 241 | -------------------------------------------------------------------------------- /static/OIG3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NTTLuke/spotify-playlist-crewai/bcc3225a4e5b0e905a79e4f25b325b94f09bccdd/static/OIG3.jpg -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 |(only premium accounts)
139 |