|
|
from typing import Dict, Any, Optional, Tuple, List |
|
|
import traceback |
|
|
import json |
|
|
import os |
|
|
from datetime import date, datetime |
|
|
import calendar |
|
|
import gradio as gr |
|
|
import plotly.graph_objects as go |
|
|
import httpx |
|
|
|
|
|
from config import ( |
|
|
APP_TITLE, APP_DESCRIPTION, THEME, |
|
|
MCC_CATEGORIES, SAMPLE_USERS, |
|
|
MERCHANTS_BY_CATEGORY) |
|
|
from utils.api_client import RewardPilotClient |
|
|
from utils.formatters import ( |
|
|
format_full_recommendation, |
|
|
format_comparison_table, |
|
|
format_analytics_metrics, |
|
|
create_spending_chart, |
|
|
create_rewards_pie_chart, |
|
|
create_optimization_gauge, |
|
|
create_trend_line_chart, |
|
|
create_card_performance_chart) |
|
|
from utils.llm_explainer import get_llm_explainer |
|
|
import config |
|
|
|
|
|
CARDS_FILE = os.path.join(os.path.dirname(__file__), "data", "cards.json") |
|
|
|
|
|
def safe_get(data: Dict, key: str, default: Any = None) -> Any: |
|
|
"""Safely get value from dictionary with fallback""" |
|
|
try: |
|
|
return data.get(key, default) |
|
|
except: |
|
|
return default |
|
|
|
|
|
def normalize_recommendation_data(data: Dict) -> Dict: |
|
|
""" |
|
|
Normalize API response to ensure all required fields exist. |
|
|
Handles both orchestrator format and mock data format. |
|
|
""" |
|
|
|
|
|
if data.get('mock_data'): |
|
|
return { |
|
|
'recommended_card': safe_get(data, 'recommended_card', 'Unknown Card'), |
|
|
'rewards_earned': float(safe_get(data, 'rewards_earned', 0)), |
|
|
'rewards_rate': safe_get(data, 'rewards_rate', 'N/A'), |
|
|
'merchant': safe_get(data, 'merchant', 'Unknown Merchant'), |
|
|
'category': safe_get(data, 'category', 'Unknown'), |
|
|
'amount': float(safe_get(data, 'amount', 0)), |
|
|
'annual_potential': float(safe_get(data, 'annual_potential', 0)), |
|
|
'optimization_score': int(safe_get(data, 'optimization_score', 85)), |
|
|
'reasoning': safe_get(data, 'reasoning', 'Optimal choice'), |
|
|
'warnings': safe_get(data, 'warnings', []), |
|
|
'alternatives': safe_get(data, 'alternatives', []), |
|
|
'mock_data': True |
|
|
} |
|
|
|
|
|
recommended_card = safe_get(data, 'recommended_card', {}) |
|
|
if isinstance(recommended_card, dict): |
|
|
card_name = safe_get(recommended_card, 'card_name', 'Unknown Card') |
|
|
reward_amount = float(safe_get(recommended_card, 'reward_amount', 0)) |
|
|
reward_rate = float(safe_get(recommended_card, 'reward_rate', 0)) |
|
|
category = safe_get(recommended_card, 'category', 'Unknown') |
|
|
reasoning = safe_get(recommended_card, 'reasoning', 'Optimal choice') |
|
|
|
|
|
if reward_rate > 0: |
|
|
rewards_rate_str = f"{reward_rate}x points" |
|
|
else: |
|
|
rewards_rate_str = "N/A" |
|
|
else: |
|
|
card_name = str(recommended_card) if recommended_card else 'Unknown Card' |
|
|
reward_amount = float(safe_get(data, 'rewards_earned', 0)) |
|
|
reward_rate = 0 |
|
|
rewards_rate_str = safe_get(data, 'rewards_rate', 'N/A') |
|
|
category = safe_get(data, 'category', 'Unknown') |
|
|
reasoning = safe_get(data, 'reasoning', 'Optimal choice') |
|
|
|
|
|
merchant = safe_get(data, 'merchant', 'Unknown Merchant') |
|
|
amount = float(safe_get(data, 'amount_usd', safe_get(data, 'amount', 0))) |
|
|
annual_potential = reward_amount * 12 if reward_amount > 0 else 0 |
|
|
|
|
|
alternatives = [] |
|
|
alt_cards = safe_get(data, 'alternative_cards', safe_get(data, 'alternatives', [])) |
|
|
|
|
|
for alt in alt_cards[:3]: |
|
|
if isinstance(alt, dict): |
|
|
alt_name = safe_get(alt, 'card_name', safe_get(alt, 'card', 'Unknown')) |
|
|
alt_reward = float(safe_get(alt, 'reward_amount', safe_get(alt, 'rewards', 0))) |
|
|
alt_rate = safe_get(alt, 'reward_rate', safe_get(alt, 'rate', 0)) |
|
|
|
|
|
if isinstance(alt_rate, (int, float)) and alt_rate > 0: |
|
|
alt_rate_str = f"{alt_rate}x points" |
|
|
else: |
|
|
alt_rate_str = str(alt_rate) if alt_rate else "N/A" |
|
|
|
|
|
alternatives.append({ |
|
|
'card': alt_name, |
|
|
'rewards': alt_reward, |
|
|
'rate': alt_rate_str |
|
|
}) |
|
|
|
|
|
warnings = safe_get(data, 'warnings', []) |
|
|
forecast_warning = safe_get(data, 'forecast_warning') |
|
|
if forecast_warning and isinstance(forecast_warning, dict): |
|
|
warning_msg = safe_get(forecast_warning, 'warning_message') |
|
|
if warning_msg: |
|
|
warnings.append(warning_msg) |
|
|
|
|
|
normalized = { |
|
|
'recommended_card': card_name, |
|
|
'rewards_earned': round(reward_amount, 2), |
|
|
'rewards_rate': rewards_rate_str, |
|
|
'merchant': merchant, |
|
|
'category': category, |
|
|
'amount': amount, |
|
|
'annual_potential': round(annual_potential, 2), |
|
|
'optimization_score': int(safe_get(data, 'optimization_score', 75)), |
|
|
'reasoning': reasoning, |
|
|
'warnings': warnings, |
|
|
'alternatives': alternatives, |
|
|
'mock_data': safe_get(data, 'mock_data', False) |
|
|
} |
|
|
|
|
|
return normalized |
|
|
|
|
|
def create_loading_state(): |
|
|
"""Create loading indicator message""" |
|
|
return "⏳ **Loading...** Please wait while we fetch your recommendation.", None |
|
|
|
|
|
def load_card_database() -> dict: |
|
|
"""Load card database from local cards.json""" |
|
|
try: |
|
|
with open(CARDS_FILE, 'r') as f: |
|
|
cards = json.load(f) |
|
|
print(f"✅ Loaded {len(cards)} cards from database") |
|
|
return cards |
|
|
except FileNotFoundError: |
|
|
print(f"⚠️ cards.json not found at {CARDS_FILE}") |
|
|
return {} |
|
|
except json.JSONDecodeError as e: |
|
|
print(f"❌ Error parsing cards.json: {e}") |
|
|
return {} |
|
|
|
|
|
CARD_DATABASE = load_card_database() |
|
|
|
|
|
def get_card_details(card_id: str, mcc: str = None) -> dict: |
|
|
""" |
|
|
Get card details from database |
|
|
|
|
|
Args: |
|
|
card_id: Card identifier (e.g., "c_citi_custom_cash") |
|
|
mcc: Optional MCC code to get specific reward rate |
|
|
|
|
|
Returns: |
|
|
dict: Card details including name, reward rate, caps, etc. |
|
|
""" |
|
|
if card_id not in CARD_DATABASE: |
|
|
print(f"⚠️ Card {card_id} not found in database, using fallback") |
|
|
return { |
|
|
"name": card_id.replace("c_", "").replace("_", " ").title(), |
|
|
"issuer": "Unknown", |
|
|
"reward_rate": 1.0, |
|
|
"annual_fee": 0, |
|
|
"spending_caps": {}, |
|
|
"benefits": [] |
|
|
} |
|
|
|
|
|
card = CARD_DATABASE[card_id] |
|
|
reward_rate = 1.0 |
|
|
|
|
|
if mcc and "reward_structure" in card: |
|
|
reward_structure = card["reward_structure"] |
|
|
|
|
|
if mcc in reward_structure: |
|
|
reward_rate = reward_structure[mcc] |
|
|
else: |
|
|
try: |
|
|
mcc_int = int(mcc) |
|
|
for key, rate in reward_structure.items(): |
|
|
if "-" in str(key): |
|
|
start, end = str(key).split("-") |
|
|
if int(start) <= mcc_int <= int(end): |
|
|
reward_rate = rate |
|
|
break |
|
|
except (ValueError, AttributeError): |
|
|
pass |
|
|
|
|
|
if reward_rate == 1.0 and "default" in reward_structure: |
|
|
reward_rate = reward_structure["default"] |
|
|
|
|
|
spending_caps = card.get("spending_caps", {}) |
|
|
cap_info = {} |
|
|
|
|
|
if "monthly_bonus" in spending_caps: |
|
|
cap_info = { |
|
|
"type": "monthly", |
|
|
"limit": spending_caps["monthly_bonus"], |
|
|
"display": f"${spending_caps['monthly_bonus']}/month" |
|
|
} |
|
|
elif "quarterly_bonus" in spending_caps: |
|
|
cap_info = { |
|
|
"type": "quarterly", |
|
|
"limit": spending_caps["quarterly_bonus"], |
|
|
"display": f"${spending_caps['quarterly_bonus']}/quarter" |
|
|
} |
|
|
elif "annual_bonus" in spending_caps: |
|
|
cap_info = { |
|
|
"type": "annual", |
|
|
"limit": spending_caps["annual_bonus"], |
|
|
"display": f"${spending_caps['annual_bonus']}/year" |
|
|
} |
|
|
|
|
|
return { |
|
|
"name": card.get("name", "Unknown Card"), |
|
|
"issuer": card.get("issuer", "Unknown"), |
|
|
"reward_rate": reward_rate, |
|
|
"annual_fee": card.get("annual_fee", 0), |
|
|
"spending_caps": cap_info, |
|
|
"benefits": card.get("benefits", []) |
|
|
} |
|
|
|
|
|
def get_recommendation_with_agent(user_id, merchant, category, amount): |
|
|
yield """ |
|
|
<div class="thinking-dots"> |
|
|
🤖 AI Agent is thinking |
|
|
</div> |
|
|
""", None |
|
|
|
|
|
try: |
|
|
transaction = { |
|
|
"user_id": user_id, |
|
|
"merchant": merchant, |
|
|
"category": category, |
|
|
"mcc": MCC_CATEGORIES.get(category, "5999"), |
|
|
"amount_usd": float(amount) |
|
|
} |
|
|
|
|
|
print("=" * 80) |
|
|
print(f"🚀 REQUEST: {config.ORCHESTRATOR_URL}/recommend") |
|
|
print(f"PAYLOAD: {json.dumps(transaction, indent=2)}") |
|
|
|
|
|
yield """ |
|
|
<div class="thinking-dots"> |
|
|
🔍 Analyzing your wallet |
|
|
</div> |
|
|
""", None |
|
|
|
|
|
response = httpx.post( |
|
|
f"{config.ORCHESTRATOR_URL}/recommend", |
|
|
json=transaction, |
|
|
timeout=60.0 |
|
|
) |
|
|
|
|
|
yield """ |
|
|
<div class="thinking-dots"> |
|
|
💳 Comparing cards |
|
|
</div> |
|
|
""", None |
|
|
|
|
|
print(f"📥 STATUS: {response.status_code}") |
|
|
print(f"📦 RESPONSE: {response.text[:2000]}") |
|
|
print("=" * 80) |
|
|
|
|
|
if response.status_code != 200: |
|
|
yield f"❌ Error: API returned status {response.status_code}", None |
|
|
return |
|
|
|
|
|
result = response.json() |
|
|
|
|
|
if not isinstance(result, dict): |
|
|
yield f"❌ Invalid response type: {type(result)}", None |
|
|
return |
|
|
|
|
|
print(f"🔍 KEYS: {list(result.keys())}") |
|
|
|
|
|
card_id = result.get('recommended_card', 'Unknown') |
|
|
rewards_earned = float(result.get('rewards_earned', 0)) |
|
|
rewards_rate = result.get('rewards_rate', 'N/A') |
|
|
confidence = float(result.get('confidence', 0)) |
|
|
reasoning = result.get('reasoning', 'No reasoning provided') |
|
|
alternatives = result.get('alternative_options', []) |
|
|
warnings = result.get('warnings', []) |
|
|
|
|
|
card_name_map = { |
|
|
'c_citi_custom_cash': 'Citi Custom Cash', |
|
|
'c_amex_gold': 'American Express Gold', |
|
|
'c_chase_sapphire_reserve': 'Chase Sapphire Reserve', |
|
|
'c_chase_freedom_unlimited': 'Chase Freedom Unlimited', |
|
|
'c_chase_sapphire_preferred': 'Chase Sapphire Preferred', |
|
|
'c_capital_one_venture': 'Capital One Venture', |
|
|
'c_discover_it': 'Discover it', |
|
|
'c_wells_fargo_active_cash': 'Wells Fargo Active Cash' |
|
|
} |
|
|
card_name = card_name_map.get(card_id, card_id.replace('c_', '').replace('_', ' ').title()) |
|
|
|
|
|
transaction_mcc = result.get('mcc', MCC_CATEGORIES.get(category, "5999")) |
|
|
card_details_from_db = get_card_details(card_id, transaction_mcc) |
|
|
card_details = result.get('card_details', {}) |
|
|
|
|
|
if not card_details or not card_details.get('reward_rate'): |
|
|
reward_structure = CARD_DATABASE.get(card_id, {}).get('reward_structure', {}) |
|
|
|
|
|
if transaction_mcc in reward_structure: |
|
|
reward_rate_value = reward_structure[transaction_mcc] |
|
|
else: |
|
|
reward_rate_value = reward_structure.get('default', 1.0) |
|
|
|
|
|
spending_caps_db = CARD_DATABASE.get(card_id, {}).get('spending_caps', {}) |
|
|
|
|
|
card_details = { |
|
|
'reward_rate': reward_rate_value, |
|
|
'monthly_cap': spending_caps_db.get('monthly_bonus'), |
|
|
'annual_cap': spending_caps_db.get('annual_bonus'), |
|
|
'quarterly_cap': spending_caps_db.get('quarterly_bonus'), |
|
|
'base_rate': reward_structure.get('default', 1.0), |
|
|
'annual_fee': CARD_DATABASE.get(card_id, {}).get('annual_fee', 0), |
|
|
'cap_type': 'monthly' if 'monthly_bonus' in spending_caps_db else |
|
|
'annual' if 'annual_bonus' in spending_caps_db else |
|
|
'quarterly' if 'quarterly_bonus' in spending_caps_db else 'none' |
|
|
} |
|
|
print(f"✅ Using cards.json details for {card_id}") |
|
|
|
|
|
reward_rate_value = card_details.get('reward_rate', 1.0) |
|
|
monthly_cap = card_details.get('monthly_cap', None) |
|
|
annual_cap = card_details.get('annual_cap', None) |
|
|
base_rate = card_details.get('base_rate', 1.0) |
|
|
annual_fee = card_details.get('annual_fee', 0) |
|
|
|
|
|
print(f"✅ CARD DETAILS: {reward_rate_value}%, cap={monthly_cap or annual_cap}, fee=${annual_fee}") |
|
|
|
|
|
amount_float = float(amount) |
|
|
|
|
|
frequency_map = { |
|
|
'Groceries': 52, |
|
|
'Restaurants': 52, |
|
|
'Gas Stations': 52, |
|
|
'Fast Food': 52, |
|
|
'Airlines': 4, |
|
|
'Hotels': 12, |
|
|
'Online Shopping': 24, |
|
|
'Entertainment': 24, |
|
|
} |
|
|
frequency = frequency_map.get(category, 26) |
|
|
frequency_label = { |
|
|
52: 'weekly', |
|
|
26: 'bi-weekly', |
|
|
24: 'bi-weekly', |
|
|
12: 'monthly', |
|
|
4: 'quarterly' |
|
|
}.get(frequency, f'{frequency}x per year') |
|
|
|
|
|
annual_spend = amount_float * frequency |
|
|
|
|
|
if monthly_cap: |
|
|
monthly_cap_annual = monthly_cap * 12 |
|
|
|
|
|
if annual_spend <= monthly_cap_annual: |
|
|
high_rate_spend = annual_spend |
|
|
low_rate_spend = 0 |
|
|
else: |
|
|
high_rate_spend = monthly_cap_annual |
|
|
low_rate_spend = annual_spend - monthly_cap_annual |
|
|
|
|
|
high_rate_rewards = high_rate_spend * (reward_rate_value / 100) |
|
|
low_rate_rewards = low_rate_spend * (base_rate / 100) |
|
|
total_rewards = high_rate_rewards + low_rate_rewards |
|
|
|
|
|
calc_table = f""" |
|
|
| Spending Tier | Annual Amount | Rate | Rewards | |
|
|
|---------------|---------------|------|---------| |
|
|
| First ${monthly_cap}/month | ${high_rate_spend:.2f} | {reward_rate_value}% | ${high_rate_rewards:.2f} | |
|
|
| Remaining spend | ${low_rate_spend:.2f} | {base_rate}% | ${low_rate_rewards:.2f} | |
|
|
| **Subtotal** | **${annual_spend:.2f}** | - | **${total_rewards:.2f}** | |
|
|
| Annual fee | - | - | -${annual_fee:.2f} | |
|
|
| **Net Rewards** | - | - | **${total_rewards - annual_fee:.2f}** | |
|
|
""" |
|
|
|
|
|
elif annual_cap: |
|
|
if annual_spend <= annual_cap: |
|
|
high_rate_spend = annual_spend |
|
|
low_rate_spend = 0 |
|
|
else: |
|
|
high_rate_spend = annual_cap |
|
|
low_rate_spend = annual_spend - annual_cap |
|
|
|
|
|
high_rate_rewards = high_rate_spend * (reward_rate_value / 100) |
|
|
low_rate_rewards = low_rate_spend * (base_rate / 100) |
|
|
total_rewards = high_rate_rewards + low_rate_rewards |
|
|
|
|
|
calc_table = f""" |
|
|
| Spending Tier | Annual Amount | Rate | Rewards | |
|
|
|---------------|---------------|------|---------| |
|
|
| Up to ${annual_cap:,.0f}/year | ${high_rate_spend:.2f} | {reward_rate_value}% | ${high_rate_rewards:.2f} | |
|
|
| Above cap | ${low_rate_spend:.2f} | {base_rate}% | ${low_rate_rewards:.2f} | |
|
|
| **Subtotal** | **${annual_spend:.2f}** | - | **${total_rewards:.2f}** | |
|
|
| Annual fee | - | - | -${annual_fee:.2f} | |
|
|
| **Net Rewards** | - | - | **${total_rewards - annual_fee:.2f}** | |
|
|
""" |
|
|
|
|
|
else: |
|
|
total_rewards = annual_spend * (reward_rate_value / 100) |
|
|
|
|
|
calc_table = f""" |
|
|
| Spending Tier | Annual Amount | Rate | Rewards | |
|
|
|---------------|---------------|------|---------| |
|
|
| All spending | ${annual_spend:.2f} | {reward_rate_value}% | ${total_rewards:.2f} | |
|
|
| Annual fee | - | - | -${annual_fee:.2f} | |
|
|
| **Net Rewards** | - | - | **${total_rewards - annual_fee:.2f}** | |
|
|
""" |
|
|
|
|
|
baseline_rewards = annual_spend * 0.01 |
|
|
net_rewards = total_rewards - annual_fee |
|
|
net_benefit = net_rewards - baseline_rewards |
|
|
|
|
|
comparison_text = f""" |
|
|
**With {card_name}:** |
|
|
- Earnings: ${total_rewards:.2f} |
|
|
- Annual fee: -${annual_fee:.2f} |
|
|
- **Net total: ${net_rewards:.2f}/year** |
|
|
|
|
|
**With Baseline 1% Card:** |
|
|
- All spending at 1%: ${baseline_rewards:.2f}/year |
|
|
|
|
|
**Net Benefit: ${net_benefit:+.2f}/year** {"🎉" if net_benefit > 0 else "⚠️"} |
|
|
""" |
|
|
|
|
|
max_possible_rewards = annual_spend * 0.06 |
|
|
|
|
|
if max_possible_rewards > 0: |
|
|
performance_ratio = (net_rewards / max_possible_rewards) * 100 |
|
|
|
|
|
if net_rewards > baseline_rewards: |
|
|
improvement = (net_rewards - baseline_rewards) / baseline_rewards |
|
|
baseline_bonus = min(improvement * 20, 20) |
|
|
else: |
|
|
baseline_bonus = -10 |
|
|
|
|
|
optimization_score = int(min(performance_ratio + baseline_bonus, 100)) |
|
|
else: |
|
|
optimization_score = 0 |
|
|
|
|
|
score_breakdown = { |
|
|
'reward_rate': min(30, int(optimization_score * 0.30)), |
|
|
'cap_availability': min(25, int(optimization_score * 0.25)), |
|
|
'annual_fee': min(20, int(optimization_score * 0.20)), |
|
|
'category_match': min(20, int(optimization_score * 0.20)), |
|
|
'penalties': max(-5, int((optimization_score - 100) * 0.05)) |
|
|
} |
|
|
|
|
|
score_details = f""" |
|
|
**Score Components:** |
|
|
- {"✅" if score_breakdown['reward_rate'] > 20 else "⚠️"} Reward rate: **+{score_breakdown['reward_rate']} points** |
|
|
- {"✅" if score_breakdown['cap_availability'] > 15 else "⚠️"} Cap availability: **+{score_breakdown['cap_availability']} points** |
|
|
- {"✅" if score_breakdown['annual_fee'] > 15 else "⚠️"} Annual fee value: **+{score_breakdown['annual_fee']} points** |
|
|
- {"✅" if score_breakdown['category_match'] > 15 else "⚠️"} Category match: **+{score_breakdown['category_match']} points** |
|
|
- {"⚠️" if score_breakdown['penalties'] < 0 else "✅"} Limitations: **{score_breakdown['penalties']} points** |
|
|
|
|
|
**Total: {optimization_score}/100** |
|
|
|
|
|
**Score Ranges:** |
|
|
- 90-100: Optimal choice ✅ |
|
|
- 80-89: Great choice 👍 |
|
|
- 70-79: Good choice 👌 |
|
|
- 60-69: Acceptable ⚠️ |
|
|
- <60: Suboptimal ❌ |
|
|
""" |
|
|
|
|
|
output = f""" |
|
|
## 🤖 AI Agent Recommendation |
|
|
|
|
|
### 💳 Recommended Card: **{card_name}** |
|
|
|
|
|
**Rewards Earned:** ${rewards_earned:.2f} ({rewards_rate}) |
|
|
**Confidence:** {confidence*100:.0f}% |
|
|
|
|
|
--- |
|
|
|
|
|
### 🧠 Agent's Reasoning: |
|
|
|
|
|
{reasoning} |
|
|
|
|
|
--- |
|
|
""" |
|
|
|
|
|
if alternatives: |
|
|
output += "\n### 🔄 Alternative Options:\n\n" |
|
|
for alt in alternatives[:3]: |
|
|
alt_card_id = alt.get('card', '') |
|
|
alt_card_name = card_name_map.get(alt_card_id, alt_card_id.replace('c_', '').replace('_', ' ').title()) |
|
|
alt_reason = alt.get('reason', '') |
|
|
output += f"**{alt_card_name}:**\n{alt_reason}\n\n" |
|
|
|
|
|
if warnings: |
|
|
output += "\n### ⚠️ Important Warnings:\n\n" |
|
|
for warning in warnings: |
|
|
output += f"- {warning}\n" |
|
|
|
|
|
output += f""" |
|
|
### 💰 Annual Impact |
|
|
|
|
|
- **Potential Savings:** ${net_benefit:.2f}/year |
|
|
- **Optimization Score:** {optimization_score}/100 |
|
|
|
|
|
<details> |
|
|
<summary>📊 <b>How is this calculated?</b> (Click to expand)</summary> |
|
|
|
|
|
#### 💡 Calculation Assumptions: |
|
|
|
|
|
**Step 1: Estimate Annual Spending** |
|
|
|
|
|
Current transaction: ${amount_float:.2f} at {merchant} |
|
|
Category: {category} |
|
|
Frequency assumption: {frequency_label.capitalize()} |
|
|
Annual estimate: ${amount_float:.2f} × {frequency} = **${annual_spend:.2f}** |
|
|
|
|
|
**Step 2: Calculate Rewards with {card_name}** |
|
|
|
|
|
{calc_table} |
|
|
|
|
|
**Step 3: Compare to Baseline** |
|
|
|
|
|
{comparison_text} |
|
|
|
|
|
--- |
|
|
|
|
|
#### 📈 Optimization Score: {optimization_score}/100 |
|
|
|
|
|
{score_details} |
|
|
|
|
|
--- |
|
|
|
|
|
#### 🔍 Card Details: |
|
|
|
|
|
- **Reward Rate:** {reward_rate_value}% on {category} |
|
|
- **Monthly Cap:** {"$" + str(monthly_cap) if monthly_cap else "None"} |
|
|
- **Annual Cap:** {"$" + str(annual_cap) if annual_cap else "None"} |
|
|
- **Base Rate:** {base_rate}% (after cap) |
|
|
- **Annual Fee:** ${annual_fee} |
|
|
|
|
|
</details> |
|
|
|
|
|
--- |
|
|
|
|
|
#### 📊 Transaction Details: |
|
|
|
|
|
- **Amount:** ${amount_float:.2f} |
|
|
- **Merchant:** {merchant} |
|
|
- **Category:** {category} |
|
|
- **MCC Code:** {transaction['mcc']} |
|
|
""" |
|
|
|
|
|
chart = create_agent_recommendation_chart_enhanced(result) |
|
|
yield output, chart |
|
|
|
|
|
print("=" * 80) |
|
|
print("📤 FINAL OUTPUT (first 500 chars):") |
|
|
print(output[:500]) |
|
|
print("=" * 80) |
|
|
|
|
|
except Exception as e: |
|
|
print(f"❌ ERROR: {traceback.format_exc()}") |
|
|
yield f"❌ **Error:** {str(e)}", None |
|
|
|
|
|
def create_agent_recommendation_chart_enhanced(result: Dict) -> go.Figure: |
|
|
try: |
|
|
rec_name_map = { |
|
|
'c_citi_custom_cash': 'Citi Custom Cash', |
|
|
'c_amex_gold': 'Amex Gold', |
|
|
'c_chase_sapphire_reserve': 'Sapphire Reserve', |
|
|
'c_chase_freedom_unlimited': 'Freedom Unlimited' |
|
|
} |
|
|
|
|
|
rec_id = result.get('recommended_card', '') |
|
|
rec_name = rec_name_map.get(rec_id, rec_id) |
|
|
rec_reward = float(result.get('rewards_earned', 0)) |
|
|
|
|
|
cards = [rec_name] |
|
|
rewards = [rec_reward] |
|
|
colors = ['#667eea'] |
|
|
|
|
|
alternatives = result.get('alternative_options', []) |
|
|
for alt in alternatives[:3]: |
|
|
alt_id = alt.get('card', '') |
|
|
alt_name = rec_name_map.get(alt_id, alt_id) |
|
|
alt_reward = rec_reward * 0.8 |
|
|
cards.append(alt_name) |
|
|
rewards.append(alt_reward) |
|
|
colors.append('#cbd5e0') |
|
|
|
|
|
fig = go.Figure(data=[ |
|
|
go.Bar( |
|
|
x=cards, |
|
|
y=rewards, |
|
|
marker=dict(color=colors, line=dict(color='white', width=2)), |
|
|
text=[f'${r:.2f}' for r in rewards], |
|
|
textposition='outside' |
|
|
) |
|
|
]) |
|
|
|
|
|
fig.update_layout( |
|
|
title='🎯 Card Comparison', |
|
|
xaxis_title='Credit Card', |
|
|
yaxis_title='Rewards ($)', |
|
|
template='plotly_white', |
|
|
height=400, |
|
|
showlegend=False |
|
|
) |
|
|
|
|
|
return fig |
|
|
|
|
|
except Exception as e: |
|
|
print(f"Chart error: {e}") |
|
|
fig = go.Figure() |
|
|
fig.add_annotation(text="Chart unavailable", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False) |
|
|
fig.update_layout(height=400, template='plotly_white') |
|
|
return fig |
|
|
|
|
|
client = RewardPilotClient(config.ORCHESTRATOR_URL) |
|
|
llm = get_llm_explainer() |
|
|
|
|
|
def get_recommendation( |
|
|
user_id: str, |
|
|
merchant: str, |
|
|
category: str, |
|
|
amount: float, |
|
|
use_custom_mcc: bool, |
|
|
custom_mcc: str, |
|
|
transaction_date: Optional[str]) -> tuple: |
|
|
"""Get card recommendation and format response""" |
|
|
if not user_id or not merchant or amount <= 0: |
|
|
return ( |
|
|
"❌ **Error:** Please fill in all required fields.", |
|
|
None, |
|
|
None, |
|
|
) |
|
|
|
|
|
if use_custom_mcc and custom_mcc: |
|
|
mcc = custom_mcc |
|
|
else: |
|
|
mcc = MCC_CATEGORIES.get(category, "5999") |
|
|
|
|
|
if not transaction_date: |
|
|
transaction_date = str(date.today()) |
|
|
|
|
|
response: Dict[str, Any] = client.get_recommendation_sync( |
|
|
user_id=user_id, |
|
|
merchant=merchant, |
|
|
mcc=mcc, |
|
|
amount_usd=amount, |
|
|
transaction_date=transaction_date, |
|
|
) |
|
|
|
|
|
formatted_text = format_full_recommendation(response) |
|
|
|
|
|
comparison_table: Optional[str] |
|
|
stats: Optional[str] |
|
|
if not response.get("error"): |
|
|
recommended = response.get("recommended_card", {}) or {} |
|
|
alternatives: List[Dict[str, Any]] = response.get("alternative_cards", []) or [] |
|
|
all_cards = [c for c in ([recommended] + alternatives) if c] |
|
|
comparison_table = format_comparison_table(all_cards) if all_cards else None |
|
|
|
|
|
total_analyzed = response.get("total_cards_analyzed", len(all_cards)) |
|
|
best_reward = (recommended.get("reward_amount") or 0.0) |
|
|
services_used = response.get("services_used", []) |
|
|
stats = f"""**Cards Analyzed:** {total_analyzed} |
|
|
**Best Reward:** ${best_reward:.2f} |
|
|
**Services Used:** {', '.join(services_used)}""".strip() |
|
|
else: |
|
|
comparison_table = None |
|
|
stats = None |
|
|
|
|
|
return formatted_text, comparison_table, stats |
|
|
|
|
|
def get_recommendation_with_ai(user_id, merchant, category, amount): |
|
|
"""Get card recommendation with LLM-powered explanation""" |
|
|
|
|
|
if not merchant or not merchant.strip(): |
|
|
return "❌ Please enter a merchant name.", None |
|
|
|
|
|
if amount <= 0: |
|
|
return "❌ Please enter a valid amount greater than $0.", None |
|
|
|
|
|
yield "⏳ **Loading recommendation...** Analyzing your cards and transaction...", None |
|
|
|
|
|
try: |
|
|
result = client.get_recommendation( |
|
|
user_id=user_id, |
|
|
merchant=merchant, |
|
|
category=category, |
|
|
amount=float(amount), |
|
|
mcc=None |
|
|
) |
|
|
|
|
|
if not result.get('success'): |
|
|
error_msg = result.get('error', 'Unknown error') |
|
|
yield f"❌ Error: {error_msg}", None |
|
|
return |
|
|
|
|
|
data = normalize_recommendation_data(result.get('data', {})) |
|
|
|
|
|
ai_explanation = "" |
|
|
if config.LLM_ENABLED: |
|
|
try: |
|
|
ai_explanation = llm.explain_recommendation( |
|
|
card=data['recommended_card'], |
|
|
rewards=data['rewards_earned'], |
|
|
rewards_rate=data['rewards_rate'], |
|
|
merchant=merchant, |
|
|
category=category, |
|
|
amount=float(amount), |
|
|
warnings=data['warnings'] if data['warnings'] else None, |
|
|
annual_potential=data['annual_potential'], |
|
|
alternatives=data['alternatives'] |
|
|
) |
|
|
except Exception as e: |
|
|
print(f"LLM explanation failed: {e}") |
|
|
ai_explanation = "" |
|
|
|
|
|
output = f""" |
|
|
## 🎯 Recommendation for ${amount:.2f} at {merchant} |
|
|
|
|
|
### 💳 Best Card: **{data['recommended_card']}** |
|
|
|
|
|
**Rewards Earned:** ${data['rewards_earned']:.2f} ({data['rewards_rate']}) |
|
|
""" |
|
|
|
|
|
if data.get('mock_data'): |
|
|
output += """ |
|
|
> ⚠️ **Demo Mode:** Using sample data. Connect to orchestrator for real recommendations. |
|
|
""" |
|
|
|
|
|
if ai_explanation: |
|
|
output += f""" |
|
|
### 🤖 AI Insight |
|
|
|
|
|
{ai_explanation} |
|
|
|
|
|
--- |
|
|
""" |
|
|
|
|
|
output += f""" |
|
|
### 📊 Breakdown |
|
|
|
|
|
- **Category:** {data['category']} |
|
|
- **Merchant:** {data['merchant']} |
|
|
- **Reasoning:** {data['reasoning']} |
|
|
- **Annual Potential:** ${data['annual_potential']:.2f} |
|
|
- **Optimization Score:** {data['optimization_score']}/100 |
|
|
""" |
|
|
|
|
|
if data['warnings']: |
|
|
output += "\n\n### ⚠️ Important Warnings\n\n" |
|
|
for warning in data['warnings']: |
|
|
output += f"- {warning}\n" |
|
|
|
|
|
if data['alternatives']: |
|
|
output += "\n\n### 🔄 Alternative Options\n\n" |
|
|
for alt in data['alternatives']: |
|
|
output += f"- **{alt['card']}:** ${alt['rewards']:.2f} ({alt['rate']})\n" |
|
|
|
|
|
chart = create_rewards_comparison_chart(data) |
|
|
|
|
|
yield output, chart |
|
|
|
|
|
except Exception as e: |
|
|
error_details = traceback.format_exc() |
|
|
print(f"Recommendation error: {error_details}") |
|
|
yield f"❌ Error: {str(e)}\n\nPlease check your API connection or try again.", None |
|
|
|
|
|
def create_rewards_comparison_chart(data: Dict) -> go.Figure: |
|
|
"""Create rewards comparison chart with proper error handling""" |
|
|
|
|
|
try: |
|
|
cards = [data['recommended_card']] |
|
|
rewards = [data['rewards_earned']] |
|
|
colors = ['#667eea'] |
|
|
|
|
|
for alt in data.get('alternatives', [])[:3]: |
|
|
cards.append(alt['card']) |
|
|
rewards.append(float(alt['rewards'])) |
|
|
colors.append('#a0aec0') |
|
|
|
|
|
if not cards or all(r == 0 for r in rewards): |
|
|
fig = go.Figure() |
|
|
fig.add_annotation( |
|
|
text="No rewards data available for comparison", |
|
|
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 |
|
|
|
|
|
fig = go.Figure(data=[ |
|
|
go.Bar( |
|
|
x=cards, |
|
|
y=rewards, |
|
|
marker=dict( |
|
|
color=colors, |
|
|
line=dict(color='white', width=2) |
|
|
), |
|
|
text=[f'${r:.2f}' for r in rewards], |
|
|
textposition='outside', |
|
|
hovertemplate='<b>%{x}</b><br>Rewards: $%{y:.2f}<extra></extra>' |
|
|
) |
|
|
]) |
|
|
|
|
|
fig.update_layout( |
|
|
title={ |
|
|
'text': 'Rewards Comparison', |
|
|
'x': 0.5, |
|
|
'xanchor': 'center' |
|
|
}, |
|
|
xaxis_title='Credit Card', |
|
|
yaxis_title='Rewards Earned ($)', |
|
|
template='plotly_white', |
|
|
height=400, |
|
|
showlegend=False, |
|
|
margin=dict(t=60, b=50, l=50, r=50), |
|
|
hovermode='x' |
|
|
) |
|
|
|
|
|
return fig |
|
|
|
|
|
except Exception as e: |
|
|
print(f"Chart creation error: {e}") |
|
|
print(traceback.format_exc()) |
|
|
|
|
|
fig = go.Figure() |
|
|
fig.add_annotation( |
|
|
text=f"Error creating 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 get_analytics_with_insights(user_id): |
|
|
"""Get analytics with LLM-generated insights""" |
|
|
|
|
|
try: |
|
|
result = client.get_user_analytics(user_id) |
|
|
|
|
|
if not result.get('success'): |
|
|
return f"❌ Error: {result.get('error', 'Unknown error')}", None, None, None |
|
|
|
|
|
data = result['data'] |
|
|
|
|
|
ai_insights = "" |
|
|
if config.LLM_ENABLED: |
|
|
try: |
|
|
ai_insights = llm.generate_spending_insights( |
|
|
user_id=user_id, |
|
|
total_spending=data['total_spending'], |
|
|
total_rewards=data['total_rewards'], |
|
|
optimization_score=data['optimization_score'], |
|
|
top_categories=data.get('category_breakdown', []), |
|
|
recommendations_count=data.get('optimized_count', 0) |
|
|
) |
|
|
except Exception as e: |
|
|
print(f"AI insights generation failed: {e}") |
|
|
ai_insights = "" |
|
|
|
|
|
metrics = f""" |
|
|
## 📊 Your Rewards Analytics |
|
|
|
|
|
### Key Metrics |
|
|
|
|
|
- **💰 Total Rewards:** ${data['total_rewards']:.2f} |
|
|
- **📈 Potential Savings:** ${data['potential_savings']:.2f}/year |
|
|
- **⭐ Optimization Score:** {data['optimization_score']}/100 |
|
|
- **✅ Optimized Transactions:** {data.get('optimized_count', 0)} |
|
|
""" |
|
|
|
|
|
if ai_insights: |
|
|
metrics += f""" |
|
|
### 🤖 Personalized Insights |
|
|
|
|
|
{ai_insights} |
|
|
|
|
|
--- |
|
|
""" |
|
|
|
|
|
spending_chart = create_spending_chart(data) |
|
|
rewards_chart = create_rewards_distribution_chart(data) |
|
|
optimization_chart = create_optimization_gauge(data['optimization_score']) |
|
|
|
|
|
return metrics, spending_chart, rewards_chart, optimization_chart |
|
|
|
|
|
except Exception as e: |
|
|
return f"❌ Error: {str(e)}", None, None, None |
|
|
|
|
|
EXAMPLES = [ |
|
|
["u_alice", "Groceries", "Whole Foods", 125.50, False, "", "2025-01-15"], |
|
|
["u_bob", "Restaurants", "Olive Garden", 65.75, False, "", "2025-01-15"], |
|
|
["u_charlie", "Airlines", "United Airlines", 450.00, False, "", "2025-01-15"], |
|
|
["u_alice", "Fast Food", "Starbucks", 15.75, False, "", ""], |
|
|
["u_bob", "Gas Stations", "Shell", 45.00, False, "", ""], |
|
|
] |
|
|
|
|
|
def create_empty_chart(message: str) -> go.Figure: |
|
|
"""Helper to create empty chart with message""" |
|
|
fig = go.Figure() |
|
|
fig.add_annotation( |
|
|
text=message, |
|
|
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 format_current_month_summary(analytics_data): |
|
|
"""Format current month warnings with clear styling (for Analytics tab)""" |
|
|
|
|
|
warnings = [] |
|
|
|
|
|
|
|
|
for card in analytics_data.get('card_usage', []): |
|
|
if card.get('cap_percentage', 0) > 90: |
|
|
warnings.append( |
|
|
f"⚠️ <strong>{card['name']}</strong>: " |
|
|
f"${card['current_spend']:.0f} / ${card['cap']:.0f} " |
|
|
f"({card['cap_percentage']:.0f}% used)" |
|
|
) |
|
|
|
|
|
|
|
|
days_elapsed = datetime.now().day |
|
|
days_in_month = calendar.monthrange(datetime.now().year, datetime.now().month)[1] |
|
|
projection_ratio = days_in_month / days_elapsed if days_elapsed > 0 else 1 |
|
|
|
|
|
projected_spending = analytics_data.get('total_spending', 0) * projection_ratio |
|
|
projected_rewards = analytics_data.get('total_rewards', 0) * projection_ratio |
|
|
|
|
|
warnings_html = "<br>".join(warnings) if warnings else "✅ No warnings - you're on track!" |
|
|
|
|
|
return f""" |
|
|
<div class="current-month-warning"> |
|
|
<h4>⚠️ This Month's Status (as of {datetime.now().strftime('%B %d')})</h4> |
|
|
|
|
|
<p><strong>Month-End Projection:</strong></p> |
|
|
<ul style="margin: 10px 0; padding-left: 20px;"> |
|
|
<li>Estimated Total Spending: <strong>${projected_spending:.2f}</strong></li> |
|
|
<li>Estimated Total Rewards: <strong>${projected_rewards:.2f}</strong></li> |
|
|
</ul> |
|
|
|
|
|
<p><strong>Spending Cap Alerts:</strong></p> |
|
|
<p style="margin: 10px 0;">{warnings_html}</p> |
|
|
|
|
|
<p style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #ffcc80; font-size: 13px; color: #666;"> |
|
|
💡 <em>These are estimates based on your current month's activity. |
|
|
For detailed future predictions, visit the <strong>Forecast tab</strong>.</em> |
|
|
</p> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
def update_analytics_with_charts(user_id: str): |
|
|
"""Fetch and format analytics with charts for selected user""" |
|
|
|
|
|
try: |
|
|
result = client.get_user_analytics(user_id) |
|
|
|
|
|
print("=" * 60) |
|
|
print(f"DEBUG: Analytics for {user_id}") |
|
|
print(f"Success: {result.get('success')}") |
|
|
if result.get('data'): |
|
|
print(f"Data keys: {result['data'].keys()}") |
|
|
print(f"Total spending: {result['data'].get('total_spending')}") |
|
|
print(f"Total rewards: {result['data'].get('total_rewards')}") |
|
|
print("=" * 60) |
|
|
|
|
|
if not result.get('success'): |
|
|
error_msg = result.get('error', 'Unknown error') |
|
|
empty_fig = create_empty_chart(f"Error: {error_msg}") |
|
|
return ( |
|
|
f"<p>❌ Error: {error_msg}</p>", |
|
|
empty_fig, empty_fig, empty_fig, empty_fig, empty_fig, |
|
|
"Error loading data", |
|
|
"Error loading data", |
|
|
f"<p>Error: {error_msg}</p>", |
|
|
f"*Error: {error_msg}*" |
|
|
) |
|
|
|
|
|
analytics_data = result.get('data', {}) |
|
|
|
|
|
if not analytics_data: |
|
|
empty_fig = create_empty_chart("No analytics data available") |
|
|
return ( |
|
|
"<p>No data available</p>", |
|
|
empty_fig, empty_fig, empty_fig, empty_fig, empty_fig, |
|
|
"No data", |
|
|
"No data", |
|
|
"<p>No data available</p>", |
|
|
"*No data available*" |
|
|
) |
|
|
|
|
|
metrics_html, table_md, insights_md, _ = format_analytics_metrics(analytics_data) |
|
|
|
|
|
|
|
|
current_month_html = format_current_month_summary(analytics_data) |
|
|
|
|
|
spending_fig = create_spending_chart(analytics_data) |
|
|
pie_fig = create_rewards_pie_chart(analytics_data) |
|
|
gauge_fig = create_optimization_gauge(analytics_data) |
|
|
trend_fig = create_trend_line_chart(analytics_data) |
|
|
performance_fig = create_card_performance_chart(analytics_data) |
|
|
|
|
|
status = f"*Analytics updated for {user_id} at {datetime.now().strftime('%I:%M %p')}*" |
|
|
|
|
|
return ( |
|
|
metrics_html, |
|
|
spending_fig, |
|
|
gauge_fig, |
|
|
pie_fig, |
|
|
performance_fig, |
|
|
trend_fig, |
|
|
table_md, |
|
|
insights_md, |
|
|
current_month_html, |
|
|
status |
|
|
) |
|
|
|
|
|
except Exception as e: |
|
|
error_details = traceback.format_exc() |
|
|
error_msg = f"❌ Error loading analytics: {str(e)}" |
|
|
print(error_msg) |
|
|
print(error_details) |
|
|
|
|
|
empty_fig = create_empty_chart("Error loading chart") |
|
|
|
|
|
return ( |
|
|
f"<p>{error_msg}</p>", |
|
|
empty_fig, empty_fig, empty_fig, empty_fig, empty_fig, |
|
|
"Error loading table", |
|
|
"Error loading insights", |
|
|
f"<p>{error_msg}</p>", |
|
|
f"*{error_msg}*" |
|
|
) |
|
|
|
|
|
def _toggle_custom_mcc(use_custom: bool): |
|
|
return gr.update(visible=use_custom, value="") |
|
|
|
|
|
|
|
|
def load_user_wallet(user_id: str): |
|
|
"""Load and display user's credit card wallet""" |
|
|
try: |
|
|
|
|
|
|
|
|
wallet_data = { |
|
|
'u_alice': [ |
|
|
{'name': 'Amex Gold', 'issuer': 'American Express', 'status': 'Active', 'limit': '$25,000'}, |
|
|
{'name': 'Chase Sapphire Reserve', 'issuer': 'Chase', 'status': 'Active', 'limit': '$30,000'}, |
|
|
{'name': 'Citi Custom Cash', 'issuer': 'Citibank', 'status': 'Active', 'limit': '$15,000'}, |
|
|
], |
|
|
'u_bob': [ |
|
|
{'name': 'Chase Freedom Unlimited', 'issuer': 'Chase', 'status': 'Active', 'limit': '$20,000'}, |
|
|
{'name': 'Discover it', 'issuer': 'Discover', 'status': 'Active', 'limit': '$12,000'}, |
|
|
], |
|
|
'u_charlie': [ |
|
|
{'name': 'Capital One Venture', 'issuer': 'Capital One', 'status': 'Active', 'limit': '$35,000'}, |
|
|
{'name': 'Wells Fargo Active Cash', 'issuer': 'Wells Fargo', 'status': 'Active', 'limit': '$18,000'}, |
|
|
] |
|
|
} |
|
|
|
|
|
cards = wallet_data.get(user_id, []) |
|
|
|
|
|
if not cards: |
|
|
return "No cards found in wallet", create_empty_chart("No cards in wallet") |
|
|
|
|
|
output = f"## 💳 Your Credit Card Wallet ({len(cards)} cards)\n\n" |
|
|
|
|
|
for card in cards: |
|
|
output += f""" |
|
|
### {card['name']} |
|
|
- **Issuer:** {card['issuer']} |
|
|
- **Status:** {card['status']} |
|
|
- **Credit Limit:** {card['limit']} |
|
|
|
|
|
--- |
|
|
""" |
|
|
|
|
|
|
|
|
fig = go.Figure(data=[ |
|
|
go.Bar( |
|
|
x=[c['name'] for c in cards], |
|
|
y=[int(c['limit'].replace('$', '').replace(',', '')) for c in cards], |
|
|
marker=dict(color='#667eea'), |
|
|
text=[c['limit'] for c in cards], |
|
|
textposition='outside' |
|
|
) |
|
|
]) |
|
|
|
|
|
fig.update_layout( |
|
|
title='Credit Limits by Card', |
|
|
xaxis_title='Card', |
|
|
yaxis_title='Credit Limit ($)', |
|
|
template='plotly_white', |
|
|
height=400 |
|
|
) |
|
|
|
|
|
return output, fig |
|
|
|
|
|
except Exception as e: |
|
|
return f"❌ Error loading wallet: {str(e)}", create_empty_chart("Error") |
|
|
|
|
|
|
|
|
def load_user_forecast(user_id: str): |
|
|
"""Load and display spending forecast (comprehensive version for Forecast tab)""" |
|
|
try: |
|
|
|
|
|
forecast_data = { |
|
|
'next_month_spending': 3250.50, |
|
|
'predicted_rewards': 127.50, |
|
|
'confidence': 0.92, |
|
|
'top_categories': [ |
|
|
{'category': 'Groceries', 'predicted': 850.00, 'confidence': 0.92, 'emoji': '🛒'}, |
|
|
{'category': 'Restaurants', 'predicted': 650.00, 'confidence': 0.88, 'emoji': '🍽️'}, |
|
|
{'category': 'Gas', 'predicted': 450.00, 'confidence': 0.85, 'emoji': '⛽'}, |
|
|
], |
|
|
'recommendations': [ |
|
|
"Consider using Amex Gold for groceries to maximize 4x points", |
|
|
"You're approaching your Citi Custom Cash $500 monthly cap", |
|
|
"Travel spending is predicted to increase next month" |
|
|
], |
|
|
'optimization_potential': 45.50 |
|
|
} |
|
|
|
|
|
confidence = forecast_data.get('confidence', 0.85) |
|
|
confidence_color = '#4caf50' if confidence > 0.9 else '#ff9800' if confidence > 0.75 else '#f44336' |
|
|
|
|
|
|
|
|
output = f""" |
|
|
## 🔮 Next Month Forecast |
|
|
|
|
|
**Confidence Score:** {confidence*100:.0f}% <span style="background: {confidence_color}; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600;">High Confidence</span> |
|
|
|
|
|
--- |
|
|
|
|
|
### 📊 Predicted Metrics |
|
|
|
|
|
| Metric | Amount | |
|
|
|--------|--------| |
|
|
| 💰 **Total Spending** | ${forecast_data['next_month_spending']:.2f} | |
|
|
| 🎁 **Expected Rewards** | ${forecast_data['predicted_rewards']:.2f} | |
|
|
| 📈 **Optimization Potential** | +${forecast_data['optimization_potential']:.2f} | |
|
|
|
|
|
--- |
|
|
|
|
|
## 📊 Category Breakdown |
|
|
|
|
|
""" |
|
|
|
|
|
for cat in forecast_data['top_categories']: |
|
|
output += f""" |
|
|
### {cat['emoji']} {cat['category']} |
|
|
|
|
|
- **Predicted Amount:** ${cat['predicted']:.2f} (Confidence: {cat['confidence']*100:.0f}%) |
|
|
- **Recommendation:** Use best card for this category |
|
|
- **Potential Rewards:** Based on optimal card selection |
|
|
|
|
|
""" |
|
|
|
|
|
output += "\n---\n\n## 💡 Optimization Strategies\n\n" |
|
|
for i, rec in enumerate(forecast_data['recommendations'], 1): |
|
|
output += f"{i}. {rec}\n" |
|
|
|
|
|
output += """ |
|
|
|
|
|
--- |
|
|
|
|
|
### 📈 How to Maximize Rewards |
|
|
|
|
|
1. **Track your spending caps** - Don't miss out on bonus categories |
|
|
2. **Use category-specific cards** - Match each purchase to the best card |
|
|
3. **Monitor monthly limits** - Switch cards when you hit caps |
|
|
4. **Plan ahead** - Use these predictions to optimize your wallet |
|
|
|
|
|
""" |
|
|
|
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
categories = [c['category'] for c in forecast_data['top_categories']] |
|
|
amounts = [c['predicted'] for c in forecast_data['top_categories']] |
|
|
confidences = [c['confidence'] for c in forecast_data['top_categories']] |
|
|
|
|
|
|
|
|
colors = ['#4caf50' if c > 0.9 else '#ff9800' if c > 0.8 else '#f44336' for c in confidences] |
|
|
|
|
|
fig.add_trace(go.Bar( |
|
|
x=categories, |
|
|
y=amounts, |
|
|
marker=dict(color=colors), |
|
|
text=[f'${a:.0f}<br>{c*100:.0f}% conf.' for a, c in zip(amounts, confidences)], |
|
|
textposition='outside', |
|
|
hovertemplate='<b>%{x}</b><br>Predicted: $%{y:.2f}<extra></extra>' |
|
|
)) |
|
|
|
|
|
fig.update_layout( |
|
|
title='Predicted Spending by Category (Next Month)', |
|
|
xaxis_title='Category', |
|
|
yaxis_title='Predicted Amount ($)', |
|
|
template='plotly_white', |
|
|
height=400, |
|
|
showlegend=False |
|
|
) |
|
|
|
|
|
return output, fig |
|
|
|
|
|
except Exception as e: |
|
|
return f"❌ Error loading forecast: {str(e)}", create_empty_chart("Error") |
|
|
|
|
|
|
|
|
|
|
|
with gr.Blocks( |
|
|
theme=THEME if isinstance(THEME, gr.themes.ThemeClass) else gr.themes.Soft(), |
|
|
title=APP_TITLE, |
|
|
css=""" |
|
|
.gradio-container { |
|
|
max-width: 1200px !important; |
|
|
} |
|
|
.recommendation-output { |
|
|
font-size: 16px; |
|
|
line-height: 1.6; |
|
|
} |
|
|
.metric-card { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
color: white; |
|
|
padding: 30px 20px; |
|
|
border-radius: 16px; |
|
|
text-align: center; |
|
|
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3); |
|
|
transition: transform 0.3s ease, box-shadow 0.3s ease; |
|
|
margin: 10px; |
|
|
} |
|
|
.metric-card:hover { |
|
|
transform: translateY(-5px); |
|
|
box-shadow: 0 12px 32px rgba(102, 126, 234, 0.4); |
|
|
} |
|
|
.metric-card h2 { |
|
|
font-size: 48px; |
|
|
font-weight: 700; |
|
|
margin: 0 0 10px 0; |
|
|
color: white; |
|
|
} |
|
|
.metric-card p { |
|
|
font-size: 16px; |
|
|
margin: 0; |
|
|
opacity: 0.9; |
|
|
color: white; |
|
|
} |
|
|
.metric-card-green { |
|
|
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); |
|
|
} |
|
|
.metric-card-orange { |
|
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); |
|
|
} |
|
|
.metric-card-blue { |
|
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); |
|
|
} |
|
|
|
|
|
/* ===== ANALYTICS TAB - WARNING BOX ===== */ |
|
|
.current-month-warning { |
|
|
background: linear-gradient(135deg, #fff4e6 0%, #ffe8cc 100%); |
|
|
border-left: 4px solid #ff9800; |
|
|
padding: 15px 20px; |
|
|
border-radius: 8px; |
|
|
margin: 20px 0; |
|
|
box-shadow: 0 2px 8px rgba(255, 152, 0, 0.2); |
|
|
} |
|
|
|
|
|
.current-month-warning h4 { |
|
|
color: #e65100; |
|
|
margin: 0 0 10px 0; |
|
|
font-size: 18px; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.current-month-warning p { |
|
|
color: #5d4037; |
|
|
margin: 5px 0; |
|
|
font-size: 14px; |
|
|
} |
|
|
.thinking-dots { |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
gap: 8px; |
|
|
padding: 20px; |
|
|
font-size: 18px; |
|
|
color: #667eea; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
.thinking-dots::after { |
|
|
content: '●●●'; |
|
|
display: inline-block; |
|
|
letter-spacing: 4px; |
|
|
animation: thinking 1.4s infinite; |
|
|
color: #667eea; |
|
|
} |
|
|
|
|
|
@keyframes thinking { |
|
|
0%, 20% { |
|
|
content: '●○○'; |
|
|
} |
|
|
40% { |
|
|
content: '●●○'; |
|
|
} |
|
|
60%, 100% { |
|
|
content: '●●●'; |
|
|
} |
|
|
} |
|
|
|
|
|
/* Alternative bouncing dots animation */ |
|
|
.thinking-bounce { |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
gap: 5px; |
|
|
} |
|
|
|
|
|
.thinking-bounce span { |
|
|
width: 10px; |
|
|
height: 10px; |
|
|
background: #667eea; |
|
|
border-radius: 50%; |
|
|
animation: bounce 1.4s infinite ease-in-out; |
|
|
} |
|
|
|
|
|
.thinking-bounce span:nth-child(1) { |
|
|
animation-delay: 0s; |
|
|
} |
|
|
|
|
|
.thinking-bounce span:nth-child(2) { |
|
|
animation-delay: 0.2s; |
|
|
} |
|
|
|
|
|
.thinking-bounce span:nth-child(3) { |
|
|
animation-delay: 0.4s; |
|
|
} |
|
|
|
|
|
@keyframes bounce { |
|
|
0%, 80%, 100% { |
|
|
transform: translateY(0); |
|
|
opacity: 0.5; |
|
|
} |
|
|
40% { |
|
|
transform: translateY(-10px); |
|
|
opacity: 1; |
|
|
} |
|
|
} |
|
|
|
|
|
/* Pulsing effect */ |
|
|
.thinking-pulse { |
|
|
display: inline-block; |
|
|
animation: pulse 1.5s ease-in-out infinite; |
|
|
} |
|
|
|
|
|
@keyframes pulse { |
|
|
0%, 100% { |
|
|
opacity: 1; |
|
|
} |
|
|
50% { |
|
|
opacity: 0.3; |
|
|
} |
|
|
} |
|
|
/* ===== FORECAST TAB - PREDICTION BOX ===== */ |
|
|
.forecast-prediction { |
|
|
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); |
|
|
border-left: 4px solid #2196f3; |
|
|
padding: 20px; |
|
|
border-radius: 8px; |
|
|
margin: 20px 0; |
|
|
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.2); |
|
|
} |
|
|
|
|
|
.forecast-prediction h3 { |
|
|
color: #0d47a1; |
|
|
margin: 0 0 15px 0; |
|
|
font-size: 22px; |
|
|
font-weight: 700; |
|
|
} |
|
|
|
|
|
.forecast-prediction .confidence-badge { |
|
|
display: inline-block; |
|
|
padding: 4px 12px; |
|
|
background: #4caf50; |
|
|
color: white; |
|
|
border-radius: 12px; |
|
|
font-size: 12px; |
|
|
font-weight: 600; |
|
|
margin-left: 10px; |
|
|
} |
|
|
|
|
|
/* ===== SECTION DIVIDERS ===== */ |
|
|
.section-divider { |
|
|
border: 0; |
|
|
height: 2px; |
|
|
background: linear-gradient(to right, transparent, #ddd, transparent); |
|
|
margin: 30px 0; |
|
|
} |
|
|
|
|
|
/* ===== INFO BOXES ===== */ |
|
|
.info-box { |
|
|
background: #f5f5f5; |
|
|
border-radius: 8px; |
|
|
padding: 15px; |
|
|
margin: 15px 0; |
|
|
border-left: 3px solid #667eea; |
|
|
} |
|
|
|
|
|
.info-box-icon { |
|
|
font-size: 24px; |
|
|
margin-right: 10px; |
|
|
vertical-align: middle; |
|
|
} |
|
|
|
|
|
table { |
|
|
width: 100%; |
|
|
border-collapse: collapse; |
|
|
margin: 20px 0; |
|
|
background: white; |
|
|
border-radius: 8px; |
|
|
overflow: hidden; |
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1); |
|
|
} |
|
|
table th { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
color: white; |
|
|
padding: 12px; |
|
|
text-align: left; |
|
|
font-weight: 600; |
|
|
} |
|
|
table td { |
|
|
padding: 12px; |
|
|
border-bottom: 1px solid #f0f0f0; |
|
|
} |
|
|
table tr:last-child td { |
|
|
border-bottom: none; |
|
|
} |
|
|
table tr:hover { |
|
|
background: #f8f9fa; |
|
|
} |
|
|
""", |
|
|
) as app: |
|
|
gr.Markdown( |
|
|
f""" |
|
|
# {APP_TITLE} |
|
|
|
|
|
{APP_DESCRIPTION} |
|
|
|
|
|
Get AI-powered credit card recommendations that maximize your rewards based on: |
|
|
- 💰 **Reward Rates** - Optimal card selection for each purchase |
|
|
- 📚 **Card Benefits** - Detailed information from our knowledge base |
|
|
- ⚠️ **Spending Caps** - Risk warnings to avoid missing out on bonuses |
|
|
|
|
|
--- |
|
|
""" |
|
|
) |
|
|
|
|
|
agent_status = """ |
|
|
🤖 **Autonomous Agent:** ✅ Active (Claude 3.5 Sonnet) |
|
|
📊 **Mode:** Dynamic Planning + Reasoning |
|
|
⚡ **Services:** Smart Wallet + RAG + Forecast |
|
|
""" |
|
|
gr.Markdown(agent_status) |
|
|
|
|
|
with gr.Tabs(): |
|
|
|
|
|
with gr.Tab("🎯 Get Recommendation"): |
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1): |
|
|
gr.Markdown("### Transaction Details") |
|
|
user_dropdown = gr.Dropdown( |
|
|
choices=SAMPLE_USERS, |
|
|
value=SAMPLE_USERS[0], |
|
|
label="User ID", |
|
|
info="Select a user" |
|
|
) |
|
|
|
|
|
category_dropdown = gr.Dropdown( |
|
|
choices=list(MCC_CATEGORIES.keys()), |
|
|
value="Groceries", |
|
|
label="🏷️ Type of Purchase", |
|
|
info="Select the category first" |
|
|
) |
|
|
|
|
|
merchant_dropdown = gr.Dropdown( |
|
|
choices=MERCHANTS_BY_CATEGORY["Groceries"], |
|
|
value="Whole Foods", |
|
|
label="🏪 Merchant Name", |
|
|
info="Select merchant (changes based on category)", |
|
|
allow_custom_value=True |
|
|
) |
|
|
|
|
|
amount_input = gr.Number( |
|
|
label="💵 Amount (USD)", |
|
|
value=125.50, |
|
|
minimum=0.01, |
|
|
step=0.01 |
|
|
) |
|
|
|
|
|
date_input = gr.Textbox( |
|
|
label="📅 Transaction Date (Optional)", |
|
|
placeholder="YYYY-MM-DD or leave blank for today", |
|
|
value="" |
|
|
) |
|
|
|
|
|
with gr.Accordion("⚙️ Advanced Options", open=False): |
|
|
use_custom_mcc = gr.Checkbox( |
|
|
label="Use Custom MCC Code", |
|
|
value=False |
|
|
) |
|
|
custom_mcc_input = gr.Textbox( |
|
|
label="Custom MCC Code", |
|
|
placeholder="e.g., 5411", |
|
|
visible=False, |
|
|
interactive=True |
|
|
) |
|
|
|
|
|
def toggle_custom_mcc(use_custom): |
|
|
return gr.update(visible=use_custom, interactive=use_custom) |
|
|
|
|
|
use_custom_mcc.change( |
|
|
fn=toggle_custom_mcc, |
|
|
inputs=[use_custom_mcc], |
|
|
outputs=[custom_mcc_input] |
|
|
) |
|
|
|
|
|
recommend_btn = gr.Button( |
|
|
"🚀 Get Recommendation", |
|
|
variant="primary", |
|
|
size="lg" |
|
|
) |
|
|
|
|
|
with gr.Column(scale=2): |
|
|
gr.Markdown("### 💡 Recommendation") |
|
|
recommendation_output = gr.Markdown( |
|
|
value="✨ Select a category and merchant, then click 'Get Recommendation'", |
|
|
elem_classes=["recommendation-output"] |
|
|
) |
|
|
recommendation_chart = gr.Plot() |
|
|
|
|
|
def update_merchant_choices(category): |
|
|
"""Update merchant dropdown based on selected category""" |
|
|
merchants = MERCHANTS_BY_CATEGORY.get(category, ["Custom Merchant"]) |
|
|
return gr.update( |
|
|
choices=merchants, |
|
|
value=merchants[0] if merchants else "" |
|
|
) |
|
|
|
|
|
category_dropdown.change( |
|
|
fn=update_merchant_choices, |
|
|
inputs=[category_dropdown], |
|
|
outputs=[merchant_dropdown] |
|
|
) |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(): |
|
|
gr.Markdown("### 📊 Quick Stats") |
|
|
stats_output = gr.Markdown() |
|
|
|
|
|
with gr.Column(): |
|
|
gr.Markdown("### 🔄 Card Comparison") |
|
|
comparison_output = gr.Markdown() |
|
|
|
|
|
recommend_btn.click( |
|
|
fn=get_recommendation_with_agent, |
|
|
inputs=[user_dropdown, merchant_dropdown, category_dropdown, amount_input], |
|
|
outputs=[recommendation_output, recommendation_chart] |
|
|
) |
|
|
|
|
|
gr.Markdown("### 📝 Example Transactions") |
|
|
gr.Examples( |
|
|
examples=EXAMPLES, |
|
|
inputs=[ |
|
|
user_dropdown, |
|
|
category_dropdown, |
|
|
merchant_dropdown, |
|
|
amount_input, |
|
|
use_custom_mcc, |
|
|
custom_mcc_input, |
|
|
date_input |
|
|
], |
|
|
outputs=[ |
|
|
recommendation_output, |
|
|
comparison_output, |
|
|
stats_output |
|
|
], |
|
|
fn=get_recommendation, |
|
|
cache_examples=False |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Tab("💳 Smart Wallet"): |
|
|
gr.Markdown("## Your Credit Card Portfolio") |
|
|
wallet_user = gr.Dropdown( |
|
|
choices=SAMPLE_USERS, |
|
|
value=SAMPLE_USERS[0], |
|
|
label="👤 Select User" |
|
|
) |
|
|
|
|
|
refresh_wallet_btn = gr.Button("🔄 Refresh Wallet", variant="secondary") |
|
|
|
|
|
wallet_output = gr.Markdown(value="*Loading wallet...*") |
|
|
wallet_chart = gr.Plot() |
|
|
|
|
|
def update_wallet(user_id): |
|
|
return load_user_wallet(user_id) |
|
|
|
|
|
wallet_user.change( |
|
|
fn=update_wallet, |
|
|
inputs=[wallet_user], |
|
|
outputs=[wallet_output, wallet_chart] |
|
|
) |
|
|
|
|
|
refresh_wallet_btn.click( |
|
|
fn=update_wallet, |
|
|
inputs=[wallet_user], |
|
|
outputs=[wallet_output, wallet_chart] |
|
|
) |
|
|
|
|
|
app.load( |
|
|
fn=update_wallet, |
|
|
inputs=[wallet_user], |
|
|
outputs=[wallet_output, wallet_chart] |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Tab("📊 Analytics"): |
|
|
gr.Markdown("## 🎯 Your Rewards Optimization Dashboard") |
|
|
|
|
|
with gr.Row(): |
|
|
analytics_user = gr.Dropdown( |
|
|
choices=SAMPLE_USERS, |
|
|
value=SAMPLE_USERS[0], |
|
|
label="👤 View Analytics For User", |
|
|
scale=3 |
|
|
) |
|
|
refresh_analytics_btn = gr.Button( |
|
|
"🔄 Refresh Analytics", |
|
|
variant="secondary", |
|
|
scale=1 |
|
|
) |
|
|
|
|
|
metrics_display = gr.HTML( |
|
|
value=""" |
|
|
<div style="display: flex; gap: 10px; flex-wrap: wrap;"> |
|
|
<div class="metric-card" style="flex: 1; min-width: 200px;"> |
|
|
<h2>$0</h2> |
|
|
<p>💰 Potential Annual Savings</p> |
|
|
</div> |
|
|
<div class="metric-card metric-card-green" style="flex: 1; min-width: 200px;"> |
|
|
<h2>0%</h2> |
|
|
<p>📈 Rewards Rate Increase</p> |
|
|
</div> |
|
|
<div class="metric-card metric-card-orange" style="flex: 1; min-width: 200px;"> |
|
|
<h2>0</h2> |
|
|
<p>✅ Optimized Transactions</p> |
|
|
</div> |
|
|
<div class="metric-card metric-card-blue" style="flex: 1; min-width: 200px;"> |
|
|
<h2>0/100</h2> |
|
|
<p>⭐ Optimization Score</p> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
) |
|
|
|
|
|
gr.Markdown("---") |
|
|
gr.Markdown("## 📊 Visual Analytics") |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=2): |
|
|
spending_chart = gr.Plot(label="Spending vs Rewards") |
|
|
with gr.Column(scale=1): |
|
|
optimization_gauge = gr.Plot(label="Your Score") |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1): |
|
|
rewards_pie_chart = gr.Plot(label="Rewards Distribution") |
|
|
with gr.Column(scale=1): |
|
|
card_performance_chart = gr.Plot(label="Top Performing Cards") |
|
|
|
|
|
with gr.Row(): |
|
|
trend_chart = gr.Plot(label="12-Month Trends") |
|
|
|
|
|
gr.Markdown("---") |
|
|
gr.Markdown("## 📋 Detailed Breakdown") |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1): |
|
|
gr.Markdown("### 💰 Category Spending Breakdown") |
|
|
spending_table = gr.Markdown( |
|
|
value="*Loading data...*" |
|
|
) |
|
|
|
|
|
with gr.Column(scale=1): |
|
|
gr.Markdown("### 📈 Monthly Trends & Insights") |
|
|
insights_display = gr.Markdown( |
|
|
value="*Loading insights...*" |
|
|
) |
|
|
|
|
|
|
|
|
gr.HTML('<hr class="section-divider">') |
|
|
|
|
|
gr.Markdown(""" |
|
|
<div class="info-box"> |
|
|
<span class="info-box-icon">📊</span> |
|
|
<strong>Current Month Summary</strong> - Quick insights based on your spending so far this month |
|
|
</div> |
|
|
""") |
|
|
|
|
|
current_month_summary = gr.HTML( |
|
|
value=""" |
|
|
<div class="current-month-warning"> |
|
|
<h4>⚠️ This Month's Insights</h4> |
|
|
<p><em>Loading current month data...</em></p> |
|
|
</div> |
|
|
""", |
|
|
label=None |
|
|
) |
|
|
|
|
|
|
|
|
gr.Markdown(""" |
|
|
<div style="text-align: center; margin: 20px 0;"> |
|
|
<p style="color: #666; font-size: 14px;"> |
|
|
Want to see <strong>next month's predictions</strong> and optimization strategies? |
|
|
</p> |
|
|
<p style="margin-top: 10px;"> |
|
|
👉 <span style="color: #667eea; font-weight: 600; font-size: 16px;"> |
|
|
Go to the <strong>Forecast Tab</strong> above → |
|
|
</span> |
|
|
</p> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
analytics_status = gr.Markdown( |
|
|
value="*Select a user to view analytics*", |
|
|
elem_classes=["status-text"] |
|
|
) |
|
|
|
|
|
|
|
|
analytics_user.change( |
|
|
fn=update_analytics_with_charts, |
|
|
inputs=[analytics_user], |
|
|
outputs=[ |
|
|
metrics_display, |
|
|
spending_chart, |
|
|
optimization_gauge, |
|
|
rewards_pie_chart, |
|
|
card_performance_chart, |
|
|
trend_chart, |
|
|
spending_table, |
|
|
insights_display, |
|
|
current_month_summary, |
|
|
analytics_status |
|
|
] |
|
|
) |
|
|
|
|
|
refresh_analytics_btn.click( |
|
|
fn=update_analytics_with_charts, |
|
|
inputs=[analytics_user], |
|
|
outputs=[ |
|
|
metrics_display, |
|
|
spending_chart, |
|
|
optimization_gauge, |
|
|
rewards_pie_chart, |
|
|
card_performance_chart, |
|
|
trend_chart, |
|
|
spending_table, |
|
|
insights_display, |
|
|
current_month_summary, |
|
|
analytics_status |
|
|
] |
|
|
) |
|
|
|
|
|
app.load( |
|
|
fn=update_analytics_with_charts, |
|
|
inputs=[analytics_user], |
|
|
outputs=[ |
|
|
metrics_display, |
|
|
spending_chart, |
|
|
optimization_gauge, |
|
|
rewards_pie_chart, |
|
|
card_performance_chart, |
|
|
trend_chart, |
|
|
spending_table, |
|
|
insights_display, |
|
|
current_month_summary, |
|
|
analytics_status |
|
|
] |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Tab("📈 Forecast"): |
|
|
|
|
|
gr.Markdown(""" |
|
|
<div class="forecast-prediction"> |
|
|
<h3>🔮 AI-Powered Spending Forecast</h3> |
|
|
<p style="color: #1565c0; font-size: 16px; margin: 0;"> |
|
|
Machine learning predictions for your <strong>next 1-3 months</strong> |
|
|
with personalized optimization strategies |
|
|
</p> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
gr.Markdown(""" |
|
|
<div class="info-box"> |
|
|
<span class="info-box-icon">🤖</span> |
|
|
<strong>How it works:</strong> Our AI analyzes your historical spending patterns, |
|
|
seasonal trends, and card benefits to predict future spending and recommend |
|
|
the best cards to maximize your rewards. |
|
|
</div> |
|
|
""") |
|
|
|
|
|
with gr.Row(): |
|
|
forecast_user = gr.Dropdown( |
|
|
choices=SAMPLE_USERS, |
|
|
value=SAMPLE_USERS[0] |
|
|
label="👤 Select User" |
|
|
) |
|
|
|
|
|
refresh_forecast_btn = gr.Button( |
|
|
"🔄 Refresh Forecast", |
|
|
variant="primary", |
|
|
size="sm" |
|
|
) |
|
|
|
|
|
|
|
|
forecast_output = gr.Markdown(value="*Loading forecast...*") |
|
|
forecast_chart = gr.Plot() |
|
|
|
|
|
def update_forecast(user_id): |
|
|
return load_user_forecast(user_id) |
|
|
|
|
|
forecast_user.change( |
|
|
fn=update_forecast, |
|
|
inputs=[forecast_user], |
|
|
outputs=[forecast_output, forecast_chart] |
|
|
) |
|
|
|
|
|
refresh_forecast_btn.click( |
|
|
fn=update_forecast, |
|
|
inputs=[forecast_user], |
|
|
outputs=[forecast_output, forecast_chart] |
|
|
) |
|
|
|
|
|
app.load( |
|
|
fn=update_forecast, |
|
|
inputs=[forecast_user], |
|
|
outputs=[forecast_output, forecast_chart] |
|
|
) |
|
|
|
|
|
forecast_output = gr.HTML(value="<p><em>Loading forecast...</em></p>") |
|
|
forecast_chart = gr.Plot() |
|
|
|
|
|
def update_forecast(user_id): |
|
|
return load_user_forecast(user_id) |
|
|
|
|
|
forecast_user.change( |
|
|
fn=update_forecast, |
|
|
inputs=[forecast_user], |
|
|
outputs=[forecast_output, forecast_chart] |
|
|
) |
|
|
|
|
|
refresh_forecast_btn.click( |
|
|
fn=update_forecast, |
|
|
inputs=[forecast_user], |
|
|
outputs=[forecast_output, forecast_chart] |
|
|
) |
|
|
|
|
|
app.load( |
|
|
fn=update_forecast, |
|
|
inputs=[forecast_user], |
|
|
outputs=[forecast_output, forecast_chart] |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Tab("💬 Ask AI"): |
|
|
gr.Markdown("## Chat with RewardPilot AI") |
|
|
gr.Markdown("*Ask questions about credit cards, rewards, and your spending*") |
|
|
|
|
|
chatbot = gr.Chatbot(height=400, label="AI Assistant") |
|
|
with gr.Row(): |
|
|
msg = gr.Textbox( |
|
|
placeholder="Ask me anything about credit cards...", |
|
|
label="Your Question", |
|
|
scale=4 |
|
|
) |
|
|
send_btn = gr.Button("Send", variant="primary", scale=1) |
|
|
|
|
|
chat_user = gr.Dropdown( |
|
|
choices=["u_alice", "u_bob", "u_charlie"], |
|
|
label="Your Profile", |
|
|
value="u_alice", |
|
|
visible=True |
|
|
) |
|
|
|
|
|
|
|
|
def respond(message, chat_history, user_id): |
|
|
"""Handle chat responses with error handling""" |
|
|
if not message.strip(): |
|
|
return "", chat_history |
|
|
|
|
|
user_context = {} |
|
|
try: |
|
|
analytics = client.get_user_analytics(user_id) |
|
|
if analytics.get('success'): |
|
|
data = analytics.get('data', {}) |
|
|
user_context = { |
|
|
'cards': safe_get(data, 'cards', ['Amex Gold', 'Chase Sapphire Reserve']), |
|
|
'monthly_spending': safe_get(data, 'total_spending', 0), |
|
|
'top_category': safe_get(data, 'top_category', 'Groceries') |
|
|
} |
|
|
except Exception as e: |
|
|
print(f"Error getting user context: {e}") |
|
|
user_context = { |
|
|
'cards': ['Amex Gold', 'Chase Sapphire Reserve'], |
|
|
'monthly_spending': 3450.75, |
|
|
'top_category': 'Groceries' |
|
|
} |
|
|
|
|
|
try: |
|
|
if config.LLM_ENABLED: |
|
|
bot_response = llm.chat_response(message, user_context, chat_history) |
|
|
else: |
|
|
bot_response = "I'm currently in fallback mode. Ask me about specific cards or categories!" |
|
|
except Exception as e: |
|
|
print(f"Chat error: {e}") |
|
|
bot_response = f"I encountered an error. Please try asking your question differently." |
|
|
|
|
|
chat_history.append((message, bot_response)) |
|
|
return "", chat_history |
|
|
|
|
|
msg.submit(respond, [msg, chatbot, chat_user], [msg, chatbot]) |
|
|
send_btn.click(respond, [msg, chatbot, chat_user], [msg, chatbot]) |
|
|
|
|
|
gr.Markdown("### 💡 Try asking:") |
|
|
gr.Examples( |
|
|
examples=[ |
|
|
["Which card should I use at Costco?"], |
|
|
["How can I maximize my grocery rewards?"], |
|
|
["What's the best travel card for international trips?"], |
|
|
["Tell me about the Amex Gold card"], |
|
|
["Am I close to any spending caps?"], |
|
|
], |
|
|
inputs=[msg] |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Tab("ℹ️ Resources"): |
|
|
with gr.Tabs(): |
|
|
|
|
|
with gr.Tab("📖 About"): |
|
|
gr.Markdown( |
|
|
""" |
|
|
## About RewardPilot |
|
|
|
|
|
RewardPilot is an AI-powered credit card recommendation system built using the **Model Context Protocol (MCP)** architecture. |
|
|
|
|
|
### 🏗️ Architecture |
|
|
|
|
|
- 🎯 **Model Context Protocol (MCP)** architecture |
|
|
- 🤖 **LLM-powered explanations** using Llama 3.2 |
|
|
- 📚 **RAG (Retrieval-Augmented Generation)** for card benefits |
|
|
- 📈 **ML-based spending forecasts** |
|
|
- 📊 **Interactive visualizations** |
|
|
|
|
|
### ✨ Features |
|
|
|
|
|
- Smart card recommendations for every purchase |
|
|
- AI-generated personalized insights |
|
|
- Visual analytics dashboard |
|
|
- Conversational AI assistant |
|
|
- Real-time cap warnings |
|
|
- Multi-card comparison |
|
|
|
|
|
### 🔧 System Components |
|
|
|
|
|
The system consists of multiple microservices: |
|
|
|
|
|
1. **Smart Wallet** - Analyzes transaction context and selects optimal cards |
|
|
2. **Rewards-RAG** - Retrieves detailed card benefit information using RAG |
|
|
3. **Spend-Forecast** - Predicts spending patterns and warns about cap risks |
|
|
4. **Orchestrator** - Coordinates all services for comprehensive recommendations |
|
|
|
|
|
### 🎯 How It Works |
|
|
|
|
|
1. **Enter Transaction Details** - Merchant, amount, category |
|
|
2. **AI Analysis** - System analyzes your wallet and transaction context |
|
|
3. **Get Recommendation** - Receive the best card with detailed reasoning |
|
|
4. **Maximize Rewards** - Earn more points/cashback on every purchase |
|
|
|
|
|
### 🔧 Technology Stack |
|
|
|
|
|
- **Backend:** FastAPI, Python |
|
|
- **Frontend:** Gradio |
|
|
- **AI/ML:** RAG (Retrieval-Augmented Generation) |
|
|
- **LLM:** Claude 3.5 Sonnet (Anthropic) |
|
|
- **Architecture:** MCP (Model Context Protocol) |
|
|
- **Deployment:** Hugging Face Spaces |
|
|
|
|
|
### 📚 MCC Categories Supported |
|
|
|
|
|
- Groceries (5411) |
|
|
- Restaurants (5812) |
|
|
- Gas Stations (5541) |
|
|
- Airlines (3000-3999) |
|
|
- Hotels (7011) |
|
|
- Entertainment (7832, 7841) |
|
|
- Online Shopping (5999) |
|
|
- And many more... |
|
|
|
|
|
### 🎓 Built For |
|
|
|
|
|
**MCP 1st Birthday Hackathon** - Celebrating one year of the Model Context Protocol |
|
|
|
|
|
### 👨💻 Developer |
|
|
|
|
|
Built with ❤️ for the MCP community |
|
|
|
|
|
--- |
|
|
|
|
|
**Version:** 1.0.0 |
|
|
**Last Updated:** November 2025 |
|
|
""" |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Tab("🔍 Agent Insight"): |
|
|
gr.Markdown(""" |
|
|
## How the Autonomous Agent Works |
|
|
|
|
|
RewardPilot uses **Claude 3.5 Sonnet** as an autonomous agent to provide intelligent card recommendations. |
|
|
|
|
|
### 🎯 **Phase 1: Planning** |
|
|
|
|
|
The agent analyzes your transaction and decides: |
|
|
- Which microservices to call (Smart Wallet, RAG, Forecast) |
|
|
- In what order to call them |
|
|
- What to optimize for (rewards, caps, benefits) |
|
|
- Confidence level of the plan |
|
|
|
|
|
### 🤔 **Phase 2: Execution** |
|
|
|
|
|
The agent dynamically: |
|
|
- Calls services based on the plan |
|
|
- Handles failures gracefully |
|
|
- Adapts if services are unavailable |
|
|
- Collects all relevant data |
|
|
|
|
|
### 🧠 **Phase 3: Reasoning** |
|
|
|
|
|
The agent synthesizes results to: |
|
|
- Explain **why** this card is best |
|
|
- Identify potential risks or warnings |
|
|
- Suggest alternative options |
|
|
- Calculate annual impact |
|
|
|
|
|
### 📚 **Phase 4: Learning** |
|
|
|
|
|
The agent improves over time by: |
|
|
- Storing past decisions |
|
|
- Learning from user feedback |
|
|
- Adjusting strategies for similar transactions |
|
|
- Building a knowledge base |
|
|
|
|
|
--- |
|
|
|
|
|
### 🔑 **Key Features** |
|
|
|
|
|
✅ **Natural Language Explanations** - Understands context like a human |
|
|
✅ **Dynamic Planning** - Adapts to your specific situation |
|
|
✅ **Confidence Scoring** - Tells you how certain it is |
|
|
✅ **Multi-Service Coordination** - Orchestrates 3 microservices |
|
|
✅ **Self-Correction** - Learns from mistakes |
|
|
|
|
|
--- |
|
|
|
|
|
### 📊 **Example Agent Plan** |
|
|
|
|
|
```json |
|
|
{ |
|
|
"strategy": "Optimize for grocery rewards with cap monitoring", |
|
|
"service_calls": [ |
|
|
{"service": "smart_wallet", "priority": 1, "reason": "Get base recommendation"}, |
|
|
{"service": "spend_forecast", "priority": 2, "reason": "Check spending caps"}, |
|
|
{"service": "rewards_rag", "priority": 3, "reason": "Get detailed benefits"} |
|
|
], |
|
|
"confidence": 0.92, |
|
|
"expected_outcome": "Recommend Amex Gold for 4x grocery points" |
|
|
} |
|
|
``` |
|
|
|
|
|
--- |
|
|
|
|
|
### 🎓 **Powered By** |
|
|
|
|
|
- **Model**: Claude 3.5 Sonnet (Anthropic) |
|
|
- **Architecture**: Autonomous Agent Pattern |
|
|
- **Framework**: LangChain + Custom Logic |
|
|
- **Memory**: Redis (for learning) |
|
|
|
|
|
--- |
|
|
|
|
|
**Try it out in the "Get Recommendation" tab!** 🚀 |
|
|
""") |
|
|
|
|
|
|
|
|
with gr.Tab("📚 API Documentation"): |
|
|
api_docs_html = """ |
|
|
<div style="font-family: system-ui; padding: 20px;"> |
|
|
<h2>📡 API Endpoints</h2> |
|
|
|
|
|
<h3>Orchestrator API</h3> |
|
|
<p><strong>Base URL:</strong> <code>https://mcp-1st-birthday-rewardpilot-orchestrator.hf.space</code></p> |
|
|
|
|
|
<h4>POST /recommend</h4> |
|
|
<p>Get comprehensive card recommendation.</p> |
|
|
|
|
|
<pre style="background: #f5f5f5; padding: 15px; border-radius: 5px; overflow-x: auto;"> |
|
|
{ |
|
|
"user_id": "u_alice", |
|
|
"merchant": "Whole Foods", |
|
|
"mcc": "5411", |
|
|
"amount_usd": 125.50, |
|
|
"transaction_date": "2025-01-15" |
|
|
} |
|
|
</pre> |
|
|
|
|
|
<h4>GET /analytics/{user_id}</h4> |
|
|
<p>Get user analytics and spending insights.</p> |
|
|
|
|
|
<pre style="background: #f5f5f5; padding: 15px; border-radius: 5px; overflow-x: auto;"> |
|
|
GET /analytics/u_alice |
|
|
</pre> |
|
|
|
|
|
<hr> |
|
|
|
|
|
<h3>Other Services</h3> |
|
|
<ul> |
|
|
<li><strong>Smart Wallet:</strong> https://mcp-1st-birthday-rewardpilot-smart-wallet.hf.space</li> |
|
|
<li><strong>Rewards-RAG:</strong> https://mcp-1st-birthday-rewardpilot-rewards-rag.hf.space</li> |
|
|
<li><strong>Spend-Forecast:</strong> https://mcp-1st-birthday-rewardpilot-spend-forecast.hf.space</li> |
|
|
</ul> |
|
|
|
|
|
<hr> |
|
|
|
|
|
<h3>📚 Interactive Docs</h3> |
|
|
<p>Visit <code>/docs</code> on any service for Swagger UI:</p> |
|
|
<ul> |
|
|
<li><a href="https://mcp-1st-birthday-rewardpilot-orchestrator.hf.space/docs" target="_blank">Orchestrator Docs</a></li> |
|
|
<li><a href="https://mcp-1st-birthday-rewardpilot-smart-wallet.hf.space/docs" target="_blank">Smart Wallet Docs</a></li> |
|
|
<li><a href="https://mcp-1st-birthday-rewardpilot-rewards-rag.hf.space/docs" target="_blank">Rewards-RAG Docs</a></li> |
|
|
<li><a href="https://mcp-1st-birthday-rewardpilot-spend-forecast.hf.space/docs" target="_blank">Spend-Forecast Docs</a></li> |
|
|
</ul> |
|
|
|
|
|
<hr> |
|
|
|
|
|
<h3>🔧 cURL Example</h3> |
|
|
<pre style="background: #f5f5f5; padding: 15px; border-radius: 5px; overflow-x: auto;"> |
|
|
curl -X POST https://mcp-1st-birthday-rewardpilot-orchestrator.hf.space/recommend \\ |
|
|
-H "Content-Type: application/json" \\ |
|
|
-d '{ |
|
|
"user_id": "u_alice", |
|
|
"merchant": "Whole Foods", |
|
|
"mcc": "5411", |
|
|
"amount_usd": 125.50 |
|
|
}' |
|
|
</pre> |
|
|
|
|
|
<hr> |
|
|
|
|
|
<h3>🐍 Python Example</h3> |
|
|
<pre style="background: #f5f5f5; padding: 15px; border-radius: 5px; overflow-x: auto;"> |
|
|
import requests |
|
|
|
|
|
url = "https://mcp-1st-birthday-rewardpilot-orchestrator.hf.space/recommend" |
|
|
payload = { |
|
|
"user_id": "u_alice", |
|
|
"merchant": "Whole Foods", |
|
|
"mcc": "5411", |
|
|
"amount_usd": 125.50 |
|
|
} |
|
|
|
|
|
response = requests.post(url, json=payload) |
|
|
print(response.json()) |
|
|
</pre> |
|
|
|
|
|
<hr> |
|
|
|
|
|
<h3>📋 Response Format</h3> |
|
|
<pre style="background: #f5f5f5; padding: 15px; border-radius: 5px; overflow-x: auto;"> |
|
|
{ |
|
|
"recommended_card": "c_amex_gold", |
|
|
"rewards_earned": 5.02, |
|
|
"rewards_rate": "4x points", |
|
|
"confidence": 0.95, |
|
|
"reasoning": "Amex Gold offers 4x points on groceries...", |
|
|
"alternative_options": [ |
|
|
{ |
|
|
"card": "c_citi_custom_cash", |
|
|
"reward_amount": 6.28, |
|
|
"reason": "5% cashback on groceries..." |
|
|
} |
|
|
], |
|
|
"warnings": [ |
|
|
"You're approaching your $500 monthly cap" |
|
|
] |
|
|
} |
|
|
</pre> |
|
|
|
|
|
<hr> |
|
|
|
|
|
<h3>🔐 Authentication</h3> |
|
|
<p>Currently, the API is open for demo purposes. In production, you would need:</p> |
|
|
<ul> |
|
|
<li>API Key in headers: <code>X-API-Key: your_key_here</code></li> |
|
|
<li>OAuth 2.0 for user-specific data</li> |
|
|
</ul> |
|
|
|
|
|
<hr> |
|
|
|
|
|
<h3>⚡ Rate Limits</h3> |
|
|
<ul> |
|
|
<li><strong>Free Tier:</strong> 100 requests/hour</li> |
|
|
<li><strong>Pro Tier:</strong> 1000 requests/hour</li> |
|
|
<li><strong>Enterprise:</strong> Unlimited</li> |
|
|
</ul> |
|
|
|
|
|
<hr> |
|
|
|
|
|
<h3>❓ Support</h3> |
|
|
<p>For API support, please visit our <a href="https://github.com/your-repo" target="_blank">GitHub repository</a> or contact support.</p> |
|
|
</div> |
|
|
""" |
|
|
gr.HTML(api_docs_html) |
|
|
|
|
|
|
|
|
with gr.Tab("❓ FAQs"): |
|
|
gr.Markdown(""" |
|
|
## Frequently Asked Questions |
|
|
|
|
|
### General Questions |
|
|
|
|
|
**Q: What is RewardPilot?** |
|
|
A: RewardPilot is an AI-powered system that recommends the best credit card to use for each transaction to maximize your rewards. |
|
|
|
|
|
**Q: How does it work?** |
|
|
A: It analyzes your transaction details (merchant, amount, category) against your credit card portfolio and recommends the card that will earn you the most rewards. |
|
|
|
|
|
**Q: Is my data secure?** |
|
|
A: Yes! All data is encrypted and we follow industry-standard security practices. We never store sensitive card information like CVV or full card numbers. |
|
|
|
|
|
--- |
|
|
|
|
|
### Using the System |
|
|
|
|
|
**Q: How accurate are the recommendations?** |
|
|
A: Our AI agent has a 95%+ confidence rate for most recommendations. The system considers reward rates, spending caps, and category bonuses. |
|
|
|
|
|
**Q: What if I don't have the recommended card?** |
|
|
A: The system shows alternative options from your wallet. You can also view the "Alternative Options" section for other good choices. |
|
|
|
|
|
**Q: Can I add custom MCC codes?** |
|
|
A: Yes! Use the "Advanced Options" section in the Get Recommendation tab to enter custom MCC codes. |
|
|
|
|
|
--- |
|
|
|
|
|
### Analytics & Forecasts |
|
|
|
|
|
**Q: How is the optimization score calculated?** |
|
|
A: It's based on reward rates (30%), cap availability (25%), annual fee value (20%), category match (20%), and penalties (5%). |
|
|
|
|
|
**Q: How accurate are the spending forecasts?** |
|
|
A: Our ML models achieve 85-92% accuracy based on your historical spending patterns. |
|
|
|
|
|
**Q: Can I export my analytics data?** |
|
|
A: This feature is coming soon! You'll be able to export to CSV and PDF. |
|
|
|
|
|
--- |
|
|
|
|
|
### Technical Questions |
|
|
|
|
|
**Q: What APIs does RewardPilot use?** |
|
|
A: We use 4 main services: Orchestrator, Smart Wallet, Rewards-RAG, and Spend-Forecast. |
|
|
|
|
|
**Q: Can I integrate RewardPilot into my app?** |
|
|
A: Yes! Check the API Documentation tab for integration details. |
|
|
|
|
|
**Q: What LLM powers the AI agent?** |
|
|
A: We use Claude 3.5 Sonnet by Anthropic for intelligent reasoning and explanations. |
|
|
|
|
|
--- |
|
|
|
|
|
### Troubleshooting |
|
|
|
|
|
**Q: Why am I seeing "Demo Mode" warnings?** |
|
|
A: This means the system is using mock data. Ensure the orchestrator API is connected. |
|
|
|
|
|
**Q: The recommendation seems wrong. Why?** |
|
|
A: Check the "Agent Insight" tab to see the reasoning. If you still think it's wrong, please report it. |
|
|
|
|
|
**Q: How do I report a bug?** |
|
|
A: Please open an issue on our [GitHub repository](https://github.com/your-repo). |
|
|
|
|
|
--- |
|
|
|
|
|
**Still have questions?** Contact us at [email protected] |
|
|
""") |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
app.launch( |
|
|
server_name="0.0.0.0", |
|
|
server_port=7860, |
|
|
share=False, |
|
|
) |
|
|
|