[8' read]
Podcast research briefing agent with Spotify
Search Spotify's podcast catalog for episodes on any topic, enrich them with web research, and get a structured briefing with ranked recommendations, orchestrated by a Mistral agent.
The agent coordinates three tool types through the Agents API:
| Tool type | What it provides |
|---|---|
| Spotify functions | Podcast and episode search, show details, episode details |
| Briefing function | LLM-powered structured briefing generation |
| Web search (built-in) | Transcripts, guest bios, episode summaries |
All tools are defined inline as function tools, so there are no external servers or dependencies beyond the Mistral SDK and spotipy.
API status: This notebook uses
client.beta.agentsandclient.beta.conversations. These are beta endpoints and may change.
Prerequisites#
To complete this notebook, you will need:
- Python 3.11 or later
- A Mistral account and API key
- Spotify Developer credentials (Client ID and Client Secret)
Setting up Spotify credentials#
- Go to the Spotify Developer Dashboard and log in with your Spotify account. A Spotify Premium subscription is required to use the Web API.
- Click Create app.
- Fill in the form:
- App name: Any name (e.g. "Podcast Research Agent")
- App description: Any description
- Redirect URI: Enter
https://localhost:8080/callback(this won't be used, but the field is required) - Which API/SDKs are you planning to use?: Select Web API
- Check the terms of service box and click Save.
- On your app's dashboard, click Settings.
- Copy the Client ID and Client Secret (click "View client secret" to reveal it).
This cookbook uses the Client Credentials auth flow, which provides read-only access to Spotify's public catalog. No user login or OAuth redirect is needed at runtime.
Environment setup#
Install the required packages.
%pip install mistralai spotipy --quietImport the required modules, set your API keys (secure input prompts will appear if the environment variables are not already set), and initialize the Mistral client.
import getpass
import json
import os
from IPython.display import display, Markdown
from mistralai.client import Mistral
from mistralai.client.models import (
FunctionCallEvent,
FunctionResultEntry,
MessageOutputEvent,
)
if not os.environ.get("MISTRAL_API_KEY"):
os.environ["MISTRAL_API_KEY"] = getpass.getpass("Mistral API key: ")
if not os.environ.get("SPOTIFY_CLIENT_ID"):
os.environ["SPOTIFY_CLIENT_ID"] = getpass.getpass("Spotify Client ID: ")
if not os.environ.get("SPOTIFY_CLIENT_SECRET"):
os.environ["SPOTIFY_CLIENT_SECRET"] = getpass.getpass("Spotify Client Secret: ")
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])Architecture#
The agent uses function tools registered directly on the agent. When the agent calls a tool, the streaming loop executes the corresponding Python function and sends the result back via FunctionResultEntry.
┌─────────────────────┐
│ Mistral Agent │
│ (zai-glm-5-2) │
└──────┬──────┬────────┘
│ │
┌────────────────┘ └────────────────┐
│ │ │
┌─────────▼────────┐ ┌───▼──────────────┐ ┌─────▼──────────┐
│ Spotify functions │ │ Briefing function│ │ Web Search │
│ (spotipy client │ │ (mistral LLM │ │ (built-in) │
│ credentials) │ │ chat completion) │ │ │
└──────────────────┘ └──────────────────┘ └────────────────┘- Spotify functions — wrap the Spotify Web API via
spotipywith Client Credentials auth. Provide tools for searching podcasts, searching episodes, and fetching details. - Briefing function — uses
zai-glm-5-2to generate a structured research briefing from collected podcast data and web research. - Web search — Mistral's built-in web search tool finds transcripts, guest bios, and episode summaries to enrich the briefing.
Step 1 — Define tool functions#
Function tools let an agent call your own code. You write regular Python functions, and when the agent decides it needs one, the Agents API emits a FunctionCallEvent with the function name and arguments. Your code runs the function locally and sends the result back, so the agent never executes your code directly.
The tools are defined in six functions: five wrap the Spotify Web API via spotipy for podcast catalog queries, and one calls the Mistral Chat API to generate a structured briefing from collected data. Each function returns a JSON string so the agent can parse the results.
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials(
client_id=os.environ["SPOTIFY_CLIENT_ID"],
client_secret=os.environ["SPOTIFY_CLIENT_SECRET"],
))
MODEL = "zai-glm-5-2"
BRIEFING_SYSTEM_PROMPT = """You are a research analyst. Given a topic, podcast data from
Spotify, and web research, produce a concise markdown briefing with:
- Executive summary (2-3 sentences)
- Ranked episode recommendations with relevance score, episode/show name, Spotify link, duration, release date, and a one-sentence summary
- Key themes across episodes
- Notable experts and guests
- Gaps and limitations
Use ONLY exact URLs from the input data. Never fabricate Spotify links."""
def _format_duration(ms: int) -> str:
"""Convert milliseconds to a human-readable duration string."""
minutes = ms // 60000
if minutes >= 60:
hours = minutes // 60
remaining = minutes % 60
return f"{hours}h {remaining}m"
return f"{minutes}m"
def search_podcasts(query: str, limit: int = 10) -> str:
"""Search for podcast shows on Spotify."""
try:
results = sp.search(q=query, type="show", limit=limit)
shows = []
for item in results.get("shows", {}).get("items", []):
if item is None:
continue
shows.append({
"id": item["id"],
"name": item["name"],
"publisher": item.get("publisher", "Unknown"),
"description": (item.get("description") or "")[:500],
"total_episodes": item.get("total_episodes", 0),
"url": item.get("external_urls", {}).get("spotify", ""),
})
return json.dumps(shows, indent=2)
except Exception as e:
return json.dumps({"error": str(e)})
def search_episodes(query: str, limit: int = 10) -> str:
"""Search for specific podcast episodes on Spotify."""
try:
results = sp.search(q=query, type="episode", limit=limit)
episodes = []
for item in results.get("episodes", {}).get("items", []):
if item is None:
continue
episodes.append({
"id": item["id"],
"name": item["name"],
"show_name": item.get("show", {}).get("name", "Unknown"),
"description": (item.get("description") or "")[:500],
"duration": _format_duration(item.get("duration_ms", 0)),
"release_date": item.get("release_date", "Unknown"),
"url": item.get("external_urls", {}).get("spotify", ""),
})
return json.dumps(episodes, indent=2)
except Exception as e:
return json.dumps({"error": str(e)})
def get_podcast_details(show_id: str) -> str:
"""Get full details for a specific podcast show."""
try:
show = sp.show(show_id)
return json.dumps({
"id": show["id"],
"name": show["name"],
"publisher": show.get("publisher", "Unknown"),
"description": (show.get("description") or "")[:1000],
"total_episodes": show.get("total_episodes", 0),
"languages": show.get("languages", []),
"url": show.get("external_urls", {}).get("spotify", ""),
}, indent=2)
except Exception as e:
return json.dumps({"error": str(e)})
def get_podcast_episodes(show_id: str, limit: int = 10) -> str:
"""Get episodes from a specific podcast show."""
try:
results = sp.show_episodes(show_id, limit=limit)
episodes = []
for item in results.get("items", []):
if item is None:
continue
episodes.append({
"id": item["id"],
"name": item["name"],
"description": (item.get("description") or "")[:500],
"duration": _format_duration(item.get("duration_ms", 0)),
"release_date": item.get("release_date", "Unknown"),
"url": item.get("external_urls", {}).get("spotify", ""),
})
return json.dumps(episodes, indent=2)
except Exception as e:
return json.dumps({"error": str(e)})
def get_episode_details(episode_id: str) -> str:
"""Get full details for a specific podcast episode."""
try:
episode = sp.episode(episode_id)
return json.dumps({
"id": episode["id"],
"name": episode["name"],
"show_name": episode.get("show", {}).get("name", "Unknown"),
"description": (episode.get("description") or "")[:2000],
"duration": _format_duration(episode.get("duration_ms", 0)),
"release_date": episode.get("release_date", "Unknown"),
"language": episode.get("language", "Unknown"),
"url": episode.get("external_urls", {}).get("spotify", ""),
}, indent=2)
except Exception as e:
return json.dumps({"error": str(e)})
def generate_research_briefing(topic: str, podcast_data: str, web_research: str) -> str:
"""Generate a structured research briefing from podcast data and web research."""
try:
response = client.chat.complete(
model=MODEL,
messages=[
{"role": "system", "content": BRIEFING_SYSTEM_PROMPT},
{"role": "user", "content": f"""Topic: {topic}
Podcast data:
{podcast_data}
Web research:
{web_research}"""},
],
)
return response.choices[0].message.content
except Exception as e:
return json.dumps({"error": str(e)})Step 2 — Define tool schemas and create the agent#
For the agent to know which functions it can call, you provide a tool schema for each one — a dict with the function's name, description, and parameter spec following the JSON Schema format. The agent reads these schemas to decide when and how to call each tool.
You also need a functions_mapping dict that maps tool names to their Python implementations. The streaming loop uses this to dispatch calls at runtime.
Create the agent with client.beta.agents.create_async, passing the tool schemas (plus the built-in web_search tool) in the tools parameter. The instructions field tells the agent how to use its tools in a multi-step research workflow.
def _tool(name: str, description: str, parameters: dict) -> dict:
"""Helper to build a function tool schema."""
return {"type": "function", "function": {"name": name, "description": description, "parameters": parameters}}
tools = [
_tool("search_podcasts", "Search for podcast shows on Spotify matching a topic or keyword.", {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query for finding podcast shows."},
"limit": {"type": "integer", "description": "Maximum number of results (default 10)."},
},
"required": ["query"],
}),
_tool("search_episodes", "Search for podcast episodes on Spotify matching a topic or keyword.", {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query for finding podcast episodes."},
"limit": {"type": "integer", "description": "Maximum number of results (default 10)."},
},
"required": ["query"],
}),
_tool("get_podcast_details", "Get full details for a specific podcast show by its Spotify ID.", {
"type": "object",
"properties": {
"show_id": {"type": "string", "description": "The Spotify show ID."},
},
"required": ["show_id"],
}),
_tool("get_podcast_episodes", "Get episodes from a specific podcast show.", {
"type": "object",
"properties": {
"show_id": {"type": "string", "description": "The Spotify show ID."},
"limit": {"type": "integer", "description": "Maximum number of episodes (default 10)."},
},
"required": ["show_id"],
}),
_tool("get_episode_details", "Get full details for a specific podcast episode by its Spotify ID.", {
"type": "object",
"properties": {
"episode_id": {"type": "string", "description": "The Spotify episode ID."},
},
"required": ["episode_id"],
}),
_tool("generate_research_briefing", "Generate a structured research briefing from podcast data and web research.", {
"type": "object",
"properties": {
"topic": {"type": "string", "description": "The research topic being investigated."},
"podcast_data": {"type": "string", "description": "JSON string of podcast and episode data from Spotify."},
"web_research": {"type": "string", "description": "Additional context gathered from web search."},
},
"required": ["topic", "podcast_data", "web_research"],
}),
{"type": "web_search"},
]
# Map tool names to Python functions for the streaming loop
functions_mapping = {
"search_podcasts": search_podcasts,
"search_episodes": search_episodes,
"get_podcast_details": get_podcast_details,
"get_podcast_episodes": get_podcast_episodes,
"get_episode_details": get_episode_details,
"generate_research_briefing": generate_research_briefing,
}
AGENT_INSTRUCTIONS = """Search Spotify for podcasts and episodes on the user's topic using
varied queries. Get details on the top results, use web search for additional context,
then pass the raw JSON data to generate_research_briefing. Never fabricate Spotify URLs."""
agent = await client.beta.agents.create_async(
model=MODEL,
name="podcast-research-agent",
instructions=AGENT_INSTRUCTIONS,
description="Podcast research briefing agent",
tools=tools,
)
print(f"Agent ready: {agent.name} (id={agent.id})")Step 3 — Run a research query#
The Conversations API manages multi-turn interactions with an agent. Call conversations.start_stream_async to begin and receive a stream of events:
MessageOutputEvent— a chunk of the agent's text response, streamed token by token.FunctionCallEvent— the agent wants to call a function tool. Includes atool_call_id, the functionname, andargumentsas a JSON string. Multiple events may arrive for the same call (streamed argument chunks) or for different parallel calls.
The run_research function handles the full loop:
- Collect all calls from the stream, grouping argument chunks by
tool_call_id. - Execute each function locally via
functions_mapping. - Send results back with
conversations.append_stream_asyncas a list ofFunctionResultEntryobjects. - Repeat until the agent finishes with no more tool calls.
async def run_research(query: str) -> str:
"""Run a podcast research query and return the briefing text."""
result = ""
conversation_id = None
response = await client.beta.conversations.start_stream_async(
agent_id=agent.id, inputs=query,
)
while True:
tool_calls = {}
async for event in response:
if not event.data:
continue
if conversation_id is None and hasattr(event.data, "conversation_id"):
conversation_id = event.data.conversation_id
match event.data:
case MessageOutputEvent():
if isinstance(event.data.content, str):
result += event.data.content
print(".", end="", flush=True)
case FunctionCallEvent():
call_id = event.data.tool_call_id
if call_id not in tool_calls:
tool_calls[call_id] = {"name": event.data.name, "arguments": ""}
print(f"\n[Tool call] {event.data.name}")
tool_calls[call_id]["arguments"] += event.data.arguments
if not tool_calls:
break
# Execute each function call and send results back
results = [
FunctionResultEntry(
tool_call_id=call_id,
result=functions_mapping[info["name"]](**json.loads(info["arguments"])),
)
for call_id, info in tool_calls.items()
]
response = await client.beta.conversations.append_stream_async(
conversation_id=conversation_id, inputs=results,
)
print(f"\n\nBriefing complete ({len(result)} chars)")
return result
QUERY = "Research podcasts about AI safety and alignment. Find episodes featuring leading researchers and recent developments."
briefing = await run_research(QUERY)Step 4 — Display the briefing#
Render the accumulated briefing as formatted markdown.
display(Markdown(briefing))Try another topic#
Each call to run_research creates a new conversation, so the agent starts fresh with no context from the previous query. Edit QUERY and run the cell.
Example topics:
- "Find podcast episodes covering climate technology and clean energy innovations"
- "Research podcast interviews with startup founders about lessons learned from building companies"
- "Podcasts about the history and future of space exploration"
QUERY = "Find podcast episodes covering climate technology and clean energy innovations"
new_briefing = await run_research(QUERY)
display(Markdown(new_briefing))Cleanup#
Agents persist on Mistral's servers until deleted. You can delete the agent when you're done if you don't plan to use it. Any conversations associated with the agent are also cleaned up.
await client.beta.agents.delete_async(agent_id=agent.id)
print(f"Agent deleted: {agent.id}")Summary#
This notebook demonstrated how to build a podcast research agent that searches Spotify, gathers web context, and generates structured briefings.
What you built:
- Six function tools (Spotify search + briefing generation) defined inline with tool schemas
- A Mistral agent that orchestrates podcast research across all tools and built-in web search
- A streaming pipeline that executes tool calls locally and renders the final briefing as markdown
Mistral features used:
- Agents API (beta)
- Conversations API (beta) with
FunctionCallEvent/FunctionResultEntryfor tool execution - Built-in web search tool
Other services:
- Spotify Web API — podcast catalog search via
spotipy
Learn more about building agents in the Agents documentation.