Commit
·
f1019bd
1
Parent(s):
4a08c54
Fix budget _id check logic and remove excessive debug logs
Browse files- app/smart_recommendation.py +66 -96
app/smart_recommendation.py
CHANGED
|
@@ -44,7 +44,7 @@ class SmartBudgetRecommender:
|
|
| 44 |
# This ensures we only show recommendations for budgets the user actually created
|
| 45 |
if not category_data:
|
| 46 |
print(f"No budgets found for user_id: {user_id}, returning empty recommendations")
|
| 47 |
-
|
| 48 |
|
| 49 |
recommendations: List[BudgetRecommendation] = []
|
| 50 |
|
|
@@ -84,7 +84,7 @@ class SmartBudgetRecommender:
|
|
| 84 |
elif std_dev > avg_expense * 0.05:
|
| 85 |
recommended_budget = avg_expense * 1.20
|
| 86 |
action = "increase"
|
| 87 |
-
|
| 88 |
recommended_budget = avg_expense * 1.05
|
| 89 |
action = "increase"
|
| 90 |
|
|
@@ -125,8 +125,6 @@ class SmartBudgetRecommender:
|
|
| 125 |
Returns:
|
| 126 |
True if user has previous data for this category, False otherwise
|
| 127 |
"""
|
| 128 |
-
print(f"🔍 check_user_has_category_data: Checking for user_id={user_id}, category_id={category_id}")
|
| 129 |
-
|
| 130 |
# FIRST: Check if category_id is actually a budget _id
|
| 131 |
# If so, find the budget and check if it belongs to the user and has categories
|
| 132 |
try:
|
|
@@ -136,14 +134,15 @@ class SmartBudgetRecommender:
|
|
| 136 |
budget_by_id = self.db.budgets.find_one({"_id": budget_id_objid})
|
| 137 |
if budget_by_id:
|
| 138 |
budget_created_by = budget_by_id.get("createdBy")
|
| 139 |
-
# Check if this budget belongs to the user
|
| 140 |
-
budget_user_match =
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
|
|
|
|
|
|
| 144 |
|
| 145 |
if budget_user_match:
|
| 146 |
-
print(f"✅ category_id {category_id} is a budget _id for user {user_id}")
|
| 147 |
# Extract all category IDs from this budget's headCategories
|
| 148 |
head_categories = budget_by_id.get("headCategories", [])
|
| 149 |
category_ids_in_budget = []
|
|
@@ -161,22 +160,13 @@ class SmartBudgetRecommender:
|
|
| 161 |
if nc_id:
|
| 162 |
category_ids_in_budget.append(str(nc_id))
|
| 163 |
|
| 164 |
-
print(f"🔍 Found {len(category_ids_in_budget)} category IDs in budget {category_id}")
|
| 165 |
# If budget has categories, consider it as having previous data
|
| 166 |
if category_ids_in_budget:
|
| 167 |
-
print(f"✅ Budget {category_id} contains categories: {category_ids_in_budget[:5]}...")
|
| 168 |
-
print(f" Budget name: {budget_by_id.get('name', 'Unknown')}, maxAmount: {budget_by_id.get('maxAmount', 0)}")
|
| 169 |
return True
|
| 170 |
-
else:
|
| 171 |
-
print(f"⚠️ Budget {category_id} exists but has no categories")
|
| 172 |
-
else:
|
| 173 |
-
print(f"⚠️ Budget {category_id} exists but belongs to different user")
|
| 174 |
except (ValueError, TypeError):
|
| 175 |
pass # category_id is not a valid ObjectId, continue with normal check
|
| 176 |
except Exception as e:
|
| 177 |
-
|
| 178 |
-
import traceback
|
| 179 |
-
traceback.print_exc()
|
| 180 |
|
| 181 |
# Build comprehensive user query
|
| 182 |
user_conditions = []
|
|
@@ -222,79 +212,60 @@ class SmartBudgetRecommender:
|
|
| 222 |
{"headCategories.categories.category": category_id},
|
| 223 |
]
|
| 224 |
|
| 225 |
-
#
|
| 226 |
-
# Budgets have structure: headCategories[].categories[].category (ObjectId)
|
| 227 |
-
# User ID is stored in createdBy field
|
| 228 |
-
print(f"🔍 Checking nested structure: headCategories[].categories[].category for category_id {category_id}")
|
| 229 |
try:
|
| 230 |
try:
|
| 231 |
category_objid = ObjectId(category_id)
|
| 232 |
except (ValueError, TypeError):
|
| 233 |
category_objid = category_id
|
| 234 |
|
| 235 |
-
# Try multiple nested query patterns
|
| 236 |
-
nested_queries = [
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
{"headCategories.headCategory": category_id},
|
| 265 |
-
]
|
| 266 |
-
}
|
| 267 |
-
]
|
| 268 |
-
})
|
| 269 |
|
| 270 |
-
|
| 271 |
-
for i, nested_query in enumerate(nested_queries, 1):
|
| 272 |
try:
|
| 273 |
-
|
| 274 |
-
if nested_count > 0:
|
| 275 |
-
print(f"✅ Found {nested_count} budget(s) with nested category structure for user {user_id} with category_id {category_id}")
|
| 276 |
-
print(f" Nested query pattern {i} matched: headCategories[].categories[].category = {category_id}")
|
| 277 |
return True
|
| 278 |
-
except Exception
|
| 279 |
-
print(f"⚠️ Error with nested query pattern {i}: {e}")
|
| 280 |
continue
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
except Exception as e:
|
| 284 |
-
print(f"⚠️ Error checking nested categories: {e}")
|
| 285 |
-
import traceback
|
| 286 |
-
traceback.print_exc()
|
| 287 |
|
| 288 |
-
#
|
| 289 |
-
print(f"🔍 Checking direct category fields for category_id {category_id}")
|
| 290 |
for user_cond in user_conditions:
|
| 291 |
for cat_cond in category_conditions:
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
return True
|
| 298 |
|
| 299 |
# THIRD: Also try a more comprehensive query using $and and $or
|
| 300 |
try:
|
|
@@ -466,7 +437,6 @@ class SmartBudgetRecommender:
|
|
| 466 |
import traceback
|
| 467 |
traceback.print_exc()
|
| 468 |
|
| 469 |
-
print(f"❌ No previous data found for user {user_id} with category_id {category_id}")
|
| 470 |
return False
|
| 471 |
|
| 472 |
def get_recommendation_for_category(self, user_id: str, category_id: str, month: int, year: int, budget_amount: Optional[float] = None) -> List[BudgetRecommendation]:
|
|
@@ -879,7 +849,7 @@ class SmartBudgetRecommender:
|
|
| 879 |
|
| 880 |
if isinstance(date, str):
|
| 881 |
try:
|
| 882 |
-
|
| 883 |
except (ValueError, AttributeError):
|
| 884 |
continue
|
| 885 |
elif not isinstance(date, datetime):
|
|
@@ -1163,7 +1133,7 @@ class SmartBudgetRecommender:
|
|
| 1163 |
print(f"Final fallback search failed: {e}")
|
| 1164 |
|
| 1165 |
return str(category_id) if category_id else "Uncategorized"
|
| 1166 |
-
|
| 1167 |
def _get_category_stats_from_budgets(
|
| 1168 |
self, user_id: str, month: int, year: int
|
| 1169 |
) -> Dict:
|
|
@@ -1372,14 +1342,14 @@ class SmartBudgetRecommender:
|
|
| 1372 |
result[result_key] = {
|
| 1373 |
"category_name": category_name,
|
| 1374 |
"category_id": str(category_id) if category_id else None,
|
| 1375 |
-
|
| 1376 |
-
|
| 1377 |
-
|
| 1378 |
-
|
| 1379 |
-
|
| 1380 |
-
|
| 1381 |
-
|
| 1382 |
-
|
| 1383 |
result[result_key]["total"] += base_amount
|
| 1384 |
result[result_key]["count"] += 1
|
| 1385 |
result[result_key]["months_analyzed"] = result[result_key]["count"]
|
|
@@ -1404,14 +1374,14 @@ class SmartBudgetRecommender:
|
|
| 1404 |
if not OPENAI_API_KEY:
|
| 1405 |
print(f"⚠️ OpenAI API key not found in environment variables for category: {category}")
|
| 1406 |
return None
|
| 1407 |
-
|
| 1408 |
print(f"🔄 Calling OpenAI API for category: {category}...")
|
| 1409 |
|
| 1410 |
# Handle empty monthly_values
|
| 1411 |
if not data.get("monthly_values") or len(data["monthly_values"]) == 0:
|
| 1412 |
history = f"{avg_expense:.0f}"
|
| 1413 |
else:
|
| 1414 |
-
|
| 1415 |
|
| 1416 |
# Build comprehensive data summary for OpenAI to analyze
|
| 1417 |
monthly_values = data.get("monthly_values", [])
|
|
@@ -1450,8 +1420,8 @@ class SmartBudgetRecommender:
|
|
| 1450 |
|
| 1451 |
if is_new_budget:
|
| 1452 |
# This is a new budget - no historical data
|
| 1453 |
-
|
| 1454 |
-
|
| 1455 |
f"⚠️ IMPORTANT: This is a NEW BUDGET with NO historical spending data.\n"
|
| 1456 |
f"The user has provided a budget amount of {avg_expense:,.2f} for this category.\n"
|
| 1457 |
f"This is the ONLY data point available - there is NO spending history to analyze.\n\n"
|
|
@@ -1475,7 +1445,7 @@ class SmartBudgetRecommender:
|
|
| 1475 |
f"Number of months analyzed: {data['months_analyzed']}\n"
|
| 1476 |
f"Total spending: {data.get('total', avg_expense * data['months_analyzed']):,.2f}\n"
|
| 1477 |
f"Trend Analysis: {trend_analysis}\n"
|
| 1478 |
-
|
| 1479 |
|
| 1480 |
prompt = (
|
| 1481 |
"You are an expert global personal finance coach with deep knowledge of:\n"
|
|
|
|
| 44 |
# This ensures we only show recommendations for budgets the user actually created
|
| 45 |
if not category_data:
|
| 46 |
print(f"No budgets found for user_id: {user_id}, returning empty recommendations")
|
| 47 |
+
return []
|
| 48 |
|
| 49 |
recommendations: List[BudgetRecommendation] = []
|
| 50 |
|
|
|
|
| 84 |
elif std_dev > avg_expense * 0.05:
|
| 85 |
recommended_budget = avg_expense * 1.20
|
| 86 |
action = "increase"
|
| 87 |
+
else:
|
| 88 |
recommended_budget = avg_expense * 1.05
|
| 89 |
action = "increase"
|
| 90 |
|
|
|
|
| 125 |
Returns:
|
| 126 |
True if user has previous data for this category, False otherwise
|
| 127 |
"""
|
|
|
|
|
|
|
| 128 |
# FIRST: Check if category_id is actually a budget _id
|
| 129 |
# If so, find the budget and check if it belongs to the user and has categories
|
| 130 |
try:
|
|
|
|
| 134 |
budget_by_id = self.db.budgets.find_one({"_id": budget_id_objid})
|
| 135 |
if budget_by_id:
|
| 136 |
budget_created_by = budget_by_id.get("createdBy")
|
| 137 |
+
# Check if this budget belongs to the user (handle both ObjectId and string comparisons)
|
| 138 |
+
budget_user_match = False
|
| 139 |
+
if budget_created_by:
|
| 140 |
+
if isinstance(budget_created_by, ObjectId):
|
| 141 |
+
budget_user_match = (str(budget_created_by) == str(user_id) or budget_created_by == ObjectId(user_id))
|
| 142 |
+
else:
|
| 143 |
+
budget_user_match = (str(budget_created_by) == str(user_id))
|
| 144 |
|
| 145 |
if budget_user_match:
|
|
|
|
| 146 |
# Extract all category IDs from this budget's headCategories
|
| 147 |
head_categories = budget_by_id.get("headCategories", [])
|
| 148 |
category_ids_in_budget = []
|
|
|
|
| 160 |
if nc_id:
|
| 161 |
category_ids_in_budget.append(str(nc_id))
|
| 162 |
|
|
|
|
| 163 |
# If budget has categories, consider it as having previous data
|
| 164 |
if category_ids_in_budget:
|
|
|
|
|
|
|
| 165 |
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
except (ValueError, TypeError):
|
| 167 |
pass # category_id is not a valid ObjectId, continue with normal check
|
| 168 |
except Exception as e:
|
| 169 |
+
pass # Silently continue if budget check fails
|
|
|
|
|
|
|
| 170 |
|
| 171 |
# Build comprehensive user query
|
| 172 |
user_conditions = []
|
|
|
|
| 212 |
{"headCategories.categories.category": category_id},
|
| 213 |
]
|
| 214 |
|
| 215 |
+
# SECOND: Check nested structure (headCategories[].categories[].category) - most common case
|
|
|
|
|
|
|
|
|
|
| 216 |
try:
|
| 217 |
try:
|
| 218 |
category_objid = ObjectId(category_id)
|
| 219 |
except (ValueError, TypeError):
|
| 220 |
category_objid = category_id
|
| 221 |
|
| 222 |
+
# Try multiple nested query patterns
|
| 223 |
+
nested_queries = [
|
| 224 |
+
{
|
| 225 |
+
"$and": [
|
| 226 |
+
{"$or": user_conditions},
|
| 227 |
+
{
|
| 228 |
+
"$or": [
|
| 229 |
+
{"headCategories": {"$elemMatch": {"categories": {"$elemMatch": {"category": category_objid}}}}},
|
| 230 |
+
{"headCategories": {"$elemMatch": {"categories": {"$elemMatch": {"category": category_id}}}}},
|
| 231 |
+
{"headCategories": {"$elemMatch": {"headCategory": category_objid}}},
|
| 232 |
+
{"headCategories": {"$elemMatch": {"headCategory": category_id}}},
|
| 233 |
+
]
|
| 234 |
+
}
|
| 235 |
+
]
|
| 236 |
+
},
|
| 237 |
+
{
|
| 238 |
+
"$and": [
|
| 239 |
+
{"$or": user_conditions},
|
| 240 |
+
{
|
| 241 |
+
"$or": [
|
| 242 |
+
{"headCategories.categories.category": category_objid},
|
| 243 |
+
{"headCategories.categories.category": category_id},
|
| 244 |
+
{"headCategories.headCategory": category_objid},
|
| 245 |
+
{"headCategories.headCategory": category_id},
|
| 246 |
+
]
|
| 247 |
+
}
|
| 248 |
+
]
|
| 249 |
+
}
|
| 250 |
+
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
|
| 252 |
+
for nested_query in nested_queries:
|
|
|
|
| 253 |
try:
|
| 254 |
+
if self.db.budgets.count_documents(nested_query) > 0:
|
|
|
|
|
|
|
|
|
|
| 255 |
return True
|
| 256 |
+
except Exception:
|
|
|
|
| 257 |
continue
|
| 258 |
+
except Exception:
|
| 259 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
|
| 261 |
+
# THIRD: Check direct category fields
|
|
|
|
| 262 |
for user_cond in user_conditions:
|
| 263 |
for cat_cond in category_conditions:
|
| 264 |
+
try:
|
| 265 |
+
if self.db.budgets.count_documents({**user_cond, **cat_cond}) > 0:
|
| 266 |
+
return True
|
| 267 |
+
except Exception:
|
| 268 |
+
continue
|
|
|
|
| 269 |
|
| 270 |
# THIRD: Also try a more comprehensive query using $and and $or
|
| 271 |
try:
|
|
|
|
| 437 |
import traceback
|
| 438 |
traceback.print_exc()
|
| 439 |
|
|
|
|
| 440 |
return False
|
| 441 |
|
| 442 |
def get_recommendation_for_category(self, user_id: str, category_id: str, month: int, year: int, budget_amount: Optional[float] = None) -> List[BudgetRecommendation]:
|
|
|
|
| 849 |
|
| 850 |
if isinstance(date, str):
|
| 851 |
try:
|
| 852 |
+
date = datetime.fromisoformat(date.replace('Z', '+00:00'))
|
| 853 |
except (ValueError, AttributeError):
|
| 854 |
continue
|
| 855 |
elif not isinstance(date, datetime):
|
|
|
|
| 1133 |
print(f"Final fallback search failed: {e}")
|
| 1134 |
|
| 1135 |
return str(category_id) if category_id else "Uncategorized"
|
| 1136 |
+
|
| 1137 |
def _get_category_stats_from_budgets(
|
| 1138 |
self, user_id: str, month: int, year: int
|
| 1139 |
) -> Dict:
|
|
|
|
| 1342 |
result[result_key] = {
|
| 1343 |
"category_name": category_name,
|
| 1344 |
"category_id": str(category_id) if category_id else None,
|
| 1345 |
+
"average_monthly": base_amount,
|
| 1346 |
+
"total": base_amount,
|
| 1347 |
+
"count": 1,
|
| 1348 |
+
"months_analyzed": 1,
|
| 1349 |
+
"std_dev": 0.0,
|
| 1350 |
+
"monthly_values": [base_amount],
|
| 1351 |
+
}
|
| 1352 |
+
else:
|
| 1353 |
result[result_key]["total"] += base_amount
|
| 1354 |
result[result_key]["count"] += 1
|
| 1355 |
result[result_key]["months_analyzed"] = result[result_key]["count"]
|
|
|
|
| 1374 |
if not OPENAI_API_KEY:
|
| 1375 |
print(f"⚠️ OpenAI API key not found in environment variables for category: {category}")
|
| 1376 |
return None
|
| 1377 |
+
|
| 1378 |
print(f"🔄 Calling OpenAI API for category: {category}...")
|
| 1379 |
|
| 1380 |
# Handle empty monthly_values
|
| 1381 |
if not data.get("monthly_values") or len(data["monthly_values"]) == 0:
|
| 1382 |
history = f"{avg_expense:.0f}"
|
| 1383 |
else:
|
| 1384 |
+
history = ", ".join(f"{value:.0f}" for value in data["monthly_values"])
|
| 1385 |
|
| 1386 |
# Build comprehensive data summary for OpenAI to analyze
|
| 1387 |
monthly_values = data.get("monthly_values", [])
|
|
|
|
| 1420 |
|
| 1421 |
if is_new_budget:
|
| 1422 |
# This is a new budget - no historical data
|
| 1423 |
+
summary = (
|
| 1424 |
+
f"Category: {category}\n"
|
| 1425 |
f"⚠️ IMPORTANT: This is a NEW BUDGET with NO historical spending data.\n"
|
| 1426 |
f"The user has provided a budget amount of {avg_expense:,.2f} for this category.\n"
|
| 1427 |
f"This is the ONLY data point available - there is NO spending history to analyze.\n\n"
|
|
|
|
| 1445 |
f"Number of months analyzed: {data['months_analyzed']}\n"
|
| 1446 |
f"Total spending: {data.get('total', avg_expense * data['months_analyzed']):,.2f}\n"
|
| 1447 |
f"Trend Analysis: {trend_analysis}\n"
|
| 1448 |
+
)
|
| 1449 |
|
| 1450 |
prompt = (
|
| 1451 |
"You are an expert global personal finance coach with deep knowledge of:\n"
|