AWS Builder Center
De DJ local a DJ con Spotify - tools externos y multi-agente

De DJ local a DJ con Spotify - tools externos y multi-agente

Tu agente ya sabe de música. Ahora va a controlar Spotify, crear playlists reales y delegar a sub-agentes especializados.

En el  articulo anterior  construimos un agente DJ desde cero. Cuatro capas: un modelo que habla, herramientas para buscar en una biblioteca local, múltiples tools que el modelo orquesta solo, y memoria para recordar tus gustos entre sesiones.
Todo local. Todo open source. Todo en tu laptop.
Pero hay un problema.
Tu biblioteca local tiene 30 canciones. Spotify tiene más de 100 millones. Tu agente puede recomendar jazz para trabajar, pero no puede reproducir esa canción en tu parlante. Puede armar una playlist en texto, pero no puede crearla en tu cuenta.
Un agente que solo consulta datos locales es útil. Un agente que controla servicios reales es poderoso.
Y aquí es donde se pone interesante: ¿qué pasa cuando un solo agente no es suficiente? ¿Cuando necesitas un especialista en emociones, otro en eventos, y otro en gustos personales? La respuesta es un patrón que suena complejo pero es elegante: agent as a tool.
En este artículo vamos a construir las capas 5 y 6 del DJ:
  • Capa 5: Tools que se conectan a una API externa real (Spotify)
  • Capa 6: Un agente orquestador que delega a sub-agentes especializados
El flujo completo se ve así: tú le hablas al agente, el agente razona con Bedrock, invoca tools que llaman a Spotify, y la música suena en tu dispositivo.


Lo que necesitas para seguir este artículo

No necesitas haber implementado las capas anteriores. Este artículo es autocontenido, puedes clonar el repo y correr las capas 5 y 6 directamente. Pero sí te recomiendo leer el  artículo anterior  para entender los conceptos de  @tool , agent loop y model-driven que usamos aquí.
Bash
git clone https://github.com/hsaenzG/OpenSource-agents-demo.git
cd OpenSource-agents-demo
python3 -m venv .venv
source .venv/bin/activate
pip install 'strands-agents' spotipy python-dotenv
Lo que necesitas:
  1. Python 3.10+
  1. Una cuenta de Spotify Developer (gratis) , para conectar con la API
  1. AWS CLI configurado con acceso a Amazon Bedrock, porque vamos a usar un modelo en la nube
¿Por qué Bedrock y no Ollama? En el artículo anterior usamos  llama3.1:8b  corriendo local, y funciona bien con 1-2 tools. Pero las capas 5 y 6 tienen 7-8 herramientas cada una. Para tool-calling confiable con muchas herramientas, necesitas un modelo más capaz. Amazon Bedrock con Nova Pro resuelve eso y como vimos antes, cambiar de proveedor es cambiar una línea de código gracias a la abstracción del SDK.

Capa 5: El DJ controla Spotify

El concepto: tools que llaman APIs externas

Hasta ahora, nuestros tools eran funciones puras.  buscar_canciones()  filtra un JSON local.  analizar_energia()  hace cálculos sobre datos en memoria. No salen de tu proceso de Python.
Pero un  @tool  puede hacer cualquier cosa que Python pueda hacer. Incluyendo llamar APIs externas.
La mecánica es la misma: decoras una función con  @tool , escribes un docstring claro, y el modelo decide cuándo invocarla. La diferencia es que dentro de esa función, en vez de filtrar un JSON, haces un HTTP request a un servicio externo.

Fíjate: para el modelo, no hay diferencia entre un tool local y uno que llama a Spotify. El modelo ve el tool spec (nombre, descripción, parámetros) y decide si lo necesita. No sabe ni le importa si por dentro es un  json.load()  o un  requests.get() . Esa es la elegancia del patrón.

Configurar Spotify Developer

Antes del código, necesitas credenciales. Crea una app en el  Spotify Developer Dashboard :
  1. Click en Create App
  1. Nombre: lo que quieras (ej: "DJ Agent")
  1. Redirect URI:  http://127.0.0.1:8000/callback 
  1. Marca Web API
  1. Guarda el Client ID y Client Secret
