Spaces:
Running
Running
import modal | |
import json | |
from typing import Dict, Any, Optional | |
from datetime import datetime | |
import os | |
import urllib.request | |
import urllib.parse | |
# Create Modal app | |
app = modal.App("moneymate-backend") | |
# Create image with required dependencies | |
image = modal.Image.debian_slim(python_version="3.11").pip_install([ | |
"fastapi", | |
"pydantic" | |
]) | |
# vLLM server configuration | |
# TODO: Replace with your actual vLLM server URL after deployment | |
VLLM_SERVER_URL = "https://kaustubhme0--example-vllm-openai-compatible-serve.modal.run" | |
# This API key should match the API_KEY in your vLLM inference script (paste.txt) | |
# It's YOUR custom key, not from any external service | |
VLLM_API_KEY = "super-secret-key" | |
# Financial advisor system prompt | |
SYSTEM_PROMPT = """You are MoneyMate, a friendly and knowledgeable financial advisor specifically designed for young Indian professionals who are new to managing their salaries and finances. | |
PERSONALITY & TONE: | |
- Warm, approachable, and encouraging | |
- Use simple, jargon-free language | |
- Be supportive and non-judgmental | |
- Add appropriate emojis to make advice engaging | |
- Safety-first approach - always emphasize careful consideration | |
EXPERTISE AREAS: | |
- Salary allocation and budgeting | |
- Indian investment options (SIP, ELSS, PPF, NPS, etc.) | |
- Tax-saving strategies under Indian tax law | |
- Emergency fund planning | |
- Goal-based financial planning | |
- Expense optimization | |
RESPONSE FORMAT: | |
- Use markdown formatting for better readability | |
- Include practical examples with Indian Rupee amounts | |
- Provide step-by-step actionable advice | |
- Always include safety disclaimers for investment advice | |
- Suggest starting small and learning gradually | |
CULTURAL CONTEXT: | |
- Understand Indian financial instruments and regulations | |
- Consider family financial responsibilities common in India | |
- Be aware of Indian tax implications (80C, LTCG, etc.) | |
- Reference Indian banks, mutual fund companies, and financial platforms | |
SAFETY GUIDELINES: | |
- Never guarantee returns or specific outcomes | |
- Always recommend consulting financial advisors for major decisions | |
- Emphasize the importance of emergency funds before investing | |
- Warn about risks associated with investments | |
- Promote financial literacy and gradual learning | |
Remember: You're helping someone who may be handling their first salary. Be patient, educational, and encouraging while prioritizing their financial safety and security. | |
""" | |
def call_vllm_api(messages: list, max_tokens: int = 1500, temperature: float = 0.7) -> str: | |
"""Call the vLLM server with OpenAI-compatible API""" | |
headers = { | |
"Authorization": f"Bearer {VLLM_API_KEY}", | |
"Content-Type": "application/json", | |
} | |
payload = { | |
"messages": messages, | |
"model": "neuralmagic/Meta-Llama-3.1-8B-Instruct-quantized.w4a16", | |
"max_tokens": max_tokens, | |
"temperature": temperature | |
} | |
try: | |
req = urllib.request.Request( | |
f"{VLLM_SERVER_URL}/v1/chat/completions", | |
data=json.dumps(payload).encode("utf-8"), | |
headers=headers, | |
method="POST", | |
) | |
# Increased timeout to 180 seconds (3 minutes) for LLM responses | |
with urllib.request.urlopen(req, timeout=180) as response: | |
if response.getcode() == 200: | |
result = json.loads(response.read().decode()) | |
return result["choices"][0]["message"]["content"] | |
else: | |
response_text = response.read().decode() | |
raise Exception(f"API call failed with status {response.getcode()}: {response_text}") | |
except urllib.error.URLError as e: | |
if "timeout" in str(e).lower(): | |
raise Exception("Request timed out. The vLLM server might be taking too long to respond or might be cold starting.") | |
else: | |
raise Exception(f"Network error calling vLLM API: {str(e)}") | |
except Exception as e: | |
raise Exception(f"Error calling vLLM API: {str(e)}") | |
def get_financial_advice(user_input: str, context: Dict[str, Any] = None) -> str: | |
"""Generate financial advice using local vLLM server""" | |
try: | |
# Prepare context information | |
context_str = "" | |
if context: | |
salary = context.get("salary", 0) | |
expenses = context.get("expenses", {}) | |
total_expenses = context.get("total_expenses", 0) | |
remaining_salary = context.get("remaining_salary", 0) | |
savings_goal = context.get("savings_goal", "") | |
context_str = f""" | |
CURRENT FINANCIAL SITUATION: | |
- Monthly Salary: ₹{salary:,.0f} | |
- Total Monthly Expenses: ₹{total_expenses:,.0f} | |
- Remaining Amount: ₹{remaining_salary:,.0f} | |
- Savings Goal: {savings_goal} | |
EXPENSE BREAKDOWN: | |
""" | |
for category, amount in expenses.items(): | |
if amount > 0: | |
context_str += f"- {category}: ₹{amount:,.0f}\n" | |
# Create the full prompt | |
user_prompt = f""" | |
{context_str} | |
USER QUESTION: {user_input} | |
Please provide personalized financial advice based on this information. Focus on: | |
1. Specific recommendations for their situation | |
2. Practical next steps they can take | |
3. Indian financial instruments and strategies | |
4. Risk management and safety | |
5. Educational insights to help them learn | |
""" | |
# Prepare messages for vLLM | |
messages = [ | |
{"role": "system", "content": SYSTEM_PROMPT}, | |
{"role": "user", "content": user_prompt} | |
] | |
# Call vLLM API | |
advice = call_vllm_api(messages, max_tokens=1500, temperature=0.7) | |
# Add timestamp and additional context | |
current_time = datetime.now().strftime("%B %d, %Y at %I:%M %p") | |
formatted_advice = f""" | |
{advice} | |
--- | |
💡 **MoneyMate Tips:** | |
- Start small and gradually increase your investments | |
- Always maintain 6-12 months of expenses as emergency fund | |
- Review and adjust your financial plan quarterly | |
- Consider consulting a certified financial planner for major decisions | |
*Advice generated on {current_time}* | |
""" | |
return formatted_advice.strip() | |
except Exception as e: | |
error_msg = f""" | |
## 😔 Sorry, I'm having trouble right now | |
I encountered an error while processing your request: {str(e)} | |
**Here are some general tips while I'm unavailable:** | |
### 💰 Basic Financial Rules for Young Professionals: | |
- **50/30/20 Rule**: Allocate 50% for needs, 30% for wants, 20% for savings | |
- **Emergency Fund**: Build 6-12 months of expenses before investing | |
- **Start Small**: Begin with SIP of ₹1000-2000 monthly in diversified funds | |
- **Tax Planning**: Use ELSS funds to save tax under Section 80C | |
### 🏦 Safe Investment Options for Beginners: | |
- **SIP in Index Funds**: Low cost, diversified exposure | |
- **PPF**: 15-year lock-in, tax-free returns | |
- **ELSS Funds**: Tax saving with 3-year lock-in | |
- **Liquid Funds**: For emergency fund parking | |
Please try again in a few moments, or check your connection to the vLLM server. | |
""" | |
return error_msg | |
def analyze_expenses(expenses: Dict[str, float], salary: float) -> Dict[str, Any]: | |
"""Analyze expense patterns and provide optimization suggestions""" | |
total_expenses = sum(expenses.values()) | |
expense_ratio = (total_expenses / salary) * 100 if salary > 0 else 0 | |
analysis = { | |
"total_expenses": total_expenses, | |
"expense_ratio": expense_ratio, | |
"status": "healthy" if expense_ratio < 70 else "concerning", | |
"recommendations": [] | |
} | |
# Analyze each category | |
for category, amount in expenses.items(): | |
category_ratio = (amount / salary) * 100 if salary > 0 else 0 | |
# Category-specific recommendations | |
if category.lower() == "rent" and category_ratio > 40: | |
analysis["recommendations"].append(f"🏠 Rent is high ({category_ratio:.1f}% of salary). Consider relocating or finding roommates.") | |
elif category.lower() == "food" and category_ratio > 20: | |
analysis["recommendations"].append(f"🍽️ Food expenses are high ({category_ratio:.1f}% of salary). Try cooking at home more often.") | |
elif category.lower() == "entertainment" and category_ratio > 15: | |
analysis["recommendations"].append(f"🎬 Entertainment expenses are high ({category_ratio:.1f}% of salary). Set a monthly budget and stick to it.") | |
return analysis | |
def calculate_investment_returns( | |
monthly_investment: float, | |
annual_return_rate: float, | |
years: int | |
) -> Dict[str, Any]: | |
"""Calculate SIP returns with compound interest""" | |
monthly_rate = annual_return_rate / 12 / 100 | |
total_months = years * 12 | |
# SIP future value formula | |
if monthly_rate > 0: | |
future_value = monthly_investment * (((1 + monthly_rate) ** total_months - 1) / monthly_rate) * (1 + monthly_rate) | |
else: | |
future_value = monthly_investment * total_months | |
total_invested = monthly_investment * total_months | |
returns = future_value - total_invested | |
return { | |
"future_value": round(future_value, 2), | |
"total_invested": round(total_invested, 2), | |
"returns": round(returns, 2), | |
"return_percentage": round((returns / total_invested) * 100, 2) if total_invested > 0 else 0 | |
} | |
def health_check_vllm() -> Dict[str, Any]: | |
"""Check if vLLM server is healthy""" | |
try: | |
with urllib.request.urlopen(f"{VLLM_SERVER_URL}/health", timeout=30) as response: | |
if response.getcode() == 200: | |
return {"vllm_status": "healthy", "server_url": VLLM_SERVER_URL} | |
else: | |
return {"vllm_status": "unhealthy", "server_url": VLLM_SERVER_URL, "status_code": response.getcode()} | |
except Exception as e: | |
return {"vllm_status": "error", "error": str(e), "server_url": VLLM_SERVER_URL} | |
# FastAPI app for HTTP endpoints | |
def fastapi_app(): | |
from fastapi import FastAPI, HTTPException | |
from pydantic import BaseModel | |
from typing import Optional | |
api = FastAPI(title="MoneyMate Backend API with vLLM") | |
class FinancialAdviceRequest(BaseModel): | |
user_input: str | |
context: Optional[Dict[str, Any]] = None | |
class ExpenseAnalysisRequest(BaseModel): | |
expenses: Dict[str, float] | |
salary: float | |
class InvestmentCalculationRequest(BaseModel): | |
monthly_investment: float | |
annual_return_rate: float | |
years: int | |
async def get_advice(request: FinancialAdviceRequest): | |
"""Get AI-powered financial advice using vLLM""" | |
try: | |
advice = get_financial_advice.remote(request.user_input, request.context) | |
return {"advice": advice} | |
except Exception as e: | |
raise HTTPException(status_code=500, detail=str(e)) | |
async def analyze_user_expenses(request: ExpenseAnalysisRequest): | |
"""Analyze user's expense patterns""" | |
try: | |
analysis = analyze_expenses.remote(request.expenses, request.salary) | |
return {"analysis": analysis} | |
except Exception as e: | |
raise HTTPException(status_code=500, detail=str(e)) | |
async def calculate_returns(request: InvestmentCalculationRequest): | |
"""Calculate SIP investment returns""" | |
try: | |
returns = calculate_investment_returns.remote( | |
request.monthly_investment, | |
request.annual_return_rate, | |
request.years | |
) | |
return {"returns": returns} | |
except Exception as e: | |
raise HTTPException(status_code=500, detail=str(e)) | |
async def health_check(): | |
"""Health check endpoint including vLLM server status""" | |
vllm_health = health_check_vllm.remote() | |
return { | |
"status": "healthy", | |
"service": "MoneyMate Backend with vLLM", | |
"vllm_server": vllm_health | |
} | |
async def root(): | |
"""Root endpoint with service information""" | |
return { | |
"service": "MoneyMate Backend API with vLLM", | |
"version": "2.0.0", | |
"description": "AI-powered financial advice for young Indian professionals using local vLLM server", | |
"vllm_server": VLLM_SERVER_URL, | |
"endpoints": [ | |
"/financial_advice", | |
"/analyze_expenses", | |
"/calculate_returns", | |
"/health" | |
] | |
} | |
return api | |
# Test function to verify vLLM integration | |
def test_vllm_integration(): | |
"""Test the vLLM integration with a sample financial question""" | |
print(f"Testing vLLM server at: {VLLM_SERVER_URL}") | |
print("Testing vLLM server health...") | |
health = health_check_vllm.remote() | |
print(f"vLLM Health: {health}") | |
if health.get("vllm_status") == "healthy": | |
print("\nTesting simple financial advice generation...") | |
# Start with a very simple question first | |
try: | |
advice = get_financial_advice.remote("What is SIP?", None) | |
print("Generated Advice:") | |
print("=" * 50) | |
print(advice) | |
except Exception as e: | |
print(f"Error during simple test: {e}") | |
print("\nTesting with context...") | |
sample_context = { | |
"salary": 50000, | |
"expenses": {"rent": 15000, "food": 8000, "transport": 3000}, | |
"total_expenses": 26000, | |
"remaining_salary": 24000, | |
"savings_goal": "Emergency fund and investments" | |
} | |
try: | |
advice = get_financial_advice.remote( | |
"How should I invest my remaining salary?", | |
sample_context | |
) | |
print("Generated Advice with Context:") | |
print("=" * 50) | |
print(advice) | |
except Exception as e: | |
print(f"Error during context test: {e}") | |
else: | |
print("vLLM server is not healthy. Please check:") | |
print("1. Is your vLLM server deployed and running?") | |
print("2. Is the VLLM_SERVER_URL correct?") | |
print("3. Is the API_KEY matching?") | |
print(f"Current URL: {VLLM_SERVER_URL}") | |
print(f"Current API Key: {VLLM_API_KEY}") |