""" Gradio ChatInterface for BeatDebate Music Recommendation System This module provides a ChatGPT-style interface that showcases the 4-agent planning system with real-time progress indicators and planning visualization. """ import logging from typing import Dict, List, Optional, Tuple, Any import gradio as gr import requests from .response_formatter import ResponseFormatter from .planning_display import PlanningDisplay # Import fallback service components from ..services.llm_fallback_service import ( LLMFallbackService, FallbackRequest, FallbackTrigger ) # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Query examples from intent-aware recommendation system design document QUERY_EXAMPLES = { "By Artist": [ "Songs by Mk.gee", "Give me tracks by Radiohead", "Play some Beatles songs" ], "Artist Similarity": [ "Songs like Mk.gee", "Similar artists to BROCKHAMPTON", "Songs that sound like Radiohead" ], "Discovery": [ "Find me underground electronic music", "Something completely new and different", "Discover underground tracks by Kendrick Lamar" ], "Genre/Mood": [ "Upbeat electronic music", "Sad indie songs", "Chill lo-fi hip hop" ], "Contextual": [ "Music for studying", "Workout playlist songs", "Background music for coding" ], "Hybrid": [ "Songs like Kendrick Lamar but jazzy", "Songs by Michael Jackson that are R&B", "Electronic music similar to Aphex Twin" ], "Follow-ups": [ "More tracks", "More like that", "Similar to these", "More from this artist", "More underground like these", "More for studying like these" ] } class BeatDebateChatInterface: """ ChatGPT-style interface for BeatDebate music recommendations. Features: - Real-time agent progress indicators - Planning strategy visualization - Audio preview integration - Conversation history management - Last.fm player embeds - LLM fallback for unknown intents """ def __init__(self, backend_url: str = "http://localhost:8000"): """ Initialize the chat interface. Args: backend_url: URL of the FastAPI backend """ self.backend_url = backend_url self.response_formatter = ResponseFormatter() self.planning_display = PlanningDisplay() # ✅ REMOVED: Global session management - now handled per-user via gr.State # ✅ REMOVED: Global conversation history - now managed by backend session store # Initialize fallback service self.fallback_service = None self._initialize_fallback_service() logger.info( f"BeatDebate Chat Interface initialized (multi-user safe), " f"fallback_available: {self.fallback_service is not None}" ) def _initialize_fallback_service(self) -> None: """Initialize the LLM fallback service.""" try: # Import Gemini client creation function from ..services.enhanced_recommendation_service import create_gemini_client from ..api.rate_limiter import UnifiedRateLimiter import os # Get Gemini API key gemini_api_key = os.getenv('GEMINI_API_KEY', 'demo_gemini_key') if gemini_api_key and gemini_api_key != 'demo_gemini_key': # Create Gemini client gemini_client = create_gemini_client(gemini_api_key) if gemini_client: # Create rate limiter for fallback service rate_limiter = UnifiedRateLimiter.for_gemini(calls_per_minute=8) # Initialize fallback service self.fallback_service = LLMFallbackService( gemini_client=gemini_client, rate_limiter=rate_limiter ) logger.info("LLM fallback service initialized successfully") else: logger.warning("Failed to create Gemini client for fallback service") else: logger.warning("No valid Gemini API key found, fallback service disabled") except Exception as e: logger.error(f"Failed to initialize fallback service: {e}") self.fallback_service = None async def process_message( self, message: str, history: List[Tuple[str, str]], session_id: str ) -> Tuple[str, List[Tuple[str, str]], str, str]: """ Process user message and return response with track info. Enhanced with fallback support and per-user session management. Args: message: User input message history: Chat history as list of (user, assistant) tuples session_id: User-specific session identifier Returns: Tuple of (response, updated_history, lastfm_player_html, updated_session_id) """ if not message.strip(): return "", history, "", session_id logger.info(f"Processing message: {message} for session: {session_id}") try: # Primary: Get recommendations from 4-agent system with session ID recommendations_response = await self._get_recommendations(message, session_id) # Check if fallback is needed should_fallback, trigger_reason = self._should_use_fallback( recommendations_response ) if should_fallback: logger.info(f"Using LLM fallback for session {session_id} due to: {trigger_reason.value}") recommendations_response = await self._get_fallback_recommendations( message, trigger_reason, session_id ) if recommendations_response: # Get potentially updated session_id from backend updated_session_id = recommendations_response.get("session_id", session_id) # Format the response formatted_response = ( self.response_formatter.format_recommendations( recommendations_response ) ) # Add to history using tuple format history.append((message, formatted_response)) # Create Last.fm player HTML for latest recommendations lastfm_player_html = self._create_lastfm_player_html( recommendations_response.get("recommendations", []) ) return "", history, lastfm_player_html, updated_session_id else: # Final emergency fallback error_response = self._create_emergency_response(message) history.append((message, error_response)) return "", history, "", session_id except Exception as e: logger.error(f"Error processing message for session {session_id}: {e}") error_response = f"An error occurred: {str(e)}" history.append((message, error_response)) return "", history, "", session_id def _should_use_fallback( self, response: Optional[Dict] ) -> Tuple[bool, FallbackTrigger]: """ Determine if fallback should be used based on backend response. Args: response: Response from backend recommendation system Returns: Tuple of (should_fallback, trigger_reason) """ if response is None: return True, FallbackTrigger.API_ERROR # Check for explicit unknown intent intent = response.get("intent", "").lower() if intent in ["unknown", "unsupported", "fallback"]: return True, FallbackTrigger.UNKNOWN_INTENT # Check for empty recommendations recommendations = response.get("recommendations", []) if not recommendations or len(recommendations) == 0: return True, FallbackTrigger.NO_RECOMMENDATIONS # Check for error indicators if response.get("error") or response.get("detail"): return True, FallbackTrigger.API_ERROR return False, None async def _get_fallback_recommendations( self, query: str, trigger_reason: FallbackTrigger, session_id: str ) -> Optional[Dict[str, Any]]: """ Get fallback recommendations from LLM service. Args: query: User query trigger_reason: Reason fallback was triggered session_id: User-specific session identifier Returns: Fallback recommendations response or None if unavailable """ if not self.fallback_service: logger.warning("Fallback service not available") return None try: # Prepare fallback request fallback_request = FallbackRequest( query=query, session_id=session_id, chat_context=self._get_chat_context(), trigger_reason=trigger_reason, max_recommendations=10 ) # Get fallback recommendations fallback_response = await self.fallback_service.get_fallback_recommendations( fallback_request ) # Add fallback disclaimer to explanation if fallback_response and fallback_response.get("fallback_used"): original_explanation = fallback_response.get("explanation", "") fallback_explanation = ( f"**⚠️ DEFAULTING TO REGULAR LLM** - This query is outside our " f"specialized 4-agent system's scope.\n\n{original_explanation}" ) fallback_response["explanation"] = fallback_explanation return fallback_response except Exception as e: logger.error(f"Fallback service failed: {e}") return None def _get_chat_context(self) -> Optional[Dict]: """Get chat context for fallback requests.""" # ✅ UPDATED: Chat context now managed entirely by backend session store # The backend will retrieve session history based on session_id # Frontend no longer maintains global conversation history return None def _create_emergency_response(self, query: str) -> str: """Create emergency response when all systems fail.""" return ( "**🚨 SYSTEM TEMPORARILY UNAVAILABLE**\n\n" f"I apologize, but I'm unable to process your request for '{query}' " "at the moment. Our recommendation systems are experiencing issues.\n\n" "**Please try:**\n" "- Waiting a few moments and trying again\n" "- Simplifying your query (e.g., 'music like [artist name]')\n" "- Checking your internet connection\n\n" "We're working to restore full functionality. Thank you for your patience! 🎵" ) async def _get_planning_strategy(self, query: str) -> Optional[Dict]: """Get planning strategy from backend.""" try: response = requests.post( f"{self.backend_url}/planning", json={ "query": query, "session_id": "planning-session" # Planning doesn't need user session }, timeout=60 ) if response.status_code == 200: return response.json() else: logger.error( f"Planning request failed: {response.status_code}" ) return None except Exception as e: logger.error(f"Error getting planning strategy: {e}") return None async def _get_recommendations(self, query: str, session_id: str) -> Optional[Dict]: """Get recommendations from backend with user-specific session context.""" try: # Prepare request with user-specific session ID request_data = { "query": query, "session_id": session_id, "max_recommendations": 10, "include_previews": True } # ✅ UPDATED: Chat context now retrieved by backend from session store # No need to pass chat_context explicitly - backend handles it response = requests.post( f"{self.backend_url}/recommendations", json=request_data, timeout=120 ) if response.status_code == 200: response_data = response.json() return response_data else: logger.error( f"Recommendations request failed: {response.status_code} for session {session_id}" ) return None except Exception as e: logger.error(f"Error getting recommendations for session {session_id}: {e}") return None def _create_lastfm_player_html(self, recommendations: List[Dict]) -> str: """Create HTML for track preview links and info.""" if not recommendations: return """
No tracks yet!
Ask for music recommendations to see track info here
Click links to listen!