Crea un archivo  .env  en la raíz del proyecto:
Bash
SPOTIFY_CLIENT_ID=TU-CLIENT-ID
SPOTIFY_CLIENT_SECRET=TU-CLIENT-SECRET
Nota: La primera vez que ejecutes el script, se abrirá el navegador para autorizar la app con tu cuenta de Spotify. Después, el token se cachea automáticamente y no necesitas volver a autorizar.

La conexión con Spotify

Usamos  spotipy , una librería de Python que envuelve la Spotify Web API con OAuth2:
Python
from strands import Agent, tool
from strands.models import BedrockModel
import json
import os
from dotenv import load_dotenv
import spotipy
from spotipy.oauth2 import SpotifyOAuth

load_dotenv()

sp = spotipy.Spotify(
auth_manager=SpotifyOAuth(
client_id=os.getenv("SPOTIFY_CLIENT_ID"),
client_secret=os.getenv("SPOTIFY_CLIENT_SECRET"),
redirect_uri="http://127.0.0.1:8000/callback",
scope="playlist-modify-public,playlist-modify-private,user-library-read,user-top-read,user-modify-playback-state,user-read-playback-state",
)
)

usuario = sp.current_user()
print(f"✅ Conectado a Spotify como: {usuario['display_name']}")
El  scope  define qué permisos tiene tu app. Necesitamos leer tu biblioteca, crear playlists, y controlar la reproducción. Spotify usa OAuth2, el estándar de la industria para autorización delegada.

El primer tool externo: buscar en Spotify

Python
@tool
def buscar_en_spotify(query: str, limite: int = 10) -> str:
"""Busca canciones en Spotify por nombre, artista o género.
SIEMPRE usa esta herramienta cuando el usuario pregunte por canciones o artistas.
Los resultados son datos REALES y actualizados de Spotify.

Args:
query: Texto de búsqueda (ej: "Shakira", "rock alternativo", "Bad Bunny último")
limite: Número máximo de resultados (default: 10)
"""
resultados = sp.search(q=str(query), type="track", limit=min(limite, 10))
tracks = resultados["tracks"]["items"]

if not tracks:
return f"No encontré canciones en Spotify para: {query}"

canciones = []
for t in tracks:
canciones.append({
"titulo": t["name"],
"artista": t["artists"][0]["name"],
"album": t["album"]["name"],
"uri": t["uri"],
"duracion_min": round(t["duration_ms"] / 60000, 1),
})

return json.dumps(canciones, ensure_ascii=False, indent=2)
¿Qué está pasando aquí? La estructura es idéntica a  buscar_canciones  del artículo anterior. Misma firma: recibe parámetros, devuelve un string JSON. Mismo decorador  @tool . Mismo docstring descriptivo.
La diferencia está dentro: en vez de filtrar  BIBLIOTECA , llama a  sp.search()  que hace un HTTP GET a  https://api.spotify.com/v1/search . El resultado viene con datos reales: URIs de Spotify, duración exacta, álbum, fecha de lanzamiento.
Y fíjate en el  uri . Ese  spotify:track:xxx  es lo que necesitamos para reproducir o agregar a playlists. Es el identificador único de cada canción en Spotify.

Reproducir música: el agente toma acción real

Aquí es donde el agente deja de ser un "recomendador" y se convierte en un controlador:
Python
@tool
def reproducir_cancion(nombre_cancion: str, artista: str = "") -> str:
"""Reproduce una canción en el dispositivo activo de Spotify del usuario.
Busca la canción por nombre y la reproduce automáticamente.

Requiere que Spotify esté abierto en algún dispositivo (celular, computadora, etc.).

Args:
nombre_cancion: Nombre de la canción a reproducir
artista: Nombre del artista (opcional, ayuda a encontrar la correcta)
"""
dispositivos = sp.devices()
if not dispositivos["devices"]:
return ("No hay dispositivos activos de Spotify. "
"Abre Spotify en tu celular o computadora e intenta de nuevo.")

