|
|
|
import gradio as gr |
|
import requests |
|
import json |
|
import asyncio |
|
import aiohttp |
|
from typing import Optional |
|
|
|
|
|
MODAL_URL = "https://devsam2898--personal-investment-strategist-optimized-web.modal.run/strategy" |
|
HEALTH_URL = "https://devsam2898--personal-investment-strategist-optimized-web.modal.run/health" |
|
TEST_URL = "https://devsam2898--personal-investment-strategist-optimized-web.modal.run/test" |
|
|
|
async def get_investment_strategy_async(age_group, income, expenses, risk_profile, goal, timeframe, country): |
|
"""Async version with better timeout handling""" |
|
|
|
|
|
if not all([age_group, income, expenses, risk_profile, goal, timeframe, country]): |
|
return "❌ Please fill in all fields to get a personalized strategy." |
|
|
|
|
|
try: |
|
income_val = float(str(income).replace('$', '').replace(',', '')) if income else 0 |
|
expenses_val = float(str(expenses).replace('$', '').replace(',', '')) if expenses else 0 |
|
except ValueError: |
|
return "❌ Please enter valid numbers for income and expenses." |
|
|
|
|
|
if income_val <= 0: |
|
return "❌ Income must be greater than 0." |
|
if expenses_val < 0: |
|
return "❌ Expenses cannot be negative." |
|
if expenses_val >= income_val: |
|
return "⚠️ **Warning**: Your expenses are equal to or exceed your income. Consider budgeting advice before investing." |
|
|
|
payload = { |
|
"profile": { |
|
"age_group": age_group, |
|
"income": income_val, |
|
"expenses": expenses_val, |
|
"risk_profile": risk_profile, |
|
"goal": goal, |
|
"timeframe": timeframe, |
|
"country": country |
|
} |
|
} |
|
|
|
try: |
|
print(f"🚀 Sending request to: {MODAL_URL}") |
|
|
|
|
|
timeout = aiohttp.ClientTimeout(total=150) |
|
|
|
async with aiohttp.ClientSession(timeout=timeout) as session: |
|
async with session.post( |
|
MODAL_URL, |
|
json=payload, |
|
headers={ |
|
'Content-Type': 'application/json', |
|
'Accept': 'application/json' |
|
} |
|
) as response: |
|
|
|
print(f"📊 Response status: {response.status}") |
|
|
|
if response.status == 200: |
|
result = await response.json() |
|
strategy = result.get("strategy", "No strategy returned.") |
|
status = result.get("status", "unknown") |
|
|
|
|
|
if status == "basic": |
|
prefix = "## 📊 Your Investment Strategy (Rule-Based)\n*AI service was unavailable, using optimized rule-based strategy*\n\n" |
|
else: |
|
prefix = "## 📊 Your Personalized Investment Strategy\n*Powered by AI*\n\n" |
|
|
|
return f"{prefix}{strategy}" |
|
else: |
|
error_text = await response.text() |
|
return f"❌ **Service Error ({response.status})**\n\nThe backend service returned an error. Please try again in a moment.\n\nDetails: {error_text[:200]}..." |
|
|
|
except asyncio.TimeoutError: |
|
return """⏱️ **Request Timeout** |
|
|
|
The AI service is taking longer than expected. This could be due to: |
|
- High server load |
|
- Cold start (first request after idle period) |
|
- Network connectivity issues |
|
|
|
**What to try:** |
|
1. Wait 30 seconds and try again |
|
2. Simplify your goal description |
|
3. Check if the service is healthy using the 'Test Service' button""" |
|
|
|
except aiohttp.ClientError as e: |
|
return f"""🔌 **Connection Error** |
|
|
|
Unable to connect to the backend service. |
|
|
|
**Possible causes:** |
|
- Service is starting up (cold start) |
|
- Network connectivity issues |
|
- Service is temporarily down |
|
|
|
**What to try:** |
|
1. Wait 1-2 minutes and try again |
|
2. Check service health with 'Test Service' button |
|
3. Refresh the page |
|
|
|
*Technical details: {str(e)}*""" |
|
|
|
except Exception as e: |
|
return f"""❌ **Unexpected Error** |
|
|
|
An unexpected error occurred: {str(e)} |
|
|
|
Please try again or contact support if the issue persists.""" |
|
|
|
def get_investment_strategy(age_group, income, expenses, risk_profile, goal, timeframe, country): |
|
"""Sync wrapper for async function""" |
|
try: |
|
loop = asyncio.new_event_loop() |
|
asyncio.set_event_loop(loop) |
|
result = loop.run_until_complete( |
|
get_investment_strategy_async(age_group, income, expenses, risk_profile, goal, timeframe, country) |
|
) |
|
loop.close() |
|
return result |
|
except Exception as e: |
|
return f"❌ **Error**: {str(e)}" |
|
|
|
async def test_service_async(): |
|
"""Test service connectivity""" |
|
try: |
|
timeout = aiohttp.ClientTimeout(total=30) |
|
|
|
async with aiohttp.ClientSession(timeout=timeout) as session: |
|
|
|
try: |
|
async with session.get(HEALTH_URL) as response: |
|
if response.status == 200: |
|
health_data = await response.json() |
|
health_status = f"✅ Service is healthy\n- Status: {health_data.get('status')}\n- Timestamp: {health_data.get('timestamp')}" |
|
else: |
|
health_status = f"⚠️ Health check returned status {response.status}" |
|
except Exception as e: |
|
health_status = f"❌ Health check failed: {str(e)}" |
|
|
|
|
|
try: |
|
async with session.get(TEST_URL) as response: |
|
if response.status == 200: |
|
test_data = await response.json() |
|
test_status = f"✅ Test endpoint working\n- Result: {test_data.get('test_result')}" |
|
else: |
|
test_status = f"⚠️ Test endpoint returned status {response.status}" |
|
except Exception as e: |
|
test_status = f"❌ Test endpoint failed: {str(e)}" |
|
|
|
return f"""## 🔍 Service Status Check |
|
|
|
**Health Check:** |
|
{health_status} |
|
|
|
**Functionality Test:** |
|
{test_status} |
|
|
|
**Service URL:** {MODAL_URL} |
|
|
|
*Last checked: {asyncio.get_event_loop().time()}*""" |
|
|
|
except Exception as e: |
|
return f"""❌ **Service Test Failed** |
|
|
|
Unable to connect to the service. |
|
|
|
Error: {str(e)} |
|
|
|
**Troubleshooting:** |
|
1. Check if the Modal deployment is running |
|
2. Verify the service URL is correct |
|
3. Check network connectivity""" |
|
|
|
def test_service(): |
|
"""Sync wrapper for service test""" |
|
try: |
|
loop = asyncio.new_event_loop() |
|
asyncio.set_event_loop(loop) |
|
result = loop.run_until_complete(test_service_async()) |
|
loop.close() |
|
return result |
|
except Exception as e: |
|
return f"❌ **Test Error**: {str(e)}" |
|
|
|
|
|
custom_css = """ |
|
/* Global container styling */ |
|
.gradio-container { |
|
max-width: 1400px !important; |
|
margin: 0 auto !important; |
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; |
|
min-height: 100vh; |
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important; |
|
} |
|
|
|
/* Main content area */ |
|
.main-content { |
|
background: rgba(255, 255, 255, 0.95) !important; |
|
backdrop-filter: blur(20px) !important; |
|
border-radius: 20px !important; |
|
padding: 2rem !important; |
|
margin: 2rem !important; |
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1) !important; |
|
border: 1px solid rgba(255, 255, 255, 0.2) !important; |
|
} |
|
|
|
/* Header styling */ |
|
.finance-header { |
|
text-align: center; |
|
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); |
|
color: white; |
|
padding: 3rem 2rem; |
|
border-radius: 20px; |
|
margin-bottom: 2rem; |
|
position: relative; |
|
overflow: hidden; |
|
} |
|
|
|
.finance-header::before { |
|
content: ""; |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
right: 0; |
|
bottom: 0; |
|
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="50" cy="50" r="0.5" fill="rgba(255,255,255,0.1)"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>'); |
|
opacity: 0.1; |
|
} |
|
|
|
.finance-header h1 { |
|
font-size: 3rem !important; |
|
font-weight: 800 !important; |
|
margin-bottom: 1rem !important; |
|
background: linear-gradient(45deg, #ffffff, #e0e7ff); |
|
-webkit-background-clip: text; |
|
-webkit-text-fill-color: transparent; |
|
position: relative; |
|
z-index: 1; |
|
} |
|
|
|
.finance-header p { |
|
font-size: 1.25rem !important; |
|
opacity: 0.9 !important; |
|
margin-bottom: 0 !important; |
|
position: relative; |
|
z-index: 1; |
|
} |
|
|
|
/* Icon styling */ |
|
.finance-icon { |
|
font-size: 4rem; |
|
margin-bottom: 1rem; |
|
position: relative; |
|
z-index: 1; |
|
} |
|
|
|
/* Form sections */ |
|
.form-section { |
|
background: white !important; |
|
border-radius: 16px !important; |
|
padding: 2rem !important; |
|
margin-bottom: 1.5rem !important; |
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08) !important; |
|
border: 1px solid rgba(255, 255, 255, 0.2) !important; |
|
transition: all 0.3s ease !important; |
|
} |
|
|
|
.form-section:hover { |
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.12) !important; |
|
transform: translateY(-2px) !important; |
|
} |
|
|
|
.form-section h3 { |
|
color: #1e3c72 !important; |
|
font-weight: 700 !important; |
|
font-size: 1.5rem !important; |
|
margin-bottom: 1.5rem !important; |
|
display: flex !important; |
|
align-items: center !important; |
|
gap: 0.75rem !important; |
|
} |
|
|
|
/* Input styling */ |
|
.gradio-textbox, .gradio-number { |
|
border-radius: 12px !important; |
|
border: 2px solid #e5e7eb !important; |
|
transition: all 0.3s ease !important; |
|
font-size: 1rem !important; |
|
padding: 0.75rem 1rem !important; |
|
} |
|
|
|
.gradio-textbox:focus, .gradio-number:focus { |
|
border-color: #667eea !important; |
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important; |
|
outline: none !important; |
|
} |
|
|
|
/* Dropdown styling - more specific selectors */ |
|
.gradio-dropdown .wrap { |
|
border-radius: 12px !important; |
|
border: 2px solid #e5e7eb !important; |
|
transition: all 0.3s ease !important; |
|
} |
|
|
|
.gradio-dropdown .wrap:focus-within { |
|
border-color: #667eea !important; |
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important; |
|
} |
|
|
|
.gradio-dropdown select, .gradio-dropdown input { |
|
font-size: 1rem !important; |
|
padding: 0.75rem 1rem !important; |
|
border: none !important; |
|
background: transparent !important; |
|
} |
|
|
|
.gradio-dropdown .dropdown { |
|
border-radius: 12px !important; |
|
border: 2px solid #e5e7eb !important; |
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1) !important; |
|
} |
|
|
|
/* Button styling */ |
|
.primary-btn { |
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; |
|
border: none !important; |
|
border-radius: 12px !important; |
|
padding: 1rem 2rem !important; |
|
font-size: 1.1rem !important; |
|
font-weight: 600 !important; |
|
color: white !important; |
|
transition: all 0.3s ease !important; |
|
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3) !important; |
|
min-height: 56px !important; |
|
} |
|
|
|
.primary-btn:hover { |
|
transform: translateY(-2px) !important; |
|
box-shadow: 0 12px 32px rgba(102, 126, 234, 0.4) !important; |
|
} |
|
|
|
.secondary-btn { |
|
background: linear-gradient(135deg, #64748b 0%, #475569 100%) !important; |
|
border: none !important; |
|
border-radius: 12px !important; |
|
padding: 1rem 2rem !important; |
|
font-size: 1.1rem !important; |
|
font-weight: 600 !important; |
|
color: white !important; |
|
transition: all 0.3s ease !important; |
|
box-shadow: 0 8px 24px rgba(100, 116, 139, 0.3) !important; |
|
min-height: 56px !important; |
|
} |
|
|
|
.secondary-btn:hover { |
|
transform: translateY(-2px) !important; |
|
box-shadow: 0 12px 32px rgba(100, 116, 139, 0.4) !important; |
|
} |
|
|
|
/* Output area styling */ |
|
.output-area { |
|
background: white !important; |
|
border-radius: 16px !important; |
|
padding: 2rem !important; |
|
margin-top: 2rem !important; |
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08) !important; |
|
border: 1px solid rgba(255, 255, 255, 0.2) !important; |
|
min-height: 200px !important; |
|
} |
|
|
|
/* Tips section */ |
|
.tips-section { |
|
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%) !important; |
|
border-radius: 16px !important; |
|
padding: 2rem !important; |
|
margin: 2rem 0 !important; |
|
border-left: 4px solid #667eea !important; |
|
} |
|
|
|
.tips-section h4 { |
|
color: #1e3c72 !important; |
|
font-weight: 700 !important; |
|
margin-bottom: 1rem !important; |
|
} |
|
|
|
/* Radio button styling */ |
|
.gradio-radio { |
|
gap: 1rem !important; |
|
} |
|
|
|
.gradio-radio label { |
|
background: white !important; |
|
border: 2px solid #e5e7eb !important; |
|
border-radius: 12px !important; |
|
padding: 1rem !important; |
|
transition: all 0.3s ease !important; |
|
cursor: pointer !important; |
|
} |
|
|
|
.gradio-radio label:hover { |
|
border-color: #667eea !important; |
|
background: #f8fafc !important; |
|
} |
|
|
|
.gradio-radio input:checked + label { |
|
border-color: #667eea !important; |
|
background: linear-gradient(135deg, #667eea10, #764ba210) !important; |
|
color: #1e3c72 !important; |
|
} |
|
|
|
/* Progress indicator */ |
|
.loading-indicator { |
|
background: linear-gradient(90deg, #667eea, #764ba2, #667eea) !important; |
|
background-size: 200% 100% !important; |
|
animation: gradient 2s ease infinite !important; |
|
} |
|
|
|
@keyframes gradient { |
|
0% { background-position: 0% 50%; } |
|
50% { background-position: 100% 50%; } |
|
100% { background-position: 0% 50%; } |
|
} |
|
|
|
/* Responsive design */ |
|
@media (max-width: 768px) { |
|
.finance-header h1 { |
|
font-size: 2rem !important; |
|
} |
|
|
|
.form-section { |
|
padding: 1.5rem !important; |
|
} |
|
|
|
.main-content { |
|
margin: 1rem !important; |
|
padding: 1.5rem !important; |
|
} |
|
} |
|
|
|
/* Chart and finance icons */ |
|
.finance-bg { |
|
position: relative; |
|
overflow: hidden; |
|
} |
|
|
|
.finance-bg::after { |
|
content: "📈📊💰🏦💳📋"; |
|
position: absolute; |
|
top: -20px; |
|
right: -20px; |
|
font-size: 6rem; |
|
opacity: 0.05; |
|
z-index: 0; |
|
transform: rotate(12deg); |
|
} |
|
""" |
|
|
|
|
|
with gr.Blocks( |
|
theme=gr.themes.Soft(primary_hue="blue", secondary_hue="slate"), |
|
title="💼 AI Investment Strategist Pro", |
|
css=custom_css |
|
) as interface: |
|
|
|
|
|
with gr.Row(elem_classes="finance-header finance-bg"): |
|
gr.HTML(""" |
|
<div style="text-align: center; position: relative; z-index: 1;"> |
|
<div class="finance-icon">💼</div> |
|
<h1>AI Investment Strategist Pro</h1> |
|
<p>Professional-Grade Investment Strategy Generator</p> |
|
<div style="margin-top: 1rem; font-size: 0.95rem; opacity: 0.8;"> |
|
🤖 Powered by Advanced AI • 🔒 Secure & Private • 📊 Data-Driven Insights |
|
</div> |
|
</div> |
|
""") |
|
|
|
with gr.Row(elem_classes="main-content"): |
|
with gr.Column(): |
|
|
|
|
|
with gr.Group(elem_classes="form-section"): |
|
gr.HTML("<h3>👤 Personal Profile</h3>") |
|
|
|
with gr.Row(): |
|
age_group = gr.Dropdown( |
|
choices=["20s", "30s", "40s", "50s+"], |
|
label="📅 Age Group", |
|
value="30s", |
|
info="Your current life stage affects investment timeline" |
|
) |
|
|
|
country = gr.Dropdown( |
|
choices=[ |
|
"🇺🇸 United States", "🇨🇦 Canada", "🇬🇧 United Kingdom", |
|
"🇩🇪 Germany", "🇫🇷 France", "🇮🇹 Italy", "🇯🇵 Japan", "🇮🇳 India" |
|
], |
|
label="🌍 Country of Residence", |
|
value="🇺🇸 United States", |
|
info="Tax jurisdiction for investment recommendations" |
|
) |
|
|
|
|
|
with gr.Group(elem_classes="form-section"): |
|
gr.HTML("<h3>💰 Financial Overview</h3>") |
|
|
|
with gr.Row(): |
|
income = gr.Number( |
|
label="💵 Monthly Income ($)", |
|
value=6000, |
|
minimum=0, |
|
info="Total monthly income before taxes and deductions" |
|
) |
|
expenses = gr.Number( |
|
label="💸 Monthly Expenses ($)", |
|
value=4000, |
|
minimum=0, |
|
info="Total monthly living expenses and obligations" |
|
) |
|
|
|
|
|
gr.HTML(""" |
|
<div style="background: linear-gradient(90deg, #10b981, #059669); color: white; |
|
padding: 1rem; border-radius: 8px; margin-top: 1rem; font-size: 0.9rem;"> |
|
💡 <strong>Quick Tip:</strong> A healthy savings rate is typically 20% or more of your income |
|
</div> |
|
""") |
|
|
|
|
|
with gr.Group(elem_classes="form-section"): |
|
gr.HTML("<h3>🎯 Investment Strategy</h3>") |
|
|
|
risk_profile = gr.Radio( |
|
choices=["🛡️ Conservative", "⚖️ Moderate", "🚀 Aggressive"], |
|
label="📊 Risk Tolerance", |
|
value="⚖️ Moderate", |
|
info="How comfortable are you with potential investment losses?" |
|
) |
|
|
|
with gr.Row(): |
|
goal = gr.Textbox( |
|
label="🎯 Primary Financial Goal", |
|
placeholder="e.g., Down payment for house, retirement planning, children's education, emergency fund", |
|
info="Be specific about what you're working towards", |
|
lines=2 |
|
) |
|
timeframe = gr.Textbox( |
|
label="⏰ Investment Timeline", |
|
placeholder="e.g., 3-5 years, 10+ years, until age 65", |
|
info="When do you need to access these funds?", |
|
lines=2 |
|
) |
|
|
|
|
|
with gr.Row(): |
|
with gr.Column(scale=3): |
|
submit_btn = gr.Button( |
|
"🚀 Generate My Investment Strategy", |
|
variant="primary", |
|
size="lg", |
|
elem_classes="primary-btn" |
|
) |
|
with gr.Column(scale=1): |
|
test_btn = gr.Button( |
|
"🔍 Test Service", |
|
variant="secondary", |
|
size="lg", |
|
elem_classes="secondary-btn" |
|
) |
|
|
|
|
|
with gr.Group(elem_classes="tips-section"): |
|
gr.HTML(""" |
|
<h4>💡 Pro Tips for Better Results</h4> |
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1rem; margin-top: 1rem;"> |
|
<div style="display: flex; align-items: start; gap: 0.5rem;"> |
|
<span style="color: #10b981; font-size: 1.2rem;">✓</span> |
|
<span><strong>Be Specific:</strong> Detailed goals lead to better recommendations</span> |
|
</div> |
|
<div style="display: flex; align-items: start; gap: 0.5rem;"> |
|
<span style="color: #10b981; font-size: 1.2rem;">✓</span> |
|
<span><strong>Realistic Timeline:</strong> Match your goals with appropriate timeframes</span> |
|
</div> |
|
<div style="display: flex; align-items: start; gap: 0.5rem;"> |
|
<span style="color: #10b981; font-size: 1.2rem;">✓</span> |
|
<span><strong>Know Your Risk:</strong> Be honest about your comfort level</span> |
|
</div> |
|
<div style="display: flex; align-items: start; gap: 0.5rem;"> |
|
<span style="color: #10b981; font-size: 1.2rem;">✓</span> |
|
<span><strong>Positive Cash Flow:</strong> Ensure income exceeds expenses</span> |
|
</div> |
|
</div> |
|
""") |
|
|
|
|
|
with gr.Group(elem_classes="output-area"): |
|
output = gr.Markdown( |
|
value=""" |
|
## 🎯 Ready to Get Started? |
|
|
|
Fill out your financial profile above and click **"Generate My Investment Strategy"** to receive: |
|
|
|
📋 **Personalized Investment Plan** |
|
📊 **Asset Allocation Recommendations** |
|
🏦 **Account Type Suggestions** |
|
💡 **Tax Optimization Strategies** |
|
📈 **Risk Management Advice** |
|
|
|
*Your data is processed securely and never stored permanently.* |
|
""", |
|
elem_id="strategy-output" |
|
) |
|
|
|
|
|
submit_btn.click( |
|
fn=get_investment_strategy, |
|
inputs=[age_group, income, expenses, risk_profile, goal, timeframe, country], |
|
outputs=output, |
|
show_progress=True |
|
) |
|
|
|
test_btn.click( |
|
fn=test_service, |
|
outputs=output, |
|
show_progress=True |
|
) |
|
|
|
if __name__ == "__main__": |
|
interface.launch( |
|
server_name="0.0.0.0", |
|
server_port=7860, |
|
share=False, |
|
show_error=True, |
|
debug=True |
|
) |