RAG-DietAssistant / diet_tool.py
Beable's picture
Update diet_tool.py
8b203aa verified
import json
import random
import math
from typing import Dict, List, Any, Optional
from dataclasses import dataclass
from datetime import datetime, timedelta
# USDA provider removed - using local food database only
@dataclass
class UserProfile:
"""User profile information"""
age: int
sex: str # "male" or "female"
height_cm: float
weight_kg: float
activity_level: str # "sedentary", "light", "moderate", "active", "very_active"
goal: str # "weight_loss", "muscle_gain", "maintenance", "keto", "mediterranean"
dietary_restrictions: List[str] = None
allergies: List[str] = None
preferences: List[str] = None
@dataclass
class MealPlan:
"""Meal plan"""
meal_name: str
time_range: str
foods: List[Dict[str, Any]]
total_calories: float
total_protein: float
total_carbs: float
total_fat: float
total_fiber: float
@dataclass
class DailyPlan:
"""Daily diet plan"""
date: str
meals: List[MealPlan]
total_calories: float
total_protein: float
total_carbs: float
total_fat: float
total_fiber: float
class DietPlanner:
"""Diet planner class"""
def __init__(self, diet_data_file: str = "foods_DB_Example.json"):
"""Load diet data"""
with open(diet_data_file, 'r', encoding='utf-8') as f:
self.diet_data = json.load(f)
# Meal name translation mapping
self.meal_name_translation = {
"Kahvaltı": "Breakfast",
"Kuşluk": "Morning Snack",
"Öğle Yemeği": "Lunch",
"İkindi": "Afternoon Snack",
"Akşam Yemeği": "Dinner",
"Gece Atıştırması": "Evening Snack"
}
def calculate_bmr(self, profile: UserProfile) -> float:
"""Calculate basal metabolic rate (Mifflin-St Jeor formula)"""
if profile.sex.lower() == "male":
bmr = 10 * profile.weight_kg + 6.25 * profile.height_cm - 5 * profile.age + 5
else:
bmr = 10 * profile.weight_kg + 6.25 * profile.height_cm - 5 * profile.age - 161
return bmr
def calculate_tdee(self, profile: UserProfile) -> float:
"""Calculate total daily energy expenditure"""
bmr = self.calculate_bmr(profile)
activity_multipliers = {
"sedentary": 1.2, # Sedentary
"light": 1.375, # Lightly active
"moderate": 1.55, # Moderately active
"active": 1.725, # Active
"very_active": 1.9 # Very active
}
multiplier = activity_multipliers.get(profile.activity_level.lower(), 1.2)
return bmr * multiplier
def calculate_target_calories(self, profile: UserProfile) -> float:
"""Calculate target calorie amount"""
tdee = self.calculate_tdee(profile)
if profile.goal == "weight_loss":
# 15-20% calorie deficit
deficit = random.uniform(0.15, 0.20)
return tdee * (1 - deficit)
elif profile.goal == "muscle_gain":
# 10-15% calorie surplus
surplus = random.uniform(0.10, 0.15)
return tdee * (1 + surplus)
elif profile.goal == "keto":
# 10-15% calorie deficit
deficit = random.uniform(0.10, 0.15)
return tdee * (1 - deficit)
else:
# Maintenance and Mediterranean
return tdee
def get_macro_ratios(self, profile: UserProfile) -> Dict[str, float]:
"""Get macronutrient ratios"""
diet_type = self.diet_data["diet_types"].get(profile.goal, {})
if profile.goal == "weight_loss":
return {
"protein": random.uniform(0.25, 0.30),
"carbs": random.uniform(0.40, 0.45),
"fat": random.uniform(0.25, 0.30)
}
elif profile.goal == "muscle_gain":
return {
"protein": random.uniform(0.25, 0.30),
"carbs": random.uniform(0.45, 0.55),
"fat": random.uniform(0.20, 0.25)
}
elif profile.goal == "keto":
return {
"protein": random.uniform(0.20, 0.25),
"carbs": random.uniform(0.05, 0.10),
"fat": random.uniform(0.70, 0.75)
}
else:
return {
"protein": random.uniform(0.20, 0.25),
"carbs": random.uniform(0.45, 0.55),
"fat": random.uniform(0.25, 0.30)
}
def get_available_foods(self, profile: UserProfile) -> Dict[str, List[Dict]]:
"""Filter foods that the user can eat"""
available_foods = {}
for category, subcategories in self.diet_data["foods"].items():
available_foods[category] = []
for subcategory, foods in subcategories.items():
for food in foods:
# Allergy check
if profile.allergies:
food_name_lower = food["name"].lower()
if any(allergy.lower() in food_name_lower for allergy in profile.allergies):
continue
# Dietary restrictions check
if profile.dietary_restrictions:
if "vegetarian" in profile.dietary_restrictions and category == "protein_sources":
if subcategory in ["lean_meats", "fish_seafood"]:
continue
if "vegan" in profile.dietary_restrictions and category in ["protein_sources", "dairy"]:
if subcategory in ["lean_meats", "fish_seafood", "eggs", "milk_products", "cheese"]:
continue
# Calorie value check (prevent division by zero)
if food.get("calories_per_100g", 0) <= 0:
continue
available_foods[category].append(food)
return available_foods
def select_foods_for_meal(self, meal_type: str, target_calories: float,
macro_ratios: Dict[str, float], available_foods: Dict[str, List[Dict]]) -> List[Dict[str, Any]]:
"""Select foods for meal"""
meal_info = self.diet_data["meals"][meal_type]
food_categories = meal_info["food_categories"]
selected_foods = []
remaining_calories = target_calories
# Select food for each category
for category in food_categories:
if category not in available_foods or not available_foods[category]:
continue
# Calorie target for category
category_calories = remaining_calories * 0.3 # 30% for each category
if category_calories < 50: # If very few calories left
continue
# Randomly select food from category
food = random.choice(available_foods[category])
# Calculate portion amount (with division by zero check)
if food["calories_per_100g"] <= 0:
continue # Skip foods with 0 or negative calorie values
portion_grams = min(category_calories / (food["calories_per_100g"] / 100), 200)
# Minimum portion check
if portion_grams < 20:
continue
selected_food = {
"name": food["name"],
"portion_grams": round(portion_grams, 1),
"calories": round(food["calories_per_100g"] * portion_grams / 100, 1),
"protein": round(food["protein_per_100g"] * portion_grams / 100, 1),
"carbs": round(food["carbs_per_100g"] * portion_grams / 100, 1),
"fat": round(food["fat_per_100g"] * portion_grams / 100, 1),
"fiber": round(food["fiber_per_100g"] * portion_grams / 100, 1),
"vitamins": food["vitamins"],
"minerals": food["minerals"]
}
selected_foods.append(selected_food)
remaining_calories -= selected_food["calories"]
if remaining_calories < 100: # Stop if very few calories left
break
return selected_foods
def create_meal_plan(self, meal_type: str, target_calories: float,
macro_ratios: Dict[str, float], available_foods: Dict[str, List[Dict]]) -> MealPlan:
"""Create meal plan"""
meal_info = self.diet_data["meals"][meal_type]
foods = self.select_foods_for_meal(meal_type, target_calories, macro_ratios, available_foods)
# Calculate total nutritional values
total_calories = sum(food["calories"] for food in foods)
total_protein = sum(food["protein"] for food in foods)
total_carbs = sum(food["carbs"] for food in foods)
total_fat = sum(food["fat"] for food in foods)
total_fiber = sum(food["fiber"] for food in foods)
# Translate meal name to English
meal_name = self.meal_name_translation.get(meal_info["name"], meal_info["name"])
return MealPlan(
meal_name=meal_name,
time_range=meal_info["time_range"],
foods=foods,
total_calories=round(total_calories, 1),
total_protein=round(total_protein, 1),
total_carbs=round(total_carbs, 1),
total_fat=round(total_fat, 1),
total_fiber=round(total_fiber, 1)
)
def generate_daily_plan(self, profile: UserProfile) -> DailyPlan:
"""Generate daily diet plan"""
target_calories = self.calculate_target_calories(profile)
macro_ratios = self.get_macro_ratios(profile)
available_foods = self.get_available_foods(profile)
# Determine meal types
meal_types = ["breakfast", "lunch", "dinner"]
# Add snacks
if profile.goal == "muscle_gain":
meal_types.extend(["morning_snack", "afternoon_snack", "evening_snack"])
elif profile.goal == "weight_loss":
meal_types.append("morning_snack")
else:
meal_types.extend(["morning_snack", "afternoon_snack"])
meals = []
total_calories = 0
total_protein = 0
total_carbs = 0
total_fat = 0
total_fiber = 0
for meal_type in meal_types:
# Calorie target for meal
meal_calories = target_calories * float(self.diet_data["meals"][meal_type]["calorie_target"].split("-")[0]) / 100
meal_plan = self.create_meal_plan(meal_type, meal_calories, macro_ratios, available_foods)
meals.append(meal_plan)
total_calories += meal_plan.total_calories
total_protein += meal_plan.total_protein
total_carbs += meal_plan.total_carbs
total_fat += meal_plan.total_fat
total_fiber += meal_plan.total_fiber
return DailyPlan(
date=datetime.now().strftime("%Y-%m-%d"),
meals=meals,
total_calories=round(total_calories, 1),
total_protein=round(total_protein, 1),
total_carbs=round(total_carbs, 1),
total_fat=round(total_fat, 1),
total_fiber=round(total_fiber, 1)
)
def generate_weekly_plan(self, profile: UserProfile) -> List[DailyPlan]:
"""Generate weekly diet plan"""
weekly_plan = []
for i in range(7):
# Create different variations for each day
daily_plan = self.generate_daily_plan(profile)
daily_plan.date = (datetime.now() + timedelta(days=i)).strftime("%Y-%m-%d")
weekly_plan.append(daily_plan)
return weekly_plan
def plan_to_dict(self, plan: DailyPlan) -> Dict[str, Any]:
"""Convert plan to dictionary format"""
return {
"date": plan.date,
"meals": [
{
"meal_name": meal.meal_name,
"time_range": meal.time_range,
"foods": meal.foods,
"nutrition": {
"calories": meal.total_calories,
"protein": meal.total_protein,
"carbs": meal.total_carbs,
"fat": meal.total_fat,
"fiber": meal.total_fiber
}
}
for meal in plan.meals
],
"daily_totals": {
"calories": plan.total_calories,
"protein": plan.total_protein,
"carbs": plan.total_carbs,
"fat": plan.total_fat,
"fiber": plan.total_fiber
}
}
def generate_diet_plan(user_profile: Dict[str, Any], plan_type: str = "daily") -> Dict[str, Any]:
"""
Create diet plan
Args:
user_profile: User profile information
plan_type: "daily" or "weekly"
Returns:
Diet plan dictionary
"""
try:
# Create UserProfile object
profile = UserProfile(
age=user_profile["age"],
sex=user_profile["sex"],
height_cm=user_profile["height_cm"],
weight_kg=user_profile["weight_kg"],
activity_level=user_profile["activity_level"],
goal=user_profile["goal"],
dietary_restrictions=user_profile.get("dietary_restrictions", []),
allergies=user_profile.get("allergies", []),
preferences=user_profile.get("preferences", [])
)
# Create DietPlanner
planner = DietPlanner()
if plan_type == "weekly":
plans = planner.generate_weekly_plan(profile)
return {
"plan_type": "weekly",
"plans": [planner.plan_to_dict(plan) for plan in plans]
}
else:
plan = planner.generate_daily_plan(profile)
return {
"plan_type": "daily",
"plan": planner.plan_to_dict(plan)
}
except Exception as e:
return {
"error": f"Error while creating diet plan: {str(e)}"
}
def calculate_nutrition_info(food_name: str, portion_grams: float) -> Dict[str, Any]:
"""
Calculate nutritional values for a specific food
Args:
food_name: Food name
portion_grams: Portion amount (grams)
Returns:
Nutritional values dictionary
"""
try:
planner = DietPlanner()
# Find the food
for category, subcategories in planner.diet_data["foods"].items():
for subcategory, foods in subcategories.items():
for food in foods:
if food["name"].lower() == food_name.lower():
return {
"food_name": food["name"],
"portion_grams": portion_grams,
"calories": round(food["calories_per_100g"] * portion_grams / 100, 1),
"protein": round(food["protein_per_100g"] * portion_grams / 100, 1),
"carbs": round(food["carbs_per_100g"] * portion_grams / 100, 1),
"fat": round(food["fat_per_100g"] * portion_grams / 100, 1),
"fiber": round(food["fiber_per_100g"] * portion_grams / 100, 1),
"vitamins": food["vitamins"],
"minerals": food["minerals"]
}
# If not found locally, return error (USDA fetching removed)
return {"error": f"'{food_name}' not found in local database"}
except Exception as e:
return {"error": f"Error while calculating nutritional value: {str(e)}"}
def get_diet_recommendations(user_profile: Dict[str, Any]) -> Dict[str, Any]:
"""
Create diet recommendations for user
Args:
user_profile: User profile information
Returns:
Diet recommendations dictionary
"""
try:
profile = UserProfile(
age=user_profile["age"],
sex=user_profile["sex"],
height_cm=user_profile["height_cm"],
weight_kg=user_profile["weight_kg"],
activity_level=user_profile["activity_level"],
goal=user_profile["goal"],
dietary_restrictions=user_profile.get("dietary_restrictions", []),
allergies=user_profile.get("allergies", []),
preferences=user_profile.get("preferences", [])
)
planner = DietPlanner()
bmr = planner.calculate_bmr(profile)
tdee = planner.calculate_tdee(profile)
target_calories = planner.calculate_target_calories(profile)
macro_ratios = planner.get_macro_ratios(profile)
return {
"bmr": round(bmr, 1),
"tdee": round(tdee, 1),
"target_calories": round(target_calories, 1),
"macro_ratios": {
"protein": f"{macro_ratios['protein']*100:.1f}%",
"carbs": f"{macro_ratios['carbs']*100:.1f}%",
"fat": f"{macro_ratios['fat']*100:.1f}%"
},
"macro_grams": {
"protein": round(target_calories * macro_ratios["protein"] / 4, 1),
"carbs": round(target_calories * macro_ratios["carbs"] / 4, 1),
"fat": round(target_calories * macro_ratios["fat"] / 9, 1)
},
"recommendations": {
"meal_frequency": planner.diet_data["diet_types"][profile.goal]["meal_frequency"],
"fiber_target": planner.diet_data["diet_types"][profile.goal]["fiber_target"]
}
}
except Exception as e:
return {"error": f"Error while creating recommendations: {str(e)}"}
# Test function
if __name__ == "__main__":
# Example user profile
test_profile = {
"age": 30,
"sex": "male",
"height_cm": 175,
"weight_kg": 75,
"activity_level": "moderate",
"goal": "weight_loss",
"dietary_restrictions": [],
"allergies": [],
"preferences": []
}
# Create daily plan
daily_plan = generate_diet_plan(test_profile, "daily")
print("Daily Diet Plan:")
print(json.dumps(daily_plan, ensure_ascii=False, indent=2))
# Get recommendations
recommendations = get_diet_recommendations(test_profile)
print("\nDiet Recommendations:")
print(json.dumps(recommendations, ensure_ascii=False, indent=2))