# Buscar la canción
query = f"track:{nombre_cancion}"
if artista:
query += f" artist:{artista}"
resultados = sp.search(q=query, type="track", limit=5)
tracks = resultados["tracks"]["items"]

if not tracks:
return f"No encontré '{nombre_cancion}' en Spotify."

track = tracks[0]
device_id = next(
(d["id"] for d in dispositivos["devices"] if d["is_active"]),
dispositivos["devices"][0]["id"]
)

sp.start_playback(device_id=device_id, uris=[track["uri"]])

return json.dumps({
"status": "reproduciendo",
"cancion": track["name"],
"artista": track["artists"][0]["name"],
"mensaje": f"▶️ Reproduciendo: {track['name']} — {track['artists'][0]['name']}",
}, ensure_ascii=False)
Esto es un tool que modifica estado en el mundo real. Cuando el modelo lo invoca, tu parlante empieza a sonar. No es un mock, no es una simulación. Es la API de Spotify ejecutando  PUT /v1/me/player/play .

Crear playlists reales

Python
@tool
def crear_playlist_en_spotify(nombre: str, descripcion: str, canciones_uris: list) -> str:
"""Crea una playlist en la cuenta de Spotify del usuario con las canciones indicadas.

Args:
nombre: Nombre de la playlist (ej: "Viernes de Rock", "Cena Romántica")
descripcion: Descripción breve de la playlist
canciones_uris: Lista de URIs de Spotify o nombres de canciones
"""
if not canciones_uris:
return "No me diste canciones para agregar a la playlist."

# Resolver URIs — si no es una URI válida, buscar la canción
uris_validas = []
for item in canciones_uris:
item = str(item).strip()
if item.startswith("spotify:track:"):
uris_validas.append(item)
else:
r = sp.search(q=item, type="track", limit=1)
tracks = r["tracks"]["items"]
if tracks:
uris_validas.append(tracks[0]["uri"])

if not uris_validas:
return "No pude encontrar ninguna de las canciones en Spotify."

# Crear la playlist
playlist = sp.user_playlist_create(
user=sp.current_user()["id"],
name=str(nombre),
public=False,
description=str(descripcion)
)

# Agregar canciones (en batches de 100, límite de la API)
for i in range(0, len(uris_validas), 100):
sp.playlist_add_items(playlist["id"], uris_validas[i:i + 100])

return json.dumps({
"status": "ok",
"mensaje": f"Playlist '{nombre}' creada con {len(uris_validas)} canciones",
"url": playlist["external_urls"]["spotify"],
}, ensure_ascii=False)
Fíjate en un detalle importante: el tool acepta tanto URIs ( spotify:track:xxx ) como nombres de canciones. Si el modelo pasa nombres en vez de URIs, el tool los resuelve buscando en Spotify. Esto hace al tool más robusto, el modelo no necesita recordar URIs exactas entre llamadas.

Conocer al usuario: top artistas y canciones

Python
@tool
def mis_top_artistas(periodo: str = "medium_term") -> str:
"""Obtiene los artistas más escuchados del usuario en Spotify.

Args:
periodo: "short_term" (último mes), "medium_term" (6 meses), "long_term" (siempre)
"""
resultados = sp.current_user_top_artists(limit=10, time_range=periodo)
artistas = []
for a in resultados["items"]:
artistas.append({
"nombre": a["name"],
"generos": a["genres"][:3],
"popularidad": a["popularity"],
})
return json.dumps(artistas, ensure_ascii=False, indent=2)

@tool
def mis_top_canciones(periodo: str = "medium_term") -> str:
"""Obtiene las canciones más escuchadas del usuario en Spotify.

Args:
periodo: "short_term" (último mes), "medium_term" (6 meses), "long_term" (siempre)
"""
resultados = sp.current_user_top_tracks(limit=20, time_range=periodo)
canciones = []
for t in resultados["items"]:
canciones.append({
"titulo": t["name"],
"artista": t["artists"][0]["name"],
"uri": t["uri"],
})
return json.dumps(canciones, ensure_ascii=False, indent=2)
Estos tools le dan al agente algo que la memoria local no puede: datos reales de comportamiento. No es lo que el usuario dice que le gusta, es lo que realmente escucha. Esa diferencia importa cuando armas recomendaciones.

