"""Response formatting utilities"""
from typing import Any, Dict, List, Optional, Tuple
import plotly.graph_objects as go
import plotly.express as px
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(analytics_data: Dict) -> tuple:
"""Format analytics data into HTML metrics, tables, and insights"""
try:
# Extract key metrics
total_spending = float(safe_get(analytics_data, 'total_spending', 0))
total_rewards = float(safe_get(analytics_data, 'total_rewards', 0))
potential_savings = float(safe_get(analytics_data, 'potential_savings', 0))
optimization_score = int(safe_get(analytics_data, 'optimization_score', 0))
optimized_count = int(safe_get(analytics_data, 'optimized_count', 0))
total_transactions = int(safe_get(analytics_data, 'total_transactions', 0))
# Calculate optimization percentage
opt_percentage = int((optimized_count / total_transactions * 100)) if total_transactions > 0 else 0
# Calculate rewards rate
rewards_rate = (total_rewards / total_spending * 100) if total_spending > 0 else 0
# Create metrics HTML
metrics_html = f"""
${potential_savings:.0f}
💰 Potential Annual Savings
{rewards_rate:.1f}%
📈 Rewards Rate
{optimized_count}
✅ Optimized Transactions
{optimization_score}/100
⭐ Optimization Score
"""
# Create spending table
category_breakdown = safe_get(analytics_data, 'category_breakdown', [])
table_rows = []
for item in category_breakdown:
category = item.get('category', 'Unknown')
spending = float(item.get('spending', 0))
rewards = float(item.get('rewards', 0))
transactions = int(item.get('transactions', 0))
rate = (rewards / spending * 100) if spending > 0 else 0
# Category emoji mapping
emoji_map = {
'Groceries': '🛒',
'Dining': '🍽️',
'Travel': '✈️',
'Gas': '⛽',
'Online Shopping': '🛍️',
'Entertainment': '🎬'
}
emoji = emoji_map.get(category, '📦')
table_rows.append(
f"| {emoji} {category} | ${spending:.2f} | ${rewards:.2f} | {rate:.1f}% | {transactions} |"
)
total_spending_sum = sum(float(item.get('spending', 0)) for item in category_breakdown)
total_rewards_sum = sum(float(item.get('rewards', 0)) for item in category_breakdown)
total_rate = (total_rewards_sum / total_spending_sum * 100) if total_spending_sum > 0 else 0
total_txn = sum(int(item.get('transactions', 0)) for item in category_breakdown)
table_md = f"""
| Category | Monthly Spend | Rewards | Rate | Transactions |
|----------|---------------|---------|------|--------------|
{chr(10).join(table_rows)}
| **Total** | **${total_spending_sum:.2f}** | **${total_rewards_sum:.2f}** | **{total_rate:.1f}%** | **{total_txn}** |
"""
# Create insights
top_category = safe_get(analytics_data, 'top_category', 'Unknown')
cards = safe_get(analytics_data, 'cards', [])
insights_md = f"""
**🔥 Top Spending Category:**
- {top_category}: ${category_breakdown[0].get('spending', 0):.2f} ({opt_percentage}% optimized)
**💡 Optimization Opportunities:**
- ✅ You're using optimal cards {opt_percentage}% of the time
- 🎯 Potential to earn ${potential_savings:.2f} more per year
- ⚠️ {total_transactions - optimized_count} transactions could be optimized
**🏆 Your Active Cards:**
{chr(10).join([f'- {card}' for card in cards[:5]])}
**📊 Year-to-Date:**
- Total Rewards: ${total_rewards:.2f}
- Average per transaction: ${total_rewards/total_transactions:.2f}
- **Optimization Score: {optimization_score}/100**
"""
# Create forecast
forecast_md = f"""
### 🔮 Next Month Forecast
Based on your spending patterns:
- **Predicted Spend:** ${total_spending * 1.05:.2f}
- **Predicted Rewards:** ${total_rewards * 1.05:.2f}
- **Optimization Target:** {min(optimization_score + 5, 100)}/100
**Recommendations:**
1. 💳 Continue using your top performing cards
2. 🎯 Focus on optimizing {total_transactions - optimized_count} remaining transactions
3. 📈 Potential monthly savings: ${potential_savings/12:.2f}
"""
return metrics_html, table_md, insights_md, forecast_md
except Exception as e:
print(f"Error formatting analytics: {e}")
import traceback
print(traceback.format_exc())
error_msg = "Error loading analytics data"
return error_msg, error_msg, error_msg, error_msg
def create_spending_chart(analytics_data: Dict) -> go.Figure:
"""Create spending vs rewards bar chart"""
try:
# DON'T unwrap - data is already unwrapped in app.py
# Just use it directly
category_breakdown = safe_get(analytics_data, 'category_breakdown', [])
# DEBUG
print(f"DEBUG create_spending_chart: Found {len(category_breakdown)} categories")
if category_breakdown:
print(f"First category: {category_breakdown[0]}")
if not category_breakdown:
fig = go.Figure()
fig.add_annotation(
text="No spending data available",
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
categories = [item.get('category', 'Unknown') for item in category_breakdown]
spending = [float(item.get('spending', 0)) for item in category_breakdown]
rewards = [float(item.get('rewards', 0)) for item in category_breakdown]
fig = go.Figure()
# Add spending bars
fig.add_trace(go.Bar(
name='Spending',
x=categories,
y=spending,
marker_color='#667eea',
text=[f'${s:.0f}' for s in spending],
textposition='outside',
hovertemplate='%{x}
Spending: $%{y:.2f}'
))
# Add rewards bars
fig.add_trace(go.Bar(
name='Rewards',
x=categories,
y=rewards,
marker_color='#38ef7d',
text=[f'${r:.2f}' for r in rewards],
textposition='outside',
hovertemplate='%{x}
Rewards: $%{y:.2f}'
))
fig.update_layout(
title={
'text': 'Spending vs Rewards by Category',
'x': 0.5,
'xanchor': 'center'
},
xaxis_title='Category',
yaxis_title='Amount ($)',
barmode='group',
template='plotly_white',
height=400,
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
),
margin=dict(t=80, b=50, l=50, r=50)
)
return fig
except Exception as e:
print(f"Error creating spending chart: {e}")
import traceback
print(traceback.format_exc())
fig = go.Figure()
fig.add_annotation(
text="Error loading spending chart",
xref="paper", yref="paper",
x=0.5, y=0.5, showarrow=False,
font=dict(size=14, color="red")
)
fig.update_layout(height=400, template='plotly_white')
return fig
def create_rewards_pie_chart(analytics_data: Dict) -> go.Figure:
"""Create rewards distribution pie chart"""
try:
category_breakdown = safe_get(analytics_data, 'category_breakdown', [])
if not category_breakdown:
fig = go.Figure()
fig.add_annotation(
text="No rewards data available",
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
categories = [item.get('category', 'Unknown') for item in category_breakdown]
rewards = [float(item.get('rewards', 0)) for item in category_breakdown]
colors = ['#667eea', '#38ef7d', '#f093fb', '#4facfe', '#feca57', '#ff6b6b']
fig = go.Figure(data=[go.Pie(
labels=categories,
values=rewards,
hole=0.4,
marker=dict(colors=colors[:len(categories)]),
textinfo='label+percent',
textposition='outside',
hovertemplate='%{label}
Rewards: $%{value:.2f}
%{percent}'
)])
fig.update_layout(
title={
'text': 'Rewards Distribution',
'x': 0.5,
'xanchor': 'center'
},
template='plotly_white',
height=400,
showlegend=True,
legend=dict(
orientation="v",
yanchor="middle",
y=0.5,
xanchor="left",
x=1.1
),
margin=dict(t=60, b=30, l=30, r=150)
)
return fig
except Exception as e:
print(f"Error creating pie chart: {e}")
import traceback
print(traceback.format_exc())
fig = go.Figure()
fig.add_annotation(
text="Error loading rewards chart",
xref="paper", yref="paper",
x=0.5, y=0.5, showarrow=False,
font=dict(size=14, color="red")
)
fig.update_layout(height=400, template='plotly_white')
return fig
def create_optimization_gauge(analytics_data: Dict) -> go.Figure:
"""Create optimization score gauge"""
try:
# Handle both dict with 'optimization_score' key and direct score value
if isinstance(analytics_data, dict):
score = int(safe_get(analytics_data, 'optimization_score', 75))
else:
score = int(analytics_data) if analytics_data else 75
# Determine color based on score
if score >= 80:
color = "#38ef7d" # Green
elif score >= 60:
color = "#feca57" # Yellow
else:
color = "#ff6b6b" # Red
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': 75, 'increasing': {'color': "#38ef7d"}},
gauge={
'axis': {'range': [None, 100], 'tickwidth': 1, 'tickcolor': "darkblue"},
'bar': {'color': color},
'bgcolor': "white",
'borderwidth': 2,
'bordercolor': "gray",
'steps': [
{'range': [0, 60], 'color': '#ffebee'},
{'range': [60, 80], 'color': '#fff9e6'},
{'range': [80, 100], 'color': '#e8f5e9'}
],
'threshold': {
'line': {'color': "red", 'width': 4},
'thickness': 0.75,
'value': 90
}
}
))
fig.update_layout(
template='plotly_white',
height=400,
margin=dict(t=50, b=30, l=30, r=30)
)
return fig
except Exception as e:
print(f"Error creating gauge: {e}")
import traceback
print(traceback.format_exc())
fig = go.Figure()
fig.add_annotation(
text="Error loading optimization score",
xref="paper", yref="paper",
x=0.5, y=0.5, showarrow=False,
font=dict(size=14, color="red")
)
fig.update_layout(height=400, template='plotly_white')
return fig
def create_trend_line_chart(analytics_data: Dict) -> go.Figure:
"""Create monthly trend line chart"""
try:
monthly_trends = safe_get(analytics_data, 'monthly_trends', [])
if not monthly_trends:
fig = go.Figure()
fig.add_annotation(
text="No trend data available",
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
months = [item.get('month', '') for item in monthly_trends]
spending = [float(item.get('spending', 0)) for item in monthly_trends]
rewards = [float(item.get('rewards', 0)) for item in monthly_trends]
fig = go.Figure()
# Add spending line
fig.add_trace(go.Scatter(
x=months,
y=spending,
name='Spending',
mode='lines+markers',
line=dict(color='#667eea', width=3),
marker=dict(size=8),
hovertemplate='%{x}
Spending: $%{y:.0f}'
))
# Add rewards line
fig.add_trace(go.Scatter(
x=months,
y=rewards,
name='Rewards',
mode='lines+markers',
line=dict(color='#38ef7d', width=3),
marker=dict(size=8),
yaxis='y2',
hovertemplate='%{x}
Rewards: $%{y:.2f}'
))
fig.update_layout(
title={
'text': '12-Month Spending & Rewards Trends',
'x': 0.5,
'xanchor': 'center'
},
xaxis_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,
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
),
margin=dict(t=80, b=50, l=60, r=60),
hovermode='x unified'
)
return fig
except Exception as e:
print(f"Error creating trend chart: {e}")
import traceback
print(traceback.format_exc())
fig = go.Figure()
fig.add_annotation(
text="Error loading trend chart",
xref="paper", yref="paper",
x=0.5, y=0.5, showarrow=False,
font=dict(size=14, color="red")
)
fig.update_layout(height=400, template='plotly_white')
return fig
def create_card_performance_chart(analytics_data: Dict) -> go.Figure:
"""Create card performance horizontal bar chart"""
try:
card_performance = safe_get(analytics_data, 'card_performance', [])
if not card_performance:
fig = go.Figure()
fig.add_annotation(
text="No card performance data available",
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
cards = [item.get('card', 'Unknown') for item in card_performance]
rewards = [float(item.get('rewards', 0)) for item in card_performance]
usage_pct = [float(item.get('usage_pct', 0)) for item in card_performance]
colors = ['#667eea', '#38ef7d', '#f093fb']
fig = go.Figure()
fig.add_trace(go.Bar(
y=cards,
x=rewards,
orientation='h',
marker=dict(
color=colors[:len(cards)],
line=dict(color='white', width=2)
),
text=[f'${r:.2f} ({u}% usage)' for r, u in zip(rewards, usage_pct)],
textposition='outside',
hovertemplate='%{y}
Rewards: $%{x:.2f}'
))
fig.update_layout(
title={
'text': 'Card Performance',
'x': 0.5,
'xanchor': 'center'
},
xaxis_title='Rewards Earned ($)',
yaxis_title='',
template='plotly_white',
height=400,
showlegend=False,
margin=dict(t=60, b=50, l=150, r=100)
)
return fig
except Exception as e:
print(f"Error creating card performance chart: {e}")
import traceback
print(traceback.format_exc())
fig = go.Figure()
fig.add_annotation(
text="Error loading card performance",
xref="paper", yref="paper",
x=0.5, y=0.5, showarrow=False,
font=dict(size=14, color="red")
)
fig.update_layout(height=400, template='plotly_white')
return fig