Tu agente ya sabe de mĂşsica. Ahora va a controlar Spotify, crear playlists reales y delegar a sub-agentes especializados.
En el artĂculo 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Ă.
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:
- Python 3.10+
- Una cuenta de Spotify Developer, para conectar con la API
- 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:
- Click en Create App
- Nombre: lo que quieras (ej: "DJ Agent")
- Redirect URI:
http://127.0.0.1:8000/callback - Marca Web API
- Guarda el Client ID y Client Secret
Crea un archivo .env en la raĂz del proyecto:
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:
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
@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:
@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
@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
@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
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: Necesitas una cuenta premium para tener acceso a la API de Spotify.
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:
@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:
@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
@toolque consulta tu base de datos de producciĂłn - Un
@toolque envĂa emails via SendGrid - Un
@toolque crea tickets en Jira - Un
@toolque 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.
@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:
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
@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
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
🎵 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:
# Capa 5 — Spotify
python capa5_spotify.py
# Capa 6 — Multi-agente
python capa6_multi_agente.py
Repo: github.com/hsaenzG/OpenSource-agents-demo
Nota: Sin Spotify configurado, las capas 5-6 funcionan con la biblioteca local como fallback. Verás un aviso
⚠️ Spotify no disponiblepero el agente seguirá respondiendo.
Lo que aprendiste
- Un
@toolpuede hacer cualquier cosa que Python pueda hacer, incluyendo llamar APIs externas con OAuth2, crear recursos en servicios reales, y controlar dispositivos - Para el modelo, no hay diferencia entre un tool local y uno externo. La interfaz es la misma: funciĂłn + decorador + docstring
- El patrĂłn agent as a tool permite crear sistemas multi-agente donde un orquestador delega a especialistas
- Los sub-agentes se silencian con
callback_handler=None, solo el orquestador habla con el usuario - 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á:
- DocumentaciĂłn de Strands Agents — guĂas, ejemplos, y API reference
- Multi-agent patterns en Strands — swarms, graphs, y más patrones de orquestación
- Repo del demo — el código completo de las 6 capas
- Spotipy — la librerĂa de Python para Spotify
- Spotify Developer Dashboard — para crear tu app
Âż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.
