El agente completo de la Capa 5

Python
modelo = BedrockModel(model_id="us.amazon.nova-pro-v1:0", region_name="us-east-1")

dj = Agent(
model=modelo,
system_prompt="""Eres un DJ personal conectado a Spotify. Controlas la música del usuario.

REGLAS:
1. SIEMPRE usa buscar_en_spotify antes de recomendar música.
2. NUNCA inventes canciones, artistas o datos.
3. Para reproducir: usa reproducir_cancion con el nombre.
4. Para crear playlists: usa crear_playlist_en_spotify con las URIs.
5. Basa TODAS tus respuestas en datos reales de las herramientas.

Respondes en español, con onda y buen gusto musical.""",
tools=[
buscar_en_spotify,
crear_playlist_en_spotify,
reproducir_cancion,
mis_top_artistas,
mis_top_canciones,
],
)

# Conversación interactiva
while True:
mensaje = input("🎵 Tú: ").strip()
if mensaje.lower() in ("salir", "exit"):
break
print("\n🎧 DJ: ", end="", flush=True)
dj(mensaje)
print("\n")
Ahora puedes decirle "ponme algo de Daft Punk" y tu parlante empieza a sonar. Puedes decirle "arma una playlist de jazz para cenar" y aparece en tu cuenta de Spotify. Datos reales, acciones reales.
Nota: Si no tienes Spotify Premium, la reproducción no funciona (limitación de la API), pero la búsqueda y creación de playlists sí.

El salto conceptual: de tool local a tool externo

Hagamos una pausa para entender qué acaba de pasar.
En la Capa 2 del artículo anterior, un tool era esto:
Python
@tool
def buscar_canciones(genero: str = "") -> str:
"""Busca canciones en la biblioteca local."""
resultados = [c for c in BIBLIOTECA if genero.lower() in c["genero"].lower()]
return json.dumps(resultados)
En la Capa 5, un tool es esto:
Python
@tool
def buscar_en_spotify(query: str) -> str:
"""Busca canciones en Spotify."""
resultados = sp.search(q=query, type="track", limit=10)
return json.dumps(resultados["tracks"]["items"])
Misma interfaz. Misma mecánica. Diferente poder.
Para el agente, ambos son iguales: una función que recibe parámetros y devuelve un string. El modelo no sabe (ni necesita saber) si por dentro hay un filtro de lista o un HTTP request con OAuth2.
Eso significa que puedes conectar tu agente a cualquier API con el mismo patrón:
  • Un  @tool  que consulta tu base de datos de producción
  • Un  @tool  que envía emails via SendGrid
  • Un  @tool  que crea tickets en Jira
  • Un  @tool  que despliega código en AWS
El patrón es siempre el mismo: función Python + decorador  @tool  + docstring claro = el modelo decide cuándo usarlo.

Capa 6: Multi-agente el DJ delega

El problema: un agente que hace demasiado

La Capa 5 funciona. Pero tiene un system prompt largo, 7-8 tools, y tiene que manejar situaciones muy diferentes:
  • "Recomiéndame algo de rock" → necesita conocer tus gustos
  • "Arma una playlist de 3 horas para una fiesta" → necesita planificar duración y energía
  • "Estoy triste, ponme algo" → necesita entender emociones y mapearlas a música
Un solo agente puede hacer todo eso. Pero entre más responsabilidades le das, más largo es el system prompt, más tools tiene que considerar, y más probable es que se confunda.
La solución no es un agente más grande. Es varios agentes especializados.

El concepto: Agent as a Tool

