"""Response formatting utilities""" from typing import Any, Dict, List, Optional, Tuple import plotly.graph_objects as go import plotly.express as px import traceback def safe_get(data: Dict, key: str, default: Any = None) -> Any: """Safely get value from dictionary""" try: return data.get(key, default) except: return default def format_card_display(card_data: Dict) -> str: """Format card recommendation for display""" card_name = card_data.get("card_name", "Unknown Card") reward_rate = card_data.get("reward_rate", 0) reward_amount = card_data.get("reward_amount", 0) category = card_data.get("category", "General") reasoning = card_data.get("reasoning", "") return f""" ### 💳 {card_name} **Reward Rate:** {reward_rate}x points **Reward Amount:** ${reward_amount:.2f} **Category:** {category} **Why:** {reasoning} """ def format_full_recommendation(response: Dict) -> str: """Format complete recommendation response""" if response.get("error"): return f"❌ **Error:** {response.get('message', 'Unknown error')}" # Header output = f""" # 🎯 Recommendation for {response.get('merchant', 'Unknown')} **Amount:** ${response.get('amount_usd', 0):.2f} **Date:** {response.get('transaction_date', 'N/A')} **User:** {response.get('user_id', 'N/A')} --- ## 🏆 Best Card to Use """ # Recommended card recommended = response.get("recommended_card", {}) output += format_card_display(recommended) # RAG Insights rag_insights = response.get("rag_insights") if rag_insights: output += f""" --- ## 📚 Card Benefits {rag_insights.get('benefits', 'No additional information available.')} """ if rag_insights.get('tips'): output += f""" 💡 **Pro Tip:** {rag_insights.get('tips')} """ # Forecast Warning forecast = response.get("forecast_warning") if forecast: risk_level = forecast.get("risk_level", "low") message = forecast.get("message", "") if risk_level == "high": emoji = "🚨" elif risk_level == "medium": emoji = "⚠️" else: emoji = "✅" output += f""" --- ## {emoji} Spending Status {message} **Current Spend:** ${forecast.get('current_spend', 0):.2f} **Spending Cap:** ${forecast.get('cap', 0):.2f} **Projected Spend:** ${forecast.get('projected_spend', 0):.2f} """ # Alternative cards alternatives = response.get("alternative_cards", []) if alternatives: output += "\n---\n\n## 🔄 Alternative Cards\n\n" for i, alt in enumerate(alternatives[:2], 1): output += f"### Option {i}\n" output += format_card_display(alt) # Metadata services = response.get("services_used", []) time_ms = response.get("orchestration_time_ms", 0) output += f""" --- **Services Used:** {', '.join(services)} **Response Time:** {time_ms:.0f}ms """ return output def format_comparison_table(cards: list) -> str: """Format card comparison as markdown table""" if not cards: return "No cards to compare." table = """ | Card | Reward Rate | Reward Amount | Category | |------|-------------|---------------|----------| """ for card in cards: name = card.get("card_name", "Unknown") rate = card.get("reward_rate", 0) amount = card.get("reward_amount", 0) category = card.get("category", "N/A") table += f"| {name} | {rate}x | ${amount:.2f} | {category} |\n" return table # utils/formatters.py def format_analytics_metrics(data: Dict) -> Tuple[str, str, str, str]: """Format analytics data into HTML metrics, table, insights, and forecast""" user_id = data.get("user_id", "Unknown User") total_spending = data.get("total_spending", 0) total_rewards = data.get("total_rewards", 0) optimization_score = data.get("optimization_score", 0) potential_savings = data.get("potential_savings", 0) optimized_count = data.get("optimized_count", 0) # Calculate rewards rate rewards_rate = (total_rewards / total_spending * 100) if total_spending > 0 else 0 # Metrics HTML with user identifier metrics_html = f"""

${potential_savings:,.2f}

💰 Potential Annual Savings

for {user_id}

{rewards_rate:.1f}%

📈 Rewards Rate

Effective return

{optimized_count}

✅ Optimized Transactions

This month

{optimization_score}/100

⭐ Optimization Score

{"Excellent!" if optimization_score >= 85 else "Good!" if optimization_score >= 70 else "Needs work"}
""" # Spending table spending_by_category = data.get("spending_by_category", {}) if spending_by_category: table_rows = "\n".join([ f"| {category} | ${amount:,.2f} | {(amount/total_spending*100):.1f}% |" for category, amount in spending_by_category.items() ]) spending_table = f""" ## 💳 Spending Breakdown for **{user_id}** | Category | Amount | % of Total | |----------|--------|------------| {table_rows} | **TOTAL** | **${total_spending:,.2f}** | **100%** | """ else: spending_table = f"## 💳 Spending Breakdown for **{user_id}**\n\n*No spending data available*" # Insights if spending_by_category: top_category = max(spending_by_category.items(), key=lambda x: x[0])[0] top_amount = spending_by_category[top_category] else: top_category = "Unknown" top_amount = 0 insights = f""" ## 💡 Personalized Insights for **{user_id}** - 🎯 Your top spending category is **{top_category}** (${top_amount:,.2f}) - 📊 Optimization score: **{optimization_score}/100** - {"🎉 Excellent!" if optimization_score >= 85 else "👍 Good!" if optimization_score >= 70 else "⚠️ Room for improvement"} - 💰 You could save **${potential_savings:,.2f}** annually with better card selection - 🏆 Current rewards rate: **{rewards_rate:.2f}%** effective return - ✅ Optimized **{optimized_count}** transactions this period **💡 Quick Tips:** - Consider cards with higher rewards in {top_category} - Watch for spending caps on premium cards - Review quarterly bonus categories """ # Forecast projected_spending = total_spending * 1.05 projected_rewards = total_rewards * 1.05 forecast = f""" ## 🔮 Forecast for **{user_id}** Based on your spending patterns: - 📈 Projected next month spending: **${projected_spending:,.2f}** (↑ 5%) - 💎 Projected rewards: **${projected_rewards:,.2f}** - 🎯 Potential with optimization: **${projected_rewards * 1.15:,.2f}** **⚠️ Heads Up:** - Watch out for category spending caps on premium cards - Q4 bonus categories may be ending soon - Consider timing large purchases for maximum rewards """ return metrics_html, spending_table, insights, forecast def create_spending_chart(data: Dict) -> go.Figure: """Create spending vs rewards chart""" try: # Extract data with fallbacks spending_by_category = data.get('spending_by_category', {}) if not spending_by_category: # Create sample data if none exists spending_by_category = { 'Groceries': 1200.00, 'Restaurants': 850.00, 'Gas Stations': 420.00, 'Online Shopping': 800.00 } categories = list(spending_by_category.keys()) spending = list(spending_by_category.values()) # Calculate rewards (assume 2% average) rewards = [s * 0.02 for s in spending] fig = go.Figure() fig.add_trace(go.Bar( name='Spending', x=categories, y=spending, marker_color='#667eea', text=[f'${s:,.0f}' for s in spending], textposition='outside' )) fig.add_trace(go.Bar( name='Rewards', x=categories, y=rewards, marker_color='#38ef7d', text=[f'${r:.2f}' for r in rewards], textposition='outside' )) fig.update_layout( title='Spending vs Rewards by Category', xaxis_title='Category', yaxis_title='Amount ($)', barmode='group', template='plotly_white', height=400, showlegend=True, legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) ) return fig except Exception as e: print(f"Error creating spending chart: {e}") fig = go.Figure() fig.add_annotation( text="Chart data unavailable", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False, font=dict(size=14, color="#666") ) fig.update_layout(height=400, template='plotly_white') return fig def create_rewards_pie_chart(data: Dict) -> go.Figure: """Create rewards distribution pie chart""" try: rewards_by_card = data.get('rewards_by_card', {}) if not rewards_by_card: # Create sample data rewards_by_card = { 'Amex Gold': 95.50, 'Chase Sapphire Reserve': 62.00, 'Citi Double Cash': 30.00 } cards = list(rewards_by_card.keys()) rewards = list(rewards_by_card.values()) fig = go.Figure(data=[go.Pie( labels=cards, values=rewards, hole=0.4, marker=dict(colors=['#667eea', '#38ef7d', '#f093fb', '#4facfe']), textinfo='label+percent', textposition='outside', hovertemplate='%{label}
$%{value:.2f}
%{percent}' )]) fig.update_layout( title='Rewards Distribution by Card', template='plotly_white', height=400, showlegend=True, legend=dict(orientation="v", yanchor="middle", y=0.5, xanchor="left", x=1.1) ) return fig except Exception as e: print(f"Error creating pie chart: {e}") fig = go.Figure() fig.add_annotation( text="Chart data unavailable", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False, font=dict(size=14, color="#666") ) fig.update_layout(height=400, template='plotly_white') return fig def create_optimization_gauge(data: Dict) -> go.Figure: """Create optimization score gauge chart""" try: # Handle both dict and int input if isinstance(data, dict): score = data.get('optimization_score', 0) else: score = int(data) if data else 0 fig = go.Figure(go.Indicator( mode="gauge+number+delta", value=score, domain={'x': [0, 1], 'y': [0, 1]}, title={'text': "Optimization Score", 'font': {'size': 20}}, delta={'reference': 80, 'increasing': {'color': "green"}}, gauge={ 'axis': {'range': [None, 100], 'tickwidth': 1, 'tickcolor': "darkblue"}, 'bar': {'color': "#667eea"}, 'bgcolor': "white", 'borderwidth': 2, 'bordercolor': "gray", 'steps': [ {'range': [0, 60], 'color': '#ffcccc'}, {'range': [60, 80], 'color': '#fff4cc'}, {'range': [80, 100], 'color': '#ccffcc'} ], 'threshold': { 'line': {'color': "red", 'width': 4}, 'thickness': 0.75, 'value': 90 } } )) fig.update_layout( template='plotly_white', height=400, margin=dict(l=20, r=20, t=50, b=20) ) return fig except Exception as e: print(f"Error creating gauge: {e}") fig = go.Figure() fig.add_annotation( text="Chart data unavailable", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False, font=dict(size=14, color="#666") ) fig.update_layout(height=400, template='plotly_white') return fig def create_trend_line_chart(data: Dict) -> go.Figure: """Create monthly trend line chart""" try: monthly_trends = data.get('monthly_trends', []) if not monthly_trends: # Create sample data monthly_trends = [ {"month": "Aug", "spending": 1200, "rewards": 52}, {"month": "Sep", "spending": 1450, "rewards": 63}, {"month": "Oct", "spending": 1600, "rewards": 72} ] months = [t['month'] for t in monthly_trends] spending = [t['spending'] for t in monthly_trends] rewards = [t['rewards'] for t in monthly_trends] fig = go.Figure() fig.add_trace(go.Scatter( x=months, y=spending, mode='lines+markers', name='Spending', line=dict(color='#667eea', width=3), marker=dict(size=10), yaxis='y' )) fig.add_trace(go.Scatter( x=months, y=rewards, mode='lines+markers', name='Rewards', line=dict(color='#38ef7d', width=3), marker=dict(size=10), yaxis='y2' )) fig.update_layout( title='Monthly Spending & Rewards Trends', xaxis=dict(title='Month'), yaxis=dict( title='Spending ($)', titlefont=dict(color='#667eea'), tickfont=dict(color='#667eea') ), yaxis2=dict( title='Rewards ($)', titlefont=dict(color='#38ef7d'), tickfont=dict(color='#38ef7d'), overlaying='y', side='right' ), template='plotly_white', height=400, hovermode='x unified', showlegend=True, legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) ) return fig except Exception as e: print(f"Error creating trend chart: {e}") fig = go.Figure() fig.add_annotation( text="Chart data unavailable", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False, font=dict(size=14, color="#666") ) fig.update_layout(height=400, template='plotly_white') return fig def create_card_performance_chart(data: Dict) -> go.Figure: """Create card performance comparison chart""" try: # Try multiple possible data keys rewards_by_card = ( data.get('rewards_by_card') or data.get('card_performance') or data.get('top_cards') or {} ) print(f"🔍 DEBUG - rewards_by_card type: {type(rewards_by_card)}") print(f"🔍 DEBUG - rewards_by_card content: {rewards_by_card}") # Handle different data formats cards = [] rewards = [] if isinstance(rewards_by_card, list): # If it's a list of dicts like [{"card": "Amex", "rewards": 95.50}, ...] for item in rewards_by_card: card_name = str(item.get('card', item.get('card_name', 'Unknown'))) reward_value = item.get('rewards', item.get('reward_amount', 0)) cards.append(card_name) # Convert to float, handle strings try: rewards.append(float(reward_value)) except (ValueError, TypeError): print(f"⚠️ Could not convert reward value: {reward_value}") rewards.append(0.0) elif isinstance(rewards_by_card, dict): # If it's a dict like {"Amex Gold": 85.0, "Chase": 62.00} for card_name, reward_value in rewards_by_card.items(): cards.append(str(card_name)) # Convert to float, handle strings and any type try: reward_float = float(reward_value) rewards.append(reward_float) print(f"✅ Converted {card_name}: {reward_value} ({type(reward_value)}) -> {reward_float}") except (ValueError, TypeError) as e: print(f"⚠️ Could not convert reward value for {card_name}: {reward_value} - {e}") rewards.append(0.0) # Debug: Check what we have BEFORE sorting print(f"📊 BEFORE SORT - Cards: {cards}") print(f"📊 BEFORE SORT - Rewards: {rewards}") print(f"📊 BEFORE SORT - Rewards types: {[type(r) for r in rewards]}") # If still no data, create sample data if not cards or sum(rewards) == 0: print("⚠️ No card performance data, using sample data") cards = ['Amex Gold', 'Chase Sapphire Reserve', 'Citi Double Cash'] rewards = [95.50, 62.00, 30.00] # Sort by rewards (highest first) - FIXED: ensure both are in the tuple sorted_pairs = sorted( list(zip(cards, rewards)), key=lambda pair: pair[1], # Sort by reward (second element) reverse=True ) # Unpack sorted pairs cards = [pair[0] for pair in sorted_pairs] rewards = [pair[1] for pair in sorted_pairs] print(f"📊 AFTER SORT - Cards: {cards}") print(f"📊 AFTER SORT - Rewards: {rewards}") # Take top 5 only cards = cards[:5] rewards = rewards[:5] # Reverse for bottom-to-top display cards = cards[::-1] rewards = rewards[::-1] print(f"📊 FINAL - Cards: {cards}") print(f"📊 FINAL - Rewards: {rewards}") # Create color gradient (darker = better performance) if not rewards: rewards = [0.0] max_reward = max(rewards) # Already floats, no need to convert print(f"📊 Max reward: {max_reward} (type: {type(max_reward)})") # Calculate colors with explicit float division colors = [] for r in rewards: opacity = 0.4 + 0.6 * (r / max_reward) if max_reward > 0 else 0.5 color = f'rgba(102, 126, 234, {opacity:.2f})' colors.append(color) print(f" Color for ${r:.2f}: {color}") fig = go.Figure(data=[go.Bar( y=cards, # Horizontal bar chart x=rewards, orientation='h', marker=dict( color=colors, line=dict(color='#667eea', width=1) ), text=[f'${r:.2f}' for r in rewards], textposition='outside', textfont=dict(size=12, color='#333'), hovertemplate='%{y}
Total Rewards: $%{x:.2f}' )]) fig.update_layout( title={ 'text': '🏆 Top Performing Cards', 'x': 0.5, 'xanchor': 'center', 'font': {'size': 16, 'color': '#333'} }, xaxis=dict( title='Total Rewards Earned ($)', showgrid=True, gridcolor='#f0f0f0', zeroline=False ), yaxis=dict( title='', showgrid=False, tickfont=dict(size=11) ), template='plotly_white', height=400, showlegend=False, margin=dict(l=180, r=80, t=60, b=50), plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='white' ) print("✅ Chart created successfully!") return fig except Exception as e: print(f"❌ Error creating card performance chart: {e}") print(f"📋 Traceback: {traceback.format_exc()}") # Return empty chart with error message fig = go.Figure() fig.add_annotation( text="Chart data unavailable", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False, font=dict(size=14, color="#666") ) fig.update_layout( height=400, template='plotly_white', title='Top Performing Cards' ) return fig