Spaces:
Build error
Build error
feat: implement LLM fallback system for unknown intents - Add LLMFallbackService with Gemini Flash 2.0 integration - Implement fallback trigger detection in chat interface - Add response formatter support for fallback disclaimers - Include comprehensive test coverage for fallback scenarios
cde66c2 | """ | |
| Response Formatter for BeatDebate Music Recommendations | |
| Formats recommendation responses into beautiful Markdown for Gradio display. | |
| """ | |
| from typing import Dict, Any | |
| import structlog | |
| logger = structlog.get_logger(__name__) | |
| class ResponseFormatter: | |
| """ | |
| Formats music recommendation responses into beautiful Markdown. | |
| Converts recommendation data into Gradio-compatible Markdown format | |
| with proper styling and interactive elements. Enhanced with fallback support. | |
| """ | |
| def __init__(self): | |
| """Initialize the response formatter.""" | |
| self.logger = logger | |
| def format_recommendations(self, response_data: Dict[str, Any]) -> str: | |
| """ | |
| Format recommendations response into Markdown. | |
| Enhanced to handle fallback responses with appropriate disclaimers. | |
| Args: | |
| response_data: Response from recommendation engine or fallback service | |
| Returns: | |
| Formatted Markdown string | |
| """ | |
| try: | |
| recommendations = response_data.get("recommendations", []) | |
| processing_time = response_data.get("processing_time", 0) | |
| is_fallback = response_data.get("fallback_used", False) | |
| fallback_reason = response_data.get("fallback_reason", "unknown") | |
| if not recommendations: | |
| return ( | |
| "β **No recommendations found.** " | |
| "Please try a different query." | |
| ) | |
| markdown_parts = [] | |
| # Add fallback disclaimer if applicable | |
| if is_fallback: | |
| disclaimer = self._create_fallback_disclaimer(fallback_reason) | |
| markdown_parts.extend([disclaimer, ""]) | |
| # Header (adjusted for fallback) | |
| if is_fallback: | |
| markdown_parts.extend([ | |
| f"# π Found {len(recommendations)} Tracks via LLM Fallback", | |
| f"β‘ *Generated in {processing_time:.1f}s using general AI assistance*", | |
| "", | |
| ]) | |
| else: | |
| markdown_parts.extend([ | |
| f"# π΅ Found {len(recommendations)} Perfect Tracks for You!", | |
| f"β‘ *Generated in {processing_time:.1f}s by our AI agents*", | |
| "", | |
| ]) | |
| # Format each recommendation | |
| for i, rec in enumerate(recommendations, 1): | |
| rec_markdown = self._format_single_recommendation(rec, i, is_fallback) | |
| markdown_parts.append(rec_markdown) | |
| markdown_parts.append("---") # Separator | |
| # Agent summary (different for fallback) | |
| if is_fallback: | |
| agent_summary = self._format_fallback_summary(response_data) | |
| else: | |
| agent_summary = self._format_agent_summary(response_data) | |
| markdown_parts.append(agent_summary) | |
| # Reasoning details | |
| reasoning_details = self._format_reasoning_details(response_data) | |
| markdown_parts.append(reasoning_details) | |
| return "\n".join(markdown_parts) | |
| except Exception as e: | |
| self.logger.error("Failed to format recommendations", error=str(e)) | |
| return f"β **Error formatting recommendations:** {str(e)}" | |
| def _create_fallback_disclaimer(self, reason: str) -> str: | |
| """ | |
| Create styled fallback disclaimer. | |
| Args: | |
| reason: Reason why fallback was triggered | |
| Returns: | |
| Formatted disclaimer text | |
| """ | |
| reason_descriptions = { | |
| "unknown_intent": "query intent not recognized by our specialized system", | |
| "no_recommendations": "specialized agents couldn't generate recommendations", | |
| "api_error": "temporary system issue", | |
| "timeout": "system response timeout", | |
| "emergency_fallback": "multiple system failures" | |
| } | |
| description = reason_descriptions.get(reason, "system limitation") | |
| return ( | |
| "π **FALLBACK MODE ACTIVE**\n" | |
| f"*Using general AI assistance due to {description}. " | |
| "For best results, try queries like 'music like [artist]' or '[genre] music'.*\n" | |
| "---" | |
| ) | |
| def _format_single_recommendation( | |
| self, rec: Dict[str, Any], rank: int, is_fallback: bool = False | |
| ) -> str: | |
| """Format a single recommendation as Markdown.""" | |
| title = rec.get("title", "Unknown Title") | |
| artist = rec.get("artist", "Unknown Artist") | |
| confidence = rec.get("confidence", 0.0) | |
| source = rec.get("source", "unknown") | |
| # Convert confidence to percentage | |
| confidence_pct = int(confidence * 100) | |
| # Confidence badge color (adjusted for fallback) | |
| if is_fallback: | |
| # More conservative confidence indicators for fallback | |
| if confidence_pct >= 80: | |
| confidence_badge = f"π‘ **{confidence_pct}% match (AI)**" | |
| elif confidence_pct >= 60: | |
| confidence_badge = f"π **{confidence_pct}% match (AI)**" | |
| else: | |
| confidence_badge = f"π΄ **{confidence_pct}% match (AI)**" | |
| else: | |
| # Original confidence indicators for main system | |
| if confidence_pct >= 90: | |
| confidence_badge = f"π’ **{confidence_pct}% match**" | |
| elif confidence_pct >= 70: | |
| confidence_badge = f"π‘ **{confidence_pct}% match**" | |
| else: | |
| confidence_badge = f"π΄ **{confidence_pct}% match**" | |
| # Source indicator | |
| source_indicator = " β’ *via LLM fallback*" if is_fallback else f" β’ *via {source}*" | |
| markdown = [ | |
| f"## {rank}. \"{title}\" by {artist}", | |
| f"{confidence_badge}{source_indicator}", | |
| "" | |
| ] | |
| # Add Last.fm link for better preview integration | |
| lastfm_url = f"https://www.last.fm/music/{artist.replace(' ', '+')}/_/{title.replace(' ', '+')}" | |
| markdown.extend([ | |
| f"π§ **[Listen on Last.fm]({lastfm_url})**", | |
| "" | |
| ]) | |
| # Track ID for reference (useful for research/debugging) | |
| track_id = f"{artist}_{title}".replace(" ", "_").replace("(", "").replace(")", "") | |
| markdown.extend([ | |
| f"π **Track ID:** `{track_id}`", | |
| "" | |
| ]) | |
| # Add reasoning if available | |
| reasoning = self._extract_reasoning(rec, is_fallback) | |
| if reasoning: | |
| markdown.extend([ | |
| "### π€ Why this track:", | |
| reasoning, | |
| "" | |
| ]) | |
| # Add genres and moods with better formatting | |
| genres = rec.get("genres", []) | |
| moods = rec.get("moods", []) | |
| tags = rec.get("tags", []) | |
| if genres or moods or tags: | |
| tag_elements = [] | |
| if genres: | |
| tag_elements.extend([f"π {g}" for g in genres[:3]]) | |
| if moods: | |
| tag_elements.extend([f"π {m}" for m in moods[:3]]) | |
| if tags: | |
| tag_elements.extend([f"π {t}" for t in tags[:3]]) | |
| markdown.extend([ | |
| f"**Tags:** {' β’ '.join(tag_elements)}", | |
| "" | |
| ]) | |
| return "\n".join(markdown) | |
| def _extract_reasoning(self, rec: Dict[str, Any], is_fallback: bool = False) -> str: | |
| """Extract and format reasoning for a recommendation.""" | |
| # Try to get reasoning from different possible fields | |
| reasoning_sources = [ | |
| rec.get("reasoning"), | |
| rec.get("explanation"), | |
| rec.get("why_recommended") | |
| ] | |
| for reasoning in reasoning_sources: | |
| if reasoning: | |
| # Add fallback context if applicable | |
| if is_fallback and "AI-generated" not in reasoning: | |
| return f"π€ AI Analysis: {reasoning}" | |
| return reasoning | |
| # Generate basic reasoning from scores | |
| confidence = rec.get("confidence", 0.0) or 0.0 | |
| novelty_score = rec.get("novelty_score", 0.0) or 0.0 | |
| quality_score = rec.get("quality_score", 0.0) or 0.0 | |
| reasoning_parts = [] | |
| if confidence > 0.8: | |
| reasoning_parts.append("β High relevance to your request") | |
| elif confidence > 0.6: | |
| reasoning_parts.append("β Good match for your preferences") | |
| if novelty_score > 0.7: | |
| reasoning_parts.append("π Unique discovery") | |
| elif novelty_score > 0.4: | |
| reasoning_parts.append("π― Balanced familiarity") | |
| if quality_score > 0.7: | |
| reasoning_parts.append("π High quality track") | |
| default_reasoning = ( | |
| " β’ ".join(reasoning_parts) | |
| if reasoning_parts | |
| else "Recommended by our AI system" | |
| ) | |
| # Add fallback context for default reasoning | |
| if is_fallback: | |
| return f"π€ AI Analysis: {default_reasoning}" | |
| return default_reasoning | |
| def _format_agent_summary(self, response_data: Dict[str, Any]) -> str: | |
| """Format agent coordination summary.""" | |
| markdown = [ | |
| "## π€ Agent Coordination Summary", | |
| "", | |
| "β **PlannerAgent:** Strategic planning completed", | |
| "β **GenreMoodAgent:** Genre/mood recommendations generated", | |
| "β **DiscoveryAgent:** Discovery recommendations generated", | |
| "β **JudgeAgent:** Final selection and ranking completed", | |
| "" | |
| ] | |
| return "\n".join(markdown) | |
| def _format_fallback_summary(self, response_data: Dict[str, Any]) -> str: | |
| """Format fallback system summary.""" | |
| fallback_reason = response_data.get("fallback_reason", "unknown") | |
| markdown = [ | |
| "## π AI Fallback System Summary", | |
| "", | |
| f"π€ **Gemini Flash 2.0:** Generated recommendations via LLM fallback", | |
| f"β οΈ **Trigger Reason:** {fallback_reason.replace('_', ' ').title()}", | |
| "π‘ **Note:** For specialized recommendations, try more specific queries", | |
| "" | |
| ] | |
| return "\n".join(markdown) | |
| def _format_reasoning_details(self, response_data: Dict[str, Any]) -> str: | |
| """Format detailed reasoning log.""" | |
| reasoning_log = response_data.get("reasoning", []) | |
| if not reasoning_log: | |
| return "" | |
| # Handle both list and single string reasoning | |
| if isinstance(reasoning_log, str): | |
| reasoning_log = [reasoning_log] | |
| markdown = [ | |
| "<details>", | |
| ( | |
| "<summary><strong>π View Detailed Reasoning" | |
| "</strong></summary>" | |
| ), | |
| "", | |
| ] | |
| for entry in reasoning_log: | |
| markdown.append(f"β’ `{entry}`") | |
| markdown.extend([ | |
| "", | |
| "</details>" | |
| ]) | |
| return "\n".join(markdown) | |
| def format_planning_preview(self, strategy: Dict[str, Any]) -> str: | |
| """ | |
| Format a preview of the planning strategy. | |
| Args: | |
| strategy: Planning strategy from PlannerAgent | |
| Returns: | |
| Formatted HTML preview | |
| """ | |
| try: | |
| task_analysis = strategy.get("task_analysis", {}) | |
| primary_goal = task_analysis.get("primary_goal", "Music discovery") | |
| return f""" | |
| <div style=" | |
| background: #e8f5e8; | |
| border: 1px solid #c3e6c3; | |
| border-radius: 8px; | |
| padding: 10px; | |
| margin: 5px 0; | |
| "> | |
| <strong>π§ Planning:</strong> {primary_goal} | |
| </div> | |
| """ | |
| except Exception as e: | |
| self.logger.error(f"Error formatting planning preview: {e}") | |
| return "" |