Y aquí viene el patrón más elegante de este artículo.
¿Recuerdas que un  @tool  puede hacer cualquier cosa que Python pueda hacer? Incluyendo... invocar otro agente.
Python
@tool
def consultar_dj_personal(mensaje: str) -> str:
"""Delega al DJ Personal: experto en gustos musicales y recomendaciones.

Args:
mensaje: El mensaje del usuario para el DJ Personal
"""
respuesta = dj_personal(mensaje)
return str(respuesta)
Eso es todo. Un agente completo con su propio system prompt, sus propios tools, su propia personalidad, expuesto como un  @tool  de otro agente.
El agente que tiene estos tools se llama orquestador. No busca canciones, no crea playlists. Su único trabajo es entender qué necesita el usuario y decidir a cuál especialista delegarle.


Los sub-agentes especializados

Cada sub-agente tiene un rol claro y un conjunto de tools específico:
Python
modelo = BedrockModel(model_id="us.amazon.nova-pro-v1:0", region_name="us-east-1")

dj_personal = Agent(
model=modelo,
system_prompt="""Eres un DJ personal experto. Conoces los gustos del usuario.
SIEMPRE usa buscar_en_spotify antes de recomendar. NUNCA inventes datos.
Puedes consultar mis_top_artistas y mis_top_canciones para conocer al usuario.""",
tools=[buscar_en_spotify, crear_playlist_en_spotify, reproducir_cancion,
mis_top_artistas, mis_top_canciones],
callback_handler=None, # Silenciar output
)

dj_eventos = Agent(
model=modelo,
system_prompt="""Eres un DJ profesional de eventos. Armas playlists para fiestas,
bodas, cenas. Verificas que la duración cubra el evento completo.""",
tools=[buscar_en_spotify, crear_playlist_en_spotify, reproducir_cancion,
planificar_evento],
callback_handler=None,
)

dj_emocional = Agent(
model=modelo,
system_prompt="""Eres un DJ empático especializado en emociones y música.
Primero analizas la emoción, luego buscas música que la acompañe.
Eres sensible y no juzgas.""",
tools=[buscar_en_spotify, crear_playlist_en_spotify, reproducir_cancion,
analizar_emocion],
callback_handler=None,
)
Fíjate en  callback_handler=None . Eso silencia el output de los sub-agentes — solo el orquestador habla con el usuario. Los sub-agentes trabajan en silencio y devuelven su resultado al orquestador.
Cada sub-agente tiene:
  • Un system prompt enfocado en su especialidad
  • Solo los tools que necesita (no todos los disponibles)
  • Una personalidad diferente (el emocional es empático, el de eventos es profesional)

Los tools del orquestador: agentes como herramientas

Python
@tool
def consultar_dj_personal(mensaje: str) -> str:
"""Delega al DJ Personal: experto en gustos musicales y recomendaciones.
Úsalo cuando el usuario quiera recomendaciones, descubrir música nueva,
o pida algo basado en sus gustos.

Args:
mensaje: El mensaje completo del usuario para el DJ Personal
"""
respuesta = dj_personal(mensaje)
return str(respuesta)

@tool
def consultar_dj_eventos(mensaje: str) -> str:
"""Delega al DJ de Eventos: experto en armar playlists para ocasiones específicas.
Úsalo cuando el usuario mencione un evento, fiesta, boda, cena,
o pida una playlist con duración específica.

Args:
mensaje: El mensaje completo del usuario para el DJ de Eventos
"""
respuesta = dj_eventos(mensaje)
return str(respuesta)

@tool
def consultar_dj_emocional(mensaje: str) -> str:
"""Delega al DJ Emocional: experto en música y estados de ánimo.
Úsalo cuando el usuario exprese cómo se siente o quiera música
para acompañar un estado de ánimo.

Args:
mensaje: El mensaje completo del usuario para el DJ Emocional
"""
respuesta = dj_emocional(mensaje)
return str(respuesta)
El docstring de cada tool-agente es clave. Le dice al orquestador cuándo usar cada uno. "Cuando el usuario exprese cómo se siente" → DJ Emocional. "Cuando mencione un evento" → DJ Eventos. El modelo del orquestador lee estos docstrings y decide a quién delegar.

El orquestador: el punto de entrada

