"""Stop the active Lyria music stream and disconnect from voice.
Defines the ``stop_music`` tool, which the LLM uses to end a generative
Lyria music session in a Discord guild and leave the voice channel. The
real work is delegated to the adapter's ``lyria_service``; this module is
only the tool shell (metadata plus the ``run`` handler).
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from tool_context import ToolContext
logger = logging.getLogger(__name__)
TOOL_NAME = "stop_music"
TOOL_DESCRIPTION = (
"Stop the currently playing Lyria music stream and disconnect from the "
"Discord voice channel. Requires active music session."
)
TOOL_PARAMETERS = {
"type": "object",
"properties": {
"guild_id": {"type": "string", "description": "Discord server (guild) ID."},
},
"required": ["guild_id"],
}
[docs]
async def run(
guild_id: str,
ctx: "ToolContext | None" = None,
) -> str:
"""Stop the guild's active Lyria stream and disconnect the bot from voice.
The ``stop_music`` tool entry point. It validates that a Discord adapter is
present and that a real Discord client is reachable, parses the guild id, and
then hands off to the Lyria service to halt playback and leave the voice
channel. It only runs on the Discord platform.
Resolves the client via ``get_discord_client`` (from
``tools._discord_helpers``) and reads ``ctx.adapter.lyria_service``, then
awaits its ``stop`` coroutine, which cancels the streaming task, drops the
cached session state, and force-disconnects the voice client for that guild.
No Redis, knowledge graph, or LLM interaction occurs here. As a tool it is
dispatched by name through the registry — ``tool_loader.py`` registers this
module's ``run`` as the handler for ``stop_music`` and the inference worker
invokes it on the matching tool call; it has no in-repo direct callers.
Args:
guild_id: The Discord guild (server) id whose music session to stop,
as a string; it is parsed to ``int`` before being passed to the
Lyria service.
ctx: The tool context supplying ``adapter`` (and thus the Discord client
and ``lyria_service``).
Returns:
str: The Lyria service's result string (e.g. ``"Stopped"`` or a "music is
not currently playing" message), or an error string when the adapter or
Discord client is unavailable or ``guild_id`` is not a valid integer.
"""
if ctx is None or not hasattr(ctx, "adapter") or ctx.adapter is None:
return "Error: Discord adapter (ctx.adapter) is required."
from tools._discord_helpers import get_discord_client
client = get_discord_client(ctx)
if isinstance(client, str):
return client
lyria = ctx.adapter.lyria_service
try:
guild_id_int = int(guild_id)
except (ValueError, TypeError):
return "Error: Invalid guild_id."
return await lyria.stop(guild_id_int)