Wanderlust Dev
fix(chatbot): resolve 7 critical issues - KB expansion, pipeline fixes, dynamic responses
8191051
Raw
History Blame Contribute Delete
7.72 kB
from typing import Optional
from .cost_calculator import CostCalculator
class BudgetOptimizer:
"""Multi-constraint budget optimizer with 3-tier plan generation and priority allocation."""
TIER_ORDER = ['budget', 'mid_range', 'luxury']
BUDGET_PRIORITY_DEFAULTS = {
'accommodation': 0.35,
'food': 0.25,
'activities': 0.20,
'transport': 0.15,
'misc': 0.05,
}
def __init__(self, destinations: list):
self.destinations = {d['id']: d for d in destinations}
def get_destination(self, dest_id: str) -> Optional[dict]:
return self.destinations.get(dest_id)
def generate_three_tier_plans(
self,
destination: dict,
days: int,
num_adults: int = 2,
num_children: int = 0,
activities: Optional[list] = None,
month: Optional[int] = None
) -> list[dict]:
"""Generate budget/mid_range/luxury plans with full cost breakdown."""
plans = []
for tier in self.TIER_ORDER:
plan = CostCalculator.calculate_trip_total(
destination=destination,
days=days,
tier=tier,
num_adults=num_adults,
num_children=num_children,
activities=activities,
month=month,
)
tier_data = destination.get('cost_tiers', {}).get(tier, {})
plan['tier_description_vi'] = tier_data.get('description_vi', '')
plan['tier_description_en'] = tier_data.get('description_en', '')
plans.append(plan)
return plans
def check_budget_feasibility(
self,
destination: dict,
total_budget: float,
currency: str,
days: int,
num_adults: int = 2,
num_children: int = 0,
month: Optional[int] = None
) -> dict:
"""Check if a given budget is feasible and suggest the best matching tier."""
budget_vnd = total_budget
if currency == 'USD':
budget_vnd = CostCalculator.convert_currency(total_budget, 'USD', 'VND')
plans = self.generate_three_tier_plans(
destination, days, num_adults, num_children, month=month
)
feasible_tiers = []
recommended_tier = None
for plan in plans:
if plan['total'] <= budget_vnd:
feasible_tiers.append(plan['tier'])
recommended_tier = plan['tier'] # last feasible = highest affordable
if not feasible_tiers:
# Budget too low even for budget tier
budget_plan = plans[0]
shortfall = budget_plan['total'] - budget_vnd
return {
'feasible': False,
'budget_vnd': budget_vnd,
'minimum_required_vnd': budget_plan['total'],
'shortfall_vnd': int(shortfall),
'suggestion': 'reduce_days_or_people',
'recommended_days': max(1, int(days * budget_vnd / budget_plan['total'])),
'plans': plans,
}
return {
'feasible': True,
'budget_vnd': budget_vnd,
'feasible_tiers': feasible_tiers,
'recommended_tier': recommended_tier,
'remaining_budget_vnd': int(budget_vnd - next(
p['total'] for p in plans if p['tier'] == recommended_tier
)),
'plans': plans,
}
def optimize_with_priorities(
self,
destination: dict,
total_budget: float,
currency: str,
days: int,
num_adults: int = 2,
num_children: int = 0,
priorities: Optional[dict] = None,
month: Optional[int] = None
) -> dict:
"""Allocate budget based on user spending priorities.
priorities: {'accommodation': 'high', 'food': 'high', 'activities': 'low', ...}
Values: 'high', 'medium', 'low'
"""
budget_vnd = total_budget
if currency == 'USD':
budget_vnd = CostCalculator.convert_currency(total_budget, 'USD', 'VND')
# Convert priority labels to weights
priority_weights = dict(self.BUDGET_PRIORITY_DEFAULTS)
if priorities:
multipliers = {'high': 1.5, 'medium': 1.0, 'low': 0.5}
for category, level in priorities.items():
if category in priority_weights:
priority_weights[category] *= multipliers.get(level, 1.0)
# Normalize weights to sum to 1.0
total_weight = sum(priority_weights.values())
if total_weight > 0:
priority_weights = {k: v / total_weight for k, v in priority_weights.items()}
# Allocate budget per category
allocation = {}
for category, weight in priority_weights.items():
allocation[category] = int(budget_vnd * weight)
# Determine tier per category based on allocation
daily_budget_accommodation = allocation['accommodation'] // max(1, days - 1)
cost_tiers = destination.get('cost_tiers', {})
# Find best matching tier for accommodation
accom_tier = 'budget'
for tier in self.TIER_ORDER:
tier_cost = cost_tiers.get(tier, {}).get('accommodation_vnd', 0)
if tier_cost <= daily_budget_accommodation:
accom_tier = tier
# Find best matching tier for food
daily_food_budget = allocation['food'] // days
total_people = num_adults + num_children
per_person_food = daily_food_budget // max(1, total_people)
per_meal_budget = per_person_food // 3
food_tier = 'budget'
for tier in self.TIER_ORDER:
tier_cost = cost_tiers.get(tier, {}).get('food_per_meal_vnd', 0)
if tier_cost <= per_meal_budget:
food_tier = tier
return {
'total_budget_vnd': int(budget_vnd),
'allocation': allocation,
'priority_weights': {k: round(v, 3) for k, v in priority_weights.items()},
'recommended_tiers': {
'accommodation': accom_tier,
'food': food_tier,
},
'daily_breakdown': {
'accommodation_per_night': daily_budget_accommodation,
'food_per_day': daily_food_budget,
'activities_per_day': allocation['activities'] // days,
'transport_per_day': allocation['transport'] // days,
},
'per_person_per_day': int(budget_vnd // max(1, total_people) // days),
}
def compare_destinations_cost(
self,
dest_ids: list[str],
days: int,
num_adults: int = 2,
num_children: int = 0,
tier: str = 'mid_range',
month: Optional[int] = None
) -> list[dict]:
"""Compare costs across multiple destinations."""
results = []
for dest_id in dest_ids:
dest = self.destinations.get(dest_id)
if not dest:
continue
cost = CostCalculator.calculate_trip_total(
destination=dest,
days=days,
tier=tier,
num_adults=num_adults,
num_children=num_children,
month=month,
)
cost['destination_id'] = dest_id
cost['destination_name'] = dest.get('name', '')
cost['destination_name_en'] = dest.get('name_en', '')
cost['best_months'] = dest.get('best_months', [])
cost['tags'] = dest.get('tags', [])
results.append(cost)
results.sort(key=lambda x: x['total'])
return results