Python
orquestador = Agent(
model=modelo,
system_prompt="""Eres el DJ principal. Tu trabajo es entender qué necesita el usuario
y delegarlo al sub-agente especializado correcto.

Tienes 3 DJs especializados:
1. consultar_dj_personal — Recomendaciones basadas en gustos
2. consultar_dj_eventos — Playlists para eventos con duración específica
3. consultar_dj_emocional — Música para estados de ánimo

REGLAS:
- SIEMPRE delega al sub-agente apropiado.
- Pasa el mensaje COMPLETO del usuario.
- Si no estás seguro, usa consultar_dj_personal como default.
- Presenta la respuesta del sub-agente de forma natural.""",
tools=[consultar_dj_personal, consultar_dj_eventos, consultar_dj_emocional,
reproducir_cancion, reproducir_playlist],
)
El orquestador también tiene  reproducir_cancion  y  reproducir_playlist  directamente. Si el usuario dice "ponme Bohemian Rhapsody", no necesita delegar a nadie — puede reproducir directamente.

Cómo funciona en la práctica

Plain text
🎵 Tú: Estoy triste, ponme algo suave

🎧 DJ: [internamente: invoca consultar_dj_emocional("Estoy triste, ponme algo suave")]
[DJ Emocional: invoca analizar_emocion("triste")]
[DJ Emocional: invoca buscar_en_spotify("indie folk acoustic")]
[DJ Emocional: invoca reproducir_cancion("Skinny Love", "Bon Iver")]

Entiendo. Te puse "Skinny Love" de Bon Iver — indie folk suave,
perfecto para este momento. Si quieres, puedo armar una playlist
completa con ese mood.
El usuario habla con un solo agente. No sabe que detrás hay tres especialistas. No necesita elegir un menú. El orquestador decide, delega, y presenta la respuesta como si fuera suya.

¿Por qué no un solo agente con todos los tools?

Podrías meter todos los tools en un solo agente con un system prompt gigante. Funcionaría... a veces. Pero:
Aspecto
Un solo agente
Multi-agente
System prompt
Largo, genérico
Corto, enfocado por especialista
Tools por agente
10+ (confunde al modelo)
4-5 por especialista
Personalidad
Una sola para todo
Diferente por contexto
Debugging
Difícil saber qué falló
Sabes exactamente qué agente falló
Escalabilidad
Agregar tools degrada calidad
Agregas un nuevo sub-agente
El patrón multi-agente no es sobre complejidad. Es sobre separación de responsabilidades. El mismo principio que usas en microservicios, aplicado a agentes.

El código completo

El código completo de ambas capas está en el repo:
Bash
# Capa 5 — Spotify
python capa5_spotify.py

# Capa 6 — Multi-agente
python capa6_multi_agente.py
Nota: Sin Spotify configurado, las capas 5-6 funcionan con la biblioteca local como fallback. Verás un aviso  ⚠️ Spotify no disponible  pero el agente seguirá respondiendo.

Lo que aprendiste

  1. Un  @tool  puede hacer cualquier cosa que Python pueda hacer — incluyendo llamar APIs externas con OAuth2, crear recursos en servicios reales, y controlar dispositivos
  1. Para el modelo, no hay diferencia entre un tool local y uno externo. La interfaz es la misma: función + decorador + docstring
  1. El patrón agent as a tool permite crear sistemas multi-agente donde un orquestador delega a especialistas
  1. Los sub-agentes se silencian con  callback_handler=None  — solo el orquestador habla con el usuario
  1. Separar responsabilidades en agentes especializados mejora la calidad de las respuestas y facilita el debugging

Qué sigue

Con 6 capas, tienes un agente que habla, busca, razona, recuerda, controla servicios externos, y delega a especialistas. Todo con Python,  Strands Agents  y APIs abiertas.
Si quieres ir más allá:
  •  Spotipy  — la librería de Python para Spotify
¿Te resultó útil este artículo? Compártelo con tu equipo o déjame saber en los comentarios qué API te gustaría conectar a tu agente. Y si ya estás construyendo agentes multi-agente o conectando APIs externas, me encantaría escuchar tu experiencia.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article