Update app.py
Browse files
app.py
CHANGED
|
@@ -2613,10 +2613,11 @@ with gr.Blocks(
|
|
| 2613 |
def call_modal_batch_auto(user_id, time_period, max_txns, include_small):
|
| 2614 |
"""
|
| 2615 |
Automatically fetch transactions and analyze with Modal
|
|
|
|
| 2616 |
"""
|
| 2617 |
import time
|
| 2618 |
|
| 2619 |
-
# β
FIX: Define days_map
|
| 2620 |
days_map = {
|
| 2621 |
"Last 7 Days": 7,
|
| 2622 |
"Last 30 Days": 30,
|
|
@@ -2629,14 +2630,14 @@ with gr.Blocks(
|
|
| 2629 |
# Step 1: Show loading state
|
| 2630 |
yield (
|
| 2631 |
f"""
|
| 2632 |
-
|
| 2633 |
-
|
| 2634 |
-
|
| 2635 |
-
|
| 2636 |
-
|
| 2637 |
-
|
| 2638 |
-
|
| 2639 |
-
|
| 2640 |
"",
|
| 2641 |
None,
|
| 2642 |
None
|
|
@@ -2661,13 +2662,13 @@ with gr.Blocks(
|
|
| 2661 |
# Step 3: Show processing state
|
| 2662 |
yield (
|
| 2663 |
f"""
|
| 2664 |
-
|
| 2665 |
-
|
| 2666 |
-
|
| 2667 |
-
|
| 2668 |
-
|
| 2669 |
-
|
| 2670 |
-
|
| 2671 |
"",
|
| 2672 |
None,
|
| 2673 |
None
|
|
@@ -2700,13 +2701,30 @@ with gr.Blocks(
|
|
| 2700 |
# Extract recommendation
|
| 2701 |
rec = data.get('recommendation', data)
|
| 2702 |
|
|
|
|
| 2703 |
card_id = rec.get('recommended_card', 'Unknown')
|
| 2704 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2705 |
optimal_rewards = float(rec.get('rewards_earned', 0))
|
| 2706 |
|
| 2707 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2708 |
actual_rewards = txn['amount'] * 0.01
|
| 2709 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2710 |
results.append({
|
| 2711 |
'date': txn['date'],
|
| 2712 |
'merchant': txn['merchant'],
|
|
@@ -2715,7 +2733,7 @@ with gr.Blocks(
|
|
| 2715 |
'recommended_card': card_name,
|
| 2716 |
'optimal_rewards': optimal_rewards,
|
| 2717 |
'actual_rewards': actual_rewards,
|
| 2718 |
-
'missed_savings':
|
| 2719 |
})
|
| 2720 |
|
| 2721 |
total_rewards_earned += actual_rewards
|
|
@@ -2735,29 +2753,45 @@ with gr.Blocks(
|
|
| 2735 |
)
|
| 2736 |
return
|
| 2737 |
|
| 2738 |
-
# Calculate metrics
|
| 2739 |
-
total_missed = total_optimal_rewards - total_rewards_earned
|
| 2740 |
-
|
| 2741 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2742 |
|
| 2743 |
# Sort by missed savings (biggest opportunities first)
|
| 2744 |
results.sort(key=lambda x: x['missed_savings'], reverse=True)
|
| 2745 |
|
| 2746 |
-
#
|
| 2747 |
days = days_map.get(time_period, 30)
|
| 2748 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2749 |
# Format output
|
| 2750 |
status_msg = f"""
|
| 2751 |
-
|
| 2752 |
-
|
| 2753 |
-
|
| 2754 |
-
|
| 2755 |
-
|
| 2756 |
-
|
| 2757 |
|
| 2758 |
output = f"""
|
| 2759 |
## π° Optimization Report for {user_id}
|
| 2760 |
-
|
| 2761 |
### π Summary
|
| 2762 |
|
| 2763 |
| Metric | Value |
|
|
@@ -2778,16 +2812,27 @@ with gr.Blocks(
|
|
| 2778 |
for rec in results[:10]:
|
| 2779 |
output += f"| {rec['date']} | {rec['merchant']} | ${rec['amount']:.2f} | {rec['recommended_card']} | ${rec['missed_savings']:.2f} |\n"
|
| 2780 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2781 |
output += f"""
|
| 2782 |
-
|
| 2783 |
---
|
| 2784 |
|
| 2785 |
### π‘ Key Insights
|
| 2786 |
|
| 2787 |
-
- **Biggest Single Opportunity:** ${
|
| 2788 |
-
- **Most Common Category:** {
|
| 2789 |
- **Average Transaction:** ${total_spending / len(results):.2f}
|
| 2790 |
-
- **Optimization Potential:** {
|
| 2791 |
|
| 2792 |
---
|
| 2793 |
|
|
@@ -2796,9 +2841,10 @@ with gr.Blocks(
|
|
| 2796 |
<p style="margin: 0; color: #5d4037; font-size: 15px;">
|
| 2797 |
If you had used our AI recommendations for these {len(results)} transactions, you would have earned
|
| 2798 |
<strong style="color: #e65100;">${total_missed:.2f} more</strong> in rewards.
|
| 2799 |
-
Over a full year, that's <strong style="color: #e65100;">${
|
| 2800 |
</p>
|
| 2801 |
</div>
|
|
|
|
| 2802 |
---
|
| 2803 |
|
| 2804 |
<div style="background: #e8f5e9; padding: 20px; border-radius: 10px; border-left: 4px solid #4caf50;">
|
|
@@ -2818,21 +2864,21 @@ with gr.Blocks(
|
|
| 2818 |
merchant_data[r['merchant']]['optimal'] += r['optimal_rewards']
|
| 2819 |
merchant_data[r['merchant']]['actual'] += r['actual_rewards']
|
| 2820 |
|
| 2821 |
-
top_merchants = sorted(merchant_data.items(), key=lambda x: x[
|
| 2822 |
|
| 2823 |
fig1 = go.Figure()
|
| 2824 |
|
| 2825 |
fig1.add_trace(go.Bar(
|
| 2826 |
name='Optimal (with AI)',
|
| 2827 |
x=[m[0] for m in top_merchants],
|
| 2828 |
-
y=[m[
|
| 2829 |
marker_color='#4caf50'
|
| 2830 |
))
|
| 2831 |
|
| 2832 |
fig1.add_trace(go.Bar(
|
| 2833 |
name='Actual (what you earned)',
|
| 2834 |
x=[m[0] for m in top_merchants],
|
| 2835 |
-
y=[m[
|
| 2836 |
marker_color='#ff9800'
|
| 2837 |
))
|
| 2838 |
|
|
@@ -2926,32 +2972,32 @@ with gr.Blocks(
|
|
| 2926 |
)
|
| 2927 |
|
| 2928 |
gr.Markdown("""
|
| 2929 |
-
|
| 2930 |
-
|
| 2931 |
-
|
| 2932 |
-
|
| 2933 |
-
|
| 2934 |
-
|
| 2935 |
-
|
| 2936 |
-
|
| 2937 |
-
|
| 2938 |
-
|
| 2939 |
-
|
| 2940 |
-
|
| 2941 |
-
|
| 2942 |
-
|
| 2943 |
-
|
| 2944 |
-
|
| 2945 |
-
|
| 2946 |
-
|
| 2947 |
-
|
| 2948 |
-
|
| 2949 |
-
|
| 2950 |
-
|
| 2951 |
-
|
| 2952 |
-
|
| 2953 |
-
|
| 2954 |
-
|
| 2955 |
|
| 2956 |
# ==================== TAB: ASK AI (WITH VOICE) ====================
|
| 2957 |
with gr.Tab("π¬ Ask AI"):
|
|
|
|
| 2613 |
def call_modal_batch_auto(user_id, time_period, max_txns, include_small):
|
| 2614 |
"""
|
| 2615 |
Automatically fetch transactions and analyze with Modal
|
| 2616 |
+
β
FIXED VERSION - Correct calculation logic
|
| 2617 |
"""
|
| 2618 |
import time
|
| 2619 |
|
| 2620 |
+
# β
FIX 1: Define days_map at function scope
|
| 2621 |
days_map = {
|
| 2622 |
"Last 7 Days": 7,
|
| 2623 |
"Last 30 Days": 30,
|
|
|
|
| 2630 |
# Step 1: Show loading state
|
| 2631 |
yield (
|
| 2632 |
f"""
|
| 2633 |
+
## β³ Loading Transactions...
|
| 2634 |
+
|
| 2635 |
+
**User:** {user_id}
|
| 2636 |
+
**Period:** {time_period}
|
| 2637 |
+
**Status:** Fetching transaction history...
|
| 2638 |
+
|
| 2639 |
+
<div class="thinking-dots">Please wait</div>
|
| 2640 |
+
""",
|
| 2641 |
"",
|
| 2642 |
None,
|
| 2643 |
None
|
|
|
|
| 2662 |
# Step 3: Show processing state
|
| 2663 |
yield (
|
| 2664 |
f"""
|
| 2665 |
+
## β‘ Processing {len(transactions)} Transactions...
|
| 2666 |
+
|
| 2667 |
+
**Status:** Calling Modal serverless endpoint
|
| 2668 |
+
**Mode:** Parallel batch processing
|
| 2669 |
+
|
| 2670 |
+
<div class="thinking-dots">Analyzing with AI</div>
|
| 2671 |
+
""",
|
| 2672 |
"",
|
| 2673 |
None,
|
| 2674 |
None
|
|
|
|
| 2701 |
# Extract recommendation
|
| 2702 |
rec = data.get('recommendation', data)
|
| 2703 |
|
| 2704 |
+
# β
FIX 2: Extract card name properly
|
| 2705 |
card_id = rec.get('recommended_card', 'Unknown')
|
| 2706 |
+
|
| 2707 |
+
# Better card name formatting
|
| 2708 |
+
if card_id.startswith('c_'):
|
| 2709 |
+
card_name = card_id[2:].replace('_', ' ').title()
|
| 2710 |
+
else:
|
| 2711 |
+
card_name = card_id.replace('_', ' ').title()
|
| 2712 |
+
|
| 2713 |
+
# β
FIX 3: Get optimal rewards correctly
|
| 2714 |
optimal_rewards = float(rec.get('rewards_earned', 0))
|
| 2715 |
|
| 2716 |
+
# If rewards_earned is missing, calculate from rate
|
| 2717 |
+
if optimal_rewards == 0:
|
| 2718 |
+
reward_rate = float(rec.get('reward_rate', 0.01))
|
| 2719 |
+
optimal_rewards = txn['amount'] * reward_rate
|
| 2720 |
+
|
| 2721 |
+
# Estimate what they actually earned (assume 1% default card)
|
| 2722 |
actual_rewards = txn['amount'] * 0.01
|
| 2723 |
|
| 2724 |
+
# β
FIX 4: Calculate missed savings CORRECTLY
|
| 2725 |
+
# Missed savings = what you COULD have earned - what you DID earn
|
| 2726 |
+
missed_savings = optimal_rewards - actual_rewards
|
| 2727 |
+
|
| 2728 |
results.append({
|
| 2729 |
'date': txn['date'],
|
| 2730 |
'merchant': txn['merchant'],
|
|
|
|
| 2733 |
'recommended_card': card_name,
|
| 2734 |
'optimal_rewards': optimal_rewards,
|
| 2735 |
'actual_rewards': actual_rewards,
|
| 2736 |
+
'missed_savings': missed_savings # Should be POSITIVE
|
| 2737 |
})
|
| 2738 |
|
| 2739 |
total_rewards_earned += actual_rewards
|
|
|
|
| 2753 |
)
|
| 2754 |
return
|
| 2755 |
|
| 2756 |
+
# β
FIX 5: Calculate metrics CORRECTLY
|
| 2757 |
+
total_missed = total_optimal_rewards - total_rewards_earned # Should be POSITIVE
|
| 2758 |
+
|
| 2759 |
+
# Avoid division by zero
|
| 2760 |
+
if total_spending > 0:
|
| 2761 |
+
avg_optimization = (total_optimal_rewards / total_spending * 100)
|
| 2762 |
+
avg_actual = (total_rewards_earned / total_spending * 100)
|
| 2763 |
+
else:
|
| 2764 |
+
avg_optimization = 0
|
| 2765 |
+
avg_actual = 0
|
| 2766 |
+
|
| 2767 |
+
# β
FIX 6: Optimization potential calculation
|
| 2768 |
+
if total_rewards_earned > 0:
|
| 2769 |
+
optimization_potential = ((total_optimal_rewards - total_rewards_earned) / total_rewards_earned * 100)
|
| 2770 |
+
else:
|
| 2771 |
+
optimization_potential = 0
|
| 2772 |
|
| 2773 |
# Sort by missed savings (biggest opportunities first)
|
| 2774 |
results.sort(key=lambda x: x['missed_savings'], reverse=True)
|
| 2775 |
|
| 2776 |
+
# Get days for yearly projection
|
| 2777 |
days = days_map.get(time_period, 30)
|
| 2778 |
|
| 2779 |
+
# β
FIX 7: Yearly projection
|
| 2780 |
+
yearly_multiplier = 365 / days if days > 0 else 12
|
| 2781 |
+
yearly_projection = total_missed * yearly_multiplier
|
| 2782 |
+
|
| 2783 |
# Format output
|
| 2784 |
status_msg = f"""
|
| 2785 |
+
## β
Analysis Complete!
|
| 2786 |
+
|
| 2787 |
+
**Transactions Analyzed:** {len(results)}
|
| 2788 |
+
**Time Period:** {time_period}
|
| 2789 |
+
**Processing Time:** ~{len(results) * 0.05:.1f}s (Modal parallel processing)
|
| 2790 |
+
"""
|
| 2791 |
|
| 2792 |
output = f"""
|
| 2793 |
## π° Optimization Report for {user_id}
|
| 2794 |
+
|
| 2795 |
### π Summary
|
| 2796 |
|
| 2797 |
| Metric | Value |
|
|
|
|
| 2812 |
for rec in results[:10]:
|
| 2813 |
output += f"| {rec['date']} | {rec['merchant']} | ${rec['amount']:.2f} | {rec['recommended_card']} | ${rec['missed_savings']:.2f} |\n"
|
| 2814 |
|
| 2815 |
+
# Find most common category safely
|
| 2816 |
+
category_counts = {}
|
| 2817 |
+
for r in results:
|
| 2818 |
+
cat = r['category']
|
| 2819 |
+
category_counts[cat] = category_counts.get(cat, 0) + 1
|
| 2820 |
+
|
| 2821 |
+
most_common_category = max(category_counts.items(), key=lambda x: x[0])[0] if category_counts else "Unknown"
|
| 2822 |
+
|
| 2823 |
+
# Find biggest opportunity
|
| 2824 |
+
biggest_opp = max(results, key=lambda x: x['missed_savings'])
|
| 2825 |
+
|
| 2826 |
output += f"""
|
| 2827 |
+
|
| 2828 |
---
|
| 2829 |
|
| 2830 |
### π‘ Key Insights
|
| 2831 |
|
| 2832 |
+
- **Biggest Single Opportunity:** ${biggest_opp['missed_savings']:.2f} at {biggest_opp['merchant']}
|
| 2833 |
+
- **Most Common Category:** {most_common_category}
|
| 2834 |
- **Average Transaction:** ${total_spending / len(results):.2f}
|
| 2835 |
+
- **Optimization Potential:** +{optimization_potential:.1f}% more rewards possible
|
| 2836 |
|
| 2837 |
---
|
| 2838 |
|
|
|
|
| 2841 |
<p style="margin: 0; color: #5d4037; font-size: 15px;">
|
| 2842 |
If you had used our AI recommendations for these {len(results)} transactions, you would have earned
|
| 2843 |
<strong style="color: #e65100;">${total_missed:.2f} more</strong> in rewards.
|
| 2844 |
+
Over a full year, that's <strong style="color: #e65100;">${yearly_projection:.0f}+</strong> in extra rewards!
|
| 2845 |
</p>
|
| 2846 |
</div>
|
| 2847 |
+
|
| 2848 |
---
|
| 2849 |
|
| 2850 |
<div style="background: #e8f5e9; padding: 20px; border-radius: 10px; border-left: 4px solid #4caf50;">
|
|
|
|
| 2864 |
merchant_data[r['merchant']]['optimal'] += r['optimal_rewards']
|
| 2865 |
merchant_data[r['merchant']]['actual'] += r['actual_rewards']
|
| 2866 |
|
| 2867 |
+
top_merchants = sorted(merchant_data.items(), key=lambda x: x[0]['optimal'], reverse=True)[:10]
|
| 2868 |
|
| 2869 |
fig1 = go.Figure()
|
| 2870 |
|
| 2871 |
fig1.add_trace(go.Bar(
|
| 2872 |
name='Optimal (with AI)',
|
| 2873 |
x=[m[0] for m in top_merchants],
|
| 2874 |
+
y=[m[0]['optimal'] for m in top_merchants],
|
| 2875 |
marker_color='#4caf50'
|
| 2876 |
))
|
| 2877 |
|
| 2878 |
fig1.add_trace(go.Bar(
|
| 2879 |
name='Actual (what you earned)',
|
| 2880 |
x=[m[0] for m in top_merchants],
|
| 2881 |
+
y=[m[0]['actual'] for m in top_merchants],
|
| 2882 |
marker_color='#ff9800'
|
| 2883 |
))
|
| 2884 |
|
|
|
|
| 2972 |
)
|
| 2973 |
|
| 2974 |
gr.Markdown("""
|
| 2975 |
+
---
|
| 2976 |
+
|
| 2977 |
+
### π§ How Modal Powers This
|
| 2978 |
+
|
| 2979 |
+
**Traditional Approach:**
|
| 2980 |
+
- Process 50 transactions sequentially
|
| 2981 |
+
- Takes 50 Γ 2 seconds = **100 seconds**
|
| 2982 |
+
- Server must handle all load
|
| 2983 |
+
|
| 2984 |
+
**With Modal:**
|
| 2985 |
+
- Process 50 transactions in parallel
|
| 2986 |
+
- Takes **~3 seconds total**
|
| 2987 |
+
- Automatic scaling (0 to 100 containers instantly)
|
| 2988 |
+
- Pay only for compute time used
|
| 2989 |
+
|
| 2990 |
+
**Architecture:**
|
| 2991 |
+
```
|
| 2992 |
+
Gradio UI β Modal Endpoint β [Container 1, Container 2, ..., Container N]
|
| 2993 |
+
β
|
| 2994 |
+
Your Orchestrator API
|
| 2995 |
+
β
|
| 2996 |
+
Aggregated Results
|
| 2997 |
+
```
|
| 2998 |
+
|
| 2999 |
+
**Learn More:** [Modal Documentation](https://modal.com/docs)
|
| 3000 |
+
""")
|
| 3001 |
|
| 3002 |
# ==================== TAB: ASK AI (WITH VOICE) ====================
|
| 3003 |
with gr.Tab("π¬ Ask AI"):
|