annator-atom / backend /tools /media_tool.py
techprotrade's picture
Full stack ATOM backend + AIMONEYFLOW clients (port 7860) (part 6)
4e3c158 verified
Raw
History Blame Contribute Delete
20 kB
"""
Media Control Tool
Provides LangChain-style tool interface for Spotify and Sonos media control.
Integrates with agent governance system for maturity-based access control.
Features:
- Spotify playback control (play, pause, skip, volume)
- Sonos speaker control (play, pause, volume, groups)
- Governance enforcement (SUPERVISED+ maturity required)
- Audit trail for all media operations
Governance:
- Spotify: SUPERVISED+ maturity level
- Sonos: SUPERVISED+ maturity level
- STUDENT and INTERN agents blocked
"""
import logging
from typing import Any, Dict, Optional
from sqlalchemy.orm import Session
from core.governance_cache import AsyncGovernanceCache
from core.media.spotify_service import SpotifyService
from core.media.sonos_service import SonosService
from core.structured_logger import get_logger
logger = get_logger(__name__)
# ============================================================================
# Governance Check Helper
# ============================================================================
async def _check_media_governance(
db: Session,
agent_id: Optional[str],
action: str,
user_id: str
) -> Dict[str, Any]:
"""
Check if agent has permission to perform media control action.
Args:
db: Database session
agent_id: Agent ID (None if human action)
action: Action being performed (e.g., "spotify_play", "sonos_pause")
user_id: User requesting action
Returns:
Dict with allowed (bool) and reason (str)
"""
# Human actions (no agent_id) are always allowed
if not agent_id:
return {
"allowed": True,
"reason": "Human action",
"governance_check_passed": True
}
# Check governance cache
try:
# Get agent maturity level
from core.agent_context_resolver import AgentContextResolver
resolver = AgentContextResolver(db)
agent_context = await resolver.resolve_agent_context(agent_id)
maturity_level = agent_context.get("maturity_level", "STUDENT")
# Map media actions to maturity requirements
maturity_requirements = {
"spotify_current": "SUPERVISED",
"spotify_play": "SUPERVISED",
"spotify_pause": "SUPERVISED",
"spotify_next": "SUPERVISED",
"spotify_previous": "SUPERVISED",
"spotify_volume": "SUPERVISED",
"spotify_devices": "INTERN", # Read-only, lower barrier
"sonos_discover": "INTERN", # Read-only, lower barrier
"sonos_play": "SUPERVISED",
"sonos_pause": "SUPERVISED",
"sonos_volume": "SUPERVISED",
"sonos_next": "SUPERVISED",
"sonos_previous": "SUPERVISED",
"sonos_groups": "INTERN", # Read-only, lower barrier
"sonos_join": "SUPERVISED",
"sonos_leave": "SUPERVISED",
}
required_maturity = maturity_requirements.get(action, "SUPERVISED")
# Check maturity hierarchy
maturity_order = ["STUDENT", "INTERN", "SUPERVISED", "AUTONOMOUS"]
current_level = maturity_order.index(maturity_level)
required_level = maturity_order.index(required_maturity)
if current_level < required_level:
return {
"allowed": False,
"reason": f"Agent maturity {maturity_level} insufficient for {action}. Requires {required_maturity}+.",
"governance_check_passed": False,
"current_maturity": maturity_level,
"required_maturity": required_maturity
}
# Check governance cache for tool-specific permission
gc = AsyncGovernanceCache(db)
has_permission = await gc.check_permission(
agent_id=agent_id,
tool_name=action,
maturity_level=maturity_level
)
if not has_permission:
return {
"allowed": False,
"reason": f"Governance check failed for {action}",
"governance_check_passed": False,
"maturity_level": maturity_level
}
return {
"allowed": True,
"reason": "Governance check passed",
"governance_check_passed": True,
"maturity_level": maturity_level
}
except Exception as e:
logger.error(f"Governance check failed for {action}: {e}")
# Fail open for human operations, fail closed for agents
if agent_id:
return {
"allowed": False,
"reason": f"Governance check error: {str(e)}",
"governance_check_passed": False
}
return {
"allowed": True,
"reason": "Human action (governance check failed)",
"governance_check_passed": False
}
# ============================================================================
# Spotify Control Functions
# ============================================================================
async def spotify_current(
db: Session,
user_id: str,
agent_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Get currently playing track from Spotify.
Action Complexity: 2 (MODERATE)
Maturity Required: SUPERVISED+
Args:
db: Database session
user_id: User ID for Spotify account
agent_id: Agent ID (for governance)
Returns:
Dict with current track info (artist, name, album, is_playing)
"""
governance_check = await _check_media_governance(db, agent_id, "spotify_current", user_id)
if not governance_check["allowed"]:
return {
"success": False,
"error": governance_check["reason"],
"governance_blocked": True
}
try:
spotify_service = SpotifyService(db)
result = await spotify_service.get_current_track(user_id)
logger.info(f"Retrieved current track for user {user_id}")
return result
except Exception as e:
logger.error(f"Failed to get current track for user {user_id}: {e}")
return {
"success": False,
"error": str(e)
}
async def spotify_play(
db: Session,
user_id: str,
agent_id: Optional[str] = None,
track_uri: Optional[str] = None,
device_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Play track or resume playback on Spotify.
Action Complexity: 2 (MODERATE)
Maturity Required: SUPERVISED+
Args:
db: Database session
user_id: User ID for Spotify account
agent_id: Agent ID (for governance)
track_uri: Spotify track URI (optional, resumes if None)
device_id: Target device ID (optional)
Returns:
Dict with playback status
"""
governance_check = await _check_media_governance(db, agent_id, "spotify_play", user_id)
if not governance_check["allowed"]:
return {
"success": False,
"error": governance_check["reason"],
"governance_blocked": True
}
try:
spotify_service = SpotifyService(db)
result = await spotify_service.play_track(user_id, track_uri, device_id)
logger.info(f"Started Spotify playback for user {user_id} (track={track_uri})")
return result
except Exception as e:
logger.error(f"Failed to play track for user {user_id}: {e}")
return {
"success": False,
"error": str(e)
}
async def spotify_pause(
db: Session,
user_id: str,
agent_id: Optional[str] = None,
device_id: Optional[str] = None
) -> Dict[str, Any]:
"""Pause Spotify playback."""
governance_check = await _check_media_governance(db, agent_id, "spotify_pause", user_id)
if not governance_check["allowed"]:
return {"success": False, "error": governance_check["reason"], "governance_blocked": True}
try:
spotify_service = SpotifyService(db)
result = await spotify_service.pause_playback(user_id, device_id)
logger.info(f"Paused Spotify playback for user {user_id}")
return result
except Exception as e:
logger.error(f"Failed to pause Spotify for user {user_id}: {e}")
return {"success": False, "error": str(e)}
async def spotify_next(
db: Session,
user_id: str,
agent_id: Optional[str] = None,
device_id: Optional[str] = None
) -> Dict[str, Any]:
"""Skip to next track on Spotify."""
governance_check = await _check_media_governance(db, agent_id, "spotify_next", user_id)
if not governance_check["allowed"]:
return {"success": False, "error": governance_check["reason"], "governance_blocked": True}
try:
spotify_service = SpotifyService(db)
result = await spotify_service.skip_next(user_id, device_id)
logger.info(f"Skipped to next track for user {user_id}")
return result
except Exception as e:
logger.error(f"Failed to skip next for user {user_id}: {e}")
return {"success": False, "error": str(e)}
async def spotify_previous(
db: Session,
user_id: str,
agent_id: Optional[str] = None,
device_id: Optional[str] = None
) -> Dict[str, Any]:
"""Skip to previous track on Spotify."""
governance_check = await _check_media_governance(db, agent_id, "spotify_previous", user_id)
if not governance_check["allowed"]:
return {"success": False, "error": governance_check["reason"], "governance_blocked": True}
try:
spotify_service = SpotifyService(db)
result = await spotify_service.skip_previous(user_id, device_id)
logger.info(f"Skipped to previous track for user {user_id}")
return result
except Exception as e:
logger.error(f"Failed to skip previous for user {user_id}: {e}")
return {"success": False, "error": str(e)}
async def spotify_volume(
db: Session,
user_id: str,
volume_percent: int,
agent_id: Optional[str] = None,
device_id: Optional[str] = None
) -> Dict[str, Any]:
"""Set Spotify volume."""
governance_check = await _check_media_governance(db, agent_id, "spotify_volume", user_id)
if not governance_check["allowed"]:
return {"success": False, "error": governance_check["reason"], "governance_blocked": True}
try:
spotify_service = SpotifyService(db)
result = await spotify_service.set_volume(user_id, volume_percent, device_id)
logger.info(f"Set Spotify volume to {volume_percent}% for user {user_id}")
return result
except Exception as e:
logger.error(f"Failed to set volume for user {user_id}: {e}")
return {"success": False, "error": str(e)}
async def spotify_devices(
db: Session,
user_id: str,
agent_id: Optional[str] = None
) -> Dict[str, Any]:
"""Get available Spotify devices."""
governance_check = await _check_media_governance(db, agent_id, "spotify_devices", user_id)
if not governance_check["allowed"]:
return {"success": False, "error": governance_check["reason"], "governance_blocked": True}
try:
spotify_service = SpotifyService(db)
result = await spotify_service.get_available_devices(user_id)
logger.info(f"Retrieved Spotify devices for user {user_id}")
return result
except Exception as e:
logger.error(f"Failed to get devices for user {user_id}: {e}")
return {"success": False, "error": str(e)}
# ============================================================================
# Sonos Control Functions
# ============================================================================
async def sonos_discover(
db: Session,
agent_id: Optional[str] = None
) -> Dict[str, Any]:
"""Discover Sonos speakers on local network."""
governance_check = await _check_media_governance(db, agent_id, "sonos_discover", "system")
if not governance_check["allowed"]:
return {"success": False, "error": governance_check["reason"], "governance_blocked": True}
try:
sonos_service = SonosService()
speakers = await sonos_service.discover_speakers()
logger.info(f"Discovered {len(speakers)} Sonos speakers")
return {"success": True, "speakers": speakers, "count": len(speakers)}
except Exception as e:
logger.error(f"Failed to discover Sonos speakers: {e}")
return {"success": False, "error": str(e)}
async def sonos_play(
db: Session,
speaker_ip: str,
agent_id: Optional[str] = None,
uri: Optional[str] = None
) -> Dict[str, Any]:
"""Play audio or resume playback on Sonos speaker."""
governance_check = await _check_media_governance(db, agent_id, "sonos_play", "system")
if not governance_check["allowed"]:
return {"success": False, "error": governance_check["reason"], "governance_blocked": True}
try:
sonos_service = SonosService()
result = await sonos_service.play(speaker_ip, uri)
logger.info(f"Started playback on Sonos speaker {speaker_ip}")
return result
except Exception as e:
logger.error(f"Failed to play on Sonos speaker {speaker_ip}: {e}")
return {"success": False, "error": str(e)}
async def sonos_pause(
db: Session,
speaker_ip: str,
agent_id: Optional[str] = None
) -> Dict[str, Any]:
"""Pause Sonos speaker."""
governance_check = await _check_media_governance(db, agent_id, "sonos_pause", "system")
if not governance_check["allowed"]:
return {"success": False, "error": governance_check["reason"], "governance_blocked": True}
try:
sonos_service = SonosService()
result = await sonos_service.pause(speaker_ip)
logger.info(f"Paused Sonos speaker {speaker_ip}")
return result
except Exception as e:
logger.error(f"Failed to pause Sonos speaker {speaker_ip}: {e}")
return {"success": False, "error": str(e)}
async def sonos_volume(
db: Session,
speaker_ip: str,
volume: int,
agent_id: Optional[str] = None
) -> Dict[str, Any]:
"""Set Sonos speaker volume."""
governance_check = await _check_media_governance(db, agent_id, "sonos_volume", "system")
if not governance_check["allowed"]:
return {"success": False, "error": governance_check["reason"], "governance_blocked": True}
try:
sonos_service = SonosService()
result = await sonos_service.set_volume(speaker_ip, volume)
logger.info(f"Set Sonos speaker {speaker_ip} volume to {volume}")
return result
except Exception as e:
logger.error(f"Failed to set volume for Sonos speaker {speaker_ip}: {e}")
return {"success": False, "error": str(e)}
async def sonos_groups(
db: Session,
agent_id: Optional[str] = None
) -> Dict[str, Any]:
"""Get Sonos speaker groups."""
governance_check = await _check_media_governance(db, agent_id, "sonos_groups", "system")
if not governance_check["allowed"]:
return {"success": False, "error": governance_check["reason"], "governance_blocked": True}
try:
sonos_service = SonosService()
groups = await sonos_service.get_groups()
logger.info(f"Retrieved {len(groups)} Sonos groups")
return {"success": True, "groups": groups, "count": len(groups)}
except Exception as e:
logger.error(f"Failed to get Sonos groups: {e}")
return {"success": False, "error": str(e)}
# ============================================================================
# Tool Registration
# ============================================================================
# Auto-register all media control functions with tool registry
# This happens on module import
def register_media_tools():
"""Register all media control tools with the tool registry."""
from tools.registry import tool_registry
# Spotify tools
tool_registry.register(
name="spotify_current",
function=spotify_current,
version="1.0.0",
description="Get currently playing track from Spotify",
category="media",
complexity=2,
maturity_required="SUPERVISED",
tags=["music", "audio", "spotify", "streaming"]
)
tool_registry.register(
name="spotify_play",
function=spotify_play,
version="1.0.0",
description="Play track or resume playback on Spotify",
category="media",
complexity=2,
maturity_required="SUPERVISED",
tags=["music", "audio", "spotify", "streaming"]
)
tool_registry.register(
name="spotify_pause",
function=spotify_pause,
version="1.0.0",
description="Pause Spotify playback",
category="media",
complexity=2,
maturity_required="SUPERVISED",
tags=["music", "audio", "spotify", "streaming"]
)
tool_registry.register(
name="spotify_next",
function=spotify_next,
version="1.0.0",
description="Skip to next track on Spotify",
category="media",
complexity=2,
maturity_required="SUPERVISED",
tags=["music", "audio", "spotify", "streaming"]
)
tool_registry.register(
name="spotify_previous",
function=spotify_previous,
version="1.0.0",
description="Skip to previous track on Spotify",
category="media",
complexity=2,
maturity_required="SUPERVISED",
tags=["music", "audio", "spotify", "streaming"]
)
tool_registry.register(
name="spotify_volume",
function=spotify_volume,
version="1.0.0",
description="Set Spotify volume (0-100)",
category="media",
complexity=2,
maturity_required="SUPERVISED",
tags=["music", "audio", "spotify", "streaming"]
)
tool_registry.register(
name="spotify_devices",
function=spotify_devices,
version="1.0.0",
description="Get available Spotify devices",
category="media",
complexity=1,
maturity_required="INTERN", # Read-only
tags=["music", "audio", "spotify", "streaming"]
)
# Sonos tools
tool_registry.register(
name="sonos_discover",
function=sonos_discover,
version="1.0.0",
description="Discover Sonos speakers on local network",
category="media",
complexity=1,
maturity_required="INTERN", # Read-only
tags=["music", "audio", "sonos", "speakers"]
)
tool_registry.register(
name="sonos_play",
function=sonos_play,
version="1.0.0",
description="Play audio or resume playback on Sonos speaker",
category="media",
complexity=2,
maturity_required="SUPERVISED",
tags=["music", "audio", "sonos", "speakers"]
)
tool_registry.register(
name="sonos_pause",
function=sonos_pause,
version="1.0.0",
description="Pause Sonos speaker",
category="media",
complexity=2,
maturity_required="SUPERVISED",
tags=["music", "audio", "sonos", "speakers"]
)
tool_registry.register(
name="sonos_volume",
function=sonos_volume,
version="1.0.0",
description="Set Sonos speaker volume (0-100)",
category="media",
complexity=2,
maturity_required="SUPERVISED",
tags=["music", "audio", "sonos", "speakers"]
)
tool_registry.register(
name="sonos_groups",
function=sonos_groups,
version="1.0.0",
description="Get Sonos speaker groups",
category="media",
complexity=1,
maturity_required="INTERN", # Read-only
tags=["music", "audio", "sonos", "speakers"]
)
logger.info("Media control tools registered with tool registry")
# Auto-register on import
try:
register_media_tools()
except Exception as e:
logger.warning(f"Failed to register media tools: {e}")