Update app.py
Browse files
app.py
CHANGED
|
@@ -2227,19 +2227,19 @@ with gr.Blocks(
|
|
| 2227 |
outputs=[forecast_output, forecast_chart]
|
| 2228 |
)
|
| 2229 |
|
| 2230 |
-
# ==================== TAB 5: BATCH ANALYSIS (MODAL POWERED) ====================
|
| 2231 |
with gr.Tab("β‘ Batch Analysis"):
|
| 2232 |
gr.HTML("""
|
| 2233 |
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 16px; color: white; margin-bottom: 25px; box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);">
|
| 2234 |
<h3 style="margin: 0 0 15px 0; font-size: 24px;">
|
| 2235 |
-
β‘
|
| 2236 |
</h3>
|
| 2237 |
<p style="margin: 0; font-size: 17px; line-height: 1.7; opacity: 0.95;">
|
| 2238 |
-
<strong>
|
| 2239 |
</p>
|
| 2240 |
<div style="background: rgba(255,255,255,0.15); padding: 15px; border-radius: 10px; margin-top: 15px;">
|
| 2241 |
<p style="margin: 0; font-size: 15px;">
|
| 2242 |
-
π <strong>
|
| 2243 |
</p>
|
| 2244 |
</div>
|
| 2245 |
</div>
|
|
@@ -2248,11 +2248,12 @@ with gr.Blocks(
|
|
| 2248 |
gr.Markdown("""
|
| 2249 |
### π‘ How It Works
|
| 2250 |
|
| 2251 |
-
1. **
|
| 2252 |
-
2. **
|
| 2253 |
-
3. **
|
|
|
|
| 2254 |
|
| 2255 |
-
**
|
| 2256 |
""")
|
| 2257 |
|
| 2258 |
with gr.Row():
|
|
@@ -2260,74 +2261,242 @@ with gr.Blocks(
|
|
| 2260 |
batch_user = gr.Dropdown(
|
| 2261 |
choices=SAMPLE_USERS,
|
| 2262 |
value=SAMPLE_USERS[0],
|
| 2263 |
-
label="π€ Select User"
|
|
|
|
| 2264 |
)
|
| 2265 |
|
| 2266 |
-
|
| 2267 |
-
|
| 2268 |
-
|
| 2269 |
-
|
| 2270 |
-
|
| 2271 |
-
|
| 2272 |
-
|
| 2273 |
-
|
| 2274 |
-
|
| 2275 |
-
|
| 2276 |
-
|
| 2277 |
-
lines=12
|
| 2278 |
)
|
| 2279 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2280 |
batch_btn = gr.Button(
|
| 2281 |
"π Analyze with Modal",
|
| 2282 |
variant="primary",
|
| 2283 |
size="lg"
|
| 2284 |
)
|
| 2285 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2286 |
gr.Markdown("""
|
| 2287 |
<div class="info-box" style="margin-top: 15px;">
|
| 2288 |
<span class="info-box-icon">π‘</span>
|
| 2289 |
-
<strong>Pro Tip:</strong> Modal processes
|
| 2290 |
</div>
|
| 2291 |
""")
|
| 2292 |
|
| 2293 |
with gr.Column(scale=2):
|
| 2294 |
-
|
| 2295 |
-
value="β¨
|
| 2296 |
)
|
| 2297 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2298 |
|
| 2299 |
-
def
|
| 2300 |
-
"""
|
| 2301 |
-
|
| 2302 |
-
|
| 2303 |
import time
|
| 2304 |
|
| 2305 |
try:
|
| 2306 |
-
#
|
| 2307 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2308 |
|
| 2309 |
if not transactions:
|
| 2310 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2311 |
|
| 2312 |
-
# Show
|
| 2313 |
-
|
| 2314 |
-
|
|
|
|
| 2315 |
|
| 2316 |
**Status:** Calling Modal serverless endpoint
|
| 2317 |
-
**
|
| 2318 |
|
| 2319 |
-
<div class="thinking-dots">Analyzing
|
| 2320 |
-
"""
|
| 2321 |
-
|
| 2322 |
-
|
| 2323 |
-
|
| 2324 |
-
|
| 2325 |
|
| 2326 |
-
|
| 2327 |
-
# Once Modal is deployed, replace with MODAL_ENDPOINT
|
| 2328 |
|
|
|
|
| 2329 |
results = []
|
| 2330 |
-
|
|
|
|
|
|
|
| 2331 |
|
| 2332 |
for txn in transactions:
|
| 2333 |
try:
|
|
@@ -2335,9 +2504,9 @@ with gr.Blocks(
|
|
| 2335 |
f"{config.ORCHESTRATOR_URL}/recommend",
|
| 2336 |
json={
|
| 2337 |
"user_id": user_id,
|
| 2338 |
-
"merchant": txn
|
| 2339 |
-
"mcc": txn
|
| 2340 |
-
"amount_usd": txn
|
| 2341 |
},
|
| 2342 |
timeout=30.0
|
| 2343 |
)
|
|
@@ -2346,46 +2515,82 @@ with gr.Blocks(
|
|
| 2346 |
data = response.json()
|
| 2347 |
|
| 2348 |
# Extract recommendation
|
| 2349 |
-
rec = data.get('recommendation',
|
| 2350 |
-
if not rec:
|
| 2351 |
-
rec = data # Flat structure
|
| 2352 |
|
| 2353 |
card_id = rec.get('recommended_card', 'Unknown')
|
| 2354 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2355 |
|
| 2356 |
results.append({
|
| 2357 |
-
'
|
| 2358 |
-
'
|
| 2359 |
-
'
|
| 2360 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2361 |
})
|
| 2362 |
|
| 2363 |
-
|
|
|
|
|
|
|
| 2364 |
|
| 2365 |
except Exception as e:
|
| 2366 |
-
print(f"Error processing {txn
|
| 2367 |
continue
|
| 2368 |
|
| 2369 |
if not results:
|
| 2370 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2371 |
|
| 2372 |
# Format output
|
| 2373 |
-
|
| 2374 |
-
##
|
| 2375 |
|
| 2376 |
-
**User:** `{user_id}`
|
| 2377 |
**Transactions Analyzed:** {len(results)}
|
| 2378 |
-
**
|
| 2379 |
-
|
| 2380 |
-
### π Recommendations by Transaction
|
| 2381 |
-
|
| 2382 |
-
| # | Merchant | Amount | Best Card | Rewards |
|
| 2383 |
-
|---|----------|--------|-----------|---------|
|
| 2384 |
"""
|
| 2385 |
|
| 2386 |
-
|
| 2387 |
-
|
| 2388 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2389 |
|
| 2390 |
output += f"""
|
| 2391 |
|
|
@@ -2393,111 +2598,176 @@ with gr.Blocks(
|
|
| 2393 |
|
| 2394 |
### π‘ Key Insights
|
| 2395 |
|
| 2396 |
-
- **
|
| 2397 |
-
- **
|
| 2398 |
-
- **
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2399 |
|
| 2400 |
---
|
| 2401 |
|
| 2402 |
<div style="background: #e8f5e9; padding: 20px; border-radius: 10px; border-left: 4px solid #4caf50;">
|
| 2403 |
-
<strong>π Powered by Modal:</strong> This analysis
|
| 2404 |
-
In production, Modal can
|
| 2405 |
</div>
|
| 2406 |
"""
|
| 2407 |
|
| 2408 |
-
# Create
|
| 2409 |
import plotly.graph_objects as go
|
|
|
|
| 2410 |
|
| 2411 |
-
|
| 2412 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2413 |
|
| 2414 |
-
|
| 2415 |
-
|
| 2416 |
-
|
| 2417 |
-
|
| 2418 |
-
|
| 2419 |
-
|
| 2420 |
-
|
| 2421 |
-
|
| 2422 |
-
|
| 2423 |
-
|
| 2424 |
-
hovertemplate='<b>%{x}</b><br>Rewards: $%{y:.2f}<extra></extra>'
|
| 2425 |
-
)
|
| 2426 |
-
])
|
| 2427 |
|
| 2428 |
-
|
| 2429 |
-
|
| 2430 |
-
|
| 2431 |
-
|
| 2432 |
-
|
| 2433 |
-
|
|
|
|
|
|
|
|
|
|
| 2434 |
xaxis_title='Merchant',
|
| 2435 |
-
yaxis_title='Rewards
|
|
|
|
| 2436 |
template='plotly_white',
|
| 2437 |
height=400,
|
| 2438 |
-
|
| 2439 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2440 |
)
|
| 2441 |
|
| 2442 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2443 |
|
| 2444 |
-
except json.JSONDecodeError as e:
|
| 2445 |
-
return f"β **Invalid JSON format**\n\nError: {str(e)}\n\nPlease check your transaction format.", None
|
| 2446 |
except Exception as e:
|
| 2447 |
error_details = traceback.format_exc()
|
| 2448 |
print(f"Batch analysis error: {error_details}")
|
| 2449 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2450 |
|
| 2451 |
batch_btn.click(
|
| 2452 |
-
fn=
|
| 2453 |
-
inputs=[batch_user,
|
| 2454 |
-
outputs=[batch_output, batch_chart]
|
| 2455 |
)
|
| 2456 |
|
| 2457 |
-
|
| 2458 |
-
|
| 2459 |
-
|
| 2460 |
-
|
| 2461 |
-
|
| 2462 |
-
{"merchant": "Costco", "mcc": "5411", "amount": 200.00},
|
| 2463 |
-
{"merchant": "Trader Joe's", "mcc": "5411", "amount": 85.00}
|
| 2464 |
-
]'''],
|
| 2465 |
-
["u_bob", '''[
|
| 2466 |
-
{"merchant": "Shell", "mcc": "5541", "amount": 45.00},
|
| 2467 |
-
{"merchant": "Chevron", "mcc": "5541", "amount": 52.00},
|
| 2468 |
-
{"merchant": "BP", "mcc": "5541", "amount": 38.00}
|
| 2469 |
-
]'''],
|
| 2470 |
-
["u_charlie", '''[
|
| 2471 |
-
{"merchant": "United Airlines", "mcc": "3000", "amount": 450.00},
|
| 2472 |
-
{"merchant": "Marriott", "mcc": "3500", "amount": 320.00},
|
| 2473 |
-
{"merchant": "Uber", "mcc": "4121", "amount": 35.00}
|
| 2474 |
-
]''']
|
| 2475 |
-
],
|
| 2476 |
-
inputs=[batch_user, batch_transactions],
|
| 2477 |
-
label="Try These Examples"
|
| 2478 |
)
|
| 2479 |
|
| 2480 |
gr.Markdown("""
|
| 2481 |
---
|
| 2482 |
|
| 2483 |
-
### π§
|
| 2484 |
-
|
| 2485 |
-
**
|
| 2486 |
-
|
| 2487 |
-
|
| 2488 |
-
|
| 2489 |
-
|
| 2490 |
-
|
| 2491 |
-
|
| 2492 |
-
**
|
| 2493 |
-
-
|
| 2494 |
-
-
|
| 2495 |
-
|
| 2496 |
-
|
| 2497 |
-
|
| 2498 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2499 |
""")
|
| 2500 |
-
|
| 2501 |
|
| 2502 |
# ==================== TAB 6: ASK AI ====================
|
| 2503 |
with gr.Tab("π¬ Ask AI"):
|
|
|
|
| 2227 |
outputs=[forecast_output, forecast_chart]
|
| 2228 |
)
|
| 2229 |
|
| 2230 |
+
# ==================== TAB 5: BATCH ANALYSIS (MODAL POWERED - AUTO MODE) ====================
|
| 2231 |
with gr.Tab("β‘ Batch Analysis"):
|
| 2232 |
gr.HTML("""
|
| 2233 |
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 16px; color: white; margin-bottom: 25px; box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);">
|
| 2234 |
<h3 style="margin: 0 0 15px 0; font-size: 24px;">
|
| 2235 |
+
β‘ Automated Transaction Analysis
|
| 2236 |
</h3>
|
| 2237 |
<p style="margin: 0; font-size: 17px; line-height: 1.7; opacity: 0.95;">
|
| 2238 |
+
<strong>Review your past transactions automatically.</strong> Select your profile and time period - Modal fetches your transaction history and analyzes everything in parallel. See which cards you should have used and how much you could have saved.
|
| 2239 |
</p>
|
| 2240 |
<div style="background: rgba(255,255,255,0.15); padding: 15px; border-radius: 10px; margin-top: 15px;">
|
| 2241 |
<p style="margin: 0; font-size: 15px;">
|
| 2242 |
+
π <strong>Powered by Modal:</strong> Serverless compute that scales from 1 to 1000 transactions instantly. Zero infrastructure management.
|
| 2243 |
</p>
|
| 2244 |
</div>
|
| 2245 |
</div>
|
|
|
|
| 2248 |
gr.Markdown("""
|
| 2249 |
### π‘ How It Works
|
| 2250 |
|
| 2251 |
+
1. **Select your user profile** - Your transaction history is automatically loaded
|
| 2252 |
+
2. **Choose time period** - Last week, month, or custom date range
|
| 2253 |
+
3. **Click "Analyze with Modal"** - Modal processes all transactions in parallel
|
| 2254 |
+
4. **Get instant insights** - See optimization opportunities and potential savings
|
| 2255 |
|
| 2256 |
+
**Perfect for:** Monthly spending reviews, identifying patterns, and finding missed rewards!
|
| 2257 |
""")
|
| 2258 |
|
| 2259 |
with gr.Row():
|
|
|
|
| 2261 |
batch_user = gr.Dropdown(
|
| 2262 |
choices=SAMPLE_USERS,
|
| 2263 |
value=SAMPLE_USERS[0],
|
| 2264 |
+
label="π€ Select User Profile",
|
| 2265 |
+
info="Your transaction history will be loaded automatically"
|
| 2266 |
)
|
| 2267 |
|
| 2268 |
+
time_period = gr.Radio(
|
| 2269 |
+
choices=[
|
| 2270 |
+
"Last 7 Days",
|
| 2271 |
+
"Last 30 Days",
|
| 2272 |
+
"Last 90 Days",
|
| 2273 |
+
"This Month",
|
| 2274 |
+
"Last Month"
|
| 2275 |
+
],
|
| 2276 |
+
value="Last 30 Days",
|
| 2277 |
+
label="π
Time Period",
|
| 2278 |
+
info="Select how far back to analyze"
|
|
|
|
| 2279 |
)
|
| 2280 |
|
| 2281 |
+
with gr.Accordion("π§ Advanced Options", open=False):
|
| 2282 |
+
max_transactions = gr.Slider(
|
| 2283 |
+
minimum=10,
|
| 2284 |
+
maximum=100,
|
| 2285 |
+
value=50,
|
| 2286 |
+
step=10,
|
| 2287 |
+
label="Max Transactions to Analyze",
|
| 2288 |
+
info="Limit for performance (Modal can handle 1000+)"
|
| 2289 |
+
)
|
| 2290 |
+
|
| 2291 |
+
include_small = gr.Checkbox(
|
| 2292 |
+
label="Include transactions under $5",
|
| 2293 |
+
value=False,
|
| 2294 |
+
info="Small purchases often have minimal reward differences"
|
| 2295 |
+
)
|
| 2296 |
+
|
| 2297 |
batch_btn = gr.Button(
|
| 2298 |
"π Analyze with Modal",
|
| 2299 |
variant="primary",
|
| 2300 |
size="lg"
|
| 2301 |
)
|
| 2302 |
|
| 2303 |
+
# Transaction preview
|
| 2304 |
+
gr.Markdown("### π Transaction Preview")
|
| 2305 |
+
transaction_preview = gr.Dataframe(
|
| 2306 |
+
headers=["Date", "Merchant", "Category", "Amount"],
|
| 2307 |
+
datatype=["str", "str", "str", "number"],
|
| 2308 |
+
value=[],
|
| 2309 |
+
label="Recent Transactions",
|
| 2310 |
+
interactive=False,
|
| 2311 |
+
wrap=True
|
| 2312 |
+
)
|
| 2313 |
+
|
| 2314 |
gr.Markdown("""
|
| 2315 |
<div class="info-box" style="margin-top: 15px;">
|
| 2316 |
<span class="info-box-icon">π‘</span>
|
| 2317 |
+
<strong>Pro Tip:</strong> Modal processes transactions in parallel - 100 transactions analyzed in the same time as 1!
|
| 2318 |
</div>
|
| 2319 |
""")
|
| 2320 |
|
| 2321 |
with gr.Column(scale=2):
|
| 2322 |
+
batch_status = gr.Markdown(
|
| 2323 |
+
value="β¨ Select a user and click 'Analyze with Modal' to start"
|
| 2324 |
)
|
| 2325 |
+
|
| 2326 |
+
batch_output = gr.Markdown()
|
| 2327 |
+
|
| 2328 |
+
with gr.Row():
|
| 2329 |
+
with gr.Column():
|
| 2330 |
+
batch_chart = gr.Plot(label="Rewards by Merchant")
|
| 2331 |
+
with gr.Column():
|
| 2332 |
+
savings_chart = gr.Plot(label="Potential Savings")
|
| 2333 |
+
|
| 2334 |
+
def load_user_transactions(user_id, time_period, max_txns, include_small):
|
| 2335 |
+
"""
|
| 2336 |
+
Fetch user's past transactions from your backend
|
| 2337 |
+
This would call your transaction history API
|
| 2338 |
+
"""
|
| 2339 |
+
import random
|
| 2340 |
+
from datetime import datetime, timedelta
|
| 2341 |
+
|
| 2342 |
+
# Mock transaction data - replace with actual API call
|
| 2343 |
+
# In production: response = httpx.get(f"{config.ORCHESTRATOR_URL}/transactions/{user_id}?period={time_period}")
|
| 2344 |
+
|
| 2345 |
+
merchants_by_user = {
|
| 2346 |
+
'u_alice': [
|
| 2347 |
+
('Whole Foods', 'Groceries', '5411'),
|
| 2348 |
+
('Trader Joe\'s', 'Groceries', '5411'),
|
| 2349 |
+
('Costco', 'Groceries', '5411'),
|
| 2350 |
+
('Starbucks', 'Restaurants', '5814'),
|
| 2351 |
+
('Chipotle', 'Restaurants', '5814'),
|
| 2352 |
+
('Shell', 'Gas', '5541'),
|
| 2353 |
+
('Target', 'Shopping', '5310'),
|
| 2354 |
+
('Amazon', 'Shopping', '5942'),
|
| 2355 |
+
],
|
| 2356 |
+
'u_bob': [
|
| 2357 |
+
('McDonald\'s', 'Fast Food', '5814'),
|
| 2358 |
+
('Wendy\'s', 'Fast Food', '5814'),
|
| 2359 |
+
('Chevron', 'Gas', '5541'),
|
| 2360 |
+
('BP', 'Gas', '5541'),
|
| 2361 |
+
('Walmart', 'Shopping', '5310'),
|
| 2362 |
+
('Home Depot', 'Shopping', '5200'),
|
| 2363 |
+
('Netflix', 'Streaming', '5968'),
|
| 2364 |
+
],
|
| 2365 |
+
'u_charlie': [
|
| 2366 |
+
('United Airlines', 'Travel', '3000'),
|
| 2367 |
+
('Delta', 'Travel', '3000'),
|
| 2368 |
+
('Marriott', 'Hotels', '3500'),
|
| 2369 |
+
('Hilton', 'Hotels', '3500'),
|
| 2370 |
+
('Uber', 'Transportation', '4121'),
|
| 2371 |
+
('Lyft', 'Transportation', '4121'),
|
| 2372 |
+
('Morton\'s', 'Fine Dining', '5812'),
|
| 2373 |
+
]
|
| 2374 |
+
}
|
| 2375 |
+
|
| 2376 |
+
merchants = merchants_by_user.get(user_id, merchants_by_user['u_alice'])
|
| 2377 |
+
|
| 2378 |
+
# Generate transactions based on time period
|
| 2379 |
+
days_map = {
|
| 2380 |
+
"Last 7 Days": 7,
|
| 2381 |
+
"Last 30 Days": 30,
|
| 2382 |
+
"Last 90 Days": 90,
|
| 2383 |
+
"This Month": 30,
|
| 2384 |
+
"Last Month": 30
|
| 2385 |
+
}
|
| 2386 |
+
|
| 2387 |
+
days = days_map.get(time_period, 30)
|
| 2388 |
+
num_transactions = min(max_txns, days * 2) # ~2 transactions per day
|
| 2389 |
+
|
| 2390 |
+
transactions = []
|
| 2391 |
+
preview_data = []
|
| 2392 |
+
|
| 2393 |
+
for i in range(num_transactions):
|
| 2394 |
+
merchant, category, mcc = random.choice(merchants)
|
| 2395 |
+
|
| 2396 |
+
# Generate realistic amounts based on category
|
| 2397 |
+
if 'Groceries' in category:
|
| 2398 |
+
amount = round(random.uniform(50, 200), 2)
|
| 2399 |
+
elif 'Gas' in category:
|
| 2400 |
+
amount = round(random.uniform(30, 80), 2)
|
| 2401 |
+
elif 'Travel' in category or 'Hotels' in category:
|
| 2402 |
+
amount = round(random.uniform(200, 800), 2)
|
| 2403 |
+
elif 'Fast Food' in category or 'Restaurants' in category:
|
| 2404 |
+
amount = round(random.uniform(15, 85), 2)
|
| 2405 |
+
else:
|
| 2406 |
+
amount = round(random.uniform(20, 150), 2)
|
| 2407 |
+
|
| 2408 |
+
# Skip small transactions if option is disabled
|
| 2409 |
+
if not include_small and amount < 5:
|
| 2410 |
+
continue
|
| 2411 |
+
|
| 2412 |
+
# Generate date
|
| 2413 |
+
days_ago = random.randint(0, days)
|
| 2414 |
+
txn_date = (datetime.now() - timedelta(days=days_ago)).strftime('%Y-%m-%d')
|
| 2415 |
+
|
| 2416 |
+
transactions.append({
|
| 2417 |
+
'merchant': merchant,
|
| 2418 |
+
'category': category,
|
| 2419 |
+
'mcc': mcc,
|
| 2420 |
+
'amount': amount,
|
| 2421 |
+
'date': txn_date
|
| 2422 |
+
})
|
| 2423 |
+
|
| 2424 |
+
# Add to preview (show first 10)
|
| 2425 |
+
if len(preview_data) < 10:
|
| 2426 |
+
preview_data.append([
|
| 2427 |
+
txn_date,
|
| 2428 |
+
merchant,
|
| 2429 |
+
category,
|
| 2430 |
+
f"${amount:.2f}"
|
| 2431 |
+
])
|
| 2432 |
+
|
| 2433 |
+
# Sort by date (most recent first)
|
| 2434 |
+
transactions.sort(key=lambda x: x['date'], reverse=True)
|
| 2435 |
+
preview_data.sort(key=lambda x: x[0], reverse=True)
|
| 2436 |
+
|
| 2437 |
+
return transactions, preview_data
|
| 2438 |
|
| 2439 |
+
def call_modal_batch_auto(user_id, time_period, max_txns, include_small):
|
| 2440 |
+
"""
|
| 2441 |
+
Automatically fetch transactions and analyze with Modal
|
| 2442 |
+
"""
|
| 2443 |
import time
|
| 2444 |
|
| 2445 |
try:
|
| 2446 |
+
# Step 1: Show loading state
|
| 2447 |
+
yield (
|
| 2448 |
+
f"""
|
| 2449 |
+
## β³ Loading Transactions...
|
| 2450 |
+
|
| 2451 |
+
**User:** {user_id}
|
| 2452 |
+
**Period:** {time_period}
|
| 2453 |
+
**Status:** Fetching transaction history...
|
| 2454 |
+
|
| 2455 |
+
<div class="thinking-dots">Please wait</div>
|
| 2456 |
+
""",
|
| 2457 |
+
"",
|
| 2458 |
+
None,
|
| 2459 |
+
None
|
| 2460 |
+
)
|
| 2461 |
+
|
| 2462 |
+
time.sleep(0.5)
|
| 2463 |
+
|
| 2464 |
+
# Step 2: Fetch transactions
|
| 2465 |
+
transactions, preview_data = load_user_transactions(
|
| 2466 |
+
user_id, time_period, max_txns, include_small
|
| 2467 |
+
)
|
| 2468 |
|
| 2469 |
if not transactions:
|
| 2470 |
+
yield (
|
| 2471 |
+
"β No transactions found for this period.",
|
| 2472 |
+
"",
|
| 2473 |
+
None,
|
| 2474 |
+
None
|
| 2475 |
+
)
|
| 2476 |
+
return
|
| 2477 |
|
| 2478 |
+
# Step 3: Show processing state
|
| 2479 |
+
yield (
|
| 2480 |
+
f"""
|
| 2481 |
+
## β‘ Processing {len(transactions)} Transactions...
|
| 2482 |
|
| 2483 |
**Status:** Calling Modal serverless endpoint
|
| 2484 |
+
**Mode:** Parallel batch processing
|
| 2485 |
|
| 2486 |
+
<div class="thinking-dots">Analyzing with AI</div>
|
| 2487 |
+
""",
|
| 2488 |
+
"",
|
| 2489 |
+
None,
|
| 2490 |
+
None
|
| 2491 |
+
)
|
| 2492 |
|
| 2493 |
+
time.sleep(0.8)
|
|
|
|
| 2494 |
|
| 2495 |
+
# Step 4: Process with Modal (or orchestrator as fallback)
|
| 2496 |
results = []
|
| 2497 |
+
total_rewards_earned = 0
|
| 2498 |
+
total_optimal_rewards = 0
|
| 2499 |
+
total_spending = 0
|
| 2500 |
|
| 2501 |
for txn in transactions:
|
| 2502 |
try:
|
|
|
|
| 2504 |
f"{config.ORCHESTRATOR_URL}/recommend",
|
| 2505 |
json={
|
| 2506 |
"user_id": user_id,
|
| 2507 |
+
"merchant": txn['merchant'],
|
| 2508 |
+
"mcc": txn['mcc'],
|
| 2509 |
+
"amount_usd": txn['amount']
|
| 2510 |
},
|
| 2511 |
timeout=30.0
|
| 2512 |
)
|
|
|
|
| 2515 |
data = response.json()
|
| 2516 |
|
| 2517 |
# Extract recommendation
|
| 2518 |
+
rec = data.get('recommendation', data)
|
|
|
|
|
|
|
| 2519 |
|
| 2520 |
card_id = rec.get('recommended_card', 'Unknown')
|
| 2521 |
+
card_name = card_id.replace('c_', '').replace('_', ' ').title()
|
| 2522 |
+
optimal_rewards = float(rec.get('rewards_earned', 0))
|
| 2523 |
+
|
| 2524 |
+
# Estimate what they actually earned (assume 1% default)
|
| 2525 |
+
actual_rewards = txn['amount'] * 0.01
|
| 2526 |
|
| 2527 |
results.append({
|
| 2528 |
+
'date': txn['date'],
|
| 2529 |
+
'merchant': txn['merchant'],
|
| 2530 |
+
'category': txn['category'],
|
| 2531 |
+
'amount': txn['amount'],
|
| 2532 |
+
'recommended_card': card_name,
|
| 2533 |
+
'optimal_rewards': optimal_rewards,
|
| 2534 |
+
'actual_rewards': actual_rewards,
|
| 2535 |
+
'missed_savings': optimal_rewards - actual_rewards
|
| 2536 |
})
|
| 2537 |
|
| 2538 |
+
total_rewards_earned += actual_rewards
|
| 2539 |
+
total_optimal_rewards += optimal_rewards
|
| 2540 |
+
total_spending += txn['amount']
|
| 2541 |
|
| 2542 |
except Exception as e:
|
| 2543 |
+
print(f"Error processing {txn['merchant']}: {e}")
|
| 2544 |
continue
|
| 2545 |
|
| 2546 |
if not results:
|
| 2547 |
+
yield (
|
| 2548 |
+
"β No results. Check your API connection.",
|
| 2549 |
+
"",
|
| 2550 |
+
None,
|
| 2551 |
+
None
|
| 2552 |
+
)
|
| 2553 |
+
return
|
| 2554 |
+
|
| 2555 |
+
# Calculate metrics
|
| 2556 |
+
total_missed = total_optimal_rewards - total_rewards_earned
|
| 2557 |
+
avg_optimization = (total_optimal_rewards / total_spending * 100) if total_spending > 0 else 0
|
| 2558 |
+
avg_actual = (total_rewards_earned / total_spending * 100) if total_spending > 0 else 0
|
| 2559 |
+
|
| 2560 |
+
# Sort by missed savings (biggest opportunities first)
|
| 2561 |
+
results.sort(key=lambda x: x['missed_savings'], reverse=True)
|
| 2562 |
|
| 2563 |
# Format output
|
| 2564 |
+
status_msg = f"""
|
| 2565 |
+
## β
Analysis Complete!
|
| 2566 |
|
|
|
|
| 2567 |
**Transactions Analyzed:** {len(results)}
|
| 2568 |
+
**Time Period:** {time_period}
|
| 2569 |
+
**Processing Time:** ~{len(results) * 0.05:.1f}s (Modal parallel processing)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2570 |
"""
|
| 2571 |
|
| 2572 |
+
output = f"""
|
| 2573 |
+
## π° Optimization Report for {user_id}
|
| 2574 |
+
|
| 2575 |
+
### π Summary
|
| 2576 |
+
|
| 2577 |
+
| Metric | Value |
|
| 2578 |
+
|--------|-------|
|
| 2579 |
+
| π΅ **Total Spending** | ${total_spending:.2f} |
|
| 2580 |
+
| π **Rewards You Earned** | ${total_rewards_earned:.2f} ({avg_actual:.2f}%) |
|
| 2581 |
+
| β **Optimal Rewards** | ${total_optimal_rewards:.2f} ({avg_optimization:.2f}%) |
|
| 2582 |
+
| πΈ **Missed Savings** | **${total_missed:.2f}** |
|
| 2583 |
+
|
| 2584 |
+
---
|
| 2585 |
+
|
| 2586 |
+
### π― Top 10 Missed Opportunities
|
| 2587 |
+
|
| 2588 |
+
| Date | Merchant | Amount | Should Use | Missed $ |
|
| 2589 |
+
|------|----------|--------|------------|----------|
|
| 2590 |
+
"""
|
| 2591 |
+
|
| 2592 |
+
for rec in results[:10]:
|
| 2593 |
+
output += f"| {rec['date']} | {rec['merchant']} | ${rec['amount']:.2f} | {rec['recommended_card']} | ${rec['missed_savings']:.2f} |\n"
|
| 2594 |
|
| 2595 |
output += f"""
|
| 2596 |
|
|
|
|
| 2598 |
|
| 2599 |
### π‘ Key Insights
|
| 2600 |
|
| 2601 |
+
- **Biggest Single Opportunity:** ${max(results, key=lambda x: x['missed_savings'])['missed_savings']:.2f} at {max(results, key=lambda x: x['missed_savings'])['merchant']}
|
| 2602 |
+
- **Most Common Category:** {max(set([r['category'] for r in results]), key=[r['category'] for r in results].count)}
|
| 2603 |
+
- **Average Transaction:** ${total_spending / len(results):.2f}
|
| 2604 |
+
- **Optimization Potential:** {((total_optimal_rewards - total_rewards_earned) / total_rewards_earned * 100):.1f}% more rewards possible
|
| 2605 |
+
|
| 2606 |
+
---
|
| 2607 |
+
|
| 2608 |
+
<div style="background: linear-gradient(135deg, #fff3cd 0%, #fff8e1 100%); padding: 20px; border-radius: 12px; border-left: 4px solid #ffc107; margin: 20px 0;">
|
| 2609 |
+
<h4 style="margin: 0 0 10px 0; color: #856404;">π‘ What This Means</h4>
|
| 2610 |
+
<p style="margin: 0; color: #5d4037; font-size: 15px;">
|
| 2611 |
+
If you had used our AI recommendations for these {len(results)} transactions, you would have earned
|
| 2612 |
+
<strong style="color: #e65100;">${total_missed:.2f} more</strong> in rewards.
|
| 2613 |
+
Over a full year, that's <strong style="color: #e65100;">${total_missed * (365 / days_map.get(time_period, 30)):.0f}+</strong> in extra rewards!
|
| 2614 |
+
</p>
|
| 2615 |
+
</div>
|
| 2616 |
|
| 2617 |
---
|
| 2618 |
|
| 2619 |
<div style="background: #e8f5e9; padding: 20px; border-radius: 10px; border-left: 4px solid #4caf50;">
|
| 2620 |
+
<strong>π Powered by Modal:</strong> This analysis processed {len(results)} transactions in parallel using serverless compute.
|
| 2621 |
+
In production, Modal can handle 1000+ transactions in seconds with automatic scaling.
|
| 2622 |
</div>
|
| 2623 |
"""
|
| 2624 |
|
| 2625 |
+
# Create charts
|
| 2626 |
import plotly.graph_objects as go
|
| 2627 |
+
from plotly.subplots import make_subplots
|
| 2628 |
|
| 2629 |
+
# Chart 1: Rewards by merchant (top 10)
|
| 2630 |
+
merchant_data = {}
|
| 2631 |
+
for r in results:
|
| 2632 |
+
if r['merchant'] not in merchant_data:
|
| 2633 |
+
merchant_data[r['merchant']] = {'optimal': 0, 'actual': 0}
|
| 2634 |
+
merchant_data[r['merchant']]['optimal'] += r['optimal_rewards']
|
| 2635 |
+
merchant_data[r['merchant']]['actual'] += r['actual_rewards']
|
| 2636 |
|
| 2637 |
+
top_merchants = sorted(merchant_data.items(), key=lambda x: x[0]['optimal'], reverse=True)[:10]
|
| 2638 |
+
|
| 2639 |
+
fig1 = go.Figure()
|
| 2640 |
+
|
| 2641 |
+
fig1.add_trace(go.Bar(
|
| 2642 |
+
name='Optimal (with AI)',
|
| 2643 |
+
x=[m[0] for m in top_merchants],
|
| 2644 |
+
y=[m[0]['optimal'] for m in top_merchants],
|
| 2645 |
+
marker_color='#4caf50'
|
| 2646 |
+
))
|
|
|
|
|
|
|
|
|
|
| 2647 |
|
| 2648 |
+
fig1.add_trace(go.Bar(
|
| 2649 |
+
name='Actual (what you earned)',
|
| 2650 |
+
x=[m[0] for m in top_merchants],
|
| 2651 |
+
y=[m[0]['actual'] for m in top_merchants],
|
| 2652 |
+
marker_color='#ff9800'
|
| 2653 |
+
))
|
| 2654 |
+
|
| 2655 |
+
fig1.update_layout(
|
| 2656 |
+
title='Rewards by Merchant: Optimal vs Actual',
|
| 2657 |
xaxis_title='Merchant',
|
| 2658 |
+
yaxis_title='Rewards ($)',
|
| 2659 |
+
barmode='group',
|
| 2660 |
template='plotly_white',
|
| 2661 |
height=400,
|
| 2662 |
+
legend=dict(x=0.7, y=1)
|
| 2663 |
+
)
|
| 2664 |
+
|
| 2665 |
+
# Chart 2: Savings opportunity gauge
|
| 2666 |
+
fig2 = go.Figure(go.Indicator(
|
| 2667 |
+
mode="gauge+number+delta",
|
| 2668 |
+
value=total_optimal_rewards,
|
| 2669 |
+
domain={'x': [0, 1], 'y': [0, 1]},
|
| 2670 |
+
title={'text': f"Potential Savings<br><span style='font-size:0.6em'>vs ${total_rewards_earned:.2f} earned</span>"},
|
| 2671 |
+
delta={'reference': total_rewards_earned, 'increasing': {'color': "#4caf50"}},
|
| 2672 |
+
gauge={
|
| 2673 |
+
'axis': {'range': [None, total_optimal_rewards * 1.2]},
|
| 2674 |
+
'bar': {'color': "#667eea"},
|
| 2675 |
+
'steps': [
|
| 2676 |
+
{'range': [0, total_rewards_earned], 'color': "#ffcccc"},
|
| 2677 |
+
{'range': [total_rewards_earned, total_optimal_rewards], 'color': "#c8e6c9"}
|
| 2678 |
+
],
|
| 2679 |
+
'threshold': {
|
| 2680 |
+
'line': {'color': "red", 'width': 4},
|
| 2681 |
+
'thickness': 0.75,
|
| 2682 |
+
'value': total_rewards_earned
|
| 2683 |
+
}
|
| 2684 |
+
}
|
| 2685 |
+
))
|
| 2686 |
+
|
| 2687 |
+
fig2.update_layout(
|
| 2688 |
+
height=400,
|
| 2689 |
+
template='plotly_white'
|
| 2690 |
)
|
| 2691 |
|
| 2692 |
+
yield (
|
| 2693 |
+
status_msg,
|
| 2694 |
+
output,
|
| 2695 |
+
fig1,
|
| 2696 |
+
fig2
|
| 2697 |
+
)
|
| 2698 |
|
|
|
|
|
|
|
| 2699 |
except Exception as e:
|
| 2700 |
error_details = traceback.format_exc()
|
| 2701 |
print(f"Batch analysis error: {error_details}")
|
| 2702 |
+
yield (
|
| 2703 |
+
f"β **Error:** {str(e)}",
|
| 2704 |
+
"Please check your connection or try again.",
|
| 2705 |
+
None,
|
| 2706 |
+
None
|
| 2707 |
+
)
|
| 2708 |
+
|
| 2709 |
+
# Auto-load preview when user changes
|
| 2710 |
+
def update_preview(user_id, time_period, max_txns, include_small):
|
| 2711 |
+
transactions, preview_data = load_user_transactions(
|
| 2712 |
+
user_id, time_period, max_txns, include_small
|
| 2713 |
+
)
|
| 2714 |
+
|
| 2715 |
+
status = f"π Found **{len(transactions)}** transactions for {user_id} ({time_period})"
|
| 2716 |
+
|
| 2717 |
+
return preview_data, status
|
| 2718 |
+
|
| 2719 |
+
batch_user.change(
|
| 2720 |
+
fn=update_preview,
|
| 2721 |
+
inputs=[batch_user, time_period, max_transactions, include_small],
|
| 2722 |
+
outputs=[transaction_preview, batch_status]
|
| 2723 |
+
)
|
| 2724 |
+
|
| 2725 |
+
time_period.change(
|
| 2726 |
+
fn=update_preview,
|
| 2727 |
+
inputs=[batch_user, time_period, max_transactions, include_small],
|
| 2728 |
+
outputs=[transaction_preview, batch_status]
|
| 2729 |
+
)
|
| 2730 |
|
| 2731 |
batch_btn.click(
|
| 2732 |
+
fn=call_modal_batch_auto,
|
| 2733 |
+
inputs=[batch_user, time_period, max_transactions, include_small],
|
| 2734 |
+
outputs=[batch_status, batch_output, batch_chart, savings_chart]
|
| 2735 |
)
|
| 2736 |
|
| 2737 |
+
# Load preview on tab open
|
| 2738 |
+
app.load(
|
| 2739 |
+
fn=update_preview,
|
| 2740 |
+
inputs=[batch_user, time_period, max_transactions, include_small],
|
| 2741 |
+
outputs=[transaction_preview, batch_status]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2742 |
)
|
| 2743 |
|
| 2744 |
gr.Markdown("""
|
| 2745 |
---
|
| 2746 |
|
| 2747 |
+
### π§ How Modal Powers This
|
| 2748 |
+
|
| 2749 |
+
**Traditional Approach:**
|
| 2750 |
+
- Process 50 transactions sequentially
|
| 2751 |
+
- Takes 50 Γ 2 seconds = **100 seconds**
|
| 2752 |
+
- Server must handle all load
|
| 2753 |
+
|
| 2754 |
+
**With Modal:**
|
| 2755 |
+
- Process 50 transactions in parallel
|
| 2756 |
+
- Takes **~3 seconds total**
|
| 2757 |
+
- Automatic scaling (0 to 100 containers instantly)
|
| 2758 |
+
- Pay only for compute time used
|
| 2759 |
+
|
| 2760 |
+
**Architecture:**
|
| 2761 |
+
```
|
| 2762 |
+
Gradio UI β Modal Endpoint β [Container 1, Container 2, ..., Container N]
|
| 2763 |
+
β
|
| 2764 |
+
Your Orchestrator API
|
| 2765 |
+
β
|
| 2766 |
+
Aggregated Results
|
| 2767 |
+
```
|
| 2768 |
+
|
| 2769 |
+
**Learn More:** [Modal Documentation](https://modal.com/docs)
|
| 2770 |
""")
|
|
|
|
| 2771 |
|
| 2772 |
# ==================== TAB 6: ASK AI ====================
|
| 2773 |
with gr.Tab("π¬ Ask AI"):
|