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