Update app/smart_recommendation.py
Browse files- app/smart_recommendation.py +78 -19
app/smart_recommendation.py
CHANGED
|
@@ -69,17 +69,22 @@ class SmartBudgetRecommender:
|
|
| 69 |
avg_expense = data["average_monthly"]
|
| 70 |
confidence = self._calculate_confidence(data)
|
| 71 |
|
| 72 |
-
#
|
| 73 |
ai_result = self._get_ai_recommendation(category, data, avg_expense)
|
| 74 |
-
if ai_result:
|
| 75 |
recommended_budget = ai_result.get("recommended_budget")
|
| 76 |
-
reason = ai_result.get("reason")
|
| 77 |
action = ai_result.get("action")
|
|
|
|
| 78 |
else:
|
| 79 |
-
#
|
| 80 |
recommended_budget = self._calculate_recommended_budget(avg_expense, data)
|
| 81 |
reason = self._generate_reason(category, avg_expense, recommended_budget)
|
| 82 |
action = None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
recommendations.append(BudgetRecommendation(
|
| 85 |
category=category,
|
|
@@ -266,32 +271,85 @@ class SmartBudgetRecommender:
|
|
| 266 |
We treat each budget document (e.g. \"Office Maintenance\", \"LOGICGO\")
|
| 267 |
as a spending category and derive an \"average\" from its amounts.
|
| 268 |
"""
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
query
|
|
|
|
| 272 |
try:
|
| 273 |
-
|
|
|
|
|
|
|
|
|
|
| 274 |
except Exception:
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
|
| 279 |
if not budgets:
|
|
|
|
| 280 |
return {}
|
| 281 |
|
|
|
|
|
|
|
| 282 |
result: Dict[str, Dict] = {}
|
| 283 |
for b in budgets:
|
| 284 |
-
# Use budget
|
| 285 |
category = b.get("name", "Uncategorized")
|
|
|
|
|
|
|
|
|
|
| 286 |
|
| 287 |
# Derive a base amount from WalletSync fields
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
if category not in result:
|
| 297 |
result[category] = {
|
|
@@ -312,6 +370,7 @@ class SmartBudgetRecommender:
|
|
| 312 |
)
|
| 313 |
result[category]["monthly_values"].append(base_amount)
|
| 314 |
|
|
|
|
| 315 |
return result
|
| 316 |
|
| 317 |
def _get_ai_recommendation(self, category: str, data: Dict, avg_expense: float):
|
|
|
|
| 69 |
avg_expense = data["average_monthly"]
|
| 70 |
confidence = self._calculate_confidence(data)
|
| 71 |
|
| 72 |
+
# Always try OpenAI first (primary source of recommendation)
|
| 73 |
ai_result = self._get_ai_recommendation(category, data, avg_expense)
|
| 74 |
+
if ai_result and ai_result.get("recommended_budget"):
|
| 75 |
recommended_budget = ai_result.get("recommended_budget")
|
| 76 |
+
reason = ai_result.get("reason", f"AI recommendation for {category}")
|
| 77 |
action = ai_result.get("action")
|
| 78 |
+
print(f"OpenAI recommendation for {category}: {recommended_budget}")
|
| 79 |
else:
|
| 80 |
+
# Fallback to rule-based recommendation if OpenAI fails
|
| 81 |
recommended_budget = self._calculate_recommended_budget(avg_expense, data)
|
| 82 |
reason = self._generate_reason(category, avg_expense, recommended_budget)
|
| 83 |
action = None
|
| 84 |
+
if not ai_result:
|
| 85 |
+
print(f"OpenAI unavailable, using rule-based for {category}: {recommended_budget}")
|
| 86 |
+
else:
|
| 87 |
+
print(f"OpenAI returned invalid data, using rule-based for {category}: {recommended_budget}")
|
| 88 |
|
| 89 |
recommendations.append(BudgetRecommendation(
|
| 90 |
category=category,
|
|
|
|
| 271 |
We treat each budget document (e.g. \"Office Maintenance\", \"LOGICGO\")
|
| 272 |
as a spending category and derive an \"average\" from its amounts.
|
| 273 |
"""
|
| 274 |
+
budgets = []
|
| 275 |
+
|
| 276 |
+
# Try multiple query patterns to find budgets
|
| 277 |
+
# Pattern 1: Try with ObjectId (most common in WalletSync)
|
| 278 |
try:
|
| 279 |
+
query_objid = {"status": "OPEN", "createdBy": ObjectId(user_id)}
|
| 280 |
+
budgets_objid = list(self.db.budgets.find(query_objid))
|
| 281 |
+
if budgets_objid:
|
| 282 |
+
budgets.extend(budgets_objid)
|
| 283 |
except Exception:
|
| 284 |
+
pass
|
| 285 |
+
|
| 286 |
+
# Pattern 2: Try with string user_id
|
| 287 |
+
try:
|
| 288 |
+
query_str = {"status": "OPEN", "createdBy": user_id}
|
| 289 |
+
budgets_str = list(self.db.budgets.find(query_str))
|
| 290 |
+
if budgets_str:
|
| 291 |
+
budgets.extend(budgets_str)
|
| 292 |
+
except Exception:
|
| 293 |
+
pass
|
| 294 |
+
|
| 295 |
+
# Pattern 3: Try with user_id field (alternative field name)
|
| 296 |
+
try:
|
| 297 |
+
query_userid = {"status": "OPEN", "user_id": user_id}
|
| 298 |
+
budgets_userid = list(self.db.budgets.find(query_userid))
|
| 299 |
+
if budgets_userid:
|
| 300 |
+
budgets.extend(budgets_userid)
|
| 301 |
+
except Exception:
|
| 302 |
+
pass
|
| 303 |
+
|
| 304 |
+
# Pattern 4: Try ObjectId with user_id field
|
| 305 |
+
try:
|
| 306 |
+
query_objid_userid = {"status": "OPEN", "user_id": ObjectId(user_id)}
|
| 307 |
+
budgets_objid_userid = list(self.db.budgets.find(query_objid_userid))
|
| 308 |
+
if budgets_objid_userid:
|
| 309 |
+
budgets.extend(budgets_objid_userid)
|
| 310 |
+
except Exception:
|
| 311 |
+
pass
|
| 312 |
+
|
| 313 |
+
# Remove duplicates based on _id
|
| 314 |
+
seen_ids = set()
|
| 315 |
+
unique_budgets = []
|
| 316 |
+
for b in budgets:
|
| 317 |
+
budget_id = str(b.get("_id", ""))
|
| 318 |
+
if budget_id not in seen_ids:
|
| 319 |
+
seen_ids.add(budget_id)
|
| 320 |
+
unique_budgets.append(b)
|
| 321 |
+
|
| 322 |
+
budgets = unique_budgets
|
| 323 |
|
| 324 |
if not budgets:
|
| 325 |
+
print(f"No budgets found for user_id: {user_id}")
|
| 326 |
return {}
|
| 327 |
|
| 328 |
+
print(f"Found {len(budgets)} budgets for user_id: {user_id}")
|
| 329 |
+
|
| 330 |
result: Dict[str, Dict] = {}
|
| 331 |
for b in budgets:
|
| 332 |
+
# Use budget "name" as category label
|
| 333 |
category = b.get("name", "Uncategorized")
|
| 334 |
+
if not category or category == "Uncategorized":
|
| 335 |
+
# Try alternative field names
|
| 336 |
+
category = b.get("category") or b.get("title") or "Uncategorized"
|
| 337 |
|
| 338 |
# Derive a base amount from WalletSync fields
|
| 339 |
+
# Try multiple field name variations
|
| 340 |
+
max_amount = float(b.get("maxAmount", 0) or b.get("max_amount", 0) or b.get("amount", 0) or 0)
|
| 341 |
+
spend_amount = float(b.get("spendAmount", 0) or b.get("spend_amount", 0) or b.get("spent", 0) or 0)
|
| 342 |
+
budget_amount = float(b.get("budget", 0) or b.get("budgetAmount", 0) or 0)
|
| 343 |
+
|
| 344 |
+
# Priority: spendAmount > maxAmount > budgetAmount > budget
|
| 345 |
+
if spend_amount > 0:
|
| 346 |
+
base_amount = spend_amount
|
| 347 |
+
elif max_amount > 0:
|
| 348 |
+
base_amount = max_amount
|
| 349 |
+
elif budget_amount > 0:
|
| 350 |
+
base_amount = budget_amount
|
| 351 |
+
else:
|
| 352 |
+
continue # Skip if no valid amount found
|
| 353 |
|
| 354 |
if category not in result:
|
| 355 |
result[category] = {
|
|
|
|
| 370 |
)
|
| 371 |
result[category]["monthly_values"].append(base_amount)
|
| 372 |
|
| 373 |
+
print(f"Processed {len(result)} budget categories for recommendations")
|
| 374 |
return result
|
| 375 |
|
| 376 |
def _get_ai_recommendation(self, category: str, data: Dict, avg_expense: float):
|