Update app.py
Browse files
app.py
CHANGED
|
@@ -1,11 +1,32 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
|
|
|
| 9 |
|
| 10 |
def safe_get(data: Dict, key: str, default: Any = None) -> Any:
|
| 11 |
"""Safely get value from dictionary with fallback"""
|
|
@@ -14,16 +35,13 @@ def safe_get(data: Dict, key: str, default: Any = None) -> Any:
|
|
| 14 |
except:
|
| 15 |
return default
|
| 16 |
|
| 17 |
-
|
| 18 |
def normalize_recommendation_data(data: Dict) -> Dict:
|
| 19 |
"""
|
| 20 |
Normalize API response to ensure all required fields exist.
|
| 21 |
Handles both orchestrator format and mock data format.
|
| 22 |
"""
|
| 23 |
|
| 24 |
-
# Check if this is mock data (simpler format)
|
| 25 |
if data.get('mock_data'):
|
| 26 |
-
# Mock data has simpler structure
|
| 27 |
return {
|
| 28 |
'recommended_card': safe_get(data, 'recommended_card', 'Unknown Card'),
|
| 29 |
'rewards_earned': float(safe_get(data, 'rewards_earned', 0)),
|
|
@@ -39,7 +57,6 @@ def normalize_recommendation_data(data: Dict) -> Dict:
|
|
| 39 |
'mock_data': True
|
| 40 |
}
|
| 41 |
|
| 42 |
-
# Handle orchestrator format (recommended_card is a dict)
|
| 43 |
recommended_card = safe_get(data, 'recommended_card', {})
|
| 44 |
if isinstance(recommended_card, dict):
|
| 45 |
card_name = safe_get(recommended_card, 'card_name', 'Unknown Card')
|
|
@@ -48,13 +65,11 @@ def normalize_recommendation_data(data: Dict) -> Dict:
|
|
| 48 |
category = safe_get(recommended_card, 'category', 'Unknown')
|
| 49 |
reasoning = safe_get(recommended_card, 'reasoning', 'Optimal choice')
|
| 50 |
|
| 51 |
-
# Format reward rate as string
|
| 52 |
if reward_rate > 0:
|
| 53 |
rewards_rate_str = f"{reward_rate}x points"
|
| 54 |
else:
|
| 55 |
rewards_rate_str = "N/A"
|
| 56 |
else:
|
| 57 |
-
# Handle case where it's already a string
|
| 58 |
card_name = str(recommended_card) if recommended_card else 'Unknown Card'
|
| 59 |
reward_amount = float(safe_get(data, 'rewards_earned', 0))
|
| 60 |
reward_rate = 0
|
|
@@ -62,18 +77,14 @@ def normalize_recommendation_data(data: Dict) -> Dict:
|
|
| 62 |
category = safe_get(data, 'category', 'Unknown')
|
| 63 |
reasoning = safe_get(data, 'reasoning', 'Optimal choice')
|
| 64 |
|
| 65 |
-
# Get merchant and amount from top level
|
| 66 |
merchant = safe_get(data, 'merchant', 'Unknown Merchant')
|
| 67 |
amount = float(safe_get(data, 'amount_usd', safe_get(data, 'amount', 0)))
|
| 68 |
-
|
| 69 |
-
# Calculate annual potential
|
| 70 |
annual_potential = reward_amount * 12 if reward_amount > 0 else 0
|
| 71 |
|
| 72 |
-
# Process alternative cards
|
| 73 |
alternatives = []
|
| 74 |
alt_cards = safe_get(data, 'alternative_cards', safe_get(data, 'alternatives', []))
|
| 75 |
|
| 76 |
-
for alt in alt_cards[:3]:
|
| 77 |
if isinstance(alt, dict):
|
| 78 |
alt_name = safe_get(alt, 'card_name', safe_get(alt, 'card', 'Unknown'))
|
| 79 |
alt_reward = float(safe_get(alt, 'reward_amount', safe_get(alt, 'rewards', 0)))
|
|
@@ -90,7 +101,6 @@ def normalize_recommendation_data(data: Dict) -> Dict:
|
|
| 90 |
'rate': alt_rate_str
|
| 91 |
})
|
| 92 |
|
| 93 |
-
# Extract warnings
|
| 94 |
warnings = safe_get(data, 'warnings', [])
|
| 95 |
forecast_warning = safe_get(data, 'forecast_warning')
|
| 96 |
if forecast_warning and isinstance(forecast_warning, dict):
|
|
@@ -98,7 +108,6 @@ def normalize_recommendation_data(data: Dict) -> Dict:
|
|
| 98 |
if warning_msg:
|
| 99 |
warnings.append(warning_msg)
|
| 100 |
|
| 101 |
-
# Build normalized response
|
| 102 |
normalized = {
|
| 103 |
'recommended_card': card_name,
|
| 104 |
'rewards_earned': round(reward_amount, 2),
|
|
@@ -120,39 +129,6 @@ def create_loading_state():
|
|
| 120 |
"""Create loading indicator message"""
|
| 121 |
return "⏳ **Loading...** Please wait while we fetch your recommendation.", None
|
| 122 |
|
| 123 |
-
|
| 124 |
-
from datetime import date
|
| 125 |
-
from typing import Optional, Tuple, List, Dict, Any
|
| 126 |
-
import gradio as gr
|
| 127 |
-
from config import (
|
| 128 |
-
APP_TITLE, APP_DESCRIPTION, THEME,
|
| 129 |
-
MCC_CATEGORIES, SAMPLE_USERS,
|
| 130 |
-
MERCHANTS_BY_CATEGORY
|
| 131 |
-
)
|
| 132 |
-
from utils.api_client import RewardPilotClient
|
| 133 |
-
from utils.formatters import (
|
| 134 |
-
format_full_recommendation,
|
| 135 |
-
format_comparison_table,
|
| 136 |
-
format_analytics_metrics,
|
| 137 |
-
create_spending_chart,
|
| 138 |
-
create_rewards_pie_chart,
|
| 139 |
-
create_optimization_gauge,
|
| 140 |
-
create_trend_line_chart,
|
| 141 |
-
create_card_performance_chart
|
| 142 |
-
)
|
| 143 |
-
import plotly.graph_objects as go
|
| 144 |
-
import gradio as gr
|
| 145 |
-
from utils.api_client import RewardPilotClient
|
| 146 |
-
from utils.llm_explainer import get_llm_explainer
|
| 147 |
-
import config
|
| 148 |
-
|
| 149 |
-
# ===================== CARD DATABASE LOADER =====================
|
| 150 |
-
import json
|
| 151 |
-
import os
|
| 152 |
-
|
| 153 |
-
# Path to cards.json
|
| 154 |
-
CARDS_FILE = os.path.join(os.path.dirname(__file__), "data", "cards.json")
|
| 155 |
-
|
| 156 |
def load_card_database() -> dict:
|
| 157 |
"""Load card database from local cards.json"""
|
| 158 |
try:
|
|
@@ -167,7 +143,6 @@ def load_card_database() -> dict:
|
|
| 167 |
print(f"❌ Error parsing cards.json: {e}")
|
| 168 |
return {}
|
| 169 |
|
| 170 |
-
# Load at startup
|
| 171 |
CARD_DATABASE = load_card_database()
|
| 172 |
|
| 173 |
def get_card_details(card_id: str, mcc: str = None) -> dict:
|
|
@@ -193,17 +168,14 @@ def get_card_details(card_id: str, mcc: str = None) -> dict:
|
|
| 193 |
}
|
| 194 |
|
| 195 |
card = CARD_DATABASE[card_id]
|
| 196 |
-
|
| 197 |
-
# Get reward rate for specific MCC (if provided)
|
| 198 |
reward_rate = 1.0
|
|
|
|
| 199 |
if mcc and "reward_structure" in card:
|
| 200 |
reward_structure = card["reward_structure"]
|
| 201 |
|
| 202 |
-
# Check exact MCC match
|
| 203 |
if mcc in reward_structure:
|
| 204 |
reward_rate = reward_structure[mcc]
|
| 205 |
else:
|
| 206 |
-
# Check MCC ranges (e.g., "3001-3999")
|
| 207 |
try:
|
| 208 |
mcc_int = int(mcc)
|
| 209 |
for key, rate in reward_structure.items():
|
|
@@ -215,11 +187,9 @@ def get_card_details(card_id: str, mcc: str = None) -> dict:
|
|
| 215 |
except (ValueError, AttributeError):
|
| 216 |
pass
|
| 217 |
|
| 218 |
-
# Use default if no match found
|
| 219 |
if reward_rate == 1.0 and "default" in reward_structure:
|
| 220 |
reward_rate = reward_structure["default"]
|
| 221 |
|
| 222 |
-
# Extract spending cap info
|
| 223 |
spending_caps = card.get("spending_caps", {})
|
| 224 |
cap_info = {}
|
| 225 |
|
|
@@ -250,13 +220,8 @@ def get_card_details(card_id: str, mcc: str = None) -> dict:
|
|
| 250 |
"spending_caps": cap_info,
|
| 251 |
"benefits": card.get("benefits", [])
|
| 252 |
}
|
| 253 |
-
# ===================== END CARD DATABASE =====================
|
| 254 |
-
|
| 255 |
|
| 256 |
def get_recommendation_with_agent(user_id, merchant, category, amount):
|
| 257 |
-
import httpx
|
| 258 |
-
import json
|
| 259 |
-
|
| 260 |
yield "⏳ **Agent is thinking...** Analyzing your transaction and cards...", None
|
| 261 |
|
| 262 |
try:
|
|
@@ -294,7 +259,6 @@ def get_recommendation_with_agent(user_id, merchant, category, amount):
|
|
| 294 |
|
| 295 |
print(f"🔍 KEYS: {list(result.keys())}")
|
| 296 |
|
| 297 |
-
# ========== EXTRACT BASIC DATA ==========
|
| 298 |
card_id = result.get('recommended_card', 'Unknown')
|
| 299 |
rewards_earned = float(result.get('rewards_earned', 0))
|
| 300 |
rewards_rate = result.get('rewards_rate', 'N/A')
|
|
@@ -303,7 +267,6 @@ def get_recommendation_with_agent(user_id, merchant, category, amount):
|
|
| 303 |
alternatives = result.get('alternative_options', [])
|
| 304 |
warnings = result.get('warnings', [])
|
| 305 |
|
| 306 |
-
# Map card_id to card_name
|
| 307 |
card_name_map = {
|
| 308 |
'c_citi_custom_cash': 'Citi Custom Cash',
|
| 309 |
'c_amex_gold': 'American Express Gold',
|
|
@@ -316,67 +279,18 @@ def get_recommendation_with_agent(user_id, merchant, category, amount):
|
|
| 316 |
}
|
| 317 |
card_name = card_name_map.get(card_id, card_id.replace('c_', '').replace('_', ' ').title())
|
| 318 |
|
| 319 |
-
# # ========== CARD DATABASE (FALLBACK) ==========
|
| 320 |
-
# # If API doesn't provide card_details, use this database
|
| 321 |
-
# CARD_DATABASE = {
|
| 322 |
-
# 'c_citi_custom_cash': {
|
| 323 |
-
# 'reward_rate': 5.0,
|
| 324 |
-
# 'monthly_cap': 500,
|
| 325 |
-
# 'base_rate': 1.0,
|
| 326 |
-
# 'annual_fee': 0,
|
| 327 |
-
# 'cap_type': 'monthly'
|
| 328 |
-
# },
|
| 329 |
-
# 'c_amex_gold': {
|
| 330 |
-
# 'reward_rate': 4.0,
|
| 331 |
-
# 'annual_cap': 25000,
|
| 332 |
-
# 'base_rate': 1.0,
|
| 333 |
-
# 'annual_fee': 250,
|
| 334 |
-
# 'cap_type': 'annual'
|
| 335 |
-
# },
|
| 336 |
-
# 'c_chase_sapphire_reserve': {
|
| 337 |
-
# 'reward_rate': 3.0,
|
| 338 |
-
# 'monthly_cap': None,
|
| 339 |
-
# 'base_rate': 3.0,
|
| 340 |
-
# 'annual_fee': 550,
|
| 341 |
-
# 'cap_type': 'none'
|
| 342 |
-
# },
|
| 343 |
-
# 'c_chase_freedom_unlimited': {
|
| 344 |
-
# 'reward_rate': 1.5,
|
| 345 |
-
# 'monthly_cap': None,
|
| 346 |
-
# 'base_rate': 1.5,
|
| 347 |
-
# 'annual_fee': 0,
|
| 348 |
-
# 'cap_type': 'none'
|
| 349 |
-
# },
|
| 350 |
-
# 'c_chase_sapphire_preferred': {
|
| 351 |
-
# 'reward_rate': 2.0,
|
| 352 |
-
# 'monthly_cap': None,
|
| 353 |
-
# 'base_rate': 2.0,
|
| 354 |
-
# 'annual_fee': 95,
|
| 355 |
-
# 'cap_type': 'none'
|
| 356 |
-
# }
|
| 357 |
-
# }
|
| 358 |
-
|
| 359 |
-
# ========== GET CARD DETAILS (FROM cards.json) ==========
|
| 360 |
-
# Get MCC from transaction
|
| 361 |
transaction_mcc = result.get('mcc', MCC_CATEGORIES.get(category, "5999"))
|
| 362 |
-
|
| 363 |
-
# Load card details from our database
|
| 364 |
card_details_from_db = get_card_details(card_id, transaction_mcc)
|
| 365 |
-
|
| 366 |
-
# Use API card_details if available, otherwise use our database
|
| 367 |
card_details = result.get('card_details', {})
|
| 368 |
|
| 369 |
if not card_details or not card_details.get('reward_rate'):
|
| 370 |
-
# Convert our database format to the format expected by the rest of the code
|
| 371 |
reward_structure = CARD_DATABASE.get(card_id, {}).get('reward_structure', {})
|
| 372 |
|
| 373 |
-
# Get reward rate for this MCC
|
| 374 |
if transaction_mcc in reward_structure:
|
| 375 |
reward_rate_value = reward_structure[transaction_mcc]
|
| 376 |
else:
|
| 377 |
reward_rate_value = reward_structure.get('default', 1.0)
|
| 378 |
|
| 379 |
-
# Get spending caps
|
| 380 |
spending_caps_db = CARD_DATABASE.get(card_id, {}).get('spending_caps', {})
|
| 381 |
|
| 382 |
card_details = {
|
|
@@ -400,17 +314,15 @@ def get_recommendation_with_agent(user_id, merchant, category, amount):
|
|
| 400 |
|
| 401 |
print(f"✅ CARD DETAILS: {reward_rate_value}%, cap={monthly_cap or annual_cap}, fee=${annual_fee}")
|
| 402 |
|
| 403 |
-
# ========== CALCULATE ANNUAL PROJECTION ==========
|
| 404 |
amount_float = float(amount)
|
| 405 |
|
| 406 |
-
# Frequency assumptions by category
|
| 407 |
frequency_map = {
|
| 408 |
-
'Groceries': 52,
|
| 409 |
'Restaurants': 52,
|
| 410 |
'Gas Stations': 52,
|
| 411 |
'Fast Food': 52,
|
| 412 |
-
'Airlines': 4,
|
| 413 |
-
'Hotels': 12,
|
| 414 |
'Online Shopping': 24,
|
| 415 |
'Entertainment': 24,
|
| 416 |
}
|
|
@@ -425,9 +337,7 @@ def get_recommendation_with_agent(user_id, merchant, category, amount):
|
|
| 425 |
|
| 426 |
annual_spend = amount_float * frequency
|
| 427 |
|
| 428 |
-
# ========== TIERED CALCULATION ==========
|
| 429 |
if monthly_cap:
|
| 430 |
-
# Monthly cap logic (e.g., Citi Custom Cash)
|
| 431 |
monthly_cap_annual = monthly_cap * 12
|
| 432 |
|
| 433 |
if annual_spend <= monthly_cap_annual:
|
|
@@ -450,7 +360,6 @@ def get_recommendation_with_agent(user_id, merchant, category, amount):
|
|
| 450 |
| **Net Rewards** | - | - | **${total_rewards - annual_fee:.2f}** |"""
|
| 451 |
|
| 452 |
elif annual_cap:
|
| 453 |
-
# Annual cap logic (e.g., Amex Gold)
|
| 454 |
if annual_spend <= annual_cap:
|
| 455 |
high_rate_spend = annual_spend
|
| 456 |
low_rate_spend = 0
|
|
@@ -471,7 +380,6 @@ def get_recommendation_with_agent(user_id, merchant, category, amount):
|
|
| 471 |
| **Net Rewards** | - | - | **${total_rewards - annual_fee:.2f}** |"""
|
| 472 |
|
| 473 |
else:
|
| 474 |
-
# No cap - flat rate
|
| 475 |
total_rewards = annual_spend * (reward_rate_value / 100)
|
| 476 |
|
| 477 |
calc_table = f"""| Spending Tier | Annual Amount | Rate | Rewards |
|
|
@@ -480,7 +388,6 @@ def get_recommendation_with_agent(user_id, merchant, category, amount):
|
|
| 480 |
| Annual fee | - | - | -${annual_fee:.2f} |
|
| 481 |
| **Net Rewards** | - | - | **${total_rewards - annual_fee:.2f}** |"""
|
| 482 |
|
| 483 |
-
# ========== BASELINE COMPARISON ==========
|
| 484 |
baseline_rewards = annual_spend * 0.01
|
| 485 |
net_rewards = total_rewards - annual_fee
|
| 486 |
net_benefit = net_rewards - baseline_rewards
|
|
@@ -495,25 +402,21 @@ def get_recommendation_with_agent(user_id, merchant, category, amount):
|
|
| 495 |
|
| 496 |
**Net Benefit: ${net_benefit:+.2f}/year** {"🎉" if net_benefit > 0 else "⚠️"}"""
|
| 497 |
|
| 498 |
-
|
| 499 |
-
# Calculate score based on performance
|
| 500 |
-
max_possible_rewards = annual_spend * 0.06 # Theoretical max (6%)
|
| 501 |
|
| 502 |
if max_possible_rewards > 0:
|
| 503 |
performance_ratio = (net_rewards / max_possible_rewards) * 100
|
| 504 |
|
| 505 |
-
# Bonus for beating baseline
|
| 506 |
if net_rewards > baseline_rewards:
|
| 507 |
improvement = (net_rewards - baseline_rewards) / baseline_rewards
|
| 508 |
baseline_bonus = min(improvement * 20, 20)
|
| 509 |
else:
|
| 510 |
-
baseline_bonus = -10
|
| 511 |
|
| 512 |
optimization_score = int(min(performance_ratio + baseline_bonus, 100))
|
| 513 |
else:
|
| 514 |
optimization_score = 0
|
| 515 |
|
| 516 |
-
# Score breakdown
|
| 517 |
score_breakdown = {
|
| 518 |
'reward_rate': min(30, int(optimization_score * 0.30)),
|
| 519 |
'cap_availability': min(25, int(optimization_score * 0.25)),
|
|
@@ -538,7 +441,6 @@ def get_recommendation_with_agent(user_id, merchant, category, amount):
|
|
| 538 |
- 60-69: Acceptable ⚠️
|
| 539 |
- <60: Suboptimal ❌"""
|
| 540 |
|
| 541 |
-
# ========== FORMAT OUTPUT ==========
|
| 542 |
output = f"""## 🤖 AI Agent Recommendation
|
| 543 |
|
| 544 |
### 💳 Recommended Card: **{card_name}**
|
|
@@ -549,12 +451,10 @@ def get_recommendation_with_agent(user_id, merchant, category, amount):
|
|
| 549 |
---
|
| 550 |
|
| 551 |
### 🧠 Agent's Reasoning:
|
| 552 |
-
|
| 553 |
{reasoning}
|
| 554 |
|
| 555 |
---"""
|
| 556 |
|
| 557 |
-
# Add alternatives
|
| 558 |
if alternatives:
|
| 559 |
output += "\n### 🔄 Alternative Options:\n\n"
|
| 560 |
for alt in alternatives[:3]:
|
|
@@ -563,16 +463,13 @@ def get_recommendation_with_agent(user_id, merchant, category, amount):
|
|
| 563 |
alt_reason = alt.get('reason', '')
|
| 564 |
output += f"**{alt_card_name}:**\n{alt_reason}\n\n"
|
| 565 |
|
| 566 |
-
# Add warnings
|
| 567 |
if warnings:
|
| 568 |
output += "\n### ⚠️ Important Warnings:\n\n"
|
| 569 |
for warning in warnings:
|
| 570 |
output += f"- {warning}\n"
|
| 571 |
|
| 572 |
-
# Add annual impact with expandable details
|
| 573 |
output += f"""
|
| 574 |
### 💰 Annual Impact
|
| 575 |
-
|
| 576 |
- **Potential Savings:** ${net_benefit:.2f}/year
|
| 577 |
- **Optimization Score:** {optimization_score}/100
|
| 578 |
|
|
@@ -582,24 +479,20 @@ def get_recommendation_with_agent(user_id, merchant, category, amount):
|
|
| 582 |
#### 💡 Calculation Assumptions:
|
| 583 |
|
| 584 |
**Step 1: Estimate Annual Spending**
|
| 585 |
-
|
| 586 |
Current transaction: ${amount_float:.2f} at {merchant}
|
| 587 |
Category: {category}
|
| 588 |
Frequency assumption: {frequency_label.capitalize()}
|
| 589 |
Annual estimate: ${amount_float:.2f} × {frequency} = **${annual_spend:.2f}**
|
| 590 |
|
| 591 |
**Step 2: Calculate Rewards with {card_name}**
|
| 592 |
-
|
| 593 |
{calc_table}
|
| 594 |
|
| 595 |
**Step 3: Compare to Baseline**
|
| 596 |
-
|
| 597 |
{comparison_text}
|
| 598 |
|
| 599 |
---
|
| 600 |
|
| 601 |
#### 📈 Optimization Score: {optimization_score}/100
|
| 602 |
-
|
| 603 |
{score_details}
|
| 604 |
|
| 605 |
---
|
|
@@ -613,10 +506,9 @@ Annual estimate: ${amount_float:.2f} × {frequency} = **${annual_spend:.2f}**
|
|
| 613 |
|
| 614 |
</details>
|
| 615 |
|
| 616 |
-
---
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
output += f"""### 📊 Transaction Details:
|
| 620 |
- **Amount:** ${amount_float:.2f}
|
| 621 |
- **Merchant:** {merchant}
|
| 622 |
- **Category:** {category}
|
|
@@ -630,13 +522,11 @@ Annual estimate: ${amount_float:.2f} × {frequency} = **${annual_spend:.2f}**
|
|
| 630 |
print("=" * 80)
|
| 631 |
|
| 632 |
except Exception as e:
|
| 633 |
-
import traceback
|
| 634 |
print(f"❌ ERROR: {traceback.format_exc()}")
|
| 635 |
yield f"❌ **Error:** {str(e)}", None
|
| 636 |
|
| 637 |
def create_agent_recommendation_chart_enhanced(result: Dict) -> go.Figure:
|
| 638 |
try:
|
| 639 |
-
# ✅ FIX: Extract from top level
|
| 640 |
rec_name_map = {
|
| 641 |
'c_citi_custom_cash': 'Citi Custom Cash',
|
| 642 |
'c_amex_gold': 'Amex Gold',
|
|
@@ -656,8 +546,7 @@ def create_agent_recommendation_chart_enhanced(result: Dict) -> go.Figure:
|
|
| 656 |
for alt in alternatives[:3]:
|
| 657 |
alt_id = alt.get('card', '')
|
| 658 |
alt_name = rec_name_map.get(alt_id, alt_id)
|
| 659 |
-
|
| 660 |
-
alt_reward = rec_reward * 0.8 # Estimate
|
| 661 |
cards.append(alt_name)
|
| 662 |
rewards.append(alt_reward)
|
| 663 |
colors.append('#cbd5e0')
|
|
@@ -689,13 +578,10 @@ def create_agent_recommendation_chart_enhanced(result: Dict) -> go.Figure:
|
|
| 689 |
fig.add_annotation(text="Chart unavailable", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False)
|
| 690 |
fig.update_layout(height=400, template='plotly_white')
|
| 691 |
return fig
|
| 692 |
-
|
| 693 |
-
# Initialize clients
|
| 694 |
client = RewardPilotClient(config.ORCHESTRATOR_URL)
|
| 695 |
llm = get_llm_explainer()
|
| 696 |
|
| 697 |
-
|
| 698 |
-
# ===================== Main Recommendation Function =====================
|
| 699 |
def get_recommendation(
|
| 700 |
user_id: str,
|
| 701 |
merchant: str,
|
|
@@ -706,7 +592,6 @@ def get_recommendation(
|
|
| 706 |
transaction_date: Optional[str]
|
| 707 |
) -> tuple:
|
| 708 |
"""Get card recommendation and format response"""
|
| 709 |
-
# Validate inputs
|
| 710 |
if not user_id or not merchant or amount <= 0:
|
| 711 |
return (
|
| 712 |
"❌ **Error:** Please fill in all required fields.",
|
|
@@ -714,17 +599,14 @@ def get_recommendation(
|
|
| 714 |
None,
|
| 715 |
)
|
| 716 |
|
| 717 |
-
# Determine MCC code
|
| 718 |
if use_custom_mcc and custom_mcc:
|
| 719 |
mcc = custom_mcc
|
| 720 |
else:
|
| 721 |
mcc = MCC_CATEGORIES.get(category, "5999")
|
| 722 |
|
| 723 |
-
# Set default date if not provided
|
| 724 |
if not transaction_date:
|
| 725 |
transaction_date = str(date.today())
|
| 726 |
|
| 727 |
-
# Call API
|
| 728 |
response: Dict[str, Any] = client.get_recommendation_sync(
|
| 729 |
user_id=user_id,
|
| 730 |
merchant=merchant,
|
|
@@ -733,10 +615,8 @@ def get_recommendation(
|
|
| 733 |
transaction_date=transaction_date,
|
| 734 |
)
|
| 735 |
|
| 736 |
-
# Format response
|
| 737 |
formatted_text = format_full_recommendation(response)
|
| 738 |
|
| 739 |
-
# Extract card details for comparison
|
| 740 |
comparison_table: Optional[str]
|
| 741 |
stats: Optional[str]
|
| 742 |
if not response.get("error"):
|
|
@@ -745,7 +625,6 @@ def get_recommendation(
|
|
| 745 |
all_cards = [c for c in ([recommended] + alternatives) if c]
|
| 746 |
comparison_table = format_comparison_table(all_cards) if all_cards else None
|
| 747 |
|
| 748 |
-
# Create summary stats
|
| 749 |
total_analyzed = response.get("total_cards_analyzed", len(all_cards))
|
| 750 |
best_reward = (recommended.get("reward_amount") or 0.0)
|
| 751 |
services_used = response.get("services_used", [])
|
|
@@ -760,22 +639,18 @@ def get_recommendation(
|
|
| 760 |
|
| 761 |
return formatted_text, comparison_table, stats
|
| 762 |
|
| 763 |
-
|
| 764 |
def get_recommendation_with_ai(user_id, merchant, category, amount):
|
| 765 |
"""Get card recommendation with LLM-powered explanation"""
|
| 766 |
|
| 767 |
-
# Validate inputs
|
| 768 |
if not merchant or not merchant.strip():
|
| 769 |
return "❌ Please enter a merchant name.", None
|
| 770 |
|
| 771 |
if amount <= 0:
|
| 772 |
return "❌ Please enter a valid amount greater than $0.", None
|
| 773 |
|
| 774 |
-
# Show loading state
|
| 775 |
yield "⏳ **Loading recommendation...** Analyzing your cards and transaction...", None
|
| 776 |
|
| 777 |
try:
|
| 778 |
-
# Get base recommendation from orchestrator
|
| 779 |
result = client.get_recommendation(
|
| 780 |
user_id=user_id,
|
| 781 |
merchant=merchant,
|
|
@@ -784,16 +659,13 @@ def get_recommendation_with_ai(user_id, merchant, category, amount):
|
|
| 784 |
mcc=None
|
| 785 |
)
|
| 786 |
|
| 787 |
-
# Check for errors
|
| 788 |
if not result.get('success'):
|
| 789 |
error_msg = result.get('error', 'Unknown error')
|
| 790 |
yield f"❌ Error: {error_msg}", None
|
| 791 |
return
|
| 792 |
|
| 793 |
-
# Normalize the data
|
| 794 |
data = normalize_recommendation_data(result.get('data', {}))
|
| 795 |
|
| 796 |
-
# Generate LLM explanation if enabled
|
| 797 |
ai_explanation = ""
|
| 798 |
if config.LLM_ENABLED:
|
| 799 |
try:
|
|
@@ -812,7 +684,6 @@ def get_recommendation_with_ai(user_id, merchant, category, amount):
|
|
| 812 |
print(f"LLM explanation failed: {e}")
|
| 813 |
ai_explanation = ""
|
| 814 |
|
| 815 |
-
# Format output
|
| 816 |
output = f"""
|
| 817 |
## 🎯 Recommendation for ${amount:.2f} at {merchant}
|
| 818 |
|
|
@@ -830,7 +701,6 @@ def get_recommendation_with_ai(user_id, merchant, category, amount):
|
|
| 830 |
output += f"""
|
| 831 |
### 🤖 AI Insight
|
| 832 |
{ai_explanation}
|
| 833 |
-
|
| 834 |
---
|
| 835 |
"""
|
| 836 |
|
|
@@ -853,13 +723,11 @@ def get_recommendation_with_ai(user_id, merchant, category, amount):
|
|
| 853 |
for alt in data['alternatives']:
|
| 854 |
output += f"- **{alt['card']}:** ${alt['rewards']:.2f} ({alt['rate']})\n"
|
| 855 |
|
| 856 |
-
# Create visualization
|
| 857 |
chart = create_rewards_comparison_chart(data)
|
| 858 |
|
| 859 |
yield output, chart
|
| 860 |
|
| 861 |
except Exception as e:
|
| 862 |
-
import traceback
|
| 863 |
error_details = traceback.format_exc()
|
| 864 |
print(f"Recommendation error: {error_details}")
|
| 865 |
yield f"❌ Error: {str(e)}\n\nPlease check your API connection or try again.", None
|
|
@@ -868,18 +736,15 @@ def create_rewards_comparison_chart(data: Dict) -> go.Figure:
|
|
| 868 |
"""Create rewards comparison chart with proper error handling"""
|
| 869 |
|
| 870 |
try:
|
| 871 |
-
# Prepare data for chart
|
| 872 |
cards = [data['recommended_card']]
|
| 873 |
rewards = [data['rewards_earned']]
|
| 874 |
-
colors = ['#667eea']
|
| 875 |
|
| 876 |
-
# Add alternatives
|
| 877 |
for alt in data.get('alternatives', [])[:3]:
|
| 878 |
cards.append(alt['card'])
|
| 879 |
rewards.append(float(alt['rewards']))
|
| 880 |
-
colors.append('#a0aec0')
|
| 881 |
|
| 882 |
-
# Only create chart if we have valid data
|
| 883 |
if not cards or all(r == 0 for r in rewards):
|
| 884 |
fig = go.Figure()
|
| 885 |
fig.add_annotation(
|
|
@@ -894,7 +759,6 @@ def create_rewards_comparison_chart(data: Dict) -> go.Figure:
|
|
| 894 |
)
|
| 895 |
return fig
|
| 896 |
|
| 897 |
-
# Create bar chart
|
| 898 |
fig = go.Figure(data=[
|
| 899 |
go.Bar(
|
| 900 |
x=cards,
|
|
@@ -928,10 +792,8 @@ def create_rewards_comparison_chart(data: Dict) -> go.Figure:
|
|
| 928 |
|
| 929 |
except Exception as e:
|
| 930 |
print(f"Chart creation error: {e}")
|
| 931 |
-
import traceback
|
| 932 |
print(traceback.format_exc())
|
| 933 |
|
| 934 |
-
# Return empty chart with error message
|
| 935 |
fig = go.Figure()
|
| 936 |
fig.add_annotation(
|
| 937 |
text=f"Error creating chart",
|
|
@@ -941,12 +803,11 @@ def create_rewards_comparison_chart(data: Dict) -> go.Figure:
|
|
| 941 |
)
|
| 942 |
fig.update_layout(height=400, template='plotly_white')
|
| 943 |
return fig
|
| 944 |
-
|
| 945 |
def get_analytics_with_insights(user_id):
|
| 946 |
"""Get analytics with LLM-generated insights"""
|
| 947 |
|
| 948 |
try:
|
| 949 |
-
# Get analytics data
|
| 950 |
result = client.get_user_analytics(user_id)
|
| 951 |
|
| 952 |
if not result.get('success'):
|
|
@@ -954,7 +815,6 @@ def get_analytics_with_insights(user_id):
|
|
| 954 |
|
| 955 |
data = result['data']
|
| 956 |
|
| 957 |
-
# Generate AI insights if enabled
|
| 958 |
ai_insights = ""
|
| 959 |
if config.LLM_ENABLED:
|
| 960 |
try:
|
|
@@ -970,29 +830,23 @@ def get_analytics_with_insights(user_id):
|
|
| 970 |
print(f"AI insights generation failed: {e}")
|
| 971 |
ai_insights = ""
|
| 972 |
|
| 973 |
-
# Format metrics
|
| 974 |
metrics = f"""
|
| 975 |
## 📊 Your Rewards Analytics
|
| 976 |
|
| 977 |
### Key Metrics
|
| 978 |
-
|
| 979 |
- **💰 Total Rewards:** ${data['total_rewards']:.2f}
|
| 980 |
- **📈 Potential Savings:** ${data['potential_savings']:.2f}/year
|
| 981 |
- **⭐ Optimization Score:** {data['optimization_score']}/100
|
| 982 |
- **✅ Optimized Transactions:** {data.get('optimized_count', 0)}
|
| 983 |
"""
|
| 984 |
|
| 985 |
-
# Add AI insights
|
| 986 |
if ai_insights:
|
| 987 |
metrics += f"""
|
| 988 |
### 🤖 Personalized Insights
|
| 989 |
-
|
| 990 |
{ai_insights}
|
| 991 |
-
|
| 992 |
---
|
| 993 |
"""
|
| 994 |
|
| 995 |
-
# Create charts (your existing chart code)
|
| 996 |
spending_chart = create_spending_chart(data)
|
| 997 |
rewards_chart = create_rewards_distribution_chart(data)
|
| 998 |
optimization_chart = create_optimization_gauge(data['optimization_score'])
|
|
@@ -1002,7 +856,6 @@ def get_analytics_with_insights(user_id):
|
|
| 1002 |
except Exception as e:
|
| 1003 |
return f"❌ Error: {str(e)}", None, None, None
|
| 1004 |
|
| 1005 |
-
# ===================== Sample Transaction Examples =====================
|
| 1006 |
EXAMPLES = [
|
| 1007 |
["u_alice", "Groceries", "Whole Foods", 125.50, False, "", "2025-01-15"],
|
| 1008 |
["u_bob", "Restaurants", "Olive Garden", 65.75, False, "", "2025-01-15"],
|
|
@@ -1011,7 +864,6 @@ EXAMPLES = [
|
|
| 1011 |
["u_bob", "Gas Stations", "Shell", 45.00, False, "", ""],
|
| 1012 |
]
|
| 1013 |
|
| 1014 |
-
# ===================== Build Gradio Interface =====================
|
| 1015 |
def _toggle_custom_mcc(use_custom: bool):
|
| 1016 |
return gr.update(visible=use_custom, value="")
|
| 1017 |
|
|
@@ -1026,7 +878,6 @@ with gr.Blocks(
|
|
| 1026 |
font-size: 16px;
|
| 1027 |
line-height: 1.6;
|
| 1028 |
}
|
| 1029 |
-
/* Metric Cards Styling */
|
| 1030 |
.metric-card {
|
| 1031 |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 1032 |
color: white;
|
|
@@ -1062,7 +913,6 @@ with gr.Blocks(
|
|
| 1062 |
.metric-card-blue {
|
| 1063 |
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
| 1064 |
}
|
| 1065 |
-
/* Table styling */
|
| 1066 |
table {
|
| 1067 |
width: 100%;
|
| 1068 |
border-collapse: collapse;
|
|
@@ -1091,7 +941,6 @@ with gr.Blocks(
|
|
| 1091 |
}
|
| 1092 |
""",
|
| 1093 |
) as app:
|
| 1094 |
-
# Header
|
| 1095 |
gr.Markdown(
|
| 1096 |
f"""
|
| 1097 |
# {APP_TITLE}
|
|
@@ -1105,7 +954,7 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1105 |
---
|
| 1106 |
"""
|
| 1107 |
)
|
| 1108 |
-
|
| 1109 |
agent_status = """
|
| 1110 |
🤖 **Autonomous Agent:** ✅ Active (Claude 3.5 Sonnet)
|
| 1111 |
📊 **Mode:** Dynamic Planning + Reasoning
|
|
@@ -1113,10 +962,7 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1113 |
"""
|
| 1114 |
gr.Markdown(agent_status)
|
| 1115 |
|
| 1116 |
-
|
| 1117 |
-
# Ensure all tabs are siblings at the same level
|
| 1118 |
with gr.Tabs():
|
| 1119 |
-
# ========== Tab 1: Get Recommendation ==========
|
| 1120 |
with gr.Tab("🎯 Get Recommendation"):
|
| 1121 |
with gr.Row():
|
| 1122 |
with gr.Column(scale=1):
|
|
@@ -1128,7 +974,6 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1128 |
info="Select a user"
|
| 1129 |
)
|
| 1130 |
|
| 1131 |
-
# CATEGORY FIRST (moved up)
|
| 1132 |
category_dropdown = gr.Dropdown(
|
| 1133 |
choices=list(MCC_CATEGORIES.keys()),
|
| 1134 |
value="Groceries",
|
|
@@ -1136,13 +981,12 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1136 |
info="Select the category first"
|
| 1137 |
)
|
| 1138 |
|
| 1139 |
-
# MERCHANT DROPDOWN (now dynamic)
|
| 1140 |
merchant_dropdown = gr.Dropdown(
|
| 1141 |
choices=MERCHANTS_BY_CATEGORY["Groceries"],
|
| 1142 |
value="Whole Foods",
|
| 1143 |
label="🏪 Merchant Name",
|
| 1144 |
info="Select merchant (changes based on category)",
|
| 1145 |
-
allow_custom_value=True
|
| 1146 |
)
|
| 1147 |
|
| 1148 |
amount_input = gr.Number(
|
|
@@ -1158,7 +1002,6 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1158 |
value=""
|
| 1159 |
)
|
| 1160 |
|
| 1161 |
-
# Advanced options
|
| 1162 |
with gr.Accordion("⚙️ Advanced Options", open=False):
|
| 1163 |
use_custom_mcc = gr.Checkbox(
|
| 1164 |
label="Use Custom MCC Code",
|
|
@@ -1184,7 +1027,6 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1184 |
variant="primary",
|
| 1185 |
size="lg"
|
| 1186 |
)
|
| 1187 |
-
|
| 1188 |
|
| 1189 |
with gr.Column(scale=2):
|
| 1190 |
gr.Markdown("### 💡 Recommendation")
|
|
@@ -1193,8 +1035,6 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1193 |
elem_classes=["recommendation-output"]
|
| 1194 |
)
|
| 1195 |
recommendation_chart = gr.Plot()
|
| 1196 |
-
|
| 1197 |
-
|
| 1198 |
|
| 1199 |
def update_merchant_choices(category):
|
| 1200 |
"""Update merchant dropdown based on selected category"""
|
|
@@ -1204,14 +1044,12 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1204 |
value=merchants[0] if merchants else ""
|
| 1205 |
)
|
| 1206 |
|
| 1207 |
-
# Connect category change to merchant update
|
| 1208 |
category_dropdown.change(
|
| 1209 |
fn=update_merchant_choices,
|
| 1210 |
inputs=[category_dropdown],
|
| 1211 |
outputs=[merchant_dropdown]
|
| 1212 |
)
|
| 1213 |
|
| 1214 |
-
# Stats and comparison below
|
| 1215 |
with gr.Row():
|
| 1216 |
with gr.Column():
|
| 1217 |
gr.Markdown("### 📊 Quick Stats")
|
|
@@ -1221,34 +1059,19 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1221 |
gr.Markdown("### 🔄 Card Comparison")
|
| 1222 |
comparison_output = gr.Markdown()
|
| 1223 |
|
| 1224 |
-
|
| 1225 |
recommend_btn.click(
|
| 1226 |
-
|
| 1227 |
-
|
| 1228 |
-
|
| 1229 |
-
|
| 1230 |
-
# def get_recommendation_with_loading(user_id, merchant, category, amount):
|
| 1231 |
-
# """Wrapper to show loading state"""
|
| 1232 |
-
# # Show loading first
|
| 1233 |
-
# yield "⏳ **Loading recommendation...** Please wait...", None
|
| 1234 |
-
|
| 1235 |
-
# # Get actual result
|
| 1236 |
-
# result = get_recommendation_with_ai(user_id, merchant, category, amount)
|
| 1237 |
-
# yield result
|
| 1238 |
|
| 1239 |
-
# recommend_btn.click(
|
| 1240 |
-
# fn=get_recommendation_with_loading,
|
| 1241 |
-
# inputs=[user_dropdown, merchant_dropdown, category_dropdown, amount_input],
|
| 1242 |
-
# outputs=[recommendation_output, recommendation_chart]
|
| 1243 |
-
# )
|
| 1244 |
-
# Examples
|
| 1245 |
gr.Markdown("### 📝 Example Transactions")
|
| 1246 |
gr.Examples(
|
| 1247 |
examples=EXAMPLES,
|
| 1248 |
inputs=[
|
| 1249 |
user_dropdown,
|
| 1250 |
-
category_dropdown,
|
| 1251 |
-
merchant_dropdown,
|
| 1252 |
amount_input,
|
| 1253 |
use_custom_mcc,
|
| 1254 |
custom_mcc_input,
|
|
@@ -1263,11 +1086,9 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1263 |
cache_examples=False
|
| 1264 |
)
|
| 1265 |
|
| 1266 |
-
# ========== Tab 2: Analytics with Charts (ENHANCED) ==========
|
| 1267 |
with gr.Tab("📊 Analytics"):
|
| 1268 |
gr.Markdown("## 🎯 Your Rewards Optimization Dashboard")
|
| 1269 |
|
| 1270 |
-
# User selector for analytics
|
| 1271 |
with gr.Row():
|
| 1272 |
analytics_user = gr.Dropdown(
|
| 1273 |
choices=SAMPLE_USERS,
|
|
@@ -1281,7 +1102,6 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1281 |
scale=1
|
| 1282 |
)
|
| 1283 |
|
| 1284 |
-
# Top Metrics Row (Dynamic)
|
| 1285 |
metrics_display = gr.HTML(
|
| 1286 |
value="""
|
| 1287 |
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
|
@@ -1306,34 +1126,26 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1306 |
)
|
| 1307 |
|
| 1308 |
gr.Markdown("---")
|
| 1309 |
-
|
| 1310 |
-
# ========== CHARTS SECTION (NEW) ==========
|
| 1311 |
gr.Markdown("## 📊 Visual Analytics")
|
| 1312 |
|
| 1313 |
-
# Row 1: Spending Chart + Optimization Gauge
|
| 1314 |
with gr.Row():
|
| 1315 |
with gr.Column(scale=2):
|
| 1316 |
spending_chart = gr.Plot(label="Spending vs Rewards")
|
| 1317 |
with gr.Column(scale=1):
|
| 1318 |
optimization_gauge = gr.Plot(label="Your Score")
|
| 1319 |
|
| 1320 |
-
# Row 2: Pie Chart + Card Performance
|
| 1321 |
with gr.Row():
|
| 1322 |
with gr.Column(scale=1):
|
| 1323 |
rewards_pie_chart = gr.Plot(label="Rewards Distribution")
|
| 1324 |
with gr.Column(scale=1):
|
| 1325 |
card_performance_chart = gr.Plot(label="Top Performing Cards")
|
| 1326 |
|
| 1327 |
-
# Row 3: Trend Line Chart (Full Width)
|
| 1328 |
with gr.Row():
|
| 1329 |
trend_chart = gr.Plot(label="12-Month Trends")
|
| 1330 |
|
| 1331 |
gr.Markdown("---")
|
| 1332 |
-
|
| 1333 |
-
# ========== DATA TABLES SECTION ==========
|
| 1334 |
gr.Markdown("## 📋 Detailed Breakdown")
|
| 1335 |
|
| 1336 |
-
# Detailed Analytics (Dynamic)
|
| 1337 |
with gr.Row():
|
| 1338 |
with gr.Column(scale=1):
|
| 1339 |
gr.Markdown("### 💰 Category Spending Breakdown")
|
|
@@ -1377,7 +1189,6 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1377 |
|
| 1378 |
gr.Markdown("---")
|
| 1379 |
|
| 1380 |
-
# Spending Forecast (Dynamic)
|
| 1381 |
forecast_display = gr.Markdown(
|
| 1382 |
value="""
|
| 1383 |
### 🔮 Next Month Forecast
|
|
@@ -1394,21 +1205,17 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1394 |
"""
|
| 1395 |
)
|
| 1396 |
|
| 1397 |
-
# Status indicator
|
| 1398 |
analytics_status = gr.Markdown(
|
| 1399 |
value="*Analytics loaded for u_alice*",
|
| 1400 |
elem_classes=["status-text"]
|
| 1401 |
)
|
| 1402 |
|
| 1403 |
-
# ===================== Analytics Update Function (ENHANCED) =====================
|
| 1404 |
def update_analytics_with_charts(user_id: str):
|
| 1405 |
"""Fetch and format analytics with charts for selected user"""
|
| 1406 |
|
| 1407 |
try:
|
| 1408 |
-
# Fetch analytics data from API
|
| 1409 |
result = client.get_user_analytics(user_id)
|
| 1410 |
|
| 1411 |
-
# DEBUG: Print what we received
|
| 1412 |
print("=" * 60)
|
| 1413 |
print(f"DEBUG: Analytics for {user_id}")
|
| 1414 |
print(f"Success: {result.get('success')}")
|
|
@@ -1418,7 +1225,6 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1418 |
print(f"Total rewards: {result['data'].get('total_rewards')}")
|
| 1419 |
print("=" * 60)
|
| 1420 |
|
| 1421 |
-
# Check if request was successful
|
| 1422 |
if not result.get('success'):
|
| 1423 |
error_msg = result.get('error', 'Unknown error')
|
| 1424 |
empty_fig = create_empty_chart(f"Error: {error_msg}")
|
|
@@ -1431,10 +1237,8 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1431 |
f"*Error: {error_msg}*"
|
| 1432 |
)
|
| 1433 |
|
| 1434 |
-
# ✅ Extract the actual data
|
| 1435 |
analytics_data = result.get('data', {})
|
| 1436 |
|
| 1437 |
-
# Verify we have data
|
| 1438 |
if not analytics_data:
|
| 1439 |
empty_fig = create_empty_chart("No analytics data available")
|
| 1440 |
return (
|
|
@@ -1446,51 +1250,36 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1446 |
"*No data available*"
|
| 1447 |
)
|
| 1448 |
|
| 1449 |
-
# Import chart functions
|
| 1450 |
-
from utils.formatters import (
|
| 1451 |
-
format_analytics_metrics,
|
| 1452 |
-
create_spending_chart,
|
| 1453 |
-
create_rewards_pie_chart,
|
| 1454 |
-
create_optimization_gauge,
|
| 1455 |
-
create_trend_line_chart,
|
| 1456 |
-
create_card_performance_chart
|
| 1457 |
-
)
|
| 1458 |
-
|
| 1459 |
-
# Format text data (pass unwrapped data)
|
| 1460 |
metrics_html, table_md, insights_md, forecast_md = format_analytics_metrics(analytics_data)
|
| 1461 |
|
| 1462 |
-
# Generate charts (pass unwrapped data)
|
| 1463 |
spending_fig = create_spending_chart(analytics_data)
|
| 1464 |
pie_fig = create_rewards_pie_chart(analytics_data)
|
| 1465 |
gauge_fig = create_optimization_gauge(analytics_data)
|
| 1466 |
trend_fig = create_trend_line_chart(analytics_data)
|
| 1467 |
performance_fig = create_card_performance_chart(analytics_data)
|
| 1468 |
|
| 1469 |
-
# Status message
|
| 1470 |
from datetime import datetime
|
| 1471 |
status = f"*Analytics updated for {user_id} at {datetime.now().strftime('%I:%M %p')}*"
|
| 1472 |
|
| 1473 |
return (
|
| 1474 |
-
metrics_html,
|
| 1475 |
-
spending_fig,
|
| 1476 |
-
gauge_fig,
|
| 1477 |
-
pie_fig,
|
| 1478 |
-
performance_fig,
|
| 1479 |
-
trend_fig,
|
| 1480 |
-
table_md,
|
| 1481 |
-
insights_md,
|
| 1482 |
-
forecast_md,
|
| 1483 |
-
status
|
| 1484 |
)
|
| 1485 |
|
| 1486 |
except Exception as e:
|
| 1487 |
-
import traceback
|
| 1488 |
error_details = traceback.format_exc()
|
| 1489 |
error_msg = f"❌ Error loading analytics: {str(e)}"
|
| 1490 |
print(error_msg)
|
| 1491 |
print(error_details)
|
| 1492 |
|
| 1493 |
-
# Return empty/error states
|
| 1494 |
empty_fig = create_empty_chart("Error loading chart")
|
| 1495 |
|
| 1496 |
return (
|
|
@@ -1501,7 +1290,6 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1501 |
"Error loading forecast",
|
| 1502 |
f"*{error_msg}*"
|
| 1503 |
)
|
| 1504 |
-
|
| 1505 |
|
| 1506 |
def create_empty_chart(message: str) -> go.Figure:
|
| 1507 |
"""Helper to create empty chart with message"""
|
|
@@ -1515,7 +1303,6 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1515 |
fig.update_layout(height=400, template='plotly_white')
|
| 1516 |
return fig
|
| 1517 |
|
| 1518 |
-
# Connect analytics refresh to button and dropdown
|
| 1519 |
refresh_analytics_btn.click(
|
| 1520 |
fn=update_analytics_with_charts,
|
| 1521 |
inputs=[analytics_user],
|
|
@@ -1549,8 +1336,7 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1549 |
analytics_status
|
| 1550 |
]
|
| 1551 |
)
|
| 1552 |
-
|
| 1553 |
-
# Tab 3: Chat (NEW!)
|
| 1554 |
with gr.Tab("💬 Ask AI"):
|
| 1555 |
gr.Markdown("## Chat with RewardPilot AI")
|
| 1556 |
gr.Markdown("*Ask questions about credit cards, rewards, and your spending*")
|
|
@@ -1576,7 +1362,6 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1576 |
if not message.strip():
|
| 1577 |
return "", chat_history
|
| 1578 |
|
| 1579 |
-
# Get user context
|
| 1580 |
user_context = {}
|
| 1581 |
try:
|
| 1582 |
analytics = client.get_user_analytics(user_id)
|
|
@@ -1595,7 +1380,6 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1595 |
'top_category': 'Groceries'
|
| 1596 |
}
|
| 1597 |
|
| 1598 |
-
# Generate AI response
|
| 1599 |
try:
|
| 1600 |
if config.LLM_ENABLED:
|
| 1601 |
bot_response = llm.chat_response(message, user_context, chat_history)
|
|
@@ -1611,7 +1395,6 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1611 |
msg.submit(respond, [msg, chatbot, chat_user], [msg, chatbot])
|
| 1612 |
send_btn.click(respond, [msg, chatbot, chat_user], [msg, chatbot])
|
| 1613 |
|
| 1614 |
-
# ✅ RESTORED: Example questions
|
| 1615 |
gr.Markdown("### 💡 Try asking:")
|
| 1616 |
gr.Examples(
|
| 1617 |
examples=[
|
|
@@ -1623,7 +1406,7 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1623 |
],
|
| 1624 |
inputs=[msg]
|
| 1625 |
)
|
| 1626 |
-
|
| 1627 |
with gr.Tab("🤖 Agent Insights"):
|
| 1628 |
gr.Markdown("""
|
| 1629 |
## How the Autonomous Agent Works
|
|
@@ -1697,8 +1480,7 @@ Get AI-powered credit card recommendations that maximize your rewards based on:
|
|
| 1697 |
|
| 1698 |
**Try it out in the "Get Recommendation" tab!** 🚀
|
| 1699 |
""")
|
| 1700 |
-
|
| 1701 |
-
# ========== Tab 3: About ==========
|
| 1702 |
with gr.Tab("ℹ️ About"):
|
| 1703 |
gr.Markdown(
|
| 1704 |
"""
|
|
@@ -1714,7 +1496,6 @@ RewardPilot is an AI-powered credit card recommendation system built using the *
|
|
| 1714 |
- 📊 **Interactive visualizations**
|
| 1715 |
|
| 1716 |
### Features
|
| 1717 |
-
|
| 1718 |
- Smart card recommendations for every purchase
|
| 1719 |
- AI-generated personalized insights
|
| 1720 |
- Visual analytics dashboard
|
|
@@ -1722,22 +1503,19 @@ RewardPilot is an AI-powered credit card recommendation system built using the *
|
|
| 1722 |
- Real-time cap warnings
|
| 1723 |
- Multi-card comparison
|
| 1724 |
|
| 1725 |
-
The system consists of multiple microservices
|
| 1726 |
-
|
| 1727 |
1. **Smart Wallet** - Analyzes transaction context and selects optimal cards
|
| 1728 |
2. **Rewards-RAG** - Retrieves detailed card benefit information using RAG
|
| 1729 |
3. **Spend-Forecast** - Predicts spending patterns and warns about cap risks
|
| 1730 |
4. **Orchestrator** - Coordinates all services for comprehensive recommendations
|
| 1731 |
|
| 1732 |
### 🎯 How It Works
|
| 1733 |
-
|
| 1734 |
1. **Enter Transaction Details** - Merchant, amount, category
|
| 1735 |
2. **AI Analysis** - System analyzes your wallet and transaction context
|
| 1736 |
3. **Get Recommendation** - Receive the best card with detailed reasoning
|
| 1737 |
4. **Maximize Rewards** - Earn more points/cashback on every purchase
|
| 1738 |
|
| 1739 |
### 🔧 Technology Stack
|
| 1740 |
-
|
| 1741 |
- **Backend:** FastAPI, Python
|
| 1742 |
- **Frontend:** Gradio
|
| 1743 |
- **AI/ML:** RAG (Retrieval-Augmented Generation)
|
|
@@ -1745,7 +1523,6 @@ The system consists of multiple microservices:
|
|
| 1745 |
- **Deployment:** Hugging Face Spaces
|
| 1746 |
|
| 1747 |
### 📚 MCC Categories Supported
|
| 1748 |
-
|
| 1749 |
- Groceries (5411)
|
| 1750 |
- Restaurants (5812)
|
| 1751 |
- Gas Stations (5541)
|
|
@@ -1755,11 +1532,9 @@ The system consists of multiple microservices:
|
|
| 1755 |
- And many more...
|
| 1756 |
|
| 1757 |
### 🎓 Built For
|
| 1758 |
-
|
| 1759 |
**MCP 1st Birthday Hackathon** - Celebrating one year of the Model Context Protocol
|
| 1760 |
|
| 1761 |
### 👨💻 Developer
|
| 1762 |
-
|
| 1763 |
Built with ❤️ for the MCP community
|
| 1764 |
|
| 1765 |
---
|
|
@@ -1769,18 +1544,15 @@ Built with ❤️ for the MCP community
|
|
| 1769 |
"""
|
| 1770 |
)
|
| 1771 |
|
| 1772 |
-
# ========== Tab 4: API Documentation ==========
|
| 1773 |
with gr.Tab("📖 API Docs"):
|
| 1774 |
gr.Markdown(
|
| 1775 |
"""
|
| 1776 |
## API Endpoints
|
| 1777 |
|
| 1778 |
### Orchestrator API
|
| 1779 |
-
|
| 1780 |
**Base URL:** `https://mcp-1st-birthday-rewardpilot-orchestrator.hf.space`
|
| 1781 |
|
| 1782 |
#### POST `/recommend`
|
| 1783 |
-
|
| 1784 |
Get comprehensive card recommendation.
|
| 1785 |
|
| 1786 |
**Request:**
|
|
@@ -1792,44 +1564,11 @@ Get comprehensive card recommendation.
|
|
| 1792 |
"amount_usd": 125.50,
|
| 1793 |
"transaction_date": "2025-01-15"
|
| 1794 |
}
|
| 1795 |
-
|
| 1796 |
-
|
| 1797 |
-
|
| 1798 |
-
|
| 1799 |
-
{
|
| 1800 |
-
"user_id": "u_alice",
|
| 1801 |
-
"merchant": "Whole Foods",
|
| 1802 |
-
"amount_usd": 125.5,
|
| 1803 |
-
"recommended_card": {
|
| 1804 |
-
"card_id": "c_amex_gold",
|
| 1805 |
-
"card_name": "American Express Gold Card",
|
| 1806 |
-
"reward_rate": 4.0,
|
| 1807 |
-
"reward_amount": 502.0,
|
| 1808 |
-
"category": "Groceries",
|
| 1809 |
-
"reasoning": "Earns 4x points on Groceries"
|
| 1810 |
-
},
|
| 1811 |
-
"alternative_cards": ["..."],
|
| 1812 |
-
"rag_insights": { "...": "..." },
|
| 1813 |
-
"forecast_warning": { "...": "..." },
|
| 1814 |
-
"services_used": ["smart_wallet", "rewards_rag", "spend_forecast"],
|
| 1815 |
-
"final_recommendation": "..."
|
| 1816 |
-
}
|
| 1817 |
-
```
|
| 1818 |
-
|
| 1819 |
-
### Other Services
|
| 1820 |
-
|
| 1821 |
-
- Smart Wallet: https://mcp-1st-birthday-rewardpilot-smart-wallet.hf.space
|
| 1822 |
-
- Rewards-RAG: https://mcp-1st-birthday-rewardpilot-rewards-rag.hf.space
|
| 1823 |
-
- Spend-Forecast: https://mcp-1st-birthday-rewardpilot-spend-forecast.hf.space
|
| 1824 |
-
|
| 1825 |
-
### Interactive Docs
|
| 1826 |
-
|
| 1827 |
-
Visit `/docs` on any service for interactive Swagger UI documentation.
|
| 1828 |
-
|
| 1829 |
-
### cURL Examples
|
| 1830 |
-
|
| 1831 |
-
```bash
|
| 1832 |
-
# Get recommendation
|
| 1833 |
curl -X POST https://mcp-1st-birthday-rewardpilot-orchestrator.hf.space/recommend \\
|
| 1834 |
-H "Content-Type: application/json" \\
|
| 1835 |
-d '{
|
|
@@ -1837,10 +1576,8 @@ curl -X POST https://mcp-1st-birthday-rewardpilot-orchestrator.hf.space/recommen
|
|
| 1837 |
"merchant": "Whole Foods",
|
| 1838 |
"mcc": "5411",
|
| 1839 |
"amount_usd": 125.50
|
| 1840 |
-
}'
|
| 1841 |
-
|
| 1842 |
-
"""
|
| 1843 |
-
)
|
| 1844 |
|
| 1845 |
# ===================== Launch App =====================
|
| 1846 |
if __name__ == "__main__":
|
|
@@ -1848,4 +1585,4 @@ if __name__ == "__main__":
|
|
| 1848 |
server_name="0.0.0.0",
|
| 1849 |
server_port=7860,
|
| 1850 |
share=False,
|
| 1851 |
-
)
|
|
|
|
| 1 |
+
from typing import Dict, Any, Optional, Tuple, List
|
| 2 |
+
import traceback
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
from datetime import date
|
| 6 |
+
import gradio as gr
|
| 7 |
+
import plotly.graph_objects as go
|
| 8 |
+
import httpx
|
| 9 |
|
| 10 |
+
from config import (
|
| 11 |
+
APP_TITLE, APP_DESCRIPTION, THEME,
|
| 12 |
+
MCC_CATEGORIES, SAMPLE_USERS,
|
| 13 |
+
MERCHANTS_BY_CATEGORY
|
| 14 |
+
)
|
| 15 |
+
from utils.api_client import RewardPilotClient
|
| 16 |
+
from utils.formatters import (
|
| 17 |
+
format_full_recommendation,
|
| 18 |
+
format_comparison_table,
|
| 19 |
+
format_analytics_metrics,
|
| 20 |
+
create_spending_chart,
|
| 21 |
+
create_rewards_pie_chart,
|
| 22 |
+
create_optimization_gauge,
|
| 23 |
+
create_trend_line_chart,
|
| 24 |
+
create_card_performance_chart
|
| 25 |
+
)
|
| 26 |
+
from utils.llm_explainer import get_llm_explainer
|
| 27 |
+
import config
|
| 28 |
|
| 29 |
+
CARDS_FILE = os.path.join(os.path.dirname(__file__), "data", "cards.json")
|
| 30 |
|
| 31 |
def safe_get(data: Dict, key: str, default: Any = None) -> Any:
|
| 32 |
"""Safely get value from dictionary with fallback"""
|
|
|
|
| 35 |
except:
|
| 36 |
return default
|
| 37 |
|
|
|
|
| 38 |
def normalize_recommendation_data(data: Dict) -> Dict:
|
| 39 |
"""
|
| 40 |
Normalize API response to ensure all required fields exist.
|
| 41 |
Handles both orchestrator format and mock data format.
|
| 42 |
"""
|
| 43 |
|
|
|
|
| 44 |
if data.get('mock_data'):
|
|
|
|
| 45 |
return {
|
| 46 |
'recommended_card': safe_get(data, 'recommended_card', 'Unknown Card'),
|
| 47 |
'rewards_earned': float(safe_get(data, 'rewards_earned', 0)),
|
|
|
|
| 57 |
'mock_data': True
|
| 58 |
}
|
| 59 |
|
|
|
|
| 60 |
recommended_card = safe_get(data, 'recommended_card', {})
|
| 61 |
if isinstance(recommended_card, dict):
|
| 62 |
card_name = safe_get(recommended_card, 'card_name', 'Unknown Card')
|
|
|
|
| 65 |
category = safe_get(recommended_card, 'category', 'Unknown')
|
| 66 |
reasoning = safe_get(recommended_card, 'reasoning', 'Optimal choice')
|
| 67 |
|
|
|
|
| 68 |
if reward_rate > 0:
|
| 69 |
rewards_rate_str = f"{reward_rate}x points"
|
| 70 |
else:
|
| 71 |
rewards_rate_str = "N/A"
|
| 72 |
else:
|
|
|
|
| 73 |
card_name = str(recommended_card) if recommended_card else 'Unknown Card'
|
| 74 |
reward_amount = float(safe_get(data, 'rewards_earned', 0))
|
| 75 |
reward_rate = 0
|
|
|
|
| 77 |
category = safe_get(data, 'category', 'Unknown')
|
| 78 |
reasoning = safe_get(data, 'reasoning', 'Optimal choice')
|
| 79 |
|
|
|
|
| 80 |
merchant = safe_get(data, 'merchant', 'Unknown Merchant')
|
| 81 |
amount = float(safe_get(data, 'amount_usd', safe_get(data, 'amount', 0)))
|
|
|
|
|
|
|
| 82 |
annual_potential = reward_amount * 12 if reward_amount > 0 else 0
|
| 83 |
|
|
|
|
| 84 |
alternatives = []
|
| 85 |
alt_cards = safe_get(data, 'alternative_cards', safe_get(data, 'alternatives', []))
|
| 86 |
|
| 87 |
+
for alt in alt_cards[:3]:
|
| 88 |
if isinstance(alt, dict):
|
| 89 |
alt_name = safe_get(alt, 'card_name', safe_get(alt, 'card', 'Unknown'))
|
| 90 |
alt_reward = float(safe_get(alt, 'reward_amount', safe_get(alt, 'rewards', 0)))
|
|
|
|
| 101 |
'rate': alt_rate_str
|
| 102 |
})
|
| 103 |
|
|
|
|
| 104 |
warnings = safe_get(data, 'warnings', [])
|
| 105 |
forecast_warning = safe_get(data, 'forecast_warning')
|
| 106 |
if forecast_warning and isinstance(forecast_warning, dict):
|
|
|
|
| 108 |
if warning_msg:
|
| 109 |
warnings.append(warning_msg)
|
| 110 |
|
|
|
|
| 111 |
normalized = {
|
| 112 |
'recommended_card': card_name,
|
| 113 |
'rewards_earned': round(reward_amount, 2),
|
|
|
|
| 129 |
"""Create loading indicator message"""
|
| 130 |
return "⏳ **Loading...** Please wait while we fetch your recommendation.", None
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
def load_card_database() -> dict:
|
| 133 |
"""Load card database from local cards.json"""
|
| 134 |
try:
|
|
|
|
| 143 |
print(f"❌ Error parsing cards.json: {e}")
|
| 144 |
return {}
|
| 145 |
|
|
|
|
| 146 |
CARD_DATABASE = load_card_database()
|
| 147 |
|
| 148 |
def get_card_details(card_id: str, mcc: str = None) -> dict:
|
|
|
|
| 168 |
}
|
| 169 |
|
| 170 |
card = CARD_DATABASE[card_id]
|
|
|
|
|
|
|
| 171 |
reward_rate = 1.0
|
| 172 |
+
|
| 173 |
if mcc and "reward_structure" in card:
|
| 174 |
reward_structure = card["reward_structure"]
|
| 175 |
|
|
|
|
| 176 |
if mcc in reward_structure:
|
| 177 |
reward_rate = reward_structure[mcc]
|
| 178 |
else:
|
|
|
|
| 179 |
try:
|
| 180 |
mcc_int = int(mcc)
|
| 181 |
for key, rate in reward_structure.items():
|
|
|
|
| 187 |
except (ValueError, AttributeError):
|
| 188 |
pass
|
| 189 |
|
|
|
|
| 190 |
if reward_rate == 1.0 and "default" in reward_structure:
|
| 191 |
reward_rate = reward_structure["default"]
|
| 192 |
|
|
|
|
| 193 |
spending_caps = card.get("spending_caps", {})
|
| 194 |
cap_info = {}
|
| 195 |
|
|
|
|
| 220 |
"spending_caps": cap_info,
|
| 221 |
"benefits": card.get("benefits", [])
|
| 222 |
}
|
|
|
|
|
|
|
| 223 |
|
| 224 |
def get_recommendation_with_agent(user_id, merchant, category, amount):
|
|
|
|
|
|
|
|
|
|
| 225 |
yield "⏳ **Agent is thinking...** Analyzing your transaction and cards...", None
|
| 226 |
|
| 227 |
try:
|
|
|
|
| 259 |
|
| 260 |
print(f"🔍 KEYS: {list(result.keys())}")
|
| 261 |
|
|
|
|
| 262 |
card_id = result.get('recommended_card', 'Unknown')
|
| 263 |
rewards_earned = float(result.get('rewards_earned', 0))
|
| 264 |
rewards_rate = result.get('rewards_rate', 'N/A')
|
|
|
|
| 267 |
alternatives = result.get('alternative_options', [])
|
| 268 |
warnings = result.get('warnings', [])
|
| 269 |
|
|
|
|
| 270 |
card_name_map = {
|
| 271 |
'c_citi_custom_cash': 'Citi Custom Cash',
|
| 272 |
'c_amex_gold': 'American Express Gold',
|
|
|
|
| 279 |
}
|
| 280 |
card_name = card_name_map.get(card_id, card_id.replace('c_', '').replace('_', ' ').title())
|
| 281 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
transaction_mcc = result.get('mcc', MCC_CATEGORIES.get(category, "5999"))
|
|
|
|
|
|
|
| 283 |
card_details_from_db = get_card_details(card_id, transaction_mcc)
|
|
|
|
|
|
|
| 284 |
card_details = result.get('card_details', {})
|
| 285 |
|
| 286 |
if not card_details or not card_details.get('reward_rate'):
|
|
|
|
| 287 |
reward_structure = CARD_DATABASE.get(card_id, {}).get('reward_structure', {})
|
| 288 |
|
|
|
|
| 289 |
if transaction_mcc in reward_structure:
|
| 290 |
reward_rate_value = reward_structure[transaction_mcc]
|
| 291 |
else:
|
| 292 |
reward_rate_value = reward_structure.get('default', 1.0)
|
| 293 |
|
|
|
|
| 294 |
spending_caps_db = CARD_DATABASE.get(card_id, {}).get('spending_caps', {})
|
| 295 |
|
| 296 |
card_details = {
|
|
|
|
| 314 |
|
| 315 |
print(f"✅ CARD DETAILS: {reward_rate_value}%, cap={monthly_cap or annual_cap}, fee=${annual_fee}")
|
| 316 |
|
|
|
|
| 317 |
amount_float = float(amount)
|
| 318 |
|
|
|
|
| 319 |
frequency_map = {
|
| 320 |
+
'Groceries': 52,
|
| 321 |
'Restaurants': 52,
|
| 322 |
'Gas Stations': 52,
|
| 323 |
'Fast Food': 52,
|
| 324 |
+
'Airlines': 4,
|
| 325 |
+
'Hotels': 12,
|
| 326 |
'Online Shopping': 24,
|
| 327 |
'Entertainment': 24,
|
| 328 |
}
|
|
|
|
| 337 |
|
| 338 |
annual_spend = amount_float * frequency
|
| 339 |
|
|
|
|
| 340 |
if monthly_cap:
|
|
|
|
| 341 |
monthly_cap_annual = monthly_cap * 12
|
| 342 |
|
| 343 |
if annual_spend <= monthly_cap_annual:
|
|
|
|
| 360 |
| **Net Rewards** | - | - | **${total_rewards - annual_fee:.2f}** |"""
|
| 361 |
|
| 362 |
elif annual_cap:
|
|
|
|
| 363 |
if annual_spend <= annual_cap:
|
| 364 |
high_rate_spend = annual_spend
|
| 365 |
low_rate_spend = 0
|
|
|
|
| 380 |
| **Net Rewards** | - | - | **${total_rewards - annual_fee:.2f}** |"""
|
| 381 |
|
| 382 |
else:
|
|
|
|
| 383 |
total_rewards = annual_spend * (reward_rate_value / 100)
|
| 384 |
|
| 385 |
calc_table = f"""| Spending Tier | Annual Amount | Rate | Rewards |
|
|
|
|
| 388 |
| Annual fee | - | - | -${annual_fee:.2f} |
|
| 389 |
| **Net Rewards** | - | - | **${total_rewards - annual_fee:.2f}** |"""
|
| 390 |
|
|
|
|
| 391 |
baseline_rewards = annual_spend * 0.01
|
| 392 |
net_rewards = total_rewards - annual_fee
|
| 393 |
net_benefit = net_rewards - baseline_rewards
|
|
|
|
| 402 |
|
| 403 |
**Net Benefit: ${net_benefit:+.2f}/year** {"🎉" if net_benefit > 0 else "⚠️"}"""
|
| 404 |
|
| 405 |
+
max_possible_rewards = annual_spend * 0.06
|
|
|
|
|
|
|
| 406 |
|
| 407 |
if max_possible_rewards > 0:
|
| 408 |
performance_ratio = (net_rewards / max_possible_rewards) * 100
|
| 409 |
|
|
|
|
| 410 |
if net_rewards > baseline_rewards:
|
| 411 |
improvement = (net_rewards - baseline_rewards) / baseline_rewards
|
| 412 |
baseline_bonus = min(improvement * 20, 20)
|
| 413 |
else:
|
| 414 |
+
baseline_bonus = -10
|
| 415 |
|
| 416 |
optimization_score = int(min(performance_ratio + baseline_bonus, 100))
|
| 417 |
else:
|
| 418 |
optimization_score = 0
|
| 419 |
|
|
|
|
| 420 |
score_breakdown = {
|
| 421 |
'reward_rate': min(30, int(optimization_score * 0.30)),
|
| 422 |
'cap_availability': min(25, int(optimization_score * 0.25)),
|
|
|
|
| 441 |
- 60-69: Acceptable ⚠️
|
| 442 |
- <60: Suboptimal ❌"""
|
| 443 |
|
|
|
|
| 444 |
output = f"""## 🤖 AI Agent Recommendation
|
| 445 |
|
| 446 |
### 💳 Recommended Card: **{card_name}**
|
|
|
|
| 451 |
---
|
| 452 |
|
| 453 |
### 🧠 Agent's Reasoning:
|
|
|
|
| 454 |
{reasoning}
|
| 455 |
|
| 456 |
---"""
|
| 457 |
|
|
|
|
| 458 |
if alternatives:
|
| 459 |
output += "\n### 🔄 Alternative Options:\n\n"
|
| 460 |
for alt in alternatives[:3]:
|
|
|
|
| 463 |
alt_reason = alt.get('reason', '')
|
| 464 |
output += f"**{alt_card_name}:**\n{alt_reason}\n\n"
|
| 465 |
|
|
|
|
| 466 |
if warnings:
|
| 467 |
output += "\n### ⚠️ Important Warnings:\n\n"
|
| 468 |
for warning in warnings:
|
| 469 |
output += f"- {warning}\n"
|
| 470 |
|
|
|
|
| 471 |
output += f"""
|
| 472 |
### 💰 Annual Impact
|
|
|
|
| 473 |
- **Potential Savings:** ${net_benefit:.2f}/year
|
| 474 |
- **Optimization Score:** {optimization_score}/100
|
| 475 |
|
|
|
|
| 479 |
#### 💡 Calculation Assumptions:
|
| 480 |
|
| 481 |
**Step 1: Estimate Annual Spending**
|
|
|
|
| 482 |
Current transaction: ${amount_float:.2f} at {merchant}
|
| 483 |
Category: {category}
|
| 484 |
Frequency assumption: {frequency_label.capitalize()}
|
| 485 |
Annual estimate: ${amount_float:.2f} × {frequency} = **${annual_spend:.2f}**
|
| 486 |
|
| 487 |
**Step 2: Calculate Rewards with {card_name}**
|
|
|
|
| 488 |
{calc_table}
|
| 489 |
|
| 490 |
**Step 3: Compare to Baseline**
|
|
|
|
| 491 |
{comparison_text}
|
| 492 |
|
| 493 |
---
|
| 494 |
|
| 495 |
#### 📈 Optimization Score: {optimization_score}/100
|
|
|
|
| 496 |
{score_details}
|
| 497 |
|
| 498 |
---
|
|
|
|
| 506 |
|
| 507 |
</details>
|
| 508 |
|
| 509 |
+
---
|
| 510 |
+
|
| 511 |
+
#### 📊 Transaction Details:
|
|
|
|
| 512 |
- **Amount:** ${amount_float:.2f}
|
| 513 |
- **Merchant:** {merchant}
|
| 514 |
- **Category:** {category}
|
|
|
|
| 522 |
print("=" * 80)
|
| 523 |
|
| 524 |
except Exception as e:
|
|
|
|
| 525 |
print(f"❌ ERROR: {traceback.format_exc()}")
|
| 526 |
yield f"❌ **Error:** {str(e)}", None
|
| 527 |
|
| 528 |
def create_agent_recommendation_chart_enhanced(result: Dict) -> go.Figure:
|
| 529 |
try:
|
|
|
|
| 530 |
rec_name_map = {
|
| 531 |
'c_citi_custom_cash': 'Citi Custom Cash',
|
| 532 |
'c_amex_gold': 'Amex Gold',
|
|
|
|
| 546 |
for alt in alternatives[:3]:
|
| 547 |
alt_id = alt.get('card', '')
|
| 548 |
alt_name = rec_name_map.get(alt_id, alt_id)
|
| 549 |
+
alt_reward = rec_reward * 0.8
|
|
|
|
| 550 |
cards.append(alt_name)
|
| 551 |
rewards.append(alt_reward)
|
| 552 |
colors.append('#cbd5e0')
|
|
|
|
| 578 |
fig.add_annotation(text="Chart unavailable", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False)
|
| 579 |
fig.update_layout(height=400, template='plotly_white')
|
| 580 |
return fig
|
| 581 |
+
|
|
|
|
| 582 |
client = RewardPilotClient(config.ORCHESTRATOR_URL)
|
| 583 |
llm = get_llm_explainer()
|
| 584 |
|
|
|
|
|
|
|
| 585 |
def get_recommendation(
|
| 586 |
user_id: str,
|
| 587 |
merchant: str,
|
|
|
|
| 592 |
transaction_date: Optional[str]
|
| 593 |
) -> tuple:
|
| 594 |
"""Get card recommendation and format response"""
|
|
|
|
| 595 |
if not user_id or not merchant or amount <= 0:
|
| 596 |
return (
|
| 597 |
"❌ **Error:** Please fill in all required fields.",
|
|
|
|
| 599 |
None,
|
| 600 |
)
|
| 601 |
|
|
|
|
| 602 |
if use_custom_mcc and custom_mcc:
|
| 603 |
mcc = custom_mcc
|
| 604 |
else:
|
| 605 |
mcc = MCC_CATEGORIES.get(category, "5999")
|
| 606 |
|
|
|
|
| 607 |
if not transaction_date:
|
| 608 |
transaction_date = str(date.today())
|
| 609 |
|
|
|
|
| 610 |
response: Dict[str, Any] = client.get_recommendation_sync(
|
| 611 |
user_id=user_id,
|
| 612 |
merchant=merchant,
|
|
|
|
| 615 |
transaction_date=transaction_date,
|
| 616 |
)
|
| 617 |
|
|
|
|
| 618 |
formatted_text = format_full_recommendation(response)
|
| 619 |
|
|
|
|
| 620 |
comparison_table: Optional[str]
|
| 621 |
stats: Optional[str]
|
| 622 |
if not response.get("error"):
|
|
|
|
| 625 |
all_cards = [c for c in ([recommended] + alternatives) if c]
|
| 626 |
comparison_table = format_comparison_table(all_cards) if all_cards else None
|
| 627 |
|
|
|
|
| 628 |
total_analyzed = response.get("total_cards_analyzed", len(all_cards))
|
| 629 |
best_reward = (recommended.get("reward_amount") or 0.0)
|
| 630 |
services_used = response.get("services_used", [])
|
|
|
|
| 639 |
|
| 640 |
return formatted_text, comparison_table, stats
|
| 641 |
|
|
|
|
| 642 |
def get_recommendation_with_ai(user_id, merchant, category, amount):
|
| 643 |
"""Get card recommendation with LLM-powered explanation"""
|
| 644 |
|
|
|
|
| 645 |
if not merchant or not merchant.strip():
|
| 646 |
return "❌ Please enter a merchant name.", None
|
| 647 |
|
| 648 |
if amount <= 0:
|
| 649 |
return "❌ Please enter a valid amount greater than $0.", None
|
| 650 |
|
|
|
|
| 651 |
yield "⏳ **Loading recommendation...** Analyzing your cards and transaction...", None
|
| 652 |
|
| 653 |
try:
|
|
|
|
| 654 |
result = client.get_recommendation(
|
| 655 |
user_id=user_id,
|
| 656 |
merchant=merchant,
|
|
|
|
| 659 |
mcc=None
|
| 660 |
)
|
| 661 |
|
|
|
|
| 662 |
if not result.get('success'):
|
| 663 |
error_msg = result.get('error', 'Unknown error')
|
| 664 |
yield f"❌ Error: {error_msg}", None
|
| 665 |
return
|
| 666 |
|
|
|
|
| 667 |
data = normalize_recommendation_data(result.get('data', {}))
|
| 668 |
|
|
|
|
| 669 |
ai_explanation = ""
|
| 670 |
if config.LLM_ENABLED:
|
| 671 |
try:
|
|
|
|
| 684 |
print(f"LLM explanation failed: {e}")
|
| 685 |
ai_explanation = ""
|
| 686 |
|
|
|
|
| 687 |
output = f"""
|
| 688 |
## 🎯 Recommendation for ${amount:.2f} at {merchant}
|
| 689 |
|
|
|
|
| 701 |
output += f"""
|
| 702 |
### 🤖 AI Insight
|
| 703 |
{ai_explanation}
|
|
|
|
| 704 |
---
|
| 705 |
"""
|
| 706 |
|
|
|
|
| 723 |
for alt in data['alternatives']:
|
| 724 |
output += f"- **{alt['card']}:** ${alt['rewards']:.2f} ({alt['rate']})\n"
|
| 725 |
|
|
|
|
| 726 |
chart = create_rewards_comparison_chart(data)
|
| 727 |
|
| 728 |
yield output, chart
|
| 729 |
|
| 730 |
except Exception as e:
|
|
|
|
| 731 |
error_details = traceback.format_exc()
|
| 732 |
print(f"Recommendation error: {error_details}")
|
| 733 |
yield f"❌ Error: {str(e)}\n\nPlease check your API connection or try again.", None
|
|
|
|
| 736 |
"""Create rewards comparison chart with proper error handling"""
|
| 737 |
|
| 738 |
try:
|
|
|
|
| 739 |
cards = [data['recommended_card']]
|
| 740 |
rewards = [data['rewards_earned']]
|
| 741 |
+
colors = ['#667eea']
|
| 742 |
|
|
|
|
| 743 |
for alt in data.get('alternatives', [])[:3]:
|
| 744 |
cards.append(alt['card'])
|
| 745 |
rewards.append(float(alt['rewards']))
|
| 746 |
+
colors.append('#a0aec0')
|
| 747 |
|
|
|
|
| 748 |
if not cards or all(r == 0 for r in rewards):
|
| 749 |
fig = go.Figure()
|
| 750 |
fig.add_annotation(
|
|
|
|
| 759 |
)
|
| 760 |
return fig
|
| 761 |
|
|
|
|
| 762 |
fig = go.Figure(data=[
|
| 763 |
go.Bar(
|
| 764 |
x=cards,
|
|
|
|
| 792 |
|
| 793 |
except Exception as e:
|
| 794 |
print(f"Chart creation error: {e}")
|
|
|
|
| 795 |
print(traceback.format_exc())
|
| 796 |
|
|
|
|
| 797 |
fig = go.Figure()
|
| 798 |
fig.add_annotation(
|
| 799 |
text=f"Error creating chart",
|
|
|
|
| 803 |
)
|
| 804 |
fig.update_layout(height=400, template='plotly_white')
|
| 805 |
return fig
|
| 806 |
+
|
| 807 |
def get_analytics_with_insights(user_id):
|
| 808 |
"""Get analytics with LLM-generated insights"""
|
| 809 |
|
| 810 |
try:
|
|
|
|
| 811 |
result = client.get_user_analytics(user_id)
|
| 812 |
|
| 813 |
if not result.get('success'):
|
|
|
|
| 815 |
|
| 816 |
data = result['data']
|
| 817 |
|
|
|
|
| 818 |
ai_insights = ""
|
| 819 |
if config.LLM_ENABLED:
|
| 820 |
try:
|
|
|
|
| 830 |
print(f"AI insights generation failed: {e}")
|
| 831 |
ai_insights = ""
|
| 832 |
|
|
|
|
| 833 |
metrics = f"""
|
| 834 |
## 📊 Your Rewards Analytics
|
| 835 |
|
| 836 |
### Key Metrics
|
|
|
|
| 837 |
- **💰 Total Rewards:** ${data['total_rewards']:.2f}
|
| 838 |
- **📈 Potential Savings:** ${data['potential_savings']:.2f}/year
|
| 839 |
- **⭐ Optimization Score:** {data['optimization_score']}/100
|
| 840 |
- **✅ Optimized Transactions:** {data.get('optimized_count', 0)}
|
| 841 |
"""
|
| 842 |
|
|
|
|
| 843 |
if ai_insights:
|
| 844 |
metrics += f"""
|
| 845 |
### 🤖 Personalized Insights
|
|
|
|
| 846 |
{ai_insights}
|
|
|
|
| 847 |
---
|
| 848 |
"""
|
| 849 |
|
|
|
|
| 850 |
spending_chart = create_spending_chart(data)
|
| 851 |
rewards_chart = create_rewards_distribution_chart(data)
|
| 852 |
optimization_chart = create_optimization_gauge(data['optimization_score'])
|
|
|
|
| 856 |
except Exception as e:
|
| 857 |
return f"❌ Error: {str(e)}", None, None, None
|
| 858 |
|
|
|
|
| 859 |
EXAMPLES = [
|
| 860 |
["u_alice", "Groceries", "Whole Foods", 125.50, False, "", "2025-01-15"],
|
| 861 |
["u_bob", "Restaurants", "Olive Garden", 65.75, False, "", "2025-01-15"],
|
|
|
|
| 864 |
["u_bob", "Gas Stations", "Shell", 45.00, False, "", ""],
|
| 865 |
]
|
| 866 |
|
|
|
|
| 867 |
def _toggle_custom_mcc(use_custom: bool):
|
| 868 |
return gr.update(visible=use_custom, value="")
|
| 869 |
|
|
|
|
| 878 |
font-size: 16px;
|
| 879 |
line-height: 1.6;
|
| 880 |
}
|
|
|
|
| 881 |
.metric-card {
|
| 882 |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 883 |
color: white;
|
|
|
|
| 913 |
.metric-card-blue {
|
| 914 |
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
| 915 |
}
|
|
|
|
| 916 |
table {
|
| 917 |
width: 100%;
|
| 918 |
border-collapse: collapse;
|
|
|
|
| 941 |
}
|
| 942 |
""",
|
| 943 |
) as app:
|
|
|
|
| 944 |
gr.Markdown(
|
| 945 |
f"""
|
| 946 |
# {APP_TITLE}
|
|
|
|
| 954 |
---
|
| 955 |
"""
|
| 956 |
)
|
| 957 |
+
|
| 958 |
agent_status = """
|
| 959 |
🤖 **Autonomous Agent:** ✅ Active (Claude 3.5 Sonnet)
|
| 960 |
📊 **Mode:** Dynamic Planning + Reasoning
|
|
|
|
| 962 |
"""
|
| 963 |
gr.Markdown(agent_status)
|
| 964 |
|
|
|
|
|
|
|
| 965 |
with gr.Tabs():
|
|
|
|
| 966 |
with gr.Tab("🎯 Get Recommendation"):
|
| 967 |
with gr.Row():
|
| 968 |
with gr.Column(scale=1):
|
|
|
|
| 974 |
info="Select a user"
|
| 975 |
)
|
| 976 |
|
|
|
|
| 977 |
category_dropdown = gr.Dropdown(
|
| 978 |
choices=list(MCC_CATEGORIES.keys()),
|
| 979 |
value="Groceries",
|
|
|
|
| 981 |
info="Select the category first"
|
| 982 |
)
|
| 983 |
|
|
|
|
| 984 |
merchant_dropdown = gr.Dropdown(
|
| 985 |
choices=MERCHANTS_BY_CATEGORY["Groceries"],
|
| 986 |
value="Whole Foods",
|
| 987 |
label="🏪 Merchant Name",
|
| 988 |
info="Select merchant (changes based on category)",
|
| 989 |
+
allow_custom_value=True
|
| 990 |
)
|
| 991 |
|
| 992 |
amount_input = gr.Number(
|
|
|
|
| 1002 |
value=""
|
| 1003 |
)
|
| 1004 |
|
|
|
|
| 1005 |
with gr.Accordion("⚙️ Advanced Options", open=False):
|
| 1006 |
use_custom_mcc = gr.Checkbox(
|
| 1007 |
label="Use Custom MCC Code",
|
|
|
|
| 1027 |
variant="primary",
|
| 1028 |
size="lg"
|
| 1029 |
)
|
|
|
|
| 1030 |
|
| 1031 |
with gr.Column(scale=2):
|
| 1032 |
gr.Markdown("### 💡 Recommendation")
|
|
|
|
| 1035 |
elem_classes=["recommendation-output"]
|
| 1036 |
)
|
| 1037 |
recommendation_chart = gr.Plot()
|
|
|
|
|
|
|
| 1038 |
|
| 1039 |
def update_merchant_choices(category):
|
| 1040 |
"""Update merchant dropdown based on selected category"""
|
|
|
|
| 1044 |
value=merchants[0] if merchants else ""
|
| 1045 |
)
|
| 1046 |
|
|
|
|
| 1047 |
category_dropdown.change(
|
| 1048 |
fn=update_merchant_choices,
|
| 1049 |
inputs=[category_dropdown],
|
| 1050 |
outputs=[merchant_dropdown]
|
| 1051 |
)
|
| 1052 |
|
|
|
|
| 1053 |
with gr.Row():
|
| 1054 |
with gr.Column():
|
| 1055 |
gr.Markdown("### 📊 Quick Stats")
|
|
|
|
| 1059 |
gr.Markdown("### 🔄 Card Comparison")
|
| 1060 |
comparison_output = gr.Markdown()
|
| 1061 |
|
|
|
|
| 1062 |
recommend_btn.click(
|
| 1063 |
+
fn=get_recommendation_with_agent,
|
| 1064 |
+
inputs=[user_dropdown, merchant_dropdown, category_dropdown, amount_input],
|
| 1065 |
+
outputs=[recommendation_output, recommendation_chart]
|
| 1066 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1067 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1068 |
gr.Markdown("### 📝 Example Transactions")
|
| 1069 |
gr.Examples(
|
| 1070 |
examples=EXAMPLES,
|
| 1071 |
inputs=[
|
| 1072 |
user_dropdown,
|
| 1073 |
+
category_dropdown,
|
| 1074 |
+
merchant_dropdown,
|
| 1075 |
amount_input,
|
| 1076 |
use_custom_mcc,
|
| 1077 |
custom_mcc_input,
|
|
|
|
| 1086 |
cache_examples=False
|
| 1087 |
)
|
| 1088 |
|
|
|
|
| 1089 |
with gr.Tab("📊 Analytics"):
|
| 1090 |
gr.Markdown("## 🎯 Your Rewards Optimization Dashboard")
|
| 1091 |
|
|
|
|
| 1092 |
with gr.Row():
|
| 1093 |
analytics_user = gr.Dropdown(
|
| 1094 |
choices=SAMPLE_USERS,
|
|
|
|
| 1102 |
scale=1
|
| 1103 |
)
|
| 1104 |
|
|
|
|
| 1105 |
metrics_display = gr.HTML(
|
| 1106 |
value="""
|
| 1107 |
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
|
|
|
| 1126 |
)
|
| 1127 |
|
| 1128 |
gr.Markdown("---")
|
|
|
|
|
|
|
| 1129 |
gr.Markdown("## 📊 Visual Analytics")
|
| 1130 |
|
|
|
|
| 1131 |
with gr.Row():
|
| 1132 |
with gr.Column(scale=2):
|
| 1133 |
spending_chart = gr.Plot(label="Spending vs Rewards")
|
| 1134 |
with gr.Column(scale=1):
|
| 1135 |
optimization_gauge = gr.Plot(label="Your Score")
|
| 1136 |
|
|
|
|
| 1137 |
with gr.Row():
|
| 1138 |
with gr.Column(scale=1):
|
| 1139 |
rewards_pie_chart = gr.Plot(label="Rewards Distribution")
|
| 1140 |
with gr.Column(scale=1):
|
| 1141 |
card_performance_chart = gr.Plot(label="Top Performing Cards")
|
| 1142 |
|
|
|
|
| 1143 |
with gr.Row():
|
| 1144 |
trend_chart = gr.Plot(label="12-Month Trends")
|
| 1145 |
|
| 1146 |
gr.Markdown("---")
|
|
|
|
|
|
|
| 1147 |
gr.Markdown("## 📋 Detailed Breakdown")
|
| 1148 |
|
|
|
|
| 1149 |
with gr.Row():
|
| 1150 |
with gr.Column(scale=1):
|
| 1151 |
gr.Markdown("### 💰 Category Spending Breakdown")
|
|
|
|
| 1189 |
|
| 1190 |
gr.Markdown("---")
|
| 1191 |
|
|
|
|
| 1192 |
forecast_display = gr.Markdown(
|
| 1193 |
value="""
|
| 1194 |
### 🔮 Next Month Forecast
|
|
|
|
| 1205 |
"""
|
| 1206 |
)
|
| 1207 |
|
|
|
|
| 1208 |
analytics_status = gr.Markdown(
|
| 1209 |
value="*Analytics loaded for u_alice*",
|
| 1210 |
elem_classes=["status-text"]
|
| 1211 |
)
|
| 1212 |
|
|
|
|
| 1213 |
def update_analytics_with_charts(user_id: str):
|
| 1214 |
"""Fetch and format analytics with charts for selected user"""
|
| 1215 |
|
| 1216 |
try:
|
|
|
|
| 1217 |
result = client.get_user_analytics(user_id)
|
| 1218 |
|
|
|
|
| 1219 |
print("=" * 60)
|
| 1220 |
print(f"DEBUG: Analytics for {user_id}")
|
| 1221 |
print(f"Success: {result.get('success')}")
|
|
|
|
| 1225 |
print(f"Total rewards: {result['data'].get('total_rewards')}")
|
| 1226 |
print("=" * 60)
|
| 1227 |
|
|
|
|
| 1228 |
if not result.get('success'):
|
| 1229 |
error_msg = result.get('error', 'Unknown error')
|
| 1230 |
empty_fig = create_empty_chart(f"Error: {error_msg}")
|
|
|
|
| 1237 |
f"*Error: {error_msg}*"
|
| 1238 |
)
|
| 1239 |
|
|
|
|
| 1240 |
analytics_data = result.get('data', {})
|
| 1241 |
|
|
|
|
| 1242 |
if not analytics_data:
|
| 1243 |
empty_fig = create_empty_chart("No analytics data available")
|
| 1244 |
return (
|
|
|
|
| 1250 |
"*No data available*"
|
| 1251 |
)
|
| 1252 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1253 |
metrics_html, table_md, insights_md, forecast_md = format_analytics_metrics(analytics_data)
|
| 1254 |
|
|
|
|
| 1255 |
spending_fig = create_spending_chart(analytics_data)
|
| 1256 |
pie_fig = create_rewards_pie_chart(analytics_data)
|
| 1257 |
gauge_fig = create_optimization_gauge(analytics_data)
|
| 1258 |
trend_fig = create_trend_line_chart(analytics_data)
|
| 1259 |
performance_fig = create_card_performance_chart(analytics_data)
|
| 1260 |
|
|
|
|
| 1261 |
from datetime import datetime
|
| 1262 |
status = f"*Analytics updated for {user_id} at {datetime.now().strftime('%I:%M %p')}*"
|
| 1263 |
|
| 1264 |
return (
|
| 1265 |
+
metrics_html,
|
| 1266 |
+
spending_fig,
|
| 1267 |
+
gauge_fig,
|
| 1268 |
+
pie_fig,
|
| 1269 |
+
performance_fig,
|
| 1270 |
+
trend_fig,
|
| 1271 |
+
table_md,
|
| 1272 |
+
insights_md,
|
| 1273 |
+
forecast_md,
|
| 1274 |
+
status
|
| 1275 |
)
|
| 1276 |
|
| 1277 |
except Exception as e:
|
|
|
|
| 1278 |
error_details = traceback.format_exc()
|
| 1279 |
error_msg = f"❌ Error loading analytics: {str(e)}"
|
| 1280 |
print(error_msg)
|
| 1281 |
print(error_details)
|
| 1282 |
|
|
|
|
| 1283 |
empty_fig = create_empty_chart("Error loading chart")
|
| 1284 |
|
| 1285 |
return (
|
|
|
|
| 1290 |
"Error loading forecast",
|
| 1291 |
f"*{error_msg}*"
|
| 1292 |
)
|
|
|
|
| 1293 |
|
| 1294 |
def create_empty_chart(message: str) -> go.Figure:
|
| 1295 |
"""Helper to create empty chart with message"""
|
|
|
|
| 1303 |
fig.update_layout(height=400, template='plotly_white')
|
| 1304 |
return fig
|
| 1305 |
|
|
|
|
| 1306 |
refresh_analytics_btn.click(
|
| 1307 |
fn=update_analytics_with_charts,
|
| 1308 |
inputs=[analytics_user],
|
|
|
|
| 1336 |
analytics_status
|
| 1337 |
]
|
| 1338 |
)
|
| 1339 |
+
|
|
|
|
| 1340 |
with gr.Tab("💬 Ask AI"):
|
| 1341 |
gr.Markdown("## Chat with RewardPilot AI")
|
| 1342 |
gr.Markdown("*Ask questions about credit cards, rewards, and your spending*")
|
|
|
|
| 1362 |
if not message.strip():
|
| 1363 |
return "", chat_history
|
| 1364 |
|
|
|
|
| 1365 |
user_context = {}
|
| 1366 |
try:
|
| 1367 |
analytics = client.get_user_analytics(user_id)
|
|
|
|
| 1380 |
'top_category': 'Groceries'
|
| 1381 |
}
|
| 1382 |
|
|
|
|
| 1383 |
try:
|
| 1384 |
if config.LLM_ENABLED:
|
| 1385 |
bot_response = llm.chat_response(message, user_context, chat_history)
|
|
|
|
| 1395 |
msg.submit(respond, [msg, chatbot, chat_user], [msg, chatbot])
|
| 1396 |
send_btn.click(respond, [msg, chatbot, chat_user], [msg, chatbot])
|
| 1397 |
|
|
|
|
| 1398 |
gr.Markdown("### 💡 Try asking:")
|
| 1399 |
gr.Examples(
|
| 1400 |
examples=[
|
|
|
|
| 1406 |
],
|
| 1407 |
inputs=[msg]
|
| 1408 |
)
|
| 1409 |
+
|
| 1410 |
with gr.Tab("🤖 Agent Insights"):
|
| 1411 |
gr.Markdown("""
|
| 1412 |
## How the Autonomous Agent Works
|
|
|
|
| 1480 |
|
| 1481 |
**Try it out in the "Get Recommendation" tab!** 🚀
|
| 1482 |
""")
|
| 1483 |
+
|
|
|
|
| 1484 |
with gr.Tab("ℹ️ About"):
|
| 1485 |
gr.Markdown(
|
| 1486 |
"""
|
|
|
|
| 1496 |
- 📊 **Interactive visualizations**
|
| 1497 |
|
| 1498 |
### Features
|
|
|
|
| 1499 |
- Smart card recommendations for every purchase
|
| 1500 |
- AI-generated personalized insights
|
| 1501 |
- Visual analytics dashboard
|
|
|
|
| 1503 |
- Real-time cap warnings
|
| 1504 |
- Multi-card comparison
|
| 1505 |
|
| 1506 |
+
The system consists of multiple microservices
|
|
|
|
| 1507 |
1. **Smart Wallet** - Analyzes transaction context and selects optimal cards
|
| 1508 |
2. **Rewards-RAG** - Retrieves detailed card benefit information using RAG
|
| 1509 |
3. **Spend-Forecast** - Predicts spending patterns and warns about cap risks
|
| 1510 |
4. **Orchestrator** - Coordinates all services for comprehensive recommendations
|
| 1511 |
|
| 1512 |
### 🎯 How It Works
|
|
|
|
| 1513 |
1. **Enter Transaction Details** - Merchant, amount, category
|
| 1514 |
2. **AI Analysis** - System analyzes your wallet and transaction context
|
| 1515 |
3. **Get Recommendation** - Receive the best card with detailed reasoning
|
| 1516 |
4. **Maximize Rewards** - Earn more points/cashback on every purchase
|
| 1517 |
|
| 1518 |
### 🔧 Technology Stack
|
|
|
|
| 1519 |
- **Backend:** FastAPI, Python
|
| 1520 |
- **Frontend:** Gradio
|
| 1521 |
- **AI/ML:** RAG (Retrieval-Augmented Generation)
|
|
|
|
| 1523 |
- **Deployment:** Hugging Face Spaces
|
| 1524 |
|
| 1525 |
### 📚 MCC Categories Supported
|
|
|
|
| 1526 |
- Groceries (5411)
|
| 1527 |
- Restaurants (5812)
|
| 1528 |
- Gas Stations (5541)
|
|
|
|
| 1532 |
- And many more...
|
| 1533 |
|
| 1534 |
### 🎓 Built For
|
|
|
|
| 1535 |
**MCP 1st Birthday Hackathon** - Celebrating one year of the Model Context Protocol
|
| 1536 |
|
| 1537 |
### 👨💻 Developer
|
|
|
|
| 1538 |
Built with ❤️ for the MCP community
|
| 1539 |
|
| 1540 |
---
|
|
|
|
| 1544 |
"""
|
| 1545 |
)
|
| 1546 |
|
|
|
|
| 1547 |
with gr.Tab("📖 API Docs"):
|
| 1548 |
gr.Markdown(
|
| 1549 |
"""
|
| 1550 |
## API Endpoints
|
| 1551 |
|
| 1552 |
### Orchestrator API
|
|
|
|
| 1553 |
**Base URL:** `https://mcp-1st-birthday-rewardpilot-orchestrator.hf.space`
|
| 1554 |
|
| 1555 |
#### POST `/recommend`
|
|
|
|
| 1556 |
Get comprehensive card recommendation.
|
| 1557 |
|
| 1558 |
**Request:**
|
|
|
|
| 1564 |
"amount_usd": 125.50,
|
| 1565 |
"transaction_date": "2025-01-15"
|
| 1566 |
}
|
| 1567 |
+
Other Services
|
| 1568 |
+
Smart Wallet: https://mcp-1st-birthday-rewardpilot-smart-wallet.hf.space
|
| 1569 |
+
Rewards-RAG: https://mcp-1st-birthday-rewardpilot-rewards-rag.hf.space
|
| 1570 |
+
Spend-Forecast: https://mcp-1st-birthday-rewardpilot-spend-forecast.hf.space
|
| 1571 |
+
Interactive DocsVisit /docs on any service for interactive Swagger UI documentation.cURL ExamplesbashCopy code[data-radix-scroll-area-viewport]{scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;}[data-radix-scroll-area-viewport]::-webkit-scrollbar{display:none}# Get recommendation
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1572 |
curl -X POST https://mcp-1st-birthday-rewardpilot-orchestrator.hf.space/recommend \\
|
| 1573 |
-H "Content-Type: application/json" \\
|
| 1574 |
-d '{
|
|
|
|
| 1576 |
"merchant": "Whole Foods",
|
| 1577 |
"mcc": "5411",
|
| 1578 |
"amount_usd": 125.50
|
| 1579 |
+
}'"""
|
| 1580 |
+
)
|
|
|
|
|
|
|
| 1581 |
|
| 1582 |
# ===================== Launch App =====================
|
| 1583 |
if __name__ == "__main__":
|
|
|
|
| 1585 |
server_name="0.0.0.0",
|
| 1586 |
server_port=7860,
|
| 1587 |
share=False,
|
| 1588 |
+
)
|