import logging from chatbot.agents.states.state import AgentState import numpy as np from scipy.optimize import minimize # --- Cấu hình logging --- logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def optimize_portions_scipy(state: AgentState): logger.info("---NODE: SCIPY OPTIMIZER (FINAL VERSION)---") profile = state.get("user_profile", {}) menu = state.get("selected_structure", []) if not menu: print("⚠️ Menu rỗng, bỏ qua tối ưu hóa.") return {"final_menu": [], "user_profile": profile} # --- BƯỚC 1: XÁC ĐỊNH MỤC TIÊU TỐI ƯU HÓA (CRITICAL STEP) --- daily_targets = np.array([ float(profile.get("targetcalories", 1314)), float(profile.get("protein", 98)), float(profile.get("totalfat", 43)), float(profile.get("carbohydrate", 131)) ]) meal_ratios = {"sáng": 0.25, "trưa": 0.40, "tối": 0.35} generated_meals = set(d.get("assigned_meal", "").lower() for d in menu) # Tính Target Thực Tế (Optimization Target) # Ví dụ: Nếu chỉ có bữa Trưa -> Target = Daily * 0.4 # Nếu có Trưa + Tối -> Target = Daily * (0.4 + 0.35) active_target = np.zeros(4) active_ratios_sum = 0 for m in ["sáng", "trưa", "tối"]: if m in generated_meals: active_target += daily_targets * meal_ratios[m] active_ratios_sum += meal_ratios[m] # Fallback: Nếu không xác định được bữa nào, dùng Target Ngày if np.sum(active_target) == 0: active_target = daily_targets logger.info(f" 🎯 Mục tiêu tối ưu hóa (Active Target): {active_target.astype(int)}") # --- BƯỚC 2: THIẾT LẬP MA TRẬN & BOUNDS --- matrix = [] bounds = [] meal_indices = {"sáng": [], "trưa": [], "tối": []} # Tính target riêng từng bữa để dùng cho Distribution Loss target_kcal_per_meal = { k: daily_targets[0] * v for k, v in meal_ratios.items() } for i, dish in enumerate(menu): nutrients = [ float(dish.get("kcal", 0)), float(dish.get("protein", 0)), float(dish.get("totalfat", 0)), float(dish.get("carbs", 0)) ] matrix.append(nutrients) # Logic Bounds Thông minh current_kcal = nutrients[0] t_meal_name = dish.get("assigned_meal", "").lower() t_meal_target = target_kcal_per_meal.get(t_meal_name, 500) # Nếu 1 món chiếm > 90% Kcal mục tiêu của bữa đó -> Phải cho giảm sâu if current_kcal > (t_meal_target * 0.9): bounds.append((0.3, 1.0)) elif "solver_bounds" in dish: bounds.append(dish["solver_bounds"]) else: bounds.append((0.5, 1.5)) if "sáng" in t_meal_name: meal_indices["sáng"].append(i) elif "trưa" in t_meal_name: meal_indices["trưa"].append(i) elif "tối" in t_meal_name: meal_indices["tối"].append(i) matrix = np.array(matrix).T n_dishes = len(menu) initial_guess = np.ones(n_dishes) # --- BƯỚC 3: ADAPTIVE WEIGHTS --- optimized_portions = initial_guess try: # Tính dinh dưỡng tối đa có thể đạt được (nếu ăn x2.5 suất tất cả) max_possible = matrix.dot(np.full(n_dishes, 2.5)) # Trọng số mặc định: [Kcal, P, L, C] adaptive_weights = np.array([3.0, 2.0, 1.0, 1.0]) nutri_names = ["Kcal", "Protein", "Lipid", "Carb"] for i in range(1, 4): # Check P, L, C # Nếu Max khả thi vẫn < 70% Target -> Menu này quá thiếu chất đó # -> Giảm trọng số về gần 0 để Solver không cố gắng cứu nó if max_possible[i] < (active_target[i] * 0.7): logger.info(f" ⚠️ Thiếu hụt {nutri_names[i]} nghiêm trọng (Max {int(max_possible[i])} < Target {int(active_target[i])}). Bỏ qua tối ưu chỉ số này.") adaptive_weights[i] = 0.01 # --- BƯỚC 4: LOSS FUNCTION --- def objective(portions): # A. Loss Macro (So với Active Target) current_macros = matrix.dot(portions) # Dùng adaptive_weights để tránh bẫy diff = (current_macros - active_target) / (active_target + 1e-5) loss_macro = np.sum(adaptive_weights * (diff ** 2)) # B. Loss Phân bổ Bữa ăn (Chỉ cần thiết nếu sinh nhiều bữa) loss_dist = 0 if active_ratios_sum > 0.5: # Chỉ tính nếu sinh > 1 bữa kcal_row = matrix[0] for m_type, indices in meal_indices.items(): if not indices: continue current_meal_kcal = np.sum(kcal_row[indices] * portions[indices]) target_meal = target_kcal_per_meal.get(m_type, 0) d = (current_meal_kcal - target_meal) / (target_meal + 1e-5) loss_dist += (d ** 2) return 3 * loss_macro + loss_dist # 5. Run Optimization logger.info("Đang tối ưu hóa phần suất món ăn...") res = minimize(objective, initial_guess, method='SLSQP', bounds=bounds) if res.success: optimized_portions = res.x else: logger.warning(f"⚠️ Solver không hội tụ: {res.message}. Dùng portions mặc định.") except Exception as e: logger.error(f"🔥 LỖI CRITICAL KHI CHẠY SOLVER: {e}") optimized_portions = np.ones(n_dishes) # 6. Apply Results final_menu = [] total_stats = np.zeros(4) achieved_meal_kcal = {"sáng": 0, "trưa": 0, "tối": 0} for i, dish in enumerate(menu): ratio = optimized_portions[i] final_dish = dish.copy() final_dish["portion_scale"] = float(round(ratio, 2)) final_dish["final_kcal"] = int(dish.get("kcal", 0) * ratio) final_dish["final_protein"] = int(dish.get("protein", 0) * ratio) final_dish["final_totalfat"] = int(dish.get("totalfat", 0) * ratio) final_dish["final_carbs"] = int(dish.get("carbs", 0) * ratio) logger.info(f" - {dish['name']} ({dish['assigned_meal']}): x{final_dish['portion_scale']} suất -> {final_dish['final_kcal']}kcal, {final_dish['final_protein']}g Protein, {final_dish['final_totalfat']}g Total Fat, {final_dish['final_carbs']}g Carbs") final_menu.append(final_dish) total_stats += np.array([ final_dish["final_kcal"], final_dish["final_protein"], final_dish["final_totalfat"], final_dish["final_carbs"] ]) m_type = dish.get("assigned_meal", "").lower() if "sáng" in m_type: achieved_meal_kcal["sáng"] += final_dish["final_kcal"] elif "trưa" in m_type: achieved_meal_kcal["trưa"] += final_dish["final_kcal"] elif "tối" in m_type: achieved_meal_kcal["tối"] += final_dish["final_kcal"] # --- BƯỚC 7: BÁO CÁO KẾT QUẢ --- logger.info("\n 📊 BÁO CÁO TỐI ƯU HÓA CHI TIẾT:") headers = ["Chỉ số", "Mục tiêu (Bữa)", "Kết quả", "Độ lệch"] row_format = " | {:<12} | {:<15} | {:<15} | {:<15} |" logger.info(" " + "-"*65) logger.info(row_format.format(*headers)) logger.info(" " + "-"*65) labels = ["Năng lượng", "Protein", "TotalFat", "Carb"] units = ["Kcal", "g", "g", "g"] for i in range(4): t_val = int(active_target[i]) # So sánh với Active Target r_val = int(total_stats[i]) diff = r_val - t_val diff_str = f"{diff:+d} {units[i]}" status = "" percent_diff = abs(diff) / (t_val + 1e-5) # Nếu weight bị giảm về 0.01 thì không cảnh báo lỗi nữa (vì đã chấp nhận bỏ qua) if percent_diff > 0.15 and adaptive_weights[i] > 0.1: status = "⚠️" else: status = "✅" logger.info(row_format.format( labels[i], f"{t_val} {units[i]}", f"{r_val} {units[i]}", f"{diff_str} {status}" )) logger.info(" " + "-"*65) logger.info("\n ⚖️ PHÂN BỔ TỪNG BỮA (Kcal):") for meal in ["sáng", "trưa", "tối"]: if meal in generated_meals: t_meal = int(target_kcal_per_meal[meal]) r_meal = int(achieved_meal_kcal[meal]) logger.info(f" - {meal.capitalize():<5}: Đạt {r_meal} / {t_meal} Kcal") return { "final_menu": final_menu, "user_profile": profile }