""" 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}")