| """ |
| 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__) |
|
|
|
|
| |
| |
| |
|
|
| 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) |
| """ |
| |
| if not agent_id: |
| return { |
| "allowed": True, |
| "reason": "Human action", |
| "governance_check_passed": True |
| } |
|
|
| |
| try: |
| |
| 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") |
|
|
| |
| maturity_requirements = { |
| "spotify_current": "SUPERVISED", |
| "spotify_play": "SUPERVISED", |
| "spotify_pause": "SUPERVISED", |
| "spotify_next": "SUPERVISED", |
| "spotify_previous": "SUPERVISED", |
| "spotify_volume": "SUPERVISED", |
| "spotify_devices": "INTERN", |
| "sonos_discover": "INTERN", |
| "sonos_play": "SUPERVISED", |
| "sonos_pause": "SUPERVISED", |
| "sonos_volume": "SUPERVISED", |
| "sonos_next": "SUPERVISED", |
| "sonos_previous": "SUPERVISED", |
| "sonos_groups": "INTERN", |
| "sonos_join": "SUPERVISED", |
| "sonos_leave": "SUPERVISED", |
| } |
|
|
| required_maturity = maturity_requirements.get(action, "SUPERVISED") |
|
|
| |
| 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 |
| } |
|
|
| |
| 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}") |
| |
| 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 |
| } |
|
|
|
|
| |
| |
| |
|
|
| 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)} |
|
|
|
|
| |
| |
| |
|
|
| 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)} |
|
|
|
|
| |
| |
| |
|
|
| |
| |
|
|
| def register_media_tools(): |
| """Register all media control tools with the tool registry.""" |
| from tools.registry import tool_registry |
|
|
| |
| 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", |
| tags=["music", "audio", "spotify", "streaming"] |
| ) |
|
|
| |
| 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", |
| 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", |
| tags=["music", "audio", "sonos", "speakers"] |
| ) |
|
|
| logger.info("Media control tools registered with tool registry") |
|
|
|
|
| |
| try: |
| register_media_tools() |
| except Exception as e: |
| logger.warning(f"Failed to register media tools: {e}") |
|
|