Spaces:
Running
Running
| '''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 | |
| )''' | |
| 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 | |
| from utils import ( | |
| validate_salary, validate_expenses, calculate_50_30_20_breakdown, | |
| calculate_emergency_fund_target, calculate_sip_returns, get_tax_saving_instruments, | |
| get_investment_allocation_by_age, calculate_goal_based_savings, get_expense_optimization_tips, | |
| format_currency, get_financial_milestones_by_age, calculate_retirement_corpus | |
| ) | |
| # 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); | |
| } | |
| """ | |
| # Modal backend URL | |
| MODAL_BACKEND_URL = os.getenv("MODAL_BACKEND_URL", "https://kaustubhme0--moneymate-backend-fastapi-app.modal.run") | |
| def call_modal_financial_advice(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=60 | |
| ) | |
| 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 call_modal_analyze_expenses(expenses: Dict[str, float], salary: float) -> Dict[str, Any]: | |
| """Call Modal backend for expense analysis""" | |
| try: | |
| payload = { | |
| "expenses": expenses, | |
| "salary": salary | |
| } | |
| response = requests.post( | |
| f"{MODAL_BACKEND_URL}/analyze_expenses", | |
| json=payload, | |
| timeout=30 | |
| ) | |
| if response.status_code == 200: | |
| return response.json().get("analysis", {}) | |
| else: | |
| # Fallback to local utils function | |
| return {} | |
| except requests.exceptions.RequestException: | |
| # Fallback to local utils function | |
| return {} | |
| def call_modal_calculate_returns(monthly_investment: float, annual_return: float, years: int) -> Dict[str, Any]: | |
| """Call Modal backend for investment return calculations""" | |
| try: | |
| payload = { | |
| "monthly_investment": monthly_investment, | |
| "annual_return_rate": annual_return, | |
| "years": years | |
| } | |
| response = requests.post( | |
| f"{MODAL_BACKEND_URL}/calculate_returns", | |
| json=payload, | |
| timeout=30 | |
| ) | |
| if response.status_code == 200: | |
| return response.json().get("returns", {}) | |
| else: | |
| # Fallback to local utils function | |
| return calculate_sip_returns(monthly_investment, annual_return, years) | |
| except requests.exceptions.RequestException: | |
| # Fallback to local utils function | |
| return calculate_sip_returns(monthly_investment, annual_return, years) | |
| def check_modal_health() -> Dict[str, Any]: | |
| """Check Modal backend health""" | |
| try: | |
| response = requests.get(f"{MODAL_BACKEND_URL}/health", timeout=10) | |
| if response.status_code == 200: | |
| return response.json() | |
| else: | |
| return {"status": "unhealthy", "message": "Backend not responding"} | |
| except requests.exceptions.RequestException as e: | |
| return {"status": "error", "message": str(e)} | |
| 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 create_investment_projection_chart(monthly_investment: float, years: int): | |
| """Create investment projection chart using Modal backend""" | |
| returns_data = call_modal_calculate_returns(monthly_investment, 12.0, years) | |
| if not returns_data: | |
| return go.Figure() | |
| # Create year-wise projection | |
| years_list = list(range(1, years + 1)) | |
| investment_values = [] | |
| for year in years_list: | |
| year_data = call_modal_calculate_returns(monthly_investment, 12.0, year) | |
| investment_values.append(year_data.get('future_value', 0)) | |
| fig = go.Figure() | |
| fig.add_trace(go.Scatter( | |
| x=years_list, | |
| y=investment_values, | |
| mode='lines+markers', | |
| name='Investment Growth', | |
| line=dict(color='#667eea', width=3), | |
| marker=dict(size=8) | |
| )) | |
| fig.update_layout( | |
| title=f"SIP Growth Projection (βΉ{monthly_investment:,.0f}/month @ 12% annual return)", | |
| xaxis_title="Years", | |
| 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 with enhanced Modal integration""" | |
| # Validate inputs using utils | |
| salary_valid, salary_msg = validate_salary(salary) | |
| if not salary_valid: | |
| return None, None, f"β {salary_msg}", "Please fix the salary amount and try again." | |
| # 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 | |
| } | |
| # Validate expenses using utils | |
| expenses_valid, expenses_msg, warnings = validate_expenses(expenses, salary) | |
| if not expenses_valid: | |
| return None, None, f"β {expenses_msg}", "Please adjust your expenses and try again." | |
| # Get salary breakdown using utils | |
| breakdown = calculate_50_30_20_breakdown(salary) | |
| needs = breakdown["needs"] | |
| wants = breakdown["wants"] | |
| savings = breakdown["savings"] | |
| # Get expense analysis from Modal backend | |
| modal_analysis = call_modal_analyze_expenses(expenses, salary) | |
| # Get optimization tips from utils | |
| optimization_tips = get_expense_optimization_tips(expenses, salary) | |
| # Create charts | |
| salary_chart = create_salary_breakdown_chart(salary, needs, wants, savings) | |
| expense_chart = create_expense_analysis_chart({k.title(): v for k, v in expenses.items()}) | |
| # Calculate emergency fund target using utils | |
| emergency_fund = calculate_emergency_fund_target(total_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, | |
| "emergency_fund": emergency_fund, | |
| "optimization_tips": optimization_tips[:3] # Top 3 tips | |
| } | |
| # Get AI advice from Modal | |
| if user_question.strip(): | |
| advice = call_modal_financial_advice(user_question, context) | |
| else: | |
| advice = call_modal_financial_advice(f"Analyze my finances: Salary βΉ{salary}, Total expenses βΉ{total_expenses}", context) | |
| # Create enhanced 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.'} | |
| ### π¨ Emergency Fund Target | |
| **Target Amount:** βΉ{emergency_fund['target_amount']:,.0f} ({emergency_fund['months_coverage']} months) | |
| ### π‘ Quick Optimization Tips | |
| {chr(10).join(f"β’ {tip}" for tip in optimization_tips[:3])} | |
| ### β οΈ Warnings | |
| {chr(10).join(f"β’ {warning}" for warning in warnings) if warnings else "No warnings - you're doing great!"} | |
| """ | |
| return salary_chart, expense_chart, summary, advice | |
| def calculate_sip_projection(monthly_investment: float, annual_return: float, years: int) -> tuple: | |
| """Calculate and visualize SIP projections using Modal backend""" | |
| if monthly_investment <= 0 or years <= 0: | |
| return None, "Please enter valid investment amount and timeline." | |
| # Get returns data from Modal backend | |
| returns_data = call_modal_calculate_returns(monthly_investment, annual_return, years) | |
| if not returns_data: | |
| return None, "Unable to calculate returns. Please try again." | |
| # Create projection chart | |
| projection_chart = create_investment_projection_chart(monthly_investment, years) | |
| # Create summary | |
| summary = f""" | |
| ## π SIP Investment Projection | |
| **Monthly Investment:** βΉ{monthly_investment:,.0f} | |
| **Annual Return:** {annual_return}% | |
| **Investment Period:** {years} years | |
| ### π° Projected Results | |
| - **Total Invested:** βΉ{returns_data.get('total_invested', 0):,.0f} | |
| - **Future Value:** βΉ{returns_data.get('future_value', 0):,.0f} | |
| - **Returns Generated:** βΉ{returns_data.get('returns', 0):,.0f} | |
| - **Return Percentage:** {returns_data.get('return_percentage', 0):.1f}% | |
| ### π― Wealth Creation | |
| Your βΉ{monthly_investment:,.0f} monthly investment will grow to βΉ{returns_data.get('future_value', 0):,.0f} in {years} years! | |
| """ | |
| return projection_chart, summary | |
| def handle_quick_question(question: str, salary: float = 50000) -> str: | |
| """Handle pre-defined quick questions""" | |
| context = {"salary": salary} | |
| return call_modal_financial_advice(question, context) | |
| def show_health_status() -> str: | |
| """Show Modal backend health status""" | |
| health = check_modal_health() | |
| if health.get("status") == "healthy": | |
| return "β Backend is healthy and running!" | |
| else: | |
| return f"β Backend issue: {health.get('message', 'Unknown error')}" | |
| # 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.Tabs(): | |
| # Main Financial Analysis Tab | |
| with gr.Tab("πΌ Financial Analysis"): | |
| 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") | |
| health_btn = gr.Button("Check Backend Status π", variant="secondary", size="small") | |
| health_status = gr.Textbox(label="Status", interactive=False) | |
| 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>') | |
| # SIP Calculator Tab | |
| with gr.Tab("π SIP Calculator"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### π SIP Investment Calculator") | |
| monthly_investment_input = gr.Number( | |
| label="Monthly Investment (βΉ)", | |
| value=5000, | |
| minimum=500, | |
| step=500 | |
| ) | |
| annual_return_input = gr.Number( | |
| label="Expected Annual Return (%)", | |
| value=12.0, | |
| minimum=1.0, | |
| maximum=30.0, | |
| step=0.5 | |
| ) | |
| years_input = gr.Number( | |
| label="Investment Period (Years)", | |
| value=10, | |
| minimum=1, | |
| maximum=50, | |
| step=1 | |
| ) | |
| calculate_sip_btn = gr.Button("Calculate SIP Returns π", variant="primary") | |
| with gr.Column(scale=2): | |
| sip_chart_output = gr.Plot() | |
| sip_summary_output = gr.Markdown() | |
| # 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] | |
| ) | |
| calculate_sip_btn.click( | |
| fn=calculate_sip_projection, | |
| inputs=[monthly_investment_input, annual_return_input, years_input], | |
| outputs=[sip_chart_output, sip_summary_output] | |
| ) | |
| health_btn.click( | |
| fn=show_health_status, | |
| outputs=[health_status] | |
| ) | |
| # 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 | |
| if __name__ == "__main__": | |
| app = create_moneymate_app() | |
| app.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=True | |
| ) | |