Resolve merge conflicts - keep latest improvements
Browse files- app/main.py +131 -0
- app/smart_recommendation.py +395 -0
app/main.py
CHANGED
|
@@ -215,3 +215,134 @@ if __name__ == "__main__":
|
|
| 215 |
import uvicorn
|
| 216 |
uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 217 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
import uvicorn
|
| 216 |
uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 217 |
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
# from fastapi import FastAPI, HTTPException, Depends
|
| 223 |
+
# from fastapi.middleware.cors import CORSMiddleware
|
| 224 |
+
# from pymongo import MongoClient
|
| 225 |
+
# from pymongo.errors import ConnectionFailure
|
| 226 |
+
# import os
|
| 227 |
+
# from typing import List, Optional
|
| 228 |
+
# from datetime import datetime, timedelta
|
| 229 |
+
# from app.models import BudgetRecommendation, Expense, Budget, CategoryExpense
|
| 230 |
+
# from app.smart_recommendation import SmartBudgetRecommender
|
| 231 |
+
|
| 232 |
+
# app = FastAPI(title="Smart Budget Recommendation API", version="1.0.0")
|
| 233 |
+
|
| 234 |
+
# # CORS middleware
|
| 235 |
+
# app.add_middleware(
|
| 236 |
+
# CORSMiddleware,
|
| 237 |
+
# allow_origins=["*"],
|
| 238 |
+
# allow_credentials=True,
|
| 239 |
+
# allow_methods=["*"],
|
| 240 |
+
# allow_headers=["*"],
|
| 241 |
+
# )
|
| 242 |
+
|
| 243 |
+
# # MongoDB connection - Get from environment variable (set as secret in Hugging Face)
|
| 244 |
+
# MONGODB_URI = os.getenv("MONGODB_URI")
|
| 245 |
+
# if not MONGODB_URI:
|
| 246 |
+
# raise ValueError("MONGODB_URI environment variable is required. Please set it in Hugging Face secrets.")
|
| 247 |
+
|
| 248 |
+
# try:
|
| 249 |
+
# client = MongoClient(MONGODB_URI)
|
| 250 |
+
# db = client.expense
|
| 251 |
+
# # Test connection
|
| 252 |
+
# client.admin.command('ping')
|
| 253 |
+
# print("Successfully connected to MongoDB")
|
| 254 |
+
# except ConnectionFailure as e:
|
| 255 |
+
# print(f"Failed to connect to MongoDB: {e}")
|
| 256 |
+
# raise
|
| 257 |
+
|
| 258 |
+
# # Initialize Smart Budget Recommender
|
| 259 |
+
# recommender = SmartBudgetRecommender(db)
|
| 260 |
+
|
| 261 |
+
# @app.get("/")
|
| 262 |
+
# async def root():
|
| 263 |
+
# return {"message": "Smart Budget Recommendation API", "status": "running"}
|
| 264 |
+
|
| 265 |
+
# @app.get("/health")
|
| 266 |
+
# async def health_check():
|
| 267 |
+
# try:
|
| 268 |
+
# client.admin.command('ping')
|
| 269 |
+
# return {"status": "healthy", "database": "connected"}
|
| 270 |
+
# except Exception as e:
|
| 271 |
+
# return {"status": "unhealthy", "error": str(e)}
|
| 272 |
+
|
| 273 |
+
# @app.post("/expenses", response_model=dict)
|
| 274 |
+
# async def create_expense(expense: Expense):
|
| 275 |
+
# """
|
| 276 |
+
# Disabled: this service does not create expenses.
|
| 277 |
+
|
| 278 |
+
# All expenses should be created by the main WalletSync app and stored
|
| 279 |
+
# directly in MongoDB. This API only reads existing data for analytics.
|
| 280 |
+
# """
|
| 281 |
+
# raise HTTPException(
|
| 282 |
+
# status_code=405,
|
| 283 |
+
# detail="Creating expenses is disabled. Use the main WalletSync app to add expenses.",
|
| 284 |
+
# )
|
| 285 |
+
|
| 286 |
+
# @app.get("/expenses", response_model=List[Expense])
|
| 287 |
+
# async def get_expenses(user_id: str, limit: int = 100):
|
| 288 |
+
# """Get expenses for a user"""
|
| 289 |
+
# expenses = list(db.expenses.find({"user_id": user_id}).sort("date", -1).limit(limit))
|
| 290 |
+
# for expense in expenses:
|
| 291 |
+
# expense["id"] = str(expense["_id"])
|
| 292 |
+
# del expense["_id"]
|
| 293 |
+
# return expenses
|
| 294 |
+
|
| 295 |
+
# @app.post("/budgets", response_model=dict)
|
| 296 |
+
# async def create_budget(budget: Budget):
|
| 297 |
+
# """
|
| 298 |
+
# Disabled: this service does not create budgets.
|
| 299 |
+
|
| 300 |
+
# All budgets should be created by the main WalletSync app and stored
|
| 301 |
+
# directly in MongoDB. This API only reads existing data for analytics.
|
| 302 |
+
# """
|
| 303 |
+
# raise HTTPException(
|
| 304 |
+
# status_code=405,
|
| 305 |
+
# detail="Creating budgets is disabled. Use the main WalletSync app to add budgets.",
|
| 306 |
+
# )
|
| 307 |
+
|
| 308 |
+
# @app.get("/budgets", response_model=List[Budget])
|
| 309 |
+
# async def get_budgets(user_id: str):
|
| 310 |
+
# """Get budgets for a user"""
|
| 311 |
+
# budgets = list(db.budgets.find({"user_id": user_id}))
|
| 312 |
+
# for budget in budgets:
|
| 313 |
+
# budget["id"] = str(budget["_id"])
|
| 314 |
+
# del budget["_id"]
|
| 315 |
+
# return budgets
|
| 316 |
+
|
| 317 |
+
# @app.get("/recommendations/{user_id}", response_model=List[BudgetRecommendation])
|
| 318 |
+
# async def get_budget_recommendations(user_id: str, month: Optional[int] = None, year: Optional[int] = None):
|
| 319 |
+
# """
|
| 320 |
+
# Get smart budget recommendations for a user based on past spending behavior.
|
| 321 |
+
|
| 322 |
+
# Example response:
|
| 323 |
+
# {
|
| 324 |
+
# "category": "Groceries",
|
| 325 |
+
# "average_expense": 3800,
|
| 326 |
+
# "recommended_budget": 4000,
|
| 327 |
+
# "reason": "Your average monthly grocery expense is Rs.3,800. We suggest setting your budget to Rs.4,000 for next month."
|
| 328 |
+
# }
|
| 329 |
+
# """
|
| 330 |
+
# if not month or not year:
|
| 331 |
+
# # Default to next month
|
| 332 |
+
# next_month = datetime.now().replace(day=1) + timedelta(days=32)
|
| 333 |
+
# month = next_month.month
|
| 334 |
+
# year = next_month.year
|
| 335 |
+
|
| 336 |
+
# recommendations = recommender.get_recommendations(user_id, month, year)
|
| 337 |
+
# return recommendations
|
| 338 |
+
|
| 339 |
+
# @app.get("/category-expenses/{user_id}", response_model=List[CategoryExpense])
|
| 340 |
+
# async def get_category_expenses(user_id: str, months: int = 3):
|
| 341 |
+
# """Get average expenses by category for the past N months"""
|
| 342 |
+
# category_expenses = recommender.get_category_averages(user_id, months)
|
| 343 |
+
# return category_expenses
|
| 344 |
+
|
| 345 |
+
# if __name__ == "__main__":
|
| 346 |
+
# import uvicorn
|
| 347 |
+
# uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 348 |
+
|
app/smart_recommendation.py
CHANGED
|
@@ -315,28 +315,41 @@ class SmartBudgetRecommender:
|
|
| 315 |
try:
|
| 316 |
query_str = {"createdBy": user_id}
|
| 317 |
budgets_str = list(self.db.budgets.find(query_str))
|
|
|
|
| 318 |
print(f"Pattern 2 (createdBy string): Found {len(budgets_str)} budgets")
|
| 319 |
if budgets_str:
|
| 320 |
budgets.extend(budgets_str)
|
| 321 |
except Exception as e:
|
| 322 |
print(f"Pattern 2 failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
pass
|
| 324 |
|
| 325 |
# Pattern 3: Try with user_id field (alternative field name) - no status filter
|
| 326 |
try:
|
| 327 |
query_userid = {"user_id": user_id}
|
| 328 |
budgets_userid = list(self.db.budgets.find(query_userid))
|
|
|
|
| 329 |
print(f"Pattern 3 (user_id string): Found {len(budgets_userid)} budgets")
|
| 330 |
if budgets_userid:
|
| 331 |
budgets.extend(budgets_userid)
|
| 332 |
except Exception as e:
|
| 333 |
print(f"Pattern 3 failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
pass
|
| 335 |
|
| 336 |
# Pattern 4: Try ObjectId with user_id field - no status filter
|
| 337 |
try:
|
| 338 |
query_objid_userid = {"user_id": ObjectId(user_id)}
|
| 339 |
budgets_objid_userid = list(self.db.budgets.find(query_objid_userid))
|
|
|
|
| 340 |
print(f"Pattern 4 (user_id ObjectId): Found {len(budgets_objid_userid)} budgets")
|
| 341 |
if budgets_objid_userid:
|
| 342 |
budgets.extend(budgets_objid_userid)
|
|
@@ -369,6 +382,11 @@ class SmartBudgetRecommender:
|
|
| 369 |
budgets.append(budget_by_id_str)
|
| 370 |
except Exception as e:
|
| 371 |
print(f"Pattern 6 failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
pass
|
| 373 |
|
| 374 |
# Remove duplicates based on _id
|
|
@@ -384,12 +402,15 @@ class SmartBudgetRecommender:
|
|
| 384 |
|
| 385 |
if not budgets:
|
| 386 |
print(f"No budgets found for user_id: {user_id}")
|
|
|
|
| 387 |
print(f"Tried all query patterns. Checking sample budget structure...")
|
| 388 |
# Get a sample budget to see the structure
|
| 389 |
sample = self.db.budgets.find_one()
|
| 390 |
if sample:
|
| 391 |
print(f"Sample budget structure - createdBy type: {type(sample.get('createdBy')).__name__}, value: {sample.get('createdBy')}")
|
| 392 |
print(f"Sample budget has user_id field: {'user_id' in sample}")
|
|
|
|
|
|
|
| 393 |
return {}
|
| 394 |
|
| 395 |
print(f"Found {len(budgets)} budgets for user_id: {user_id}")
|
|
@@ -407,12 +428,17 @@ class SmartBudgetRecommender:
|
|
| 407 |
|
| 408 |
# Get headCategory ID and amounts
|
| 409 |
head_cat_id = head_cat.get("headCategory")
|
|
|
|
| 410 |
try:
|
| 411 |
head_cat_max = float(head_cat.get("maxAmount", 0) or 0)
|
| 412 |
head_cat_spend = float(head_cat.get("spendAmount", 0) or 0)
|
| 413 |
except (ValueError, TypeError):
|
| 414 |
head_cat_max = 0
|
| 415 |
head_cat_spend = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
|
| 417 |
# Process nested categories within headCategory
|
| 418 |
nested_categories = head_cat.get("categories", [])
|
|
@@ -422,6 +448,7 @@ class SmartBudgetRecommender:
|
|
| 422 |
continue
|
| 423 |
|
| 424 |
nested_cat_id = nested_cat.get("category")
|
|
|
|
| 425 |
try:
|
| 426 |
nested_cat_max = float(nested_cat.get("maxAmount", 0) or 0)
|
| 427 |
nested_cat_spend = float(nested_cat.get("spendAmount", 0) or 0)
|
|
@@ -432,6 +459,14 @@ class SmartBudgetRecommender:
|
|
| 432 |
|
| 433 |
# Only include categories with limits (must have maxAmount > 0)
|
| 434 |
if nested_cat_max > 0:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
# Look up actual category name
|
| 436 |
nested_category_name = self._get_category_name(nested_cat_id)
|
| 437 |
nested_base_amount = nested_cat_spend if nested_cat_spend > 0 else nested_cat_max
|
|
@@ -483,6 +518,7 @@ class SmartBudgetRecommender:
|
|
| 483 |
budget_name = b.get("category") or b.get("title") or "Uncategorized"
|
| 484 |
|
| 485 |
# Derive a base amount from WalletSync fields
|
|
|
|
| 486 |
try:
|
| 487 |
max_amount = float(b.get("maxAmount", 0) or b.get("max_amount", 0) or b.get("amount", 0) or 0)
|
| 488 |
spend_amount = float(b.get("spendAmount", 0) or b.get("spend_amount", 0) or b.get("spent", 0) or 0)
|
|
@@ -491,6 +527,11 @@ class SmartBudgetRecommender:
|
|
| 491 |
max_amount = 0
|
| 492 |
spend_amount = 0
|
| 493 |
budget_amount = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
|
| 495 |
# Priority: spendAmount > maxAmount > budgetAmount > budget
|
| 496 |
if spend_amount > 0:
|
|
@@ -579,3 +620,357 @@ class SmartBudgetRecommender:
|
|
| 579 |
print(f"OpenAI recommendation error for {category}: {exc}")
|
| 580 |
return None
|
| 581 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
try:
|
| 316 |
query_str = {"createdBy": user_id}
|
| 317 |
budgets_str = list(self.db.budgets.find(query_str))
|
| 318 |
+
<<<<<<< HEAD
|
| 319 |
print(f"Pattern 2 (createdBy string): Found {len(budgets_str)} budgets")
|
| 320 |
if budgets_str:
|
| 321 |
budgets.extend(budgets_str)
|
| 322 |
except Exception as e:
|
| 323 |
print(f"Pattern 2 failed: {e}")
|
| 324 |
+
=======
|
| 325 |
+
if budgets_str:
|
| 326 |
+
budgets.extend(budgets_str)
|
| 327 |
+
except Exception:
|
| 328 |
+
>>>>>>> fa62699d4d97bb637c5b2d83906f22ae98ede56a
|
| 329 |
pass
|
| 330 |
|
| 331 |
# Pattern 3: Try with user_id field (alternative field name) - no status filter
|
| 332 |
try:
|
| 333 |
query_userid = {"user_id": user_id}
|
| 334 |
budgets_userid = list(self.db.budgets.find(query_userid))
|
| 335 |
+
<<<<<<< HEAD
|
| 336 |
print(f"Pattern 3 (user_id string): Found {len(budgets_userid)} budgets")
|
| 337 |
if budgets_userid:
|
| 338 |
budgets.extend(budgets_userid)
|
| 339 |
except Exception as e:
|
| 340 |
print(f"Pattern 3 failed: {e}")
|
| 341 |
+
=======
|
| 342 |
+
if budgets_userid:
|
| 343 |
+
budgets.extend(budgets_userid)
|
| 344 |
+
except Exception:
|
| 345 |
+
>>>>>>> fa62699d4d97bb637c5b2d83906f22ae98ede56a
|
| 346 |
pass
|
| 347 |
|
| 348 |
# Pattern 4: Try ObjectId with user_id field - no status filter
|
| 349 |
try:
|
| 350 |
query_objid_userid = {"user_id": ObjectId(user_id)}
|
| 351 |
budgets_objid_userid = list(self.db.budgets.find(query_objid_userid))
|
| 352 |
+
<<<<<<< HEAD
|
| 353 |
print(f"Pattern 4 (user_id ObjectId): Found {len(budgets_objid_userid)} budgets")
|
| 354 |
if budgets_objid_userid:
|
| 355 |
budgets.extend(budgets_objid_userid)
|
|
|
|
| 382 |
budgets.append(budget_by_id_str)
|
| 383 |
except Exception as e:
|
| 384 |
print(f"Pattern 6 failed: {e}")
|
| 385 |
+
=======
|
| 386 |
+
if budgets_objid_userid:
|
| 387 |
+
budgets.extend(budgets_objid_userid)
|
| 388 |
+
except Exception:
|
| 389 |
+
>>>>>>> fa62699d4d97bb637c5b2d83906f22ae98ede56a
|
| 390 |
pass
|
| 391 |
|
| 392 |
# Remove duplicates based on _id
|
|
|
|
| 402 |
|
| 403 |
if not budgets:
|
| 404 |
print(f"No budgets found for user_id: {user_id}")
|
| 405 |
+
<<<<<<< HEAD
|
| 406 |
print(f"Tried all query patterns. Checking sample budget structure...")
|
| 407 |
# Get a sample budget to see the structure
|
| 408 |
sample = self.db.budgets.find_one()
|
| 409 |
if sample:
|
| 410 |
print(f"Sample budget structure - createdBy type: {type(sample.get('createdBy')).__name__}, value: {sample.get('createdBy')}")
|
| 411 |
print(f"Sample budget has user_id field: {'user_id' in sample}")
|
| 412 |
+
=======
|
| 413 |
+
>>>>>>> fa62699d4d97bb637c5b2d83906f22ae98ede56a
|
| 414 |
return {}
|
| 415 |
|
| 416 |
print(f"Found {len(budgets)} budgets for user_id: {user_id}")
|
|
|
|
| 428 |
|
| 429 |
# Get headCategory ID and amounts
|
| 430 |
head_cat_id = head_cat.get("headCategory")
|
| 431 |
+
<<<<<<< HEAD
|
| 432 |
try:
|
| 433 |
head_cat_max = float(head_cat.get("maxAmount", 0) or 0)
|
| 434 |
head_cat_spend = float(head_cat.get("spendAmount", 0) or 0)
|
| 435 |
except (ValueError, TypeError):
|
| 436 |
head_cat_max = 0
|
| 437 |
head_cat_spend = 0
|
| 438 |
+
=======
|
| 439 |
+
head_cat_max = float(head_cat.get("maxAmount", 0) or 0)
|
| 440 |
+
head_cat_spend = float(head_cat.get("spendAmount", 0) or 0)
|
| 441 |
+
>>>>>>> fa62699d4d97bb637c5b2d83906f22ae98ede56a
|
| 442 |
|
| 443 |
# Process nested categories within headCategory
|
| 444 |
nested_categories = head_cat.get("categories", [])
|
|
|
|
| 448 |
continue
|
| 449 |
|
| 450 |
nested_cat_id = nested_cat.get("category")
|
| 451 |
+
<<<<<<< HEAD
|
| 452 |
try:
|
| 453 |
nested_cat_max = float(nested_cat.get("maxAmount", 0) or 0)
|
| 454 |
nested_cat_spend = float(nested_cat.get("spendAmount", 0) or 0)
|
|
|
|
| 459 |
|
| 460 |
# Only include categories with limits (must have maxAmount > 0)
|
| 461 |
if nested_cat_max > 0:
|
| 462 |
+
=======
|
| 463 |
+
nested_cat_max = float(nested_cat.get("maxAmount", 0) or 0)
|
| 464 |
+
nested_cat_spend = float(nested_cat.get("spendAmount", 0) or 0)
|
| 465 |
+
spend_limit_type = nested_cat.get("spendLimitType", "NO_LIMIT")
|
| 466 |
+
|
| 467 |
+
# Only include categories with limits (maxAmount > 0 or spendLimitType != "NO_LIMIT")
|
| 468 |
+
if nested_cat_max > 0 or (spend_limit_type != "NO_LIMIT" and nested_cat_spend > 0):
|
| 469 |
+
>>>>>>> fa62699d4d97bb637c5b2d83906f22ae98ede56a
|
| 470 |
# Look up actual category name
|
| 471 |
nested_category_name = self._get_category_name(nested_cat_id)
|
| 472 |
nested_base_amount = nested_cat_spend if nested_cat_spend > 0 else nested_cat_max
|
|
|
|
| 518 |
budget_name = b.get("category") or b.get("title") or "Uncategorized"
|
| 519 |
|
| 520 |
# Derive a base amount from WalletSync fields
|
| 521 |
+
<<<<<<< HEAD
|
| 522 |
try:
|
| 523 |
max_amount = float(b.get("maxAmount", 0) or b.get("max_amount", 0) or b.get("amount", 0) or 0)
|
| 524 |
spend_amount = float(b.get("spendAmount", 0) or b.get("spend_amount", 0) or b.get("spent", 0) or 0)
|
|
|
|
| 527 |
max_amount = 0
|
| 528 |
spend_amount = 0
|
| 529 |
budget_amount = 0
|
| 530 |
+
=======
|
| 531 |
+
max_amount = float(b.get("maxAmount", 0) or b.get("max_amount", 0) or b.get("amount", 0) or 0)
|
| 532 |
+
spend_amount = float(b.get("spendAmount", 0) or b.get("spend_amount", 0) or b.get("spent", 0) or 0)
|
| 533 |
+
budget_amount = float(b.get("budget", 0) or b.get("budgetAmount", 0) or 0)
|
| 534 |
+
>>>>>>> fa62699d4d97bb637c5b2d83906f22ae98ede56a
|
| 535 |
|
| 536 |
# Priority: spendAmount > maxAmount > budgetAmount > budget
|
| 537 |
if spend_amount > 0:
|
|
|
|
| 620 |
print(f"OpenAI recommendation error for {category}: {exc}")
|
| 621 |
return None
|
| 622 |
|
| 623 |
+
|
| 624 |
+
# import json
|
| 625 |
+
# import math
|
| 626 |
+
# import os
|
| 627 |
+
# from collections import defaultdict
|
| 628 |
+
# from datetime import datetime, timedelta
|
| 629 |
+
# from typing import Dict, List
|
| 630 |
+
|
| 631 |
+
# import requests
|
| 632 |
+
# from dotenv import load_dotenv
|
| 633 |
+
# from bson import ObjectId
|
| 634 |
+
|
| 635 |
+
# from app.models import BudgetRecommendation, CategoryExpense
|
| 636 |
+
|
| 637 |
+
# load_dotenv()
|
| 638 |
+
# OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
| 639 |
+
|
| 640 |
+
# class SmartBudgetRecommender:
|
| 641 |
+
# """
|
| 642 |
+
# Smart Budget Recommendation Engine
|
| 643 |
+
|
| 644 |
+
# Analyzes past spending behavior and recommends personalized budgets
|
| 645 |
+
# for each category based on historical data.
|
| 646 |
+
# """
|
| 647 |
+
|
| 648 |
+
# def __init__(self, db):
|
| 649 |
+
# self.db = db
|
| 650 |
+
|
| 651 |
+
# def get_recommendations(self, user_id: str, month: int, year: int) -> List[BudgetRecommendation]:
|
| 652 |
+
# """
|
| 653 |
+
# Get budget recommendations for all categories based on past behavior.
|
| 654 |
+
|
| 655 |
+
# Args:
|
| 656 |
+
# user_id: User identifier
|
| 657 |
+
# month: Target month (1-12)
|
| 658 |
+
# year: Target year
|
| 659 |
+
|
| 660 |
+
# Returns:
|
| 661 |
+
# List of budget recommendations for each category
|
| 662 |
+
# """
|
| 663 |
+
# # 1) Try to build stats from existing budgets for this user (createdBy)
|
| 664 |
+
# category_data = self._get_category_stats_from_budgets(user_id, month, year)
|
| 665 |
+
|
| 666 |
+
# # 2) If there are no budgets, fall back to expenses history
|
| 667 |
+
# if not category_data:
|
| 668 |
+
# end_date = datetime(year, month, 1) - timedelta(days=1)
|
| 669 |
+
# start_date = end_date - timedelta(days=180) # ~6 months
|
| 670 |
+
|
| 671 |
+
# expenses = list(
|
| 672 |
+
# self.db.expenses.find(
|
| 673 |
+
# {
|
| 674 |
+
# "user_id": user_id,
|
| 675 |
+
# "date": {"$gte": start_date, "$lte": end_date},
|
| 676 |
+
# "type": "expense",
|
| 677 |
+
# }
|
| 678 |
+
# )
|
| 679 |
+
# )
|
| 680 |
+
|
| 681 |
+
# if not expenses:
|
| 682 |
+
# return []
|
| 683 |
+
|
| 684 |
+
# # Group expenses by category and calculate monthly averages
|
| 685 |
+
# category_data = self._calculate_category_statistics(
|
| 686 |
+
# expenses, start_date, end_date
|
| 687 |
+
# )
|
| 688 |
+
|
| 689 |
+
# recommendations: List[BudgetRecommendation] = []
|
| 690 |
+
|
| 691 |
+
# for category, data in category_data.items():
|
| 692 |
+
# avg_expense = data["average_monthly"]
|
| 693 |
+
# confidence = self._calculate_confidence(data)
|
| 694 |
+
|
| 695 |
+
# # 1) Try OpenAI first (primary source of recommendation)
|
| 696 |
+
# ai_result = self._get_ai_recommendation(category, data, avg_expense)
|
| 697 |
+
# if ai_result:
|
| 698 |
+
# recommended_budget = ai_result.get("recommended_budget")
|
| 699 |
+
# reason = ai_result.get("reason")
|
| 700 |
+
# action = ai_result.get("action")
|
| 701 |
+
# else:
|
| 702 |
+
# # 2) Fallback to rule-based recommendation
|
| 703 |
+
# recommended_budget = self._calculate_recommended_budget(avg_expense, data)
|
| 704 |
+
# reason = self._generate_reason(category, avg_expense, recommended_budget)
|
| 705 |
+
# action = None
|
| 706 |
+
|
| 707 |
+
# recommendations.append(BudgetRecommendation(
|
| 708 |
+
# category=category,
|
| 709 |
+
# average_expense=round(avg_expense, 2),
|
| 710 |
+
# recommended_budget=round(recommended_budget or 0, 2),
|
| 711 |
+
# reason=reason,
|
| 712 |
+
# confidence=confidence,
|
| 713 |
+
# action=action
|
| 714 |
+
# ))
|
| 715 |
+
|
| 716 |
+
# # Sort by average expense (highest first)
|
| 717 |
+
# recommendations.sort(key=lambda x: x.average_expense, reverse=True)
|
| 718 |
+
|
| 719 |
+
# return recommendations
|
| 720 |
+
|
| 721 |
+
# def _calculate_category_statistics(self, expenses: List[Dict], start_date: datetime, end_date: datetime) -> Dict:
|
| 722 |
+
# """Calculate statistics for each category"""
|
| 723 |
+
# category_data = defaultdict(lambda: {
|
| 724 |
+
# "total": 0,
|
| 725 |
+
# "count": 0,
|
| 726 |
+
# "months": set(),
|
| 727 |
+
# "monthly_totals": defaultdict(float)
|
| 728 |
+
# })
|
| 729 |
+
|
| 730 |
+
# for expense in expenses:
|
| 731 |
+
# category = expense.get("category", "Uncategorized")
|
| 732 |
+
# amount = expense.get("amount", 0)
|
| 733 |
+
# date = expense.get("date")
|
| 734 |
+
|
| 735 |
+
# if isinstance(date, str):
|
| 736 |
+
# date = datetime.fromisoformat(date.replace('Z', '+00:00'))
|
| 737 |
+
|
| 738 |
+
# category_data[category]["total"] += amount
|
| 739 |
+
# category_data[category]["count"] += 1
|
| 740 |
+
|
| 741 |
+
# # Track monthly totals
|
| 742 |
+
# month_key = (date.year, date.month)
|
| 743 |
+
# category_data[category]["months"].add(month_key)
|
| 744 |
+
# category_data[category]["monthly_totals"][month_key] += amount
|
| 745 |
+
|
| 746 |
+
# # Calculate averages
|
| 747 |
+
# result = {}
|
| 748 |
+
# for category, data in category_data.items():
|
| 749 |
+
# num_months = len(data["months"]) or 1
|
| 750 |
+
# avg_monthly = data["total"] / num_months
|
| 751 |
+
|
| 752 |
+
# # Calculate standard deviation for variability
|
| 753 |
+
# monthly_values = list(data["monthly_totals"].values())
|
| 754 |
+
# if len(monthly_values) > 1:
|
| 755 |
+
# mean = sum(monthly_values) / len(monthly_values)
|
| 756 |
+
# variance = sum((x - mean) ** 2 for x in monthly_values) / len(monthly_values)
|
| 757 |
+
# std_dev = math.sqrt(variance)
|
| 758 |
+
# else:
|
| 759 |
+
# std_dev = 0
|
| 760 |
+
|
| 761 |
+
# result[category] = {
|
| 762 |
+
# "average_monthly": avg_monthly,
|
| 763 |
+
# "total": data["total"],
|
| 764 |
+
# "count": data["count"],
|
| 765 |
+
# "months_analyzed": num_months,
|
| 766 |
+
# "std_dev": std_dev,
|
| 767 |
+
# "monthly_values": monthly_values
|
| 768 |
+
# }
|
| 769 |
+
|
| 770 |
+
# return result
|
| 771 |
+
|
| 772 |
+
# def _calculate_recommended_budget(self, avg_expense: float, data: Dict) -> float:
|
| 773 |
+
# """
|
| 774 |
+
# Calculate recommended budget based on average expense.
|
| 775 |
+
|
| 776 |
+
# Strategy:
|
| 777 |
+
# - Base: Average monthly expense
|
| 778 |
+
# - Add 5% buffer for variability
|
| 779 |
+
# - Round to nearest 100 for cleaner numbers
|
| 780 |
+
# """
|
| 781 |
+
# # Add 5% buffer to handle variability
|
| 782 |
+
# buffer = avg_expense * 0.05
|
| 783 |
+
|
| 784 |
+
# # If there's high variability (std_dev > 20% of mean), add more buffer
|
| 785 |
+
# if data["std_dev"] > 0:
|
| 786 |
+
# coefficient_of_variation = data["std_dev"] / avg_expense if avg_expense > 0 else 0
|
| 787 |
+
# if coefficient_of_variation > 0.2:
|
| 788 |
+
# buffer = avg_expense * 0.10 # 10% buffer for high variability
|
| 789 |
+
|
| 790 |
+
# recommended = avg_expense + buffer
|
| 791 |
+
|
| 792 |
+
# # Round to nearest 100 for cleaner budget numbers
|
| 793 |
+
# recommended = round(recommended / 100) * 100
|
| 794 |
+
|
| 795 |
+
# # Ensure minimum of 100 if there was any expense
|
| 796 |
+
# if recommended < 100 and avg_expense > 0:
|
| 797 |
+
# recommended = 100
|
| 798 |
+
|
| 799 |
+
# return recommended
|
| 800 |
+
|
| 801 |
+
# def _calculate_confidence(self, data: Dict) -> float:
|
| 802 |
+
# """
|
| 803 |
+
# Calculate confidence score (0-1) based on data quality.
|
| 804 |
+
|
| 805 |
+
# Factors:
|
| 806 |
+
# - Number of months analyzed (more = higher confidence)
|
| 807 |
+
# - Number of transactions (more = higher confidence)
|
| 808 |
+
# - Consistency of spending (lower std_dev = higher confidence)
|
| 809 |
+
# """
|
| 810 |
+
# months_score = min(data["months_analyzed"] / 6, 1.0) # Max at 6 months
|
| 811 |
+
# count_score = min(data["count"] / 10, 1.0) # Max at 10 transactions
|
| 812 |
+
|
| 813 |
+
# # Consistency score (inverse of coefficient of variation)
|
| 814 |
+
# if data["average_monthly"] > 0:
|
| 815 |
+
# cv = data["std_dev"] / data["average_monthly"]
|
| 816 |
+
# consistency_score = max(0, 1 - min(cv, 1.0)) # Lower CV = higher score
|
| 817 |
+
# else:
|
| 818 |
+
# consistency_score = 0.5
|
| 819 |
+
|
| 820 |
+
# # Weighted average
|
| 821 |
+
# confidence = (months_score * 0.4 + count_score * 0.3 + consistency_score * 0.3)
|
| 822 |
+
|
| 823 |
+
# return round(confidence, 2)
|
| 824 |
+
|
| 825 |
+
# def _generate_reason(self, category: str, avg_expense: float, recommended_budget: float) -> str:
|
| 826 |
+
# """Generate human-readable reason for the recommendation"""
|
| 827 |
+
# # Format amounts with currency symbol
|
| 828 |
+
# avg_formatted = f"Rs.{avg_expense:,.0f}"
|
| 829 |
+
# budget_formatted = f"Rs.{recommended_budget:,.0f}"
|
| 830 |
+
|
| 831 |
+
# if recommended_budget > avg_expense:
|
| 832 |
+
# buffer = recommended_budget - avg_expense
|
| 833 |
+
# buffer_pct = (buffer / avg_expense * 100) if avg_expense > 0 else 0
|
| 834 |
+
# return (
|
| 835 |
+
# f"Your average monthly {category.lower()} expense is {avg_formatted}. "
|
| 836 |
+
# f"We suggest setting your budget to {budget_formatted} for next month "
|
| 837 |
+
# f"(includes a {buffer_pct:.0f}% buffer for variability)."
|
| 838 |
+
# )
|
| 839 |
+
# else:
|
| 840 |
+
# return (
|
| 841 |
+
# f"Your average monthly {category.lower()} expense is {avg_formatted}. "
|
| 842 |
+
# f"We recommend a budget of {budget_formatted} for next month."
|
| 843 |
+
# )
|
| 844 |
+
|
| 845 |
+
# def get_category_averages(self, user_id: str, months: int = 3) -> List[CategoryExpense]:
|
| 846 |
+
# """Get average expenses by category for the past N months"""
|
| 847 |
+
# end_date = datetime.now()
|
| 848 |
+
# start_date = end_date - timedelta(days=months * 30)
|
| 849 |
+
|
| 850 |
+
# expenses = list(self.db.expenses.find({
|
| 851 |
+
# "user_id": user_id,
|
| 852 |
+
# "date": {"$gte": start_date, "$lte": end_date},
|
| 853 |
+
# "type": "expense"
|
| 854 |
+
# }))
|
| 855 |
+
|
| 856 |
+
# if not expenses:
|
| 857 |
+
# return []
|
| 858 |
+
|
| 859 |
+
# category_data = self._calculate_category_statistics(expenses, start_date, end_date)
|
| 860 |
+
|
| 861 |
+
# result = []
|
| 862 |
+
# for category, data in category_data.items():
|
| 863 |
+
# result.append(CategoryExpense(
|
| 864 |
+
# category=category,
|
| 865 |
+
# average_monthly_expense=round(data["average_monthly"], 2),
|
| 866 |
+
# total_expenses=data["count"],
|
| 867 |
+
# months_analyzed=data["months_analyzed"]
|
| 868 |
+
# ))
|
| 869 |
+
|
| 870 |
+
# result.sort(key=lambda x: x.average_monthly_expense, reverse=True)
|
| 871 |
+
# return result
|
| 872 |
+
|
| 873 |
+
# def _get_category_stats_from_budgets(
|
| 874 |
+
# self, user_id: str, month: int, year: int
|
| 875 |
+
# ) -> Dict:
|
| 876 |
+
# """
|
| 877 |
+
# Build category stats from existing budgets for this user.
|
| 878 |
+
|
| 879 |
+
# We treat each budget document (e.g. \"Office Maintenance\", \"LOGICGO\")
|
| 880 |
+
# as a spending category and derive an \"average\" from its amounts.
|
| 881 |
+
# """
|
| 882 |
+
# # createdBy is stored as ObjectId in WalletSync, while user_id is a string.
|
| 883 |
+
# # Try to cast to ObjectId; if it fails, fall back to matching the raw string.
|
| 884 |
+
# query: Dict = {"status": "OPEN"}
|
| 885 |
+
# try:
|
| 886 |
+
# query["createdBy"] = ObjectId(user_id)
|
| 887 |
+
# except Exception:
|
| 888 |
+
# query["createdBy"] = user_id
|
| 889 |
+
|
| 890 |
+
# budgets = list(self.db.budgets.find(query))
|
| 891 |
+
|
| 892 |
+
# if not budgets:
|
| 893 |
+
# return {}
|
| 894 |
+
|
| 895 |
+
# result: Dict[str, Dict] = {}
|
| 896 |
+
# for b in budgets:
|
| 897 |
+
# # Use budget \"name\" as category label
|
| 898 |
+
# category = b.get("name", "Uncategorized")
|
| 899 |
+
|
| 900 |
+
# # Derive a base amount from WalletSync fields
|
| 901 |
+
# max_amount = float(b.get("maxAmount", 0) or 0)
|
| 902 |
+
# spend_amount = float(b.get("spendAmount", 0) or 0)
|
| 903 |
+
|
| 904 |
+
# # If there is recorded spend, use that as \"average\"; otherwise maxAmount
|
| 905 |
+
# base_amount = spend_amount if spend_amount > 0 else max_amount
|
| 906 |
+
# if base_amount <= 0:
|
| 907 |
+
# continue
|
| 908 |
+
|
| 909 |
+
# if category not in result:
|
| 910 |
+
# result[category] = {
|
| 911 |
+
# "average_monthly": base_amount,
|
| 912 |
+
# "total": base_amount,
|
| 913 |
+
# "count": 1,
|
| 914 |
+
# "months_analyzed": 1,
|
| 915 |
+
# "std_dev": 0.0,
|
| 916 |
+
# "monthly_values": [base_amount],
|
| 917 |
+
# }
|
| 918 |
+
# else:
|
| 919 |
+
# # If multiple budgets per category, average them
|
| 920 |
+
# result[category]["total"] += base_amount
|
| 921 |
+
# result[category]["count"] += 1
|
| 922 |
+
# result[category]["months_analyzed"] = result[category]["count"]
|
| 923 |
+
# result[category]["average_monthly"] = (
|
| 924 |
+
# result[category]["total"] / result[category]["count"]
|
| 925 |
+
# )
|
| 926 |
+
# result[category]["monthly_values"].append(base_amount)
|
| 927 |
+
|
| 928 |
+
# return result
|
| 929 |
+
|
| 930 |
+
# def _get_ai_recommendation(self, category: str, data: Dict, avg_expense: float):
|
| 931 |
+
# """Use OpenAI to refine the budget recommendation."""
|
| 932 |
+
# if not OPENAI_API_KEY:
|
| 933 |
+
# return None
|
| 934 |
+
|
| 935 |
+
# history = ", ".join(f"{value:.0f}" for value in data["monthly_values"])
|
| 936 |
+
# summary = (
|
| 937 |
+
# f"Category: {category}\n"
|
| 938 |
+
# f"Monthly totals: [{history}]\n"
|
| 939 |
+
# f"Average spend: {avg_expense:.2f}\n"
|
| 940 |
+
# f"Std deviation: {data['std_dev']:.2f}\n"
|
| 941 |
+
# f"Months observed: {data['months_analyzed']}\n"
|
| 942 |
+
# )
|
| 943 |
+
|
| 944 |
+
# prompt = (
|
| 945 |
+
# "You are an Indian personal finance coach. "
|
| 946 |
+
# "Given the user's spending history, decide whether to increase, decrease, "
|
| 947 |
+
# "or keep the upcoming month's budget and provide a short explanation. "
|
| 948 |
+
# "Respond strictly as JSON with the following keys:\n"
|
| 949 |
+
# '{ "recommended_budget": number, "action": "increase|decrease|keep", "reason": "string" }.\n'
|
| 950 |
+
# "Use rupees for all amounts.\n\n"
|
| 951 |
+
# f"{summary}"
|
| 952 |
+
# )
|
| 953 |
+
|
| 954 |
+
# try:
|
| 955 |
+
# response = requests.post(
|
| 956 |
+
# "https://api.openai.com/v1/responses",
|
| 957 |
+
# headers={
|
| 958 |
+
# "Authorization": f"Bearer {OPENAI_API_KEY}",
|
| 959 |
+
# "Content-Type": "application/json",
|
| 960 |
+
# },
|
| 961 |
+
# json={
|
| 962 |
+
# "model": "gpt-4.1-mini",
|
| 963 |
+
# "input": prompt,
|
| 964 |
+
# "temperature": 0.1,
|
| 965 |
+
# "response_format": {"type": "json_object"},
|
| 966 |
+
# },
|
| 967 |
+
# timeout=30,
|
| 968 |
+
# )
|
| 969 |
+
# response.raise_for_status()
|
| 970 |
+
# data = response.json()
|
| 971 |
+
# content = data["output"][0]["content"][0]["text"]
|
| 972 |
+
# return json.loads(content)
|
| 973 |
+
# except Exception as exc:
|
| 974 |
+
# print(f"OpenAI recommendation error for {category}: {exc}")
|
| 975 |
+
# return None
|
| 976 |
+
|