MoneyMate / app.py
TheSilentOne's picture
Uploading necessary files to the Space (#1)
2fa4faa verified
raw
history blame
18.1 kB
import gradio as gr
import requests
import json
import plotly.graph_objects as go
import plotly.express as px
from fastapi import FastAPI
from typing import Dict, Any, Optional
import os
from datetime import datetime
# Custom CSS for MoneyMate branding
CUSTOM_CSS = """
.gradio-container {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: 'Inter', sans-serif;
}
.main-header {
text-align: center;
color: white;
margin-bottom: 2rem;
}
.money-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 15px;
padding: 1.5rem;
margin: 1rem 0;
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37);
backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.18);
}
.modal-branding {
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: bold;
text-align: center;
margin-top: 1rem;
}
.advice-box {
background: #f8f9ff;
border-left: 4px solid #667eea;
padding: 1rem;
margin: 1rem 0;
border-radius: 8px;
}
.quick-action-btn {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 25px;
padding: 0.5rem 1rem;
margin: 0.25rem;
cursor: pointer;
transition: all 0.3s ease;
}
.quick-action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
"""
# MCP Server configuration
class MCPServer:
def __init__(self):
self.tools = {
"salary_breakdown": self.salary_breakdown,
"investment_advice": self.investment_advice,
"expense_analysis": self.expense_analysis,
"savings_goal": self.savings_goal
}
def salary_breakdown(self, salary: float, expenses: Dict[str, float]) -> Dict[str, Any]:
"""Break down salary using 50/30/20 rule with Indian context"""
needs = salary * 0.5 # 50% for needs
wants = salary * 0.3 # 30% for wants
savings = salary * 0.2 # 20% for savings/investments
return {
"breakdown": {
"needs": needs,
"wants": wants,
"savings": savings
},
"recommendations": self._get_indian_recommendations(salary)
}
def investment_advice(self, age: int, salary: float, risk_appetite: str) -> Dict[str, Any]:
"""Provide investment advice for Indian market"""
equity_percentage = min(100 - age, 80) # Age-based equity allocation
debt_percentage = 100 - equity_percentage
return {
"allocation": {
"equity": equity_percentage,
"debt": debt_percentage
},
"instruments": self._get_indian_instruments(risk_appetite)
}
def expense_analysis(self, expenses: Dict[str, float]) -> Dict[str, Any]:
"""Analyze expenses and provide optimization suggestions"""
total_expenses = sum(expenses.values())
analysis = {}
for category, amount in expenses.items():
percentage = (amount / total_expenses) * 100
analysis[category] = {
"amount": amount,
"percentage": percentage,
"status": self._categorize_expense(category, percentage)
}
return {"analysis": analysis, "suggestions": self._get_optimization_tips()}
def savings_goal(self, goal_amount: float, timeline_months: int, current_savings: float) -> Dict[str, Any]:
"""Calculate monthly savings needed for a goal"""
remaining_amount = goal_amount - current_savings
monthly_required = remaining_amount / timeline_months if timeline_months > 0 else 0
return {
"monthly_required": monthly_required,
"total_goal": goal_amount,
"timeline": timeline_months,
"feasibility": "achievable" if monthly_required < 15000 else "challenging"
}
def _get_indian_recommendations(self, salary: float) -> list:
"""Get India-specific financial recommendations"""
recommendations = [
"Build emergency fund of 6-12 months expenses",
"Start SIP in diversified equity mutual funds",
"Consider ELSS funds for tax saving under 80C",
"Open PPF account for long-term tax-free returns"
]
if salary > 50000:
recommendations.append("Consider NPS for additional retirement planning")
if salary > 100000:
recommendations.append("Explore direct equity investment after gaining knowledge")
return recommendations
def _get_indian_instruments(self, risk_appetite: str) -> list:
"""Get Indian investment instruments based on risk appetite"""
instruments = {
"conservative": ["PPF", "NSC", "FD", "Debt Mutual Funds"],
"moderate": ["Balanced Mutual Funds", "ELSS", "Gold ETF", "Corporate Bonds"],
"aggressive": ["Large Cap Funds", "Mid Cap Funds", "Small Cap Funds", "Direct Equity"]
}
return instruments.get(risk_appetite.lower(), instruments["moderate"])
def _categorize_expense(self, category: str, percentage: float) -> str:
"""Categorize expense as optimal, high, or low"""
thresholds = {
"rent": (25, 35),
"food": (15, 25),
"transport": (10, 15),
"utilities": (5, 10),
"entertainment": (5, 15)
}
if category.lower() in thresholds:
low, high = thresholds[category.lower()]
if percentage < low:
return "low"
elif percentage > high:
return "high"
return "optimal"
def _get_optimization_tips(self) -> list:
"""Get expense optimization tips"""
return [
"Use public transport or carpool to reduce transport costs",
"Cook at home more often to save on food expenses",
"Use energy-efficient appliances to reduce utility bills",
"Set a monthly entertainment budget and stick to it",
"Review and cancel unused subscriptions"
]
# Initialize MCP Server
mcp_server = MCPServer()
# Modal backend URL (replace with your actual Modal deployment URL)
MODAL_BACKEND_URL = os.getenv("MODAL_BACKEND_URL", "https://kaustubhme0--moneymate-backend-fastapi-app.modal.run")
def call_modal_backend(user_input: str, context: Dict[str, Any] = None) -> str:
"""Call Modal backend for AI-powered financial advice"""
try:
payload = {
"user_input": user_input,
"context": context or {}
}
response = requests.post(
f"{MODAL_BACKEND_URL}/financial_advice",
json=payload,
timeout=30
)
if response.status_code == 200:
return response.json().get("advice", "Unable to get advice at the moment.")
else:
return "Sorry, I'm having trouble connecting to the financial advisor. Please try again."
except requests.exceptions.RequestException as e:
return f"Connection error: {str(e)}. Please check your internet connection."
def create_salary_breakdown_chart(salary: float, needs: float, wants: float, savings: float):
"""Create a pie chart for salary breakdown"""
labels = ['Needs (50%)', 'Wants (30%)', 'Savings (20%)']
values = [needs, wants, savings]
colors = ['#ff6b6b', '#4ecdc4', '#45b7d1']
fig = go.Figure(data=[go.Pie(
labels=labels,
values=values,
hole=0.4,
marker_colors=colors,
textinfo='label+percent',
textfont_size=12
)])
fig.update_layout(
title=f"Salary Breakdown for β‚Ή{salary:,.0f}",
font=dict(size=14),
showlegend=True,
height=400
)
return fig
def create_expense_analysis_chart(expenses: Dict[str, float]):
"""Create a bar chart for expense analysis"""
categories = list(expenses.keys())
amounts = list(expenses.values())
fig = go.Figure([go.Bar(
x=categories,
y=amounts,
marker_color='#667eea',
text=[f"β‚Ή{amount:,.0f}" for amount in amounts],
textposition='auto'
)])
fig.update_layout(
title="Monthly Expense Breakdown",
xaxis_title="Categories",
yaxis_title="Amount (β‚Ή)",
font=dict(size=12),
height=400
)
return fig
def process_financial_query(
salary: float,
rent: float,
food: float,
transport: float,
utilities: float,
entertainment: float,
other: float,
savings_goal: str,
user_question: str
) -> tuple:
"""Process user's financial query and return advice with visualizations"""
# Calculate totals
total_expenses = rent + food + transport + utilities + entertainment + other
remaining_salary = salary - total_expenses
# Create expense dictionary
expenses = {
"Rent": rent,
"Food": food,
"Transport": transport,
"Utilities": utilities,
"Entertainment": entertainment,
"Other": other
}
# Get salary breakdown using 50/30/20 rule
breakdown = mcp_server.salary_breakdown(salary, expenses)
needs = breakdown["breakdown"]["needs"]
wants = breakdown["breakdown"]["wants"]
savings = breakdown["breakdown"]["savings"]
# Create charts
salary_chart = create_salary_breakdown_chart(salary, needs, wants, savings)
expense_chart = create_expense_analysis_chart(expenses)
# Prepare context for Modal backend
context = {
"salary": salary,
"expenses": expenses,
"total_expenses": total_expenses,
"remaining_salary": remaining_salary,
"savings_goal": savings_goal,
"breakdown": breakdown
}
# Get AI advice
if user_question.strip():
advice = call_modal_backend(user_question, context)
else:
advice = call_modal_backend(f"Analyze my finances: Salary β‚Ή{salary}, Total expenses β‚Ή{total_expenses}", context)
# Create summary
summary = f"""
## πŸ’° Financial Summary
**Monthly Salary:** β‚Ή{salary:,.0f}
**Total Expenses:** β‚Ή{total_expenses:,.0f}
**Remaining Amount:** β‚Ή{remaining_salary:,.0f}
### πŸ“Š Recommended Allocation (50/30/20 Rule)
- **Needs (50%):** β‚Ή{needs:,.0f}
- **Wants (30%):** β‚Ή{wants:,.0f}
- **Savings (20%):** β‚Ή{savings:,.0f}
### 🎯 Status
{'βœ… Good job! You have money left over.' if remaining_salary > 0 else '⚠️ You are overspending. Consider reducing expenses.'}
"""
return salary_chart, expense_chart, summary, advice
def handle_quick_question(question: str, salary: float = 50000) -> str:
"""Handle pre-defined quick questions"""
context = {"salary": salary}
return call_modal_backend(question, context)
# Create Gradio interface
def create_moneymate_app():
with gr.Blocks(css=CUSTOM_CSS, title="MoneyMate - Your Financial Assistant") as app:
# Header
gr.HTML("""
<div class="main-header">
<h1>πŸ’° MoneyMate</h1>
<p>Your Personal Financial Assistant for Smart Money Management</p>
<div class="modal-branding">⚑ Powered by Modal Labs</div>
</div>
""")
with gr.Row():
with gr.Column(scale=1):
gr.HTML('<div class="money-card">')
gr.Markdown("### πŸ’Ό Your Financial Details")
salary_input = gr.Number(
label="Monthly Salary (β‚Ή)",
value=50000,
minimum=0,
step=1000
)
gr.Markdown("#### Monthly Expenses")
rent_input = gr.Number(label="Rent (β‚Ή)", value=15000, minimum=0)
food_input = gr.Number(label="Food (β‚Ή)", value=8000, minimum=0)
transport_input = gr.Number(label="Transport (β‚Ή)", value=3000, minimum=0)
utilities_input = gr.Number(label="Utilities (β‚Ή)", value=2000, minimum=0)
entertainment_input = gr.Number(label="Entertainment (β‚Ή)", value=4000, minimum=0)
other_input = gr.Number(label="Other Expenses (β‚Ή)", value=3000, minimum=0)
savings_goal_input = gr.Textbox(
label="Savings Goal",
placeholder="e.g., Emergency fund, Vacation, House down payment",
value="Emergency fund"
)
user_question_input = gr.Textbox(
label="Ask MoneyMate",
placeholder="e.g., How should I invest my savings? What's the best way to save for a house?",
lines=3
)
analyze_btn = gr.Button("Analyze My Finances πŸ“Š", variant="primary", size="large")
gr.HTML('</div>')
# Quick action buttons
gr.HTML('<div class="money-card">')
gr.Markdown("### πŸš€ Quick Questions")
with gr.Row():
quick_btn1 = gr.Button("πŸ’‘ Investment Tips", size="small")
quick_btn2 = gr.Button("🏠 Save for House", size="small")
with gr.Row():
quick_btn3 = gr.Button("✈️ Plan Vacation", size="small")
quick_btn4 = gr.Button("πŸš— Buy a Car", size="small")
gr.HTML('</div>')
with gr.Column(scale=2):
gr.HTML('<div class="money-card">')
# Output components
with gr.Tab("πŸ“Š Salary Breakdown"):
salary_chart_output = gr.Plot()
with gr.Tab("πŸ’Έ Expense Analysis"):
expense_chart_output = gr.Plot()
with gr.Tab("πŸ“‹ Summary"):
summary_output = gr.Markdown()
with gr.Tab("πŸ€– AI Advice"):
advice_output = gr.Markdown(value="Click 'Analyze My Finances' to get personalized advice!")
gr.HTML('</div>')
# Event handlers
analyze_btn.click(
fn=process_financial_query,
inputs=[
salary_input, rent_input, food_input, transport_input,
utilities_input, entertainment_input, other_input,
savings_goal_input, user_question_input
],
outputs=[salary_chart_output, expense_chart_output, summary_output, advice_output]
)
# Quick question handlers
quick_btn1.click(
fn=lambda s: handle_quick_question("What are the best investment options for a beginner in India?", s),
inputs=[salary_input],
outputs=[advice_output]
)
quick_btn2.click(
fn=lambda s: handle_quick_question("How should I save for buying a house in India?", s),
inputs=[salary_input],
outputs=[advice_output]
)
quick_btn3.click(
fn=lambda s: handle_quick_question("What's the best way to save for a vacation?", s),
inputs=[salary_input],
outputs=[advice_output]
)
quick_btn4.click(
fn=lambda s: handle_quick_question("How should I plan to buy a car with my salary?", s),
inputs=[salary_input],
outputs=[advice_output]
)
# Footer
gr.HTML("""
<div style="text-align: center; margin-top: 2rem; color: white;">
<p>Made with ❀️ for Agents & MCP Hackathon 2025</p>
<p>πŸ† Track 1 β€” MCP Tool / Server</p>
</div>
""")
return app
# FastAPI wrapper for MCP compatibility
app_fastapi = FastAPI()
# Create and mount Gradio app
gradio_app = create_moneymate_app()
# MCP endpoints
@app_fastapi.post("/mcp/tools")
async def list_tools():
"""List available MCP tools"""
return {
"tools": [
{
"name": "salary_breakdown",
"description": "Break down salary using 50/30/20 rule",
"inputSchema": {
"type": "object",
"properties": {
"salary": {"type": "number"},
"expenses": {"type": "object"}
}
}
},
{
"name": "investment_advice",
"description": "Get investment advice for Indian market",
"inputSchema": {
"type": "object",
"properties": {
"age": {"type": "integer"},
"salary": {"type": "number"},
"risk_appetite": {"type": "string"}
}
}
}
]
}
@app_fastapi.post("/mcp/call_tool")
async def call_tool(request: dict):
"""Call MCP tool"""
tool_name = request.get("name")
arguments = request.get("arguments", {})
if tool_name in mcp_server.tools:
result = mcp_server.tools[tool_name](**arguments)
return {"content": [{"type": "text", "text": json.dumps(result, indent=2)}]}
else:
return {"error": f"Tool {tool_name} not found"}
# Mount Gradio app
gr.mount_gradio_app(app_fastapi, gradio_app, path="/")
if __name__ == "__main__":
gradio_app.launch(
server_name="0.0.0.0",
server_port=7860,
share=True
)