diff --git a/.gitignore b/.gitignore index b19c34b..d4c117c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ __pycache__/ *.pyc *.pth *.h5 +*.pt .DS_Store *.tar.gz diff --git a/nutrihelp_ai/ai_reasoning.py b/nutrihelp_ai/ai_reasoning.py new file mode 100644 index 0000000..8839b9d --- /dev/null +++ b/nutrihelp_ai/ai_reasoning.py @@ -0,0 +1,236 @@ +""" AI Reasoning Module - Enhanced for Better Meal Plans """ +from config import GEMINI_API_KEY, GEMINI_MODEL +import pandas as pd +from typing import Dict, Optional, Tuple, List +from google import genai + +# Initialize client ONCE +client = genai.Client(api_key=GEMINI_API_KEY) + + +def validate_profile(user_profile: Dict) -> Tuple[bool, str]: + """ + STRICT VALIDATION ENGINE + """ + goal = user_profile.get('goal', '').lower() + valid_goals = ['lose', 'weight', 'fat', 'gain', + 'muscle', 'bulk', 'maintain', 'health', 'fitness'] + if len(goal) < 3 or goal in ['yes', 'no', 'ok', 'good', 'bad'] or not any(x in goal for x in valid_goals): + return False, "Your health goal is unclear. Please answer with: Lose weight, Gain muscle, or Maintain health." + + activity = user_profile.get('sport', '').lower() + valid_activities = ['gym', 'walk', 'run', 'yoga', 'swim', 'sport', 'none', 'sedentary', + 'active', 'moderate', 'exercise', 'training', 'cricket', 'football', 'cycling'] + if len(activity) < 3 or not any(x in activity for x in valid_activities): + return False, "Physical activity answer is invalid." + + level = user_profile.get('level', '').lower() + valid_levels = ['begin', 'interm', 'advan', 'pro'] + if not any(x in level for x in valid_levels): + return False, "Fitness level invalid." + + diet = user_profile.get('diet', '').lower() + valid_diets = ['veg', 'non', 'meat', 'egg', 'pesc', 'omni'] + if not any(x in diet for x in valid_diets): + return False, "Diet preference unclear." + + # FIXED: More flexible medical condition validation + cond = user_profile.get('condition', '').lower().strip() + safe_indicators = ['none', 'no', 'not', 'nothing', + 'na', 'n/a', 'nil', 'healthy', 'fine'] + + # Check if it's a "no condition" response + has_safe_word = any(word in cond for word in safe_indicators) + + # If it contains safe words, accept it + if has_safe_word: + pass # Valid response + # If it's too short or generic (but doesn't say "no condition"), reject + elif len(cond) < 3 or cond in ['yes', 'ok', 'good', 'bad']: + return False, "Medical condition answer is invalid. Please state 'None' or a specific condition (e.g., diabetes, hypertension)." + # Otherwise, assume it's a specific condition name, which is valid + + # FIXED: Better allergy validation + allergies = user_profile.get('allergies', []) + + # Handle string input + if isinstance(allergies, str): + allergies = [a.strip() for a in allergies.split(',') if a.strip()] + + # Filter out empty strings and clean up + cleaned_allergies = [] + for alg in allergies: + cleaned = str(alg).strip() + if cleaned: + cleaned_allergies.append(cleaned) + + # If no allergies, that's fine + if not cleaned_allergies: + return True, "Profile Validated" + + # Check each allergy + safe_allergy_words = ['none', 'no', 'nothing', 'na', 'n/a', 'nil'] + for alg in cleaned_allergies: + a = alg.lower().strip() + + # Skip if it's a "no allergy" indicator + if a in safe_allergy_words: + continue + + # If it's too short or too generic, reject + if len(a) < 2 or a in ['yes', 'ok', 'i', 'a']: + return False, f"Allergy answer '{alg}' is invalid. Please state 'None' or specific allergies (e.g., peanuts, dairy, gluten)." + + return True, "Profile Validated" + + +def generate_meal_plan(user_profile: Dict, data_loader, start_day: int = 1) -> str: + is_valid, error_msg = validate_profile(user_profile) + if not is_valid: + return f"ERROR: {error_msg}" + + # Handle condition info + condition_str = user_profile.get('condition', '').lower().strip() + condition_info = None + safe_condition_indicators = ['none', 'no', 'not', + 'nothing', 'na', 'n/a', 'nil', 'healthy', 'fine'] + + if condition_str and not any(word in condition_str for word in safe_condition_indicators): + if data_loader: + condition_info = data_loader.get_condition_info( + user_profile['condition']) + + # Get sport gear + sport_gear_list = [] + if data_loader: + sport_gear_list = data_loader.get_sport_gear( + user_profile.get('sport', 'general')) + + # Filter recipes by allergies + safe_recipes = None + if data_loader: + safe_recipes = data_loader.filter_by_allergies( + user_profile.get('allergies', [])) + + # Sort recipes based on goal + if safe_recipes is not None and not safe_recipes.empty: + user_goal = user_profile.get('goal', '').lower() + if 'muscle' in user_goal or 'gain' in user_goal: + sorted_recipes = safe_recipes.sort_values( + by='protein', ascending=False) + elif 'loss' in user_goal or 'lose' in user_goal: + sorted_recipes = safe_recipes[safe_recipes['calories'] > 0].sort_values( + by='calories') + else: + sorted_recipes = safe_recipes + + final_recipe_pool = sorted_recipes.head(25) + else: + final_recipe_pool = pd.DataFrame() + + prompt = _build_strict_prompt( + user_profile, condition_info, final_recipe_pool, sport_gear_list, start_day + ) + + try: + response = client.models.generate_content( + model=GEMINI_MODEL, + contents=prompt, + config={ + "temperature": 0.3, + "max_output_tokens": 8000 + } + ) + return response.text.strip() + + except Exception as e: + return f"System Error: {type(e).__name__}: {str(e)}" + + +def _build_strict_prompt(user_profile: Dict, condition_info: Optional[Dict], + available_recipes: pd.DataFrame, sport_gear: List[str], start_day: int) -> str: + recipe_text = "Standard healthy options." + if not available_recipes.empty: + recipe_text = "; ".join( + f"{row['recipe_name']} ({row['calories']:.0f} cal)" + for _, row in available_recipes.iterrows() + ) + + cond_str = "None" + if condition_info: + cond_str = ( + f"{condition_info['name']} " + f"Must Eat: {', '.join(condition_info['recommended'])} " + f"Avoid: {', '.join(condition_info['restricted'])}" + ) + + gear_str = ", ".join( + sport_gear) if sport_gear else "Standard Athletic Wear" + end_day = start_day + 6 + + # Clean up allergies for display + allergies = user_profile.get('allergies', []) + if isinstance(allergies, str): + allergies = [allergies] + allergy_display = ', '.join([str(a).strip() + for a in allergies if str(a).strip()]) + if not allergy_display: + allergy_display = "None" + + return f""" +ROLE: Strict Clinical Nutrition & Sports Engine. +TASK: Generate a {start_day}-Day to {end_day}-Day Meal Plan + Sport Gear Checklist. + +USER DATA: +Goal: {user_profile.get('goal')} +Activity: {user_profile.get('sport')} +Level: {user_profile.get('level')} +Diet: {user_profile.get('diet')} +Condition: {cond_str} +Allergies: {allergy_display} + +AVAILABLE RECIPES: +{recipe_text} +RECOMMENDED SPORT GEAR: +{gear_str} + +RULES: +1. Generate exactly 7 days (Day {start_day} to Day {end_day}) +2. Plain text only. No markdown formatting. +3. Each day must include: Breakfast, Mid-Morning Snack, Lunch, Evening Snack, Dinner +4. Respect dietary restrictions: {user_profile.get('diet')} +5. Avoid all allergies: {allergy_display} +6. Calories should align with goal: {user_profile.get('goal')} + +FORMAT: +Day {start_day} +Breakfast: [meal details with calories] +Mid-Morning Snack: [snack details] +Lunch: [meal details with calories] +Evening Snack: [snack details] +Dinner: [meal details with calories] +Total Calories: [number] + +...continue for all 7 days... + +Day {end_day} +[same format] + +SPORT GEAR CHECKLIST FOR {user_profile.get('sport')}: +{gear_str} +""" + + +def get_quick_nutrition_advice(question: str) -> str: + try: + response = client.models.generate_content( + model=GEMINI_MODEL, + contents=f"Answer strictly in plain text: {question}", + config={ + "temperature": 0.5, + "max_output_tokens": 300 + } + ) + return response.text.strip() + except Exception: + return "Service unavailable." diff --git a/nutrihelp_ai/app.py b/nutrihelp_ai/app.py new file mode 100644 index 0000000..67b796b --- /dev/null +++ b/nutrihelp_ai/app.py @@ -0,0 +1,209 @@ +import json + +import os +import uuid +import numpy as np +# from imutils import face_utils +from scipy.spatial import distance as dist +from fastapi.staticfiles import StaticFiles +from fastapi.middleware.cors import CORSMiddleware +from ultralytics import YOLO +from datetime import datetime +from fastapi import FastAPI, Request, Form, UploadFile, File +from fastapi.responses import HTMLResponse, JSONResponse, FileResponse +from fastapi.templating import Jinja2Templates +from fastapi import FastAPI, Request +from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse +from fastapi.templating import Jinja2Templates + + +# PREDICTOR_PATH = "shape_predictor_68_face_landmarks.dat" +USERS_FILE = "users.json" +EYE_AR_THRESH = 0.25 +EYE_AR_CONSEC_FRAMES = 3 +REQUIRED_BLINKS = 2 + +app = FastAPI(title="NutriHelp - Jenny AI") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +templates = Jinja2Templates(directory="templates") + +os.makedirs("uploads", exist_ok=True) +os.makedirs("meal_plans", exist_ok=True) + +app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads") + + +MODEL_PATH = r"C:\Users\Naman Shah\OneDrive\Documents\Assignement_Deakin\T3\SIT764\Project\NutriHelp-AI\nutrihelp_ai\model\image_class.pt" +model = YOLO(MODEL_PATH) + +CALORIE_DICT = { + "apple": 52, "banana": 89, "orange": 47, "pizza": 285, "burger": 295, + "sandwich": 300, "salad": 150, "rice": 200, "chicken": 165, "egg": 78, + "bread": 80, "pasta": 200, "cake": 350, "ice cream": 207, "hot dog": 150, + +} + + +@app.get("/", response_class=HTMLResponse) +async def home(request: Request): + return templates.TemplateResponse("index.html", {"request": request}) + + +@app.post("/upload-image") +async def upload_image(images: list[UploadFile] = File(...)): + file_paths = [] + for file in images: + if file.content_type.startswith("image/"): + ext = file.filename.split(".")[-1] + filename = f"{uuid.uuid4().hex}.{ext}" + filepath = os.path.join("uploads", filename) + with open(filepath, "wb") as f: + content = await file.read() + f.write(content) + file_paths.append(f"/uploads/{filename}") + return JSONResponse({"filePaths": file_paths}) + + +@app.post("/analyze-images") +async def analyze_images(request: Request): + data = await request.json() + image_paths = data.get("image_paths", []) + response_lines = [ + "š„ I analyzed your food photo(s)! Here's what I found:\n"] + total_calories = 0 + + for i, rel_path in enumerate(image_paths, 1): + full_path = "." + rel_path + try: + results = model(full_path, conf=0.5, iou=0.5) + classes = results[0].boxes.cls.tolist() + class_names = [model.names[int(c)].lower() for c in classes] + unique_names = list(set(class_names)) + + response_lines.append(f"Photo {i}:") + if not unique_names: + response_lines.append("- No food detected.\n") + continue + + photo_cal = 0 + for name in unique_names: + cal = CALORIE_DICT.get(name, 145) + response_lines.append(f"- {name.title()} (~{cal} calories)") + photo_cal += cal + response_lines.append( + f"Estimated for this photo: {photo_cal} calories\n") + total_calories += photo_cal + except Exception as e: + response_lines.append(f"- Error analyzing photo {i}.\n") + + response_lines.append( + f"Total estimated calories: {total_calories} calories") + response_lines.append( + "\nNote: Estimates are approximate per typical serving. Portion size affects actual calories!") + + return JSONResponse({"answer": "\n".join(response_lines)}) + + +@app.post("/ask") +async def ask_nutrition_question(request: Request): + try: + data = await request.json() + print(data) + question = data.get("question", "").strip() + print(f"Question received: {question}") + if not question: + return JSONResponse({"answer": "Please ask something! š"}) + try: + from ai_reasoning import get_quick_nutrition_advice + answer = get_quick_nutrition_advice(question) + except Exception as e: + print(f"Error in getting advice: {e}") + answer = "Service unavailable." + print(f"[AI] Question: {question}\n[AI] Answer: {answer}") + return JSONResponse({"answer": answer}) + except Exception as e: + return JSONResponse({"answer": "Trouble connecting. Try again! š"}) + + +@app.post("/generate-plan") +async def generate_plan( + goal: str = Form(...), + sport: str = Form(...), + level: str = Form(...), + diet: str = Form(...), + condition: str = Form("None"), + allergies: str = Form("None") +): + from ai_reasoning import generate_meal_plan + + # Clean and parse allergies properly + allergy_list = [] + if allergies and allergies.strip() and allergies.lower() not in ['none', 'no', 'nothing']: + # Split by comma and clean each item + allergy_list = [a.strip() for a in allergies.split(",") + if a.strip() and len(a.strip()) > 1] + + # If no valid allergies, set to None + if not allergy_list: + allergy_list = ["None"] + + # Clean condition + clean_condition = condition.strip() if condition and condition.strip() else "None" + + user_profile = { + "goal": goal, + "sport": sport, + "level": level, + "diet": diet, + "condition": clean_condition, + "allergies": allergy_list + } + + # Generate meal plan + meal_plan_text = generate_meal_plan( + user_profile, data_loader=None, start_day=1) + + # Save to file + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"meal_plan_{timestamp}_{uuid.uuid4().hex[:8]}.txt" + + # Ensure directory exists + os.makedirs("meal_plans", exist_ok=True) + filepath = os.path.join("meal_plans", filename) + + with open(filepath, "w", encoding="utf-8") as f: + f.write("=== YOUR PERSONALIZED MEAL PLAN ===\n\n") + f.write(f"Goal: {goal}\n") + f.write(f"Activity: {sport}\n") + f.write(f"Level: {level}\n") + f.write(f"Diet: {diet}\n") + f.write(f"Condition: {clean_condition}\n") + f.write(f"Allergies: {', '.join(allergy_list)}\n") + f.write("\n" + "="*50 + "\n\n") + f.write(meal_plan_text) + + return JSONResponse({ + "meal_plan": meal_plan_text, + "filename": filename + }) + + +@app.get("/download/{filename}") +async def download(filename: str): + filepath = os.path.join("meal_plans", filename) + if not os.path.exists(filepath): + return JSONResponse({"error": "File not found"}, status_code=404) + return FileResponse(filepath, filename=f"NutriHelp_MealPlan_{datetime.now().strftime('%Y%m%d')}.txt") + +if __name__ == "__main__": + import uvicorn + print("š„ NutriHelp Starting... Open http://127.0.0.1:8000") + uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True) diff --git a/nutrihelp_ai/config.py b/nutrihelp_ai/config.py new file mode 100644 index 0000000..36e2c45 --- /dev/null +++ b/nutrihelp_ai/config.py @@ -0,0 +1,19 @@ +# ============================================================================ +# FILE 1: config.py +# Configuration file with API keys and settings +# ============================================================================ + +# Gemini API Configuration +# ā ļø EXPOSED! REVOKE IMMEDIATELY! +# GEMINI_API_KEY = "AIzaSyA_kYa1kKVfpFYdIMcX9Idqq70lfYvCy7o" +GEMINI_API_KEY = "AIzaSyAaRYA2QUmt15X8JDXBD32_de2IQX49pu4" +# Note: This model does not exist. Use "gemini-1.5-flash" +GEMINI_MODEL = "gemini-2.5-flash" + +# Voice settings +VOICE_RATE = 150 # Speed of speech +VOICE_VOLUME = 0.9 # Volume (0.0 to 1.0) + +# Speech recognition settings +LISTEN_TIMEOUT = 10 # Seconds to wait for speech +PHRASE_TIME_LIMIT = 15 # Max seconds for a phrase diff --git a/nutrihelp_ai/data_loader.py b/nutrihelp_ai/data_loader.py new file mode 100644 index 0000000..1e66dd2 --- /dev/null +++ b/nutrihelp_ai/data_loader.py @@ -0,0 +1,343 @@ +# # ============================================================================ +# # FILE 2: data_loader.py +# # Loads and manages all nutrition data from CSV +# # ============================================================================ + + +import pandas as pd +from typing import List, Dict, Optional + + +class NutritionDataLoader: + """Manages all nutrition, recipe, health condition, and sport gear data""" + + def __init__(self): + self.conditions = None + self.allergies = None + self.recipes = None + self.ingredients = None + self.recipe_ingredients = None + self.categories = None + self.sport_gear = None # NEW + + def load_all_data(self): + """Load all nutrition and sport data""" + try: + print("[Data] Loading database...") + + self.conditions = self._load_conditions() + self.allergies = self._load_allergies() + self.recipes = self._load_recipes() + self.ingredients = self._load_ingredients() + self.recipe_ingredients = self._load_recipe_ingredients() + self.categories = self._load_categories() + + # --- NEW: Load Sport Gear Data --- + self.sport_gear = self._load_sport_gear() + print( + f"[Data] ā Loaded gear data for {len(self.sport_gear)} sports") + + print("[Data] ā All data loaded successfully!\n") + return True + + except Exception as e: + print(f"[Data] ā Error loading data: {e}") + return False + + # ========================================================================= + # NEW: SPORT GEAR DATA + # ========================================================================= + def _load_sport_gear(self) -> Dict[str, List[str]]: + """Returns a dictionary of sports and recommended gear""" + return { + 'gym': [ + 'Flat-soled shoes (Converse/Metcons) for stability', + 'Weightlifting Belt (for heavy compounds)', + 'Wrist Wraps (for pressing movements)', + 'Shaker Bottle', + 'Gym Towel' + ], + 'running': [ + 'Proper Running Shoes (Gait analysis recommended)', + 'Moisture-wicking Socks (prevent blisters)', + 'Running Watch/Tracker (Garmin/Apple)', + 'High-visibility vest (night running)', + 'Anti-chafe balm' + ], + 'yoga': [ + 'Non-slip Yoga Mat (4mm+ thickness)', + 'Yoga Blocks (Cork or Foam)', + 'Yoga Strap (for flexibility)', + 'Form-fitting, breathable clothing', + 'Microfiber Towel' + ], + 'swimming': [ + 'Anti-fog Goggles', + 'Silicone Swim Cap', + 'Chlorine-resistant Swimsuit', + 'Ear Plugs & Nose Clip', + 'Microfiber Quick-dry Towel' + ], + 'walking': [ + 'Supportive Walking Shoes with arch support', + 'Pedometer or Step Counter', + 'Breathable layers (weather appropriate)', + 'Small backpack or waist pack', + 'Sun protection (Hat/SPF)' + ], + 'cycling': [ + 'Padded Cycling Shorts (Bibs)', + 'Helmet (MIPS safety standard)', + 'Cycling Shoes (Clipless)', + 'Water Bottle Cage', + 'Bike Lights (Front & Rear)' + ], + 'football': [ + 'Football Cleats/Boots', + 'Shin Guards', + 'Compression Socks', + 'Hydration Bottle', + 'Breathable Jersey' + ], + 'cricket': [ + 'Cricket Spikes/Shoes', + 'Sun Hat/Cap', + 'White Athletic Wear', + 'Personal Abdominal Guard (Box)', + 'Sunscreen' + ], + 'general': [ + 'Comfortable Athletic Shoes', + 'Water Bottle (1L+)', + 'Sweat Towel', + 'Smart Watch/Fitness Tracker', + 'Wireless Earbuds' + ] + } + + def get_sport_gear(self, sport_name: str) -> List[str]: + """Get gear list based on sport name (fuzzy match)""" + if not self.sport_gear: + return [] + + sport = sport_name.lower() + + # Simple keyword matching + if 'gym' in sport or 'weight' in sport or 'lift' in sport: + return self.sport_gear['gym'] + elif 'run' in sport or 'jog' in sport: + return self.sport_gear['running'] + elif 'yoga' in sport or 'pilates' in sport: + return self.sport_gear['yoga'] + elif 'swim' in sport or 'pool' in sport: + return self.sport_gear['swimming'] + elif 'walk' in sport: + return self.sport_gear['walking'] + elif 'cycle' in sport or 'bike' in sport: + return self.sport_gear['cycling'] + elif 'football' in sport or 'soccer' in sport: + return self.sport_gear['football'] + elif 'cricket' in sport: + return self.sport_gear['cricket'] + + return self.sport_gear['general'] + + # ========================================================================= + # EXISTING DATA METHODS (Kept exactly as they were) + # ========================================================================= + def _load_conditions(self) -> pd.DataFrame: + conditions_data = { + 'condition_id': list(range(1, 131)), + 'name': ['Diabetes', 'Hypertension', 'Celiac Disease', 'Lactose Intolerance', + 'Anemia', 'Osteoporosis', 'Gout', 'IBS', 'High Cholesterol', + 'Kidney Disease', 'Arthritis', 'Asthma', 'Bronchitis', + 'Chronic Fatigue Syndrome', 'Depression', 'Anxiety Disorder', + 'Obesity', 'Migraine', 'Eczema', 'Psoriasis', 'Acid Reflux', + 'Diverticulitis', 'Ulcerative Colitis', 'Crohn\'s Disease', + 'Hyperthyroidism', 'Hypothyroidism', 'Parkinson\'s Disease', + 'Alzheimer\'s Disease', 'Multiple Sclerosis', 'Rheumatoid Arthritis' + ] + ['Other Condition'] * 100, + 'recommended_foods': [ + ['Whole Grains', 'Leafy Greens', 'Lean Proteins'], + ['Low-sodium Foods', 'Bananas', 'Beets'], + ['Rice', 'Quinoa', 'Gluten-free Oats'], + ['Almond Milk', 'Soy Milk', 'Lactose-free Products'], + ['Red Meat', 'Spinach', 'Iron-fortified Cereal'], + ['Dairy Products', 'Leafy Greens', 'Sardines'], + ['Low-fat Dairy', 'Vegetables', 'Cherries'], + ['Low-FODMAP Foods', 'Rice', 'Carrots'], + ['Oats', 'Nuts', 'Fatty Fish'], + ['Cauliflower', 'Blueberries', 'Egg Whites'], + ['Fish oils', 'Turmeric', 'Green Tea'], + ['Antioxidant-rich Fruits', 'Leafy Greens', 'Omega-3 Sources'], + ['Warm Herbal Teas', 'Honey', 'Ginger'], + ['Complex Carbohydrates', 'Lean Proteins', 'Nuts'], + ['Omega-3 Rich Foods', 'Leafy Greens', 'Berries'], + ['Chamomile Tea', 'Whole Grains', 'Bananas'], + ['Fruits', 'Vegetables', 'Lean Proteins'], + ['Magnesium-Rich Foods', 'Ginger', 'Dark Leafy Greens'], + ['Anti-inflammatory Foods', 'Fruits', 'Vegetables'], + ['Omega-3 Rich Foods', 'Fruits', 'Vegetables'], + ['Oatmeal', 'Ginger', 'Aloe Vera Juice'], + ['High-Fiber Foods', 'Fruits', 'Vegetables'], + ['Probiotic Foods', 'Bone Broth', 'Easily Digestible Fruits'], + ['Lean Proteins', 'Cooked Vegetables', 'Rice'], + ['Calcium-Rich Foods', 'High-Quality Proteins', 'Antioxidants'], + ['Whole Grains', 'Lean Proteins', 'Fruits'], + ['Fiber-Rich Foods', 'Antioxidants', 'Omega-3 Foods'], + ['Mediterranean Diet Foods', 'Leafy Greens', 'Berries'], + ['Vitamin D Rich Foods', 'Omega-3 Sources', 'Fruits'], + ['Fish Oils', 'Nuts', 'Antioxidant-Rich Vegetables'] + ] + [['Balanced Diet', 'Whole Foods', 'Fresh Produce']] * 100, + 'restricted_foods': [ + ['Refined Sugar', 'White Bread', 'Sweetened Beverages'], + ['Salted Snacks', 'Processed Meats', 'Pickles'], + ['Wheat', 'Barley', 'Rye'], + ['Milk', 'Cheese', 'Yogurt'], + ['Calcium-rich Foods during iron meals'], + ['Caffeine', 'Soft Drinks'], + ['Red Meat', 'Seafood', 'Alcohol'], + ['Onions', 'Garlic', 'Lentils'], + ['Fried Foods', 'Trans Fats', 'Full-fat Dairy'], + ['Sodium-rich Foods', 'Potassium-rich Foods', 'Red Meat'], + ['Processed Foods', 'Sugar', 'Saturated Fats'], + ['Dairy in sensitive cases', 'Processed Foods', 'Sulfites'], + ['Cold Drinks', 'Smoking'], + ['Caffeinated Beverages', 'Processed Sugars', 'High Fat Foods'], + ['High Sugar', 'Caffeine', 'Alcohol'], + ['Caffeine', 'Sugar', 'Processed Food'], + ['Sugary Snacks', 'Fried Foods', 'Sodas'], + ['Alcohol', 'Chocolate', 'Caffeine'], + ['Dairy', 'Nuts', 'Eggs'], + ['Alcohol', 'Red Meat', 'Dairy'], + ['Coffee', 'Chocolate', 'Spicy Foods'], + ['Processed Foods', 'Red Meat', 'Refined Grains'], + ['Spicy Foods', 'Caffeine', 'High-Fiber Raw Veggies'], + ['Dairy', 'High-Fiber Foods', 'Processed Foods'], + ['Iodine-Rich Foods', 'Caffeine', 'Processed Snacks'], + ['Excess Soy Products', + 'Cruciferous Vegetables in excess', 'Processed Foods'], + ['High Sugar Foods', 'Saturated Fats', 'Processed Snacks'], + ['Saturated Fats', 'Sugar', 'Processed Foods'], + ['Saturated Fats', 'Processed Foods', 'Refined Sugars'], + ['Red Meat', 'Processed Foods', 'Sugars'] + ] + [['Processed Foods', 'Excessive Sugar', 'Trans Fats']] * 100, + 'severity_level': ['high', 'high', 'high', 'medium', 'medium', + 'medium', 'high', 'medium', 'high', 'high', + 'medium', 'medium', 'medium', 'high', 'high', + 'medium', 'high', 'high', 'medium', 'medium', + 'medium', 'medium', 'high', 'high', 'medium', + 'medium', 'high', 'high', 'high', 'high'] + ['medium'] * 100 + } + return pd.DataFrame(conditions_data) + + def _load_allergies(self) -> pd.DataFrame: + allergies_data = { + 'id': list(range(1, 27)), + 'name': [ + 'Peanuts', 'Shrimp', 'Gluten', 'Dairy', 'Eggs', 'Soy', + 'Fish', 'Shellfish', 'Tree Nuts', 'Wheat', 'Sesame', 'Corn', + 'Legumes', 'Seeds', 'Coconut', 'Alcohol', 'Sulfites', + 'Spices', 'Vegetables', 'Fruits', 'Meat', 'Herbs', + 'Grains', 'Oils', 'Additives', 'Not categorized' + ] + } + return pd.DataFrame(allergies_data) + + def _load_recipes(self) -> pd.DataFrame: + recipes_data = { + 'id': [18, 19, 22, 23, 25, 26, 27, 28, 34, 82], + 'recipe_name': [ + 'Tomato Pesto Chicken Pasta', 'Spanish Tortilla', 'Quinoa Salmon Bowl', + 'Quinoa Salmon Bowl v2', 'Vegetable Curry', 'Saffron Rice', + 'Vegetable Rice', 'Vegetable Curry Deluxe', 'Pasta Puttanesca', 'Classic Spaghetti' + ], + 'cuisine_id': [5, 6, 14, 14, 2, 1, 1, 1, 2, 2], + 'total_servings': [3, 4, 3, 3, 3, 2, 0, 3, 1, 1], + 'preparation_time': [45, 15, 30, 30, 23, 23, 50, 70, 70, 70], + 'calories': [2704.97, 0, 1328.66, 1306.76, 5.75, 39, 2.6, 11.7, 0, 0], + 'protein': [23.31, 0, 87.84, 92.2, 0.44, 0.81, 0.05, 0.31, 0, 0], + 'carbohydrates': [144.13, 0, 112.46, 112.2, 1.14, 8.46, 0.56, 2.14, 0, 0], + 'fat': [242.31, 0, 57.32, 51.24, 0.06, 0.08, 0.01, 0.25, 0, 0], + 'fiber': [8.78, 0, 16.82, 19.7, 0.46, 0.12, 0.01, 0.36, 0, 0], + 'difficulty_level': ['medium', 'easy', 'easy', 'easy', 'easy', + 'easy', 'easy', 'medium', 'medium', 'easy'] + } + return pd.DataFrame(recipes_data) + + def _load_ingredients(self) -> pd.DataFrame: + ingredients_data = { + 'id': ['salmon-001', 'yogurt-001', 'almond-001', 'chicken-001', + 'apple-001', 'banana-001', 'broccoli-001', 'egg-001', + 'rice-001', 'oats-001'], + 'name': ['Salmon', 'Greek Yogurt', 'Almonds', 'Chicken Breast', + 'Apple', 'Banana', 'Broccoli', 'Eggs', 'Brown Rice', 'Oatmeal'], + 'category': ['Fish', 'Dairy', 'Nuts', 'Meat', 'Fruits', 'Fruits', + 'Vegetables', 'Dairy', 'Grains', 'Grains'], + 'calories_per_100g': [208, 59, 579, 165, 52, 89, 34, 155, 111, 68], + 'protein': [25, 10, 21, 31, 0.3, 1.1, 2.8, 13, 2.6, 2.4], + 'carbs': [0, 3.6, 22, 0, 14, 23, 7, 1.1, 23, 12], + 'fat': [12, 0.4, 50, 3.6, 0.2, 0.3, 0.4, 11, 0.9, 1.4], + 'fiber': [0, 0, 12, 0, 2.4, 2.6, 2.6, 0, 1.8, 1.7] + } + return pd.DataFrame(ingredients_data) + + def _load_recipe_ingredients(self) -> pd.DataFrame: + recipe_ing_data = { + 'recipe_id': [22, 22, 22, 23, 23, 23, 25, 25, 26, 27], + 'ingredient_name': ['Salmon', 'Quinoa', 'Broccoli', 'Salmon', + 'Quinoa', 'Brown Rice', 'Curry Powder', 'Vegetables', + 'Saffron', 'Rice'], + 'amount': [300, 200, 150, 300, 200, 150, 20, 200, 2, 250] + } + return pd.DataFrame(recipe_ing_data) + + def _load_categories(self) -> pd.DataFrame: + categories_data = { + 'id': list(range(9, 17)), + 'name': ['Main Course', 'Appetizer', 'Dessert', 'Breakfast', + 'Lunch', 'Dinner', 'Snack', 'Beverage'], + 'description': [ + 'Primary dish of a meal', 'Starter dishes', + 'Sweet dishes served after main course', 'Morning meals', + 'Midday meals', 'Evening meals', + 'Light meals between main meals', 'Drinks and cocktails' + ] + } + return pd.DataFrame(categories_data) + + def get_condition_info(self, condition_name: str) -> Optional[Dict]: + if self.conditions is None: + return None + condition = self.conditions[self.conditions['name'].str.lower( + ) == condition_name.lower()] + if condition.empty: + return None + return { + 'name': condition.iloc[0]['name'], + 'recommended': condition.iloc[0]['recommended_foods'], + 'restricted': condition.iloc[0]['restricted_foods'], + 'severity': condition.iloc[0]['severity_level'] + } + + def search_recipes(self, query: str = '', max_results: int = 10) -> pd.DataFrame: + if self.recipes is None: + return pd.DataFrame() + if not query: + return self.recipes.head(max_results) + mask = self.recipes['recipe_name'].str.contains( + query, case=False, na=False) + return self.recipes[mask].head(max_results) + + def filter_by_allergies(self, allergy_list: List[str]) -> pd.DataFrame: + return self.recipes + + def get_high_protein_recipes(self, min_protein: float = 20) -> pd.DataFrame: + if self.recipes is None: + return pd.DataFrame() + return self.recipes[self.recipes['protein'] >= min_protein] + + def get_low_calorie_recipes(self, max_calories: float = 500) -> pd.DataFrame: + if self.recipes is None: + return pd.DataFrame() + return self.recipes[(self.recipes['calories'] > 0) & (self.recipes['calories'] <= max_calories)] diff --git a/nutrihelp_ai/docs/PROJECT_DOCUMENTATION.md b/nutrihelp_ai/docs/PROJECT_DOCUMENTATION.md new file mode 100644 index 0000000..6e65509 --- /dev/null +++ b/nutrihelp_ai/docs/PROJECT_DOCUMENTATION.md @@ -0,0 +1,219 @@ +# NutriHelp-AI Backend ā Complete Project Documentation + +**Version:** 1.0 +**Date:** December 25, 2025 +**Author:** [Your Name/Team] +**Tech Stack:** FastAPI ⢠Supabase ⢠Python ⢠Pydantic + +## Project Overview + +NutriHelp-AI is a personalized nutrition backend that: + +- Generates 3-day meal plans based on user health data +- Supports dietary preferences (vegan, keto, etc.) +- Avoids user allergies automatically +- Considers health conditions (diabetes, hypertension, celiac, etc.) +- Classifies food images with allergy warnings +- Handles incomplete profiles intelligently + +**Status:** Feature complete ā Ready for production + +## Features + +- Calorie calculation using Harris-Benedict formula +- Dietary templates with macro adjustments +- Allergy keyword avoidance + safe meal fallbacks +- Health condition-aware recommendations +- Data quality feedback (completeness %, warnings) +- Swagger UI documentation +- Full Supabase integration + +## Base URLs + +- **Local Development:** `http://localhost:8000` +- **Production:** `[Update after deployment]` + +**API Docs:** + +- Swagger UI: `/docs` +- ReDoc: `/redoc` + +## Key Endpoints + +### 1. Generate Meal Plan from Profile (PRIMARY & RECOMMENDED) + +GET /ai/mealplan/from-profile/{user_id} + +**Path Parameter:** +`user_id` ā User's Supabase auth ID (string) + +**Example:** + +GET /ai/mealplan/from-profile/user123abc + +**Success Response Includes:** + +- `success`: true +- `message`: Personalized message +- `meal_plan`: Array of 3 `DailyMealPlan` objects (breakfast, lunch, dinner, snacks) +- `data_quality`: Profile completeness % + warnings + fallbacks +- `recommendations`: List of tips (allergies avoided, health conditions considered) + +**Error Handling:** + +- 404 ā User not found ā Prompt login +- Incomplete data ā Use `recommendations` to guide user + +**Backend Flow:** + +1. Fetch from Supabase: + - `users` ā age, gender, weight (kg), height (cm) + - `user_preferences` ā activity_level, dietary_preference, etc. + - `user_allergies` ā allergy names + - `user_health_conditions` ā with joined `health_conditions` details +2. Validate required fields +3. Calculate BMR Ć activity multiplier +4. Select meals from dietary templates +5. **Skip unsafe meals** (nuts, dairy, gluten, shellfish keywords) +6. Add health-specific recommendations + +### 2. Food Image Classifier v2 using YOLO model + +**Success Response Includes:** + +- `success`: true +- `message`: Personalized message +- `meal_plan`: Array of 3 `DailyMealPlan` objects (breakfast, lunch, dinner, snacks) +- `data_quality`: Profile completeness % + warnings + fallbacks +- `recommendations`: List of tips (allergies avoided, health conditions considered) + +**Error Handling:** + +- 404 ā User not found ā Prompt login +- Incomplete data ā Use `recommendations` to guide user + +**Backend Flow:** + +1. Fetch from Supabase: + - `users` ā age, gender, weight (kg), height (cm) + - `user_preferences` ā activity_level, dietary_preference, etc. + - `user_allergies` ā allergy names + - `user_health_conditions` ā with joined `health_conditions` details +2. Validate required fields +3. Calculate BMR Ć activity multiplier +4. Select meals from dietary templates +5. **Skip unsafe meals** (nuts, dairy, gluten, shellfish keywords) +6. Add health-specific recommendations + +### 2. Food Image Classifier v2 (Dummy) + +POST /ai-model/classifier/v2 + +**Form Data:** + +- `image`: Food photo (JPG/PNG file) +- `allergies`: Optional comma-separated string (e.g., "nuts,dairy") + +**Response:** + +- Top prediction + top-3 +- Allergy warning if predicted food contains user's allergen + +## Data Flow Diagram + +Frontend +ā +GET /ai/mealplan/from-profile/{user_id} +ā +Backend fetches Supabase tables: +āā users +āā user_preferences +āā user_allergies +āā user_health_conditions ā health_conditions (join) +ā +Validate + enrich missing data +ā +Calculate calories + select safe meals +ā +Generate recommendations +ā +Return full JSON response + +## Frontend Integration Tips + +- Show `recommendations` as banners/tips +- Use `data_quality.data_completeness` for profile progress bar +- Display allergy/health messages prominently +- Cache meal plan for 24 hours (optional) +- Handle errors gracefully with user-friendly messages + +## Local Development Setup + +1. Clone repository +2. Create `dbconnection.py` file: + +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_ANON_KEY=your-anon-key + +3. Install dependencies: + +```bash +pip install -r requirements.txt +``` + +4. Run server: + +```bash +Run server:Bashuvicorn main:app --reload +Open API docs: http://localhost:8000/docs +``` + +Database Schema (Supabase Tables) + +users: id (PK), age, gender, weight, height, ... +user_preferences: user_id (FK), activity_level, dietary_preference, health_goals, meals_per_day +user_allergies: user_id (FK), allergy_name +user_health_conditions: user_id (FK), condition_id (FK), severity, diagnosed_date +health_conditions: id (PK), name, description + +Testing Guidelines +Test with users who have: + +Allergies ā Verify no unsafe ingredients appear +Health conditions ā Check correct recommendations +Incomplete profile ā Verify helpful feedback and defaults +Extreme values (age 18ā80, weight 40ā150kg) ā Proper handling + +Deployment Notes +Recommended Platforms: + +Render.com +Railway.app +Fly.io + +Production Command: +Bashuvicorn main:app --host 0.0.0.0 --port $PORT +Environment Variables Required: + +SUPABASE_URL +SUPABASE_ANON_KEY + +Health Check: Visit /docs or add simple /health endpoint if needed +Project Structure +textnutrihelp_ai/ +āāā main.py # FastAPI app + routers +āāā routers/ +ā āāā classifier_v2.py # Food classifier +ā āāā mealplan_api.py # Meal plan endpoints +āāā model/ +ā āāā dbConnection.py # Supabase client +ā āāā fetchUserProfile.py +ā āāā fetchUserPreferences.py +ā āāā fetchUserAllergies.py +ā āāā fetchAllHealthConditions.py +ā āāā fetchUserHealthConditions.py +āāā docs/ +ā āāā AI_INTEGRATION_RUNBOOK.md +āāā requirements.txt +āāā .env.example +āāā PROJECT_DOCUMENTATION.md # ā This file diff --git a/nutrihelp_ai/image_cls.py b/nutrihelp_ai/image_cls.py new file mode 100644 index 0000000..6bedcbd --- /dev/null +++ b/nutrihelp_ai/image_cls.py @@ -0,0 +1,7 @@ +from ultralytics import YOLO + +model = YOLO(r"Food-Detection\dataset\runs\detect\train2\weights\best.pt") +image_path = r"Food-Detection\dataset\images\test\3_jpg.rf.f2a1a102a11832ae1d2d6edf055f020d.jpg" +result = model( + image_path, save=True, iou=0.5) + diff --git a/nutrihelp_ai/main.py b/nutrihelp_ai/main.py index c324b9e..f0a6f03 100644 --- a/nutrihelp_ai/main.py +++ b/nutrihelp_ai/main.py @@ -1,16 +1,22 @@ -from fastapi import FastAPI, Request -from fastapi.responses import JSONResponse -from fastapi.exceptions import RequestValidationError -from fastapi.middleware.cors import CORSMiddleware +from slowapi.extension import _rate_limit_exceeded_handler +from slowapi.errors import RateLimitExceeded +import logging +from nutrihelp_ai.extensions import limiter +from nutrihelp_ai.routers import multi_image_api # NEW: Multi-image router +from nutrihelp_ai.routers import medical_report_api, chatbot_api, image_api, health_plan_api, finetune_api from starlette.exceptions import HTTPException as StarletteHTTPException +from fastapi.middleware.cors import CORSMiddleware +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse +from fastapi import FastAPI, Request +from nutrihelp_ai.routers import classifier_v2 +from nutrihelp_ai.routers import mealplan_api +from dotenv import load_dotenv +import os -from nutrihelp_ai.routers import medical_report_api, chatbot_api, image_api, health_plan_api, finetune_api -from nutrihelp_ai.routers import multi_image_api # NEW: Multi-image router -from nutrihelp_ai.extensions import limiter +# Load .env file before anything else +load_dotenv() -import logging -from slowapi.errors import RateLimitExceeded -from slowapi.extension import _rate_limit_exceeded_handler # ---- Logging Setup ---- logging.basicConfig( @@ -41,35 +47,51 @@ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # ---- Health Check ---- -@app.api_route("/", methods=["GET", "HEAD"]) + + +@app.api_route("y", methods=["GET", "HEAD"]) async def root(): return JSONResponse(status_code=200, content={"status": "ok"}) + @app.get("/healthz") async def healthz(): return JSONResponse(status_code=200, content={"status": "ok"}) # ---- Register Routers ---- -app.include_router(medical_report_api, prefix="/ai-model/medical-report", tags=["Medical Report Generation"]) -app.include_router(chatbot_api, prefix="/ai-model/chatbot", tags=["AI Assistant"]) -app.include_router(image_api, prefix="/ai-model/image-analysis", tags=["Image classification"]) +app.include_router(medical_report_api, prefix="/ai-model/medical-report", + tags=["Medical Report Generation"]) +app.include_router(chatbot_api, prefix="/ai-model/chatbot", + tags=["AI Assistant"]) +app.include_router(image_api, prefix="/ai-model/image-analysis", + tags=["Image classification"]) -app.include_router(multi_image_api.router, prefix="/ai-model/image-analysis", tags=["Multi Image Classification"]) +app.include_router(multi_image_api.router, prefix="/ai-model/image-analysis", + tags=["Multi Image Classification"]) -app.include_router(health_plan_api, prefix="/ai-model/medical-report/plan", tags=["Health Plan Generation"]) -app.include_router(finetune_api, prefix="/ai-model/chatbot-finetune", tags=["AI Assistant Fine tune"]) +app.include_router(health_plan_api, prefix="/ai-model/medical-report/plan", + tags=["Health Plan Generation"]) +app.include_router(finetune_api, prefix="/ai-model/chatbot-finetune", + tags=["AI Assistant Fine tune"]) + +app.include_router(classifier_v2.router) +app.include_router(mealplan_api.router) # ---- Custom Error Handlers ---- + + @app.exception_handler(StarletteHTTPException) async def http_exception_handler(request: Request, exc: StarletteHTTPException): logger.warning(f"HTTP Error: {exc.detail} at {request.url}") return JSONResponse(status_code=exc.status_code, content={"error": exc.detail}) + @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): logger.warning(f"Validation Error: {exc.errors()} at {request.url}") return JSONResponse(status_code=422, content={"error": "Invalid request", "details": exc.errors()}) + @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): logger.error(f"Unexpected error at {request.url}: {repr(exc)}") diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260103_155653_934c4295.txt b/nutrihelp_ai/meal_plans/meal_plan_20260103_155653_934c4295.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260103_155653_934c4295.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260103_155820_60f6f34e.txt b/nutrihelp_ai/meal_plans/meal_plan_20260103_155820_60f6f34e.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260103_155820_60f6f34e.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260103_160022_762bcfc1.txt b/nutrihelp_ai/meal_plans/meal_plan_20260103_160022_762bcfc1.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260103_160022_762bcfc1.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260103_160611_d29b1914.txt b/nutrihelp_ai/meal_plans/meal_plan_20260103_160611_d29b1914.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260103_160611_d29b1914.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260103_160810_4b5bafae.txt b/nutrihelp_ai/meal_plans/meal_plan_20260103_160810_4b5bafae.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260103_160810_4b5bafae.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260103_160913_3bb37b71.txt b/nutrihelp_ai/meal_plans/meal_plan_20260103_160913_3bb37b71.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260103_160913_3bb37b71.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260103_161125_0ed6aaed.txt b/nutrihelp_ai/meal_plans/meal_plan_20260103_161125_0ed6aaed.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260103_161125_0ed6aaed.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260103_161250_30c2f848.txt b/nutrihelp_ai/meal_plans/meal_plan_20260103_161250_30c2f848.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260103_161250_30c2f848.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260103_161508_855ba8c6.txt b/nutrihelp_ai/meal_plans/meal_plan_20260103_161508_855ba8c6.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260103_161508_855ba8c6.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260103_161904_c57ea128.txt b/nutrihelp_ai/meal_plans/meal_plan_20260103_161904_c57ea128.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260103_161904_c57ea128.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260103_162711_578cc10e.txt b/nutrihelp_ai/meal_plans/meal_plan_20260103_162711_578cc10e.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260103_162711_578cc10e.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260103_162855_53a7bfde.txt b/nutrihelp_ai/meal_plans/meal_plan_20260103_162855_53a7bfde.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260103_162855_53a7bfde.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260103_163209_8837f720.txt b/nutrihelp_ai/meal_plans/meal_plan_20260103_163209_8837f720.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260103_163209_8837f720.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260103_163305_8ee835c0.txt b/nutrihelp_ai/meal_plans/meal_plan_20260103_163305_8ee835c0.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260103_163305_8ee835c0.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260103_163800_076b849c.txt b/nutrihelp_ai/meal_plans/meal_plan_20260103_163800_076b849c.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260103_163800_076b849c.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260103_164030_7df48488.txt b/nutrihelp_ai/meal_plans/meal_plan_20260103_164030_7df48488.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260103_164030_7df48488.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260103_164431_9e18073f.txt b/nutrihelp_ai/meal_plans/meal_plan_20260103_164431_9e18073f.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260103_164431_9e18073f.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260103_164804_fd0bb08b.txt b/nutrihelp_ai/meal_plans/meal_plan_20260103_164804_fd0bb08b.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260103_164804_fd0bb08b.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260113_103830_fa30e7ea.txt b/nutrihelp_ai/meal_plans/meal_plan_20260113_103830_fa30e7ea.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260113_103830_fa30e7ea.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260113_131905_c71a59d3.txt b/nutrihelp_ai/meal_plans/meal_plan_20260113_131905_c71a59d3.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260113_131905_c71a59d3.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260113_152125_40b4314c.txt b/nutrihelp_ai/meal_plans/meal_plan_20260113_152125_40b4314c.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260113_152125_40b4314c.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260113_153123_99f8b109.txt b/nutrihelp_ai/meal_plans/meal_plan_20260113_153123_99f8b109.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260113_153123_99f8b109.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260121_224221_36299f59.txt b/nutrihelp_ai/meal_plans/meal_plan_20260121_224221_36299f59.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260121_224221_36299f59.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260121_225213_c87b4bc6.txt b/nutrihelp_ai/meal_plans/meal_plan_20260121_225213_c87b4bc6.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260121_225213_c87b4bc6.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260125_225021_cf39e4e0.txt b/nutrihelp_ai/meal_plans/meal_plan_20260125_225021_cf39e4e0.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260125_225021_cf39e4e0.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260125_225248_138aa0be.txt b/nutrihelp_ai/meal_plans/meal_plan_20260125_225248_138aa0be.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260125_225248_138aa0be.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260129_151820_793a7018.txt b/nutrihelp_ai/meal_plans/meal_plan_20260129_151820_793a7018.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260129_151820_793a7018.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260129_221500_601b16e5.txt b/nutrihelp_ai/meal_plans/meal_plan_20260129_221500_601b16e5.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260129_221500_601b16e5.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260129_221719_dc454ced.txt b/nutrihelp_ai/meal_plans/meal_plan_20260129_221719_dc454ced.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260129_221719_dc454ced.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260129_222035_a77bf13e.txt b/nutrihelp_ai/meal_plans/meal_plan_20260129_222035_a77bf13e.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260129_222035_a77bf13e.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260129_222112_357a0d63.txt b/nutrihelp_ai/meal_plans/meal_plan_20260129_222112_357a0d63.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260129_222112_357a0d63.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260129_222207_5a903eb4.txt b/nutrihelp_ai/meal_plans/meal_plan_20260129_222207_5a903eb4.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260129_222207_5a903eb4.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260129_223704_2d311ed7.txt b/nutrihelp_ai/meal_plans/meal_plan_20260129_223704_2d311ed7.txt new file mode 100644 index 0000000..4b8f834 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260129_223704_2d311ed7.txt @@ -0,0 +1,6 @@ +Sample Meal Plan +Day 1: +Breakfast: Oatmeal with fruits +Lunch: Grilled chicken salad +Dinner: Baked salmon with veggies +... \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260131_151804_257c6a6b.txt b/nutrihelp_ai/meal_plans/meal_plan_20260131_151804_257c6a6b.txt new file mode 100644 index 0000000..ff5ec28 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260131_151804_257c6a6b.txt @@ -0,0 +1,3 @@ +Sample Meal Plan +ERROR: Medical condition answer is invalid. Please state 'None' or a specific condition (e.g., Diabetes). +Please provide a correct answer. \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260131_152002_e219d26b.txt b/nutrihelp_ai/meal_plans/meal_plan_20260131_152002_e219d26b.txt new file mode 100644 index 0000000..ff5ec28 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260131_152002_e219d26b.txt @@ -0,0 +1,3 @@ +Sample Meal Plan +ERROR: Medical condition answer is invalid. Please state 'None' or a specific condition (e.g., Diabetes). +Please provide a correct answer. \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260131_153324_aa3b19cc.txt b/nutrihelp_ai/meal_plans/meal_plan_20260131_153324_aa3b19cc.txt new file mode 100644 index 0000000..eac410e --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260131_153324_aa3b19cc.txt @@ -0,0 +1,3 @@ +Sample Meal Plan +ERROR: Allergy answer is invalid. Please state 'None' or specific foods (e.g., Peanuts, Dairy). +Please provide a correct answer. \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260131_154858_4edd27af.txt b/nutrihelp_ai/meal_plans/meal_plan_20260131_154858_4edd27af.txt new file mode 100644 index 0000000..1e6bf90 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260131_154858_4edd27af.txt @@ -0,0 +1,2 @@ +Sample Meal Plan +ERROR: Medical condition answer is invalid. Please state None or a specific condition. \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260131_155323_37b2f76b.txt b/nutrihelp_ai/meal_plans/meal_plan_20260131_155323_37b2f76b.txt new file mode 100644 index 0000000..3f283f7 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260131_155323_37b2f76b.txt @@ -0,0 +1,2 @@ +Sample Meal Plan +ERROR: Allergy answer is invalid. \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260131_155344_ac9a5487.txt b/nutrihelp_ai/meal_plans/meal_plan_20260131_155344_ac9a5487.txt new file mode 100644 index 0000000..d4454cf --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260131_155344_ac9a5487.txt @@ -0,0 +1,2 @@ +Sample Meal Plan +ERROR: Medical condition answer is invalid. Please state 'None' or a specific condition (e.g., diabetes, hypertension). \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260131_155410_775b0662.txt b/nutrihelp_ai/meal_plans/meal_plan_20260131_155410_775b0662.txt new file mode 100644 index 0000000..3f283f7 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260131_155410_775b0662.txt @@ -0,0 +1,2 @@ +Sample Meal Plan +ERROR: Allergy answer is invalid. \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260131_155550_6d4d835c.txt b/nutrihelp_ai/meal_plans/meal_plan_20260131_155550_6d4d835c.txt new file mode 100644 index 0000000..3f283f7 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260131_155550_6d4d835c.txt @@ -0,0 +1,2 @@ +Sample Meal Plan +ERROR: Allergy answer is invalid. \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260131_160119_0d612d3b.txt b/nutrihelp_ai/meal_plans/meal_plan_20260131_160119_0d612d3b.txt new file mode 100644 index 0000000..d4454cf --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260131_160119_0d612d3b.txt @@ -0,0 +1,2 @@ +Sample Meal Plan +ERROR: Medical condition answer is invalid. Please state 'None' or a specific condition (e.g., diabetes, hypertension). \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260131_160228_1f2223d8.txt b/nutrihelp_ai/meal_plans/meal_plan_20260131_160228_1f2223d8.txt new file mode 100644 index 0000000..3f283f7 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260131_160228_1f2223d8.txt @@ -0,0 +1,2 @@ +Sample Meal Plan +ERROR: Allergy answer is invalid. \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260131_160432_11dedb34.txt b/nutrihelp_ai/meal_plans/meal_plan_20260131_160432_11dedb34.txt new file mode 100644 index 0000000..bef1e2c --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260131_160432_11dedb34.txt @@ -0,0 +1,12 @@ +=== YOUR PERSONALIZED MEAL PLAN === + +Goal: Lose Weight +Activity: Running +Level: Beginner +Diet: Non-Vegetarian +Condition: Diabetes +Allergies: Pe, uts + +================================================== + +System Error: ClientError: 400 INVALID_ARGUMENT. {'error': {'code': 400, 'message': 'API key expired. Please renew the API key.', 'status': 'INVALID_ARGUMENT', 'details': [{'@type': 'type.googleapis.com/google.rpc.ErrorInfo', 'reason': 'API_KEY_INVALID', 'domain': 'googleapis.com', 'metadata': {'service': 'generativelanguage.googleapis.com'}}, {'@type': 'type.googleapis.com/google.rpc.LocalizedMessage', 'locale': 'en-US', 'message': 'API key expired. Please renew the API key.'}]}} \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260131_160628_e54b10cb.txt b/nutrihelp_ai/meal_plans/meal_plan_20260131_160628_e54b10cb.txt new file mode 100644 index 0000000..f6cb044 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260131_160628_e54b10cb.txt @@ -0,0 +1,79 @@ +=== YOUR PERSONALIZED MEAL PLAN === + +Goal: Lose Weight +Activity: Running +Level: Beginner +Diet: Non-Vegetarian +Condition: Diabetes +Allergies: Pe, uts + +================================================== + +Day 1 +Breakfast: Scrambled eggs (3 large) with spinach, 1 slice whole wheat toast, and 1/2 avocado. (Approx 380 calories) +Mid-Morning Snack: 1 medium apple. +Lunch: Grilled chicken breast (5oz) salad with mixed greens, cucumber, tomato, bell peppers, and 2 tbsp olive oil vinaigrette. (Approx 450 calories) +Evening Snack: 1 hard-boiled egg. +Dinner: Baked salmon (5oz) with steamed broccoli and quinoa (3/4 cup cooked). (Approx 550 calories) +Total Calories: Approx 1553 + +Day 2 +Breakfast: Oatmeal (1 cup dry) cooked with water, topped with 1/2 cup mixed berries and a sprinkle of cinnamon. (Approx 350 calories) +Mid-Morning Snack: 1 cup plain Greek yogurt (no nuts). +Lunch: Turkey breast (5oz) and vegetable wrap (large whole wheat tortilla, lettuce, tomato, cucumber) with mustard. (Approx 450 calories) +Evening Snack: 1 orange. +Dinner: Lean ground beef (5oz) stir-fry with mixed vegetables (broccoli, carrots, snap peas) and 3/4 cup brown rice. (Approx 600 calories) +Total Calories: Approx 1610 + +Day 3 +Breakfast: Whole wheat toast (2 slices) with 2 fried eggs and 1 slice low-fat cheese. (Approx 380 calories) +Mid-Morning Snack: 1 cup sliced cucumber with 2 tbsp hummus (chickpea based, no nuts). +Lunch: Leftover lean ground beef stir-fry (4oz beef, 1/2 cup rice, vegetables). (Approx 450 calories) +Evening Snack: 1 small banana. +Dinner: Baked cod (5oz) with roasted sweet potato (1 medium) and green beans. (Approx 500 calories) +Total Calories: Approx 1500 + +Day 4 +Breakfast: Smoothie with 1 scoop whey protein (no nuts), 1 cup spinach, 1/2 banana, 1 cup unsweetened dairy milk or water. (Approx 300 calories) +Mid-Morning Snack: 1 small banana. +Lunch: Large mixed green salad with grilled chicken (4oz), chickpeas (1/2 cup), and a light lemon-herb dressing. (Approx 400 calories) +Evening Snack: 1 cup cottage cheese (low fat). +Dinner: Chicken breast (5oz) baked with herbs, served with 1 cup roasted asparagus and 1 small baked potato. (Approx 500 calories) +Total Calories: Approx 1500 + +Day 5 +Breakfast: Egg white omelet (4 egg whites) with bell peppers and onions, 1 slice whole wheat toast. (Approx 280 calories) +Mid-Morning Snack: 1 cup mixed berries. +Lunch: Tuna (canned in water, 5oz) salad (with light mayo, celery) on 2 slices whole wheat bread. (Approx 450 calories) +Evening Snack: 1 rice cake with 2 slices lean turkey. +Dinner: Pork tenderloin (5oz) roasted with 1 cup roasted Brussels sprouts and 1/2 cup wild rice. (Approx 550 calories) +Total Calories: Approx 1500 + +Day 6 +Breakfast: Whole wheat pancakes (2 small) with 1/2 cup berries and 2 tbsp sugar-free syrup. (Approx 350 calories) +Mid-Morning Snack: 1 hard-boiled egg. +Lunch: Chicken and vegetable soup (broth-based, 2 cups) with 1 whole wheat roll. (Approx 400 calories) +Evening Snack: 1 medium apple. +Dinner: Baked white fish (e.g., tilapia, 5oz) with a large serving of steamed mixed vegetables and 1/2 cup couscous. (Approx 500 calories) +Total Calories: Approx 1500 + +Day 7 +Breakfast: Scrambled eggs (3 large) with 1/2 cup black beans and 1/4 avocado. (Approx 400 calories) +Mid-Morning Snack: 1 cup plain Greek yogurt (no nuts). +Lunch: Lean ground turkey (5oz) patty (no bun) with a side salad and 1/2 cup sweet potato fries (baked). (Approx 450 calories) +Evening Snack: 1 orange. +Dinner: Grilled steak (lean cut, 5oz) with 1 cup steamed green beans and 1 small baked potato. (Approx 550 calories) +Total Calories: Approx 1600 + +SPORT GEAR CHECKLIST FOR Running: +Running Shoes (properly fitted) +Moisture-Wicking Socks +Moisture-Wicking T-shirt or Top +Running Shorts or Leggings +Sports Bra (for women) +Water Bottle or Hydration Pack +Hat or Visor (for sun protection) +Sunglasses +Lightweight Jacket or Windbreaker (for cooler weather) +Headlamp or Reflective Gear (for low-light conditions) +GPS Watch (optional, for tracking progress) \ No newline at end of file diff --git a/nutrihelp_ai/meal_plans/meal_plan_20260131_161120_a61b6612.txt b/nutrihelp_ai/meal_plans/meal_plan_20260131_161120_a61b6612.txt new file mode 100644 index 0000000..bc5c878 --- /dev/null +++ b/nutrihelp_ai/meal_plans/meal_plan_20260131_161120_a61b6612.txt @@ -0,0 +1,69 @@ +=== YOUR PERSONALIZED MEAL PLAN === + +Goal: Lose Weight +Activity: Running +Level: Intermediate +Diet: Non-Vegetarian +Condition: None +Allergies: None + +================================================== + +Day 1 +Breakfast: 3 scrambled egg whites with 1 whole egg, 1/2 cup spinach, 1 slice whole-wheat toast with 1 tsp avocado. (Approx. 350 kcal) +Mid-Morning Snack: 1 medium apple with 1 tbsp peanut butter. +Lunch: 5 oz grilled chicken breast, 1 cup mixed greens, 1/2 cup quinoa, 1/4 cup chickpeas, 2 tbsp light vinaigrette. (Approx. 480 kcal) +Evening Snack: 1/2 cup plain Greek yogurt (0% fat). +Dinner: 5 oz baked salmon, 1 cup steamed green beans, 1 small sweet potato (baked). (Approx. 550 kcal) +Total Calories: 1640 + +Day 2 +Breakfast: 1 cup plain Greek yogurt (0% fat) with 1/4 cup mixed berries and 1 tbsp chopped walnuts. (Approx. 320 kcal) +Mid-Morning Snack: 1 small banana. +Lunch: Turkey and veggie wrap: 4 oz sliced turkey breast, large whole-wheat tortilla, lettuce, tomato, cucumber, 1 tbsp hummus. (Approx. 420 kcal) +Evening Snack: 1 rice cake with 1 tbsp almond butter. +Dinner: 5 oz lean ground beef stir-fry with 2 cups mixed vegetables (broccoli, carrots, bell peppers) and 1/2 cup brown rice. (Approx. 600 kcal) +Total Calories: 1610 + +Day 3 +Breakfast: Whole-wheat English muffin with 2 scrambled eggs and 1 slice low-fat cheese. (Approx. 380 kcal) +Mid-Morning Snack: 1 medium pear. +Lunch: Leftover lean ground beef stir-fry (5 oz meat, 1.5 cups veggies, 1/2 cup brown rice). (Approx. 600 kcal) +Evening Snack: 1/2 cup cottage cheese (low-fat). +Dinner: 5 oz baked chicken breast with 1 cup roasted Brussels sprouts and 1/2 cup quinoa. (Approx. 480 kcal) +Total Calories: 1640 + +Day 4 +Breakfast: Smoothie: 1 scoop whey protein, 1/2 cup spinach, 1/2 banana, 1 cup unsweetened almond milk, 1 slice whole-wheat toast. (Approx. 380 kcal) +Mid-Morning Snack: 10-12 almonds. +Lunch: Large salad with 5 oz grilled shrimp, mixed greens, cherry tomatoes, cucumber, 1/4 avocado, 2 tbsp olive oil and lemon dressing. (Approx. 480 kcal) +Evening Snack: 1 medium orange. +Dinner: 6 oz baked cod with 1 cup steamed mixed vegetables and 1 small baked sweet potato. (Approx. 600 kcal) +Total Calories: 1590 + +Day 5 +Breakfast: 1/2 cup rolled oats cooked with water, topped with 1/4 cup blueberries and 1 tbsp chia seeds. (Approx. 350 kcal) +Mid-Morning Snack: 1 hard-boiled egg. +Lunch: Chicken and vegetable soup (2 cups) with 1 small whole-wheat roll. (Approx. 400 kcal) +Evening Snack: 1/2 cup plain Greek yogurt (0% fat). +Dinner: 5 oz pan-seared sirloin steak (lean cut) with 1 cup roasted broccoli and 1/2 cup brown rice. (Approx. 650 kcal) +Total Calories: 1540 + +Day 6 +Breakfast: 2 slices whole-wheat toast with 2 scrambled eggs and 1/4 avocado. (Approx. 400 kcal) +Mid-Morning Snack: 1 medium apple. +Lunch: Large salad with 5 oz canned tuna (in water), mixed greens, cucumber, bell peppers, 1/4 avocado, 2 tbsp olive oil and lemon dressing. (Approx. 500 kcal) +Evening Snack: 1/2 cup cottage cheese (low-fat). +Dinner: 5 oz baked chicken breast with 1 cup steamed green beans and 1 small baked sweet potato. (Approx. 550 kcal) +Total Calories: 1610 + +Day 7 +Breakfast: 1 cup plain Greek yogurt (0% fat) with 1/4 cup mixed berries and 1 tbsp flax seeds. (Approx. 300 kcal) +Mid-Morning Snack: 1 small banana. +Lunch: Leftover baked chicken breast (5 oz) with 1 cup steamed green beans and 1 small baked sweet potato. (Approx. 550 kcal) +Evening Snack: 1 hard-boiled egg. +Dinner: 5 oz baked salmon with 1 cup roasted asparagus and 1/2 cup quinoa. (Approx. 550 kcal) +Total Calories: 1560 + +SPORT GEAR CHECKLIST FOR Running: +Standard Athletic Wear \ No newline at end of file diff --git a/nutrihelp_ai/model/dbConnection.py b/nutrihelp_ai/model/dbConnection.py new file mode 100644 index 0000000..20d1c9c --- /dev/null +++ b/nutrihelp_ai/model/dbConnection.py @@ -0,0 +1,75 @@ +# nutrihelp_ai/model/dbConnection.py + +import os +import sys +from supabase import create_client, Client +from typing import Optional +import logging + +logger = logging.getLogger(__name__) + + +class SupabaseConnection: + """ + Supabase database connection handler + Python equivalent of your Node.js dbConnection.js + """ + + _instance: Optional[Client] = None + + @classmethod + def get_client(cls) -> Client: + """Get or create Supabase client with environment validation""" + if cls._instance is None: + # Get environment variables + # supabase_url = os.getenv("SUPABASE_URL") + # supabase_anon_key = os.getenv("SUPABASE_ANON_KEY") + supabase_url = "https://mdauzoueyzgtqsojttkp.supabase.co" + supabase_anon_key = "sb_publishable_iPtvcFFdWB3q9YweOVHw8Q_ZEyhv_Jh" + + # Check if environment variables are loaded + if not supabase_url or not supabase_anon_key: + print('ā Missing required environment variables:') + print( + f' SUPABASE_URL: {"ā Set" if supabase_url else "ā Missing"}') + print( + f' SUPABASE_ANON_KEY: {"ā Set" if supabase_anon_key else "ā Missing"}') + print('\nš” Please check your .env file contains:') + print(' SUPABASE_URL=your_supabase_project_url') + print(' SUPABASE_ANON_KEY=your_supabase_anon_key') + sys.exit(1) + + # Create Supabase client + try: + cls._instance = create_client(supabase_url, supabase_anon_key) + logger.info("ā Supabase client connected successfully") + print("ā Supabase client connected successfully") + except Exception as e: + logger.error(f"ā Failed to connect to Supabase: {str(e)}") + print(f"ā Failed to connect to Supabase: {str(e)}") + sys.exit(1) + + return cls._instance + + +# Export the connection function (like module.exports in Node.js) +def get_supabase() -> Client: + """ + Get Supabase client instance + Equivalent to: module.exports = createClient(...) + """ + return SupabaseConnection.get_client() + + +# Test connection when run directly +if __name__ == "__main__": + from dotenv import load_dotenv + load_dotenv() + + print("Testing Supabase connection...") + try: + client = get_supabase() + print("ā Connection test successful!") + print(f"š Connected to: {os.getenv('SUPABASE_URL')}") + except Exception as e: + print(f"ā Connection test failed: {str(e)}") diff --git a/nutrihelp_ai/model/fetchAllHealthConditions.py b/nutrihelp_ai/model/fetchAllHealthConditions.py new file mode 100644 index 0000000..8195261 --- /dev/null +++ b/nutrihelp_ai/model/fetchAllHealthConditions.py @@ -0,0 +1,153 @@ +# nutrihelp_ai/model/fetchAllHealthConditions.py + +from .dbConnection import get_supabase +from typing import List, Dict, Any, Optional +import logging + +logger = logging.getLogger(__name__) + + +async def fetch_all_health_conditions() -> List[Dict[str, Any]]: + """ + Fetch all health conditions from database + + JavaScript equivalent (YOUR EXACT CODE): + async function fetchAllHealthConditions() { + let { data, error } = await supabase + .from('health_conditions') + .select('*'); + if (error) throw error; + return data; + } + + Returns: + List of health condition dicts with fields: id, name, description, etc. + Returns empty list if no conditions found + + Raises: + Exception: If database query fails + """ + try: + supabase = get_supabase() + + response = supabase.table('health_conditions').select('*').execute() + + if response.data: + logger.info(f"ā Fetched {len(response.data)} health conditions") + return response.data + else: + logger.warning("ā ļø No health conditions found in database") + return [] + + except Exception as error: + logger.error(f"ā Error fetching health conditions: {str(error)}") + raise error + + +async def fetch_health_condition_by_id(condition_id: int) -> Optional[Dict[str, Any]]: + """ + Fetch a specific health condition by ID + + Args: + condition_id: Health condition ID + + Returns: + Health condition dict or None if not found + """ + try: + supabase = get_supabase() + + response = ( + supabase.table('health_conditions') + .select('*') + .eq('id', condition_id) + .execute() + ) + + if response.data and len(response.data) > 0: + condition = response.data[0] + logger.info(f"ā Fetched health condition: {condition.get('name')}") + return condition + else: + logger.warning(f"ā ļø Health condition not found: {condition_id}") + return None + + except Exception as error: + logger.error(f"ā Error fetching health condition: {str(error)}") + raise error + + +async def fetch_health_condition_by_name(name: str) -> Optional[Dict[str, Any]]: + """ + Fetch a specific health condition by name + + Args: + name: Health condition name + + Returns: + Health condition dict or None if not found + """ + try: + supabase = get_supabase() + + response = ( + supabase.table('health_conditions') + .select('*') + .ilike('name', f'%{name}%') + .execute() + ) + + if response.data and len(response.data) > 0: + condition = response.data[0] + logger.info(f"ā Fetched health condition: {condition.get('name')}") + return condition + else: + logger.warning(f"ā ļø Health condition not found: {name}") + return None + + except Exception as error: + logger.error(f"ā Error fetching health condition: {str(error)}") + raise error + + +# Export (like module.exports) +__all__ = [ + 'fetch_all_health_conditions', + 'fetch_health_condition_by_id', + 'fetch_health_condition_by_name' +] + + +# Test function +if __name__ == "__main__": + import asyncio + from dotenv import load_dotenv + load_dotenv() + + async def test(): + print("Testing fetch_all_health_conditions...") + try: + conditions = await fetch_all_health_conditions() + print(f"ā Fetched {len(conditions)} health conditions:") + for cond in conditions: + print(f" - {cond.get('name')}: {cond.get('description')}") + + # Test by ID + if conditions: + first_id = conditions[0].get('id') + print(f"\nTesting fetch by ID ({first_id})...") + cond = await fetch_health_condition_by_id(first_id) + print(f"ā Found: {cond.get('name')}") + + # Test by name + print("\nTesting fetch by name ('Diabetes')...") + cond = await fetch_health_condition_by_name('Diabetes') + if cond: + print(f"ā Found: {cond.get('name')}") + else: + print("ā ļø Not found") + + except Exception as e: + print(f"ā Error: {e}") + + asyncio.run(test()) diff --git a/nutrihelp_ai/model/fetchUserHealthConditions.py b/nutrihelp_ai/model/fetchUserHealthConditions.py new file mode 100644 index 0000000..dae9382 --- /dev/null +++ b/nutrihelp_ai/model/fetchUserHealthConditions.py @@ -0,0 +1,225 @@ +# nutrihelp_ai/model/fetchUserHealthConditions.py + +from .dbConnection import get_supabase +from typing import List, Dict, Any, Optional +import logging + +logger = logging.getLogger(__name__) + +async def fetch_user_health_conditions(user_id: str) -> List[Dict[str, Any]]: + """ + Fetch health conditions for a specific user with full condition details + + JavaScript equivalent: + async function fetchUserHealthConditions(userId) { + const { data, error } = await supabase + .from('user_health_conditions') + .select(` + *, + health_conditions (*) + `) + .eq('user_id', userId); + if (error) throw error; + return data; + } + + Args: + user_id: User ID to fetch health conditions for + + Returns: + List of user health conditions with nested condition details + Each item contains: + - id: user_health_condition record ID + - user_id: User ID + - condition_id: Health condition ID + - severity: Condition severity (mild, moderate, severe) + - diagnosed_date: When condition was diagnosed + - health_conditions: Nested health condition details (name, description) + + Returns empty list if no conditions found + + Raises: + Exception: If database query fails + """ + try: + supabase = get_supabase() + + response = ( + supabase.table('user_health_conditions') + .select('*, health_conditions(*)') + .eq('user_id', user_id) + .execute() + ) + + if response.data: + condition_names = [ + item.get('health_conditions', {}).get('name', 'Unknown') + for item in response.data + ] + logger.info(f"ā Fetched {len(response.data)} health conditions for user: {user_id} ({', '.join(condition_names)})") + return response.data + else: + logger.info(f"ā¹ļø No health conditions found for user: {user_id}") + return [] + + except Exception as error: + logger.error(f"ā Error fetching user health conditions: {str(error)}") + raise error + + +async def fetch_user_condition_names(user_id: str) -> List[str]: + """ + Fetch only the names of user's health conditions + + Args: + user_id: User ID + + Returns: + List of condition names (strings) + + Example: + ['Diabetes', 'Hypertension', 'Celiac Disease'] + """ + try: + conditions = await fetch_user_health_conditions(user_id) + condition_names = [ + cond.get('health_conditions', {}).get('name', '') + for cond in conditions + if cond.get('health_conditions') + ] + return [name for name in condition_names if name] # Filter empty strings + + except Exception as error: + logger.error(f"ā Error fetching user condition names: {str(error)}") + raise error + + +async def add_user_health_condition( + user_id: str, + condition_id: int, + severity: str = 'moderate', + diagnosed_date: Optional[str] = None +) -> Dict[str, Any]: + """ + Add a health condition to a user + + Args: + user_id: User ID + condition_id: Health condition ID + severity: Condition severity ('mild', 'moderate', 'severe') + diagnosed_date: Date diagnosed (YYYY-MM-DD format) + + Returns: + Created user_health_condition record + + Raises: + Exception: If database insert fails + """ + try: + supabase = get_supabase() + + data = { + 'user_id': user_id, + 'condition_id': condition_id, + 'severity': severity + } + + if diagnosed_date: + data['diagnosed_date'] = diagnosed_date + + response = ( + supabase.table('user_health_conditions') + .insert(data) + .execute() + ) + + if response.data: + logger.info(f"ā Added health condition {condition_id} to user: {user_id}") + return response.data[0] + else: + raise Exception("Failed to add health condition") + + except Exception as error: + logger.error(f"ā Error adding user health condition: {str(error)}") + raise error + + +async def remove_user_health_condition(user_id: str, condition_id: int) -> bool: + """ + Remove a health condition from a user + + Args: + user_id: User ID + condition_id: Health condition ID to remove + + Returns: + True if successfully removed + + Raises: + Exception: If database delete fails + """ + try: + supabase = get_supabase() + + response = ( + supabase.table('user_health_conditions') + .delete() + .eq('user_id', user_id) + .eq('condition_id', condition_id) + .execute() + ) + + logger.info(f"ā Removed health condition {condition_id} from user: {user_id}") + return True + + except Exception as error: + logger.error(f"ā Error removing user health condition: {str(error)}") + raise error + + +# Export +__all__ = [ + 'fetch_user_health_conditions', + 'fetch_user_condition_names', + 'add_user_health_condition', + 'remove_user_health_condition' +] + + +# Test functions +if __name__ == "__main__": + import asyncio + from dotenv import load_dotenv + load_dotenv() + + async def test(): + print("Testing user health conditions functions...") + try: + user_id = "user123" + + # Test fetch user conditions + print(f"\n1. Fetching conditions for user: {user_id}") + conditions = await fetch_user_health_conditions(user_id) + print(f"ā Found {len(conditions)} conditions:") + for cond in conditions: + hc = cond.get('health_conditions', {}) + print(f" - {hc.get('name')}: {cond.get('severity')}") + + # Test fetch condition names only + print(f"\n2. Fetching condition names only...") + names = await fetch_user_condition_names(user_id) + print(f"ā Condition names: {', '.join(names)}") + + # Uncomment to test add/remove (will modify database) + # print(f"\n3. Testing add condition...") + # await add_user_health_condition(user_id, 1, 'mild') + # print("ā Condition added") + + # print(f"\n4. Testing remove condition...") + # await remove_user_health_condition(user_id, 1) + # print("ā Condition removed") + + except Exception as e: + print(f"ā Error: {e}") + + asyncio.run(test()) \ No newline at end of file diff --git a/nutrihelp_ai/model/fetchUserPreferences.py b/nutrihelp_ai/model/fetchUserPreferences.py new file mode 100644 index 0000000..ba5305e --- /dev/null +++ b/nutrihelp_ai/model/fetchUserPreferences.py @@ -0,0 +1,132 @@ +# nutrihelp_ai/model/fetchUserPreferences.py + +from .dbConnection import get_supabase +from typing import Optional, Dict, Any, List +import logging + +logger = logging.getLogger(__name__) + + +async def fetch_user_preferences(user_id: str) -> Optional[Dict[str, Any]]: + """ + Fetch user preferences (activity_level, dietary_preference, etc.) + + JavaScript equivalent: + async function fetchUserPreferences(userId) { + const { data, error } = await supabase + .from('user_preferences') + .select('*') + .eq('user_id', userId) + .single(); + if (error) throw error; + return data; + } + + Args: + user_id: User ID to fetch preferences for + + Returns: + User preferences dict with fields: activity_level, dietary_preference, + health_goals, meals_per_day, etc. + Returns None if preferences not found + + Raises: + Exception: If database query fails + """ + try: + supabase = get_supabase() + + response = ( + supabase.table('user_preferences') + .select('*') + .eq('user_id', user_id) + .execute() + ) + + if response.data and len(response.data) > 0: + prefs = response.data[0] + logger.info( + f"ā Fetched preferences for: {user_id} (activity: {prefs.get('activity_level')}, diet: {prefs.get('dietary_preference')})") + return prefs + else: + logger.warning(f"ā ļø No preferences found for: {user_id}") + return None + + except Exception as error: + logger.error(f"ā Error fetching preferences: {str(error)}") + raise error + + +async def fetch_user_allergies(user_id: str) -> List[str]: + """ + Fetch user allergies as list of strings + + JavaScript equivalent: + async function fetchUserAllergies(userId) { + const { data, error } = await supabase + .from('user_allergies') + .select('allergy_name') + .eq('user_id', userId); + if (error) throw error; + return data.map(item => item.allergy_name); + } + + Args: + user_id: User ID to fetch allergies for + + Returns: + List of allergy names (strings) + Returns empty list if no allergies found + + Raises: + Exception: If database query fails + """ + try: + supabase = get_supabase() + + response = ( + supabase.table('user_allergies') + .select('allergy_name') + .eq('user_id', user_id) + .execute() + ) + + if response.data: + allergies = [item['allergy_name'] for item in response.data] + logger.info( + f"ā Fetched {len(allergies)} allergies for: {user_id} ({', '.join(allergies)})") + return allergies + else: + logger.info(f"ā¹ļø No allergies found for: {user_id}") + return [] + + except Exception as error: + logger.error(f"ā Error fetching allergies: {str(error)}") + raise error + + +# Export +__all__ = ['fetch_user_preferences', 'fetch_user_allergies'] + + +# Test functions +if __name__ == "__main__": + import asyncio + from dotenv import load_dotenv + load_dotenv() + + async def test(): + print("Testing fetch_user_preferences and fetch_user_allergies...") + try: + # Test preferences + prefs = await fetch_user_preferences("user123") + print(f"ā Preferences: {prefs}") + + # Test allergies + allergies = await fetch_user_allergies("user123") + print(f"ā Allergies: {allergies}") + + except Exception as e: + print(f"ā Error: {e}") + + asyncio.run(test()) diff --git a/nutrihelp_ai/model/fetchUserProfile.py b/nutrihelp_ai/model/fetchUserProfile.py new file mode 100644 index 0000000..92cfb06 --- /dev/null +++ b/nutrihelp_ai/model/fetchUserProfile.py @@ -0,0 +1,80 @@ +# nutrihelp_ai/model/fetchUserProfile.py + +from .dbConnection import get_supabase +from typing import Optional, Dict, Any +import logging + +logger = logging.getLogger(__name__) + + +async def fetch_user_profile(user_id: str) -> Optional[Dict[str, Any]]: + """ + Fetch user profile (age, gender, weight, height, etc.) + + JavaScript equivalent: + async function fetchUserProfile(userId) { + const { data, error } = await supabase + .from('users') + .select('*') + .eq('id', userId) + .single(); + if (error) throw error; + return data; + } + + Args: + user_id: User ID to fetch profile for + + Returns: + User profile dict with fields: id, age, gender, weight, height, etc. + Returns None if user not found + + Raises: + Exception: If database query fails + """ + try: + supabase = get_supabase() + + response = ( + supabase.table('users') + .select('*') + .eq('user_id', user_id) + .execute() + ) + + if response.data and len(response.data) > 0: + user = response.data[0] + logger.info( + f"ā Fetched user profile: {user_id} (age: {user.get('age')}, gender: {user.get('gender')})") + return user + else: + logger.warning(f"ā ļø User not found: {user_id}") + return None + + except Exception as error: + logger.error(f"ā Error fetching user profile: {str(error)}") + raise error + + +# Export (like module.exports) +__all__ = ['fetch_user_profile'] + + +# Test function +if __name__ == "__main__": + import asyncio + from dotenv import load_dotenv + load_dotenv() + + async def test(): + print("Testing fetch_user_profile...") + try: + profile = await fetch_user_profile("user123") + if profile: + print(f"ā Profile fetched: {profile}") + else: + print("ā ļø User not found") + except Exception as e: + print(f"ā Error: {e}") + + asyncio.run(test()) diff --git a/nutrihelp_ai/others/prototype/package-lock.json b/nutrihelp_ai/others/prototype/package-lock.json index 49a393e..19b8bea 100644 --- a/nutrihelp_ai/others/prototype/package-lock.json +++ b/nutrihelp_ai/others/prototype/package-lock.json @@ -15113,16 +15113,17 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "license": "Apache-2.0", "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=14.17" + "node": ">=4.2.0" } }, "node_modules/unbox-primitive": { diff --git a/nutrihelp_ai/routers/classifier_v2.py b/nutrihelp_ai/routers/classifier_v2.py new file mode 100644 index 0000000..0d4365c --- /dev/null +++ b/nutrihelp_ai/routers/classifier_v2.py @@ -0,0 +1,163 @@ +from fastapi import APIRouter, UploadFile, File, Form, HTTPException +from pydantic import BaseModel +from typing import List, Optional +from ultralytics import YOLO +import io +from PIL import Image + +# Load YOLO model +model = YOLO(r"C:\Users\Naman Shah\OneDrive\Documents\Assignement_Deakin\T3\SIT764\Project\NutriHelp-AI\nutrihelp_ai\model\image_class.pt") + +router = APIRouter( + prefix="/ai-model/classifier", + tags=["Classifier v2"], +) + +# ============================ +# Response Models (NA-112) +# ============================ + + +class PredictionItem(BaseModel): + class_name: str + confidence: float + + +class ClassifierV2Response(BaseModel): + top_prediction: PredictionItem + top_3_predictions: List[PredictionItem] + allergy_warning: Optional[str] = None + message: str = "Classification successful" + +# ============================ +# POST /ai-model/classifier/v2 +# YOLO Food Image Classifier with Allergy Check +# ============================ + + +@router.post( + "/v2", + response_model=ClassifierV2Response, + summary="Classifier v2 ā Food Image Predictor with YOLO", + description="Classifies food images using YOLO model and checks for allergen warnings." +) +async def classifier_v2_predict( + image: UploadFile = File(..., description="Food image to analyze"), + allergies: Optional[str] = Form( + None, + description="Comma-separated allergies (e.g. 'nuts,dairy,gluten,shellfish')" + ) +): + try: + image_bytes = await image.read() + img = Image.open(io.BytesIO(image_bytes)) + print(image) + results = model(img, max_det=3, save=True, verbose=False) + print(results, "<<<<<<<<<<<") + predictions = [] + + # Check if results exist + if results and len(results) > 0: + result = results[0] + + # Check if it's a classification model (has probs attribute) + if hasattr(result, 'probs') and result.probs is not None: + # Classification model + probs = result.probs + + # Get top 5 predictions + if hasattr(probs, 'top5'): + top_indices = probs.top5 + top_confidences = probs.top5conf.tolist() + else: + # Alternative method for getting top predictions + all_probs = probs.data.cpu().numpy() + top_indices = all_probs.argsort()[-5:][::-1] + top_confidences = [float(all_probs[i]) + for i in top_indices] + + # Get class names + class_names = result.names + + # Create predictions list (limit to top 3) + for idx, conf in zip(top_indices[:3], top_confidences[:3]): + predictions.append({ + "class_name": class_names[int(idx)], + "confidence": float(conf) + }) + + # Check if it's a detection model (has boxes attribute) + elif hasattr(result, 'boxes') and result.boxes is not None and len(result.boxes) > 0: + # Detection model + boxes = result.boxes + + # Extract data + confidences = boxes.conf.cpu().tolist() + classes = boxes.cls.cpu().tolist() + class_names = result.names + + # Combine and sort by confidence + detections = [(class_names[int(cls)], float(conf)) + for cls, conf in zip(classes, confidences)] + detections.sort(key=lambda x: x[1], reverse=True) + + # Take top 3 + for class_name, conf in detections[:3]: + predictions.append({ + "class_name": class_name, + "confidence": conf + }) + + else: + # Try to extract any available predictions + if hasattr(result, 'names') and hasattr(result, 'probs'): + print(f"Result type: {type(result)}") + print(f"Result attributes: {dir(result)}") + print(f"Probs: {result.probs}") + + # Fallback if no predictions + if not predictions: + raise HTTPException( + status_code=400, + detail="No food items detected in the image. Please upload a clearer image or check model format." + ) + + # Ensure we have at least top prediction + top_pred = predictions[0] + + # Pad predictions to 3 if needed + while len(predictions) < 3: + predictions.append({ + "class_name": "unknown", + "confidence": 0.0 + }) + + # NA-113: Allergy restriction logic + warning = None + if allergies and allergies.strip(): + allergy_list = [a.strip().lower() + for a in allergies.split(",") if a.strip()] + food_lower = top_pred["class_name"].lower() + + for allergy in allergy_list: + if allergy in food_lower: + warning = f"ā ļø ALLERGY ALERT: '{top_pred['class_name']}' may contain '{allergy}'" + break + + return ClassifierV2Response( + top_prediction=PredictionItem(**top_pred), + top_3_predictions=[PredictionItem(**p) for p in predictions[:3]], + allergy_warning=warning, + message="Classification successful" + ) + + except HTTPException: + raise + except Exception as e: + import traceback + print(f"Error during prediction: {str(e)}") + print(traceback.format_exc()) + raise HTTPException( + status_code=500, + detail=f"Error processing image: {str(e)}" + ) diff --git a/nutrihelp_ai/routers/meal_library.json b/nutrihelp_ai/routers/meal_library.json new file mode 100644 index 0000000..8b32c56 --- /dev/null +++ b/nutrihelp_ai/routers/meal_library.json @@ -0,0 +1,129 @@ +[ + { + "id": "a1", + "name": "Oats with berries and chia", + "meal_type": "breakfast", + "calories": 350, + "tags": ["low_sugar", "high_fibre", "vegetarian", "soft_food", "cost_low"] + }, + { + "id": "a2", + "name": "Scrambled eggs on toast", + "meal_type": "breakfast", + "calories": 380, + "tags": ["high_protein", "contains_egg", "contains_gluten", "contains_wheat", "cost_low"] + }, + { + "id": "a3", + "name": "Grilled chicken salad", + "meal_type": "lunch", + "calories": 450, + "tags": ["low_fat", "low_salt", "high_protein", "cost_medium"] + }, + { + "id": "a4", + "name": "Vegetable stir-fry with tofu and rice", + "meal_type": "lunch", + "calories": 500, + "tags": ["soft_food", "low_fat", "low_salt", "asian", "contains_soy", "vegetarian", "cost_low"] + }, + { + "id": "a5", + "name": "Baked salmon with steamed veggies", + "meal_type": "dinner", + "calories": 520, + "tags": ["low_sugar", "high_protein", "low_salt", "contains_fish", "cost_high"] + }, + { + "id": "a6", + "name": "Lentil soup with soft bread", + "meal_type": "dinner", + "calories": 480, + "tags": ["vegetarian", "soft_food", "low_fat", "low_salt", "contains_gluten", "contains_wheat", "cost_low"] + }, + { + "id": "a7", + "name": "Apple slices with yoghurt", + "meal_type": "snack", + "calories": 150, + "tags": ["low_fat", "contains_yoghurt", "contains_dairy", "contains_apple", "cost_low"] + }, + { + "id": "a8", + "name": "Unsalted mixed nuts", + "meal_type": "snack", + "calories": 200, + "tags": ["contains_nuts", "contains_tree_nut", "low_sugar", "cost_medium"] + }, + { + "id": "a9", + "name": "Sugary cereal with milk", + "meal_type": "breakfast", + "calories": 420, + "tags": ["high_sugar", "high_gi", "refined_carb", "contains_dairy", "contains_milk", "contains_gluten", "contains_wheat", "cost_low"] + }, + { + "id": "a10", + "name": "Wholegrain toast with avocado", + "meal_type": "breakfast", + "calories": 360, + "tags": ["low_sugar", "high_fibre", "low_salt", "vegetarian", "contains_gluten", "contains_wheat", "cost_medium"] + }, + { + "id": "a11", + "name": "Chicken burger and fries", + "meal_type": "lunch", + "calories": 800, + "tags": ["high_fat", "high_saturated_fat", "fried", "processed_meat", "high_salt", "contains_gluten", "contains_wheat", "cost_medium"] + }, + { + "id": "a12", + "name": "Brown rice with grilled fish and veggies", + "meal_type": "lunch", + "calories": 520, + "tags": ["low_sugar", "low_gi", "low_fat", "low_salt", "high_protein", "contains_fish", "cost_high"] + }, + { + "id": "a13", + "name": "Instant noodles with seasoning packet", + "meal_type": "lunch", + "calories": 550, + "tags": ["high_salt", "very_high_salt", "refined_carb", "contains_gluten", "contains_wheat", "cost_low"] + }, + { + "id": "a14", + "name": "Beef stir-fry with vegetables and soy sauce", + "meal_type": "dinner", + "calories": 650, + "tags": ["high_salt", "soy_sauce_heavy", "asian", "high_protein", "contains_soy", "cost_high"] + }, + { + "id": "a15", + "name": "Cheese pizza slice", + "meal_type": "dinner", + "calories": 780, + "tags": ["high_fat", "high_saturated_fat", "contains_dairy", "contains_cheese", "contains_gluten", "contains_wheat", "cost_medium"] + }, + { + "id": "a16", + "name": "Donut and soft drink", + "meal_type": "snack", + "calories": 600, + "tags": ["dessert", "high_sugar", "added_sugar", "refined_carb", "sugary_drink", "high_fat", "contains_gluten", "contains_wheat", "cost_low"] + }, + { + "id": "a17", + "name": "Fresh fruit salad", + "meal_type": "snack", + "calories": 180, + "tags": ["low_sugar", "low_fat", "low_salt", "vegetarian", "cost_low", + "contains_fruit", "contains_apple", "contains_strawberry", "contains_kiwi"] + }, + { + "id": "a18", + "name": "Low-fat yoghurt with nuts", + "meal_type": "snack", + "calories": 220, + "tags": ["low_fat", "contains_yoghurt", "contains_dairy", "contains_nuts", "contains_tree_nut", "cost_medium"] + } +] diff --git a/nutrihelp_ai/routers/mealplan_api.py b/nutrihelp_ai/routers/mealplan_api.py new file mode 100644 index 0000000..c8a9419 --- /dev/null +++ b/nutrihelp_ai/routers/mealplan_api.py @@ -0,0 +1,1389 @@ +# # nutrihelp_ai/routers/mealplan_api.py + +# from fastapi import APIRouter, HTTPException, Query +# from pydantic import BaseModel, Field +# from typing import Optional, List, Dict, Any +# from enum import Enum + +# router = APIRouter( +# prefix="/ai/mealplan", +# tags=["Meal Plan Generation"], +# ) + +# # ============================ +# # Enums for Valid Values +# # ============================ + + +# class ActivityLevel(str, Enum): +# sedentary = "sedentary" +# light = "light" +# moderate = "moderate" +# active = "active" +# very_active = "very_active" + + +# class DietaryPreference(str, Enum): +# none = "none" +# vegetarian = "vegetarian" +# vegan = "vegan" +# keto = "keto" +# paleo = "paleo" +# mediterranean = "mediterranean" + + +# class Gender(str, Enum): +# male = "male" +# female = "female" +# other = "other" + +# # ============================ +# # Response Models +# # ============================ + + +# class MealItem(BaseModel): +# name: str +# calories: int +# protein: float +# carbs: float +# fat: float +# description: str + + +# class DailyMealPlan(BaseModel): +# day: str +# breakfast: MealItem +# lunch: MealItem +# dinner: MealItem +# snacks: List[MealItem] +# total_calories: int +# total_protein: float +# total_carbs: float +# total_fat: float + + +# class DataQualityInfo(BaseModel): +# """Information about data quality and missing fields""" +# missing_fields: List[str] = [] +# warnings: List[str] = [] +# fallbacks_applied: Dict[str, Any] = {} +# data_completeness: float = Field( +# ..., description="Percentage of required data provided (0-100)") + + +# class MealPlanResponse(BaseModel): +# success: bool +# message: str +# meal_plan: Optional[List[DailyMealPlan]] = None +# data_quality: DataQualityInfo +# recommendations: Optional[List[str]] = None + +# # ============================ +# # Data Validator +# # ============================ + + +# class MealPlanValidator: +# """Handles missing data cases for meal plan generation""" + +# REQUIRED_FIELDS = { +# "user_id": "User ID is required", +# "age": "Age is required for calorie calculation", +# "gender": "Gender is required for calorie calculation", +# "weight": "Weight is required for calorie calculation", +# "height": "Height is required for BMI calculation" +# } + +# OPTIONAL_FIELDS = { +# "activity_level": "sedentary", +# "dietary_preference": "none", +# "allergies": "", +# "health_goals": "maintain_weight", +# "meals_per_day": 3 +# } + +# @staticmethod +# def validate_and_enrich(params: Dict) -> tuple[bool, Dict, DataQualityInfo]: +# """ +# Validate parameters and apply fallbacks for missing optional data +# Returns: (is_valid, enriched_params, data_quality_info) +# """ +# missing_fields = [] +# warnings = [] +# fallbacks_applied = {} + +# # Check required fields +# for field, error_msg in MealPlanValidator.REQUIRED_FIELDS.items(): +# if field not in params or params[field] is None or params[field] == "": +# missing_fields.append(field) + +# # If required fields are missing, cannot proceed +# if missing_fields: +# data_quality = DataQualityInfo( +# missing_fields=missing_fields, +# warnings=[ +# f"Cannot generate meal plan without: {', '.join(missing_fields)}"], +# fallbacks_applied={}, +# data_completeness=0.0 +# ) +# return False, params, data_quality + +# # Apply fallbacks for optional fields +# enriched_params = params.copy() +# for field, default_value in MealPlanValidator.OPTIONAL_FIELDS.items(): +# if field not in enriched_params or enriched_params[field] is None or enriched_params[field] == "": +# enriched_params[field] = default_value +# fallbacks_applied[field] = default_value +# warnings.append( +# f"'{field}' not provided - using default: '{default_value}'") + +# # Calculate data completeness +# total_fields = len(MealPlanValidator.REQUIRED_FIELDS) + \ +# len(MealPlanValidator.OPTIONAL_FIELDS) +# # All required are present +# provided_fields = len(MealPlanValidator.REQUIRED_FIELDS) +# provided_fields += sum( +# 1 for f in MealPlanValidator.OPTIONAL_FIELDS if f in params and params[f]) +# completeness = (provided_fields / total_fields) * 100 + +# data_quality = DataQualityInfo( +# missing_fields=[], +# warnings=warnings, +# fallbacks_applied=fallbacks_applied, +# data_completeness=round(completeness, 1) +# ) + +# return True, enriched_params, data_quality + +# # ============================ +# # Dummy Meal Plan Generator +# # ============================ +# # def generate_dummy_meal_plan(params: Dict) -> List[DailyMealPlan]: +# """ +# Generate dummy 3-day meal plan based on user preferences +# This will be replaced with real logic in future sprints +# """ + +# # Calculate target calories (simplified Harris-Benedict) +# # age = params["age"] +# # weight = params["weight"] +# # height = params["height"] +# # gender = params["gender"] +# # activity_level = params["activity_level"] + +# # # BMR calculation +# # if gender == "male": +# # bmr = 88.362 + (13.397 * weight) + (4.799 * height) - (5.677 * age) +# # else: +# # bmr = 447.593 + (9.247 * weight) + (3.098 * height) - (4.330 * age) + +# # # Activity multiplier +# # activity_multipliers = { +# # "sedentary": 1.2, +# # "light": 1.375, +# # "moderate": 1.55, +# # "active": 1.725, +# # "very_active": 1.9 +# # } + +# # target_calories = int(bmr * activity_multipliers.get(activity_level, 1.2)) + +# # # Adjust for dietary preference +# # dietary_pref = params["dietary_preference"] + +# # # Generate 3-day plan +# # meal_plans = [] +# # for day_num in range(1, 4): +# # # Dummy meals (replace with real data later) +# # breakfast = MealItem( +# # name=f"{'Vegan' if dietary_pref == 'vegan' else 'Classic'} Breakfast Bowl", +# # calories=int(target_calories * 0.25), +# # protein=20.0, +# # carbs=45.0, +# # fat=12.0, +# # description="Nutritious breakfast to start your day" +# # ) + +# # lunch = MealItem( +# # name=f"{'Plant-based' if dietary_pref in ['vegan', 'vegetarian'] else 'Protein'} Lunch", +# # calories=int(target_calories * 0.35), +# # protein=30.0, +# # carbs=50.0, +# # fat=15.0, +# # description="Balanced midday meal" +# # ) + +# # dinner = MealItem( +# # name=f"{'Keto' if dietary_pref == 'keto' else 'Balanced'} Dinner", +# # calories=int(target_calories * 0.30), +# # protein=25.0, +# # carbs=40.0 if dietary_pref != "keto" else 10.0, +# # fat=18.0 if dietary_pref == "keto" else 12.0, +# # description="Satisfying evening meal" +# # ) + +# # snack = MealItem( +# # name="Healthy Snack", +# # calories=int(target_calories * 0.10), +# # protein=8.0, +# # carbs=15.0, +# # fat=5.0, +# # description="Light snack between meals" +# # ) + +# # total_cal = breakfast.calories + lunch.calories + dinner.calories + snack.calories + +# # daily_plan = DailyMealPlan( +# # day=f"Day {day_num}", +# # breakfast=breakfast, +# # lunch=lunch, +# # dinner=dinner, +# # snacks=[snack], +# # total_calories=total_cal, +# # total_protein=breakfast.protein + lunch.protein + dinner.protein + snack.protein, +# # total_carbs=breakfast.carbs + lunch.carbs + dinner.carbs + snack.carbs, +# # total_fat=breakfast.fat + lunch.fat + dinner.fat + snack.fat +# # ) + +# # meal_plans.append(daily_plan) + +# # return meal_plans +# # ============================ +# # Dummy Meal Plan Generator (Enhanced) +# # ============================ + + +# def generate_dummy_meal_plan(params: Dict) -> List[DailyMealPlan]: +# """ +# Generate dummy 3-day meal plan based on user preferences +# This will be replaced with real logic in future sprints +# """ + +# # Calculate target calories (simplified Harris-Benedict) +# age = params["age"] +# weight = params["weight"] +# height = params["height"] +# gender = params["gender"] +# activity_level = params["activity_level"] +# dietary_pref = params["dietary_preference"] + +# # BMR calculation +# if gender == "male": +# bmr = 88.362 + (13.397 * weight) + (4.799 * height) - (5.677 * age) +# else: +# bmr = 447.593 + (9.247 * weight) + (3.098 * height) - (4.330 * age) + +# # Activity multiplier +# activity_multipliers = { +# "sedentary": 1.2, +# "light": 1.375, +# "moderate": 1.55, +# "active": 1.725, +# "very_active": 1.9 +# } + +# target_calories = int(bmr * activity_multipliers.get(activity_level, 1.2)) + +# # Meal templates based on dietary preference +# meal_templates = { +# "none": { +# "breakfast": [ +# ("Scrambled Eggs with Whole Wheat Toast", +# "3 eggs, 2 slices whole wheat bread, butter"), +# ("Greek Yogurt Parfait", "Greek yogurt, granola, mixed berries, honey"), +# ("Oatmeal with Banana", "Steel-cut oats, banana, almonds, cinnamon") +# ], +# "lunch": [ +# ("Grilled Chicken Salad", +# "Grilled chicken breast, mixed greens, olive oil dressing"), +# ("Turkey Sandwich", "Whole grain bread, turkey, lettuce, tomato, avocado"), +# ("Quinoa Buddha Bowl", +# "Quinoa, roasted vegetables, chickpeas, tahini sauce") +# ], +# "dinner": [ +# ("Baked Salmon with Veggies", +# "Salmon fillet, steamed broccoli, sweet potato"), +# ("Lean Beef Stir-fry", "Lean beef strips, mixed vegetables, brown rice"), +# ("Chicken Breast with Quinoa", +# "Grilled chicken, quinoa, roasted asparagus") +# ] +# }, +# "vegetarian": { +# "breakfast": [ +# ("Veggie Omelette", "3 eggs, bell peppers, mushrooms, cheese"), +# ("Avocado Toast", "Whole grain toast, avocado, cherry tomatoes, feta"), +# ("Smoothie Bowl", "Banana, berries, protein powder, granola, chia seeds") +# ], +# "lunch": [ +# ("Mediterranean Wrap", "Hummus, falafel, vegetables in whole wheat wrap"), +# ("Caprese Panini", "Mozzarella, tomato, basil, pesto on ciabatta"), +# ("Lentil Soup with Bread", +# "Red lentil soup, whole grain roll, side salad") +# ], +# "dinner": [ +# ("Vegetable Lasagna", "Lasagna with spinach, ricotta, marinara sauce"), +# ("Stuffed Bell Peppers", "Peppers filled with quinoa, beans, cheese"), +# ("Eggplant Parmesan", "Baked eggplant, marinara, mozzarella, pasta") +# ] +# }, +# "vegan": { +# "breakfast": [ +# ("Tofu Scramble", "Tofu, turmeric, vegetables, nutritional yeast"), +# ("Chia Pudding", "Chia seeds, almond milk, berries, maple syrup"), +# ("Vegan Pancakes", "Whole wheat pancakes, banana, peanut butter") +# ], +# "lunch": [ +# ("Buddha Bowl", "Quinoa, chickpeas, tahini, roasted vegetables"), +# ("Vegan Burrito", "Black beans, rice, salsa, guacamole, lettuce"), +# ("Lentil Curry", "Red lentils, coconut milk, vegetables, brown rice") +# ], +# "dinner": [ +# ("Vegan Stir-fry", "Tofu, mixed vegetables, soy sauce, brown rice"), +# ("Pasta Primavera", "Whole wheat pasta, vegetables, olive oil, herbs"), +# ("Chickpea Curry", "Chickpeas, tomato sauce, spinach, naan bread") +# ] +# }, +# "keto": { +# "breakfast": [ +# ("Keto Omelette", "3 eggs, cheese, bacon, avocado, butter"), +# ("Bulletproof Coffee & Eggs", +# "Coffee with MCT oil, scrambled eggs, bacon"), +# ("Greek Yogurt Bowl", "Full-fat Greek yogurt, nuts, berries (limited)") +# ], +# "lunch": [ +# ("Cobb Salad", "Mixed greens, chicken, bacon, eggs, cheese, ranch"), +# ("Bunless Burger", "Beef patty, cheese, lettuce wrap, mayo, pickles"), +# ("Salmon Avocado Salad", +# "Grilled salmon, avocado, leafy greens, olive oil") +# ], +# "dinner": [ +# ("Ribeye Steak", "Grilled ribeye, butter, asparagus, cauliflower mash"), +# ("Keto Chicken Thighs", +# "Crispy chicken thighs, green beans, butter sauce"), +# ("Pork Chops", "Pan-seared pork chops, broccoli, cheese sauce") +# ] +# }, +# "paleo": { +# "breakfast": [ +# ("Paleo Egg Scramble", "Eggs, sweet potato, vegetables, coconut oil"), +# ("Berry Smoothie", "Berries, banana, almond butter, coconut milk"), +# ("Breakfast Hash", "Sweet potato, sausage, peppers, onions") +# ], +# "lunch": [ +# ("Grilled Chicken Bowl", "Chicken, roasted vegetables, olive oil"), +# ("Tuna Salad", "Tuna, mixed greens, olive oil, nuts"), +# ("Beef Lettuce Wraps", "Ground beef, lettuce, vegetables, avocado") +# ], +# "dinner": [ +# ("Baked Cod", "Cod fillet, roasted vegetables, olive oil"), +# ("Paleo Meatballs", "Grass-fed beef meatballs, marinara, zucchini noodles"), +# ("Grilled Lamb Chops", "Lamb chops, roasted root vegetables") +# ] +# }, +# "mediterranean": { +# "breakfast": [ +# ("Mediterranean Omelette", "Eggs, feta, tomatoes, olives, spinach"), +# ("Greek Yogurt Bowl", "Greek yogurt, walnuts, honey, figs"), +# ("Whole Grain Toast", "Toast, hummus, cucumber, tomato") +# ], +# "lunch": [ +# ("Greek Salad", "Cucumbers, tomatoes, olives, feta, olive oil"), +# ("Falafel Wrap", "Falafel, hummus, vegetables, whole wheat pita"), +# ("Grilled Fish Plate", "White fish, lemon, vegetables, olive oil") +# ], +# "dinner": [ +# ("Mediterranean Salmon", "Salmon, olives, tomatoes, capers, olive oil"), +# ("Chicken Souvlaki", "Grilled chicken skewers, tzatziki, vegetables"), +# ("Shrimp Pasta", "Whole wheat pasta, shrimp, garlic, olive oil, feta") +# ] +# } +# } + +# # Get templates for the selected diet +# templates = meal_templates.get(dietary_pref, meal_templates["none"]) + +# # Snack options +# snack_options = [ +# ("Mixed Nuts", "Almonds, walnuts, cashews"), +# ("Apple with Peanut Butter", "Sliced apple, natural peanut butter"), +# ("Protein Shake", "Protein powder, banana, almond milk"), +# ("Greek Yogurt", "Plain Greek yogurt with berries"), +# ("Veggie Sticks with Hummus", "Carrots, celery, cucumber, hummus") +# ] + +# # Generate 3-day plan +# meal_plans = [] +# for day_num in range(1, 4): +# # Select meals for this day +# breakfast_name, breakfast_desc = templates["breakfast"][( +# day_num - 1) % len(templates["breakfast"])] +# lunch_name, lunch_desc = templates["lunch"][( +# day_num - 1) % len(templates["lunch"])] +# dinner_name, dinner_desc = templates["dinner"][( +# day_num - 1) % len(templates["dinner"])] +# snack_name, snack_desc = snack_options[( +# day_num - 1) % len(snack_options)] + +# # Calculate calorie distribution +# breakfast_cal = int(target_calories * 0.25) +# lunch_cal = int(target_calories * 0.35) +# dinner_cal = int(target_calories * 0.30) +# snack_cal = int(target_calories * 0.10) + +# # Adjust macros based on diet type +# if dietary_pref == "keto": +# # High fat, low carb +# breakfast = MealItem( +# name=breakfast_name, +# calories=breakfast_cal, +# protein=round(breakfast_cal * 0.25 / 4, 1), +# carbs=round(breakfast_cal * 0.05 / 4, 1), +# fat=round(breakfast_cal * 0.70 / 9, 1), +# description=breakfast_desc +# ) +# lunch = MealItem( +# name=lunch_name, +# calories=lunch_cal, +# protein=round(lunch_cal * 0.30 / 4, 1), +# carbs=round(lunch_cal * 0.05 / 4, 1), +# fat=round(lunch_cal * 0.65 / 9, 1), +# description=lunch_desc +# ) +# dinner = MealItem( +# name=dinner_name, +# calories=dinner_cal, +# protein=round(dinner_cal * 0.30 / 4, 1), +# carbs=round(dinner_cal * 0.05 / 4, 1), +# fat=round(dinner_cal * 0.65 / 9, 1), +# description=dinner_desc +# ) +# else: +# # Balanced macros +# breakfast = MealItem( +# name=breakfast_name, +# calories=breakfast_cal, +# protein=round(breakfast_cal * 0.20 / 4, 1), +# carbs=round(breakfast_cal * 0.50 / 4, 1), +# fat=round(breakfast_cal * 0.30 / 9, 1), +# description=breakfast_desc +# ) +# lunch = MealItem( +# name=lunch_name, +# calories=lunch_cal, +# protein=round(lunch_cal * 0.25 / 4, 1), +# carbs=round(lunch_cal * 0.45 / 4, 1), +# fat=round(lunch_cal * 0.30 / 9, 1), +# description=lunch_desc +# ) +# dinner = MealItem( +# name=dinner_name, +# calories=dinner_cal, +# protein=round(dinner_cal * 0.30 / 4, 1), +# carbs=round(dinner_cal * 0.40 / 4, 1), +# fat=round(dinner_cal * 0.30 / 9, 1), +# description=dinner_desc +# ) + +# snack = MealItem( +# name=snack_name, +# calories=snack_cal, +# protein=round(snack_cal * 0.20 / 4, 1), +# carbs=round(snack_cal * 0.40 / 4, 1), +# fat=round(snack_cal * 0.40 / 9, 1), +# description=snack_desc +# ) + +# total_cal = breakfast.calories + lunch.calories + dinner.calories + snack.calories + +# daily_plan = DailyMealPlan( +# day=f"Day {day_num}", +# breakfast=breakfast, +# lunch=lunch, +# dinner=dinner, +# snacks=[snack], +# total_calories=total_cal, +# total_protein=round( +# breakfast.protein + lunch.protein + dinner.protein + snack.protein, 1), +# total_carbs=round(breakfast.carbs + lunch.carbs + +# dinner.carbs + snack.carbs, 1), +# total_fat=round(breakfast.fat + lunch.fat + +# dinner.fat + snack.fat, 1) +# ) + +# meal_plans.append(daily_plan) + +# return meal_plans + +# # ============================ +# # GET /ai/mealplan Endpoint +# # ============================ + + +# @router.get( +# "", +# response_model=MealPlanResponse, +# summary="Generate Personalized Meal Plan", +# description=""" +# Generate a 3-day personalized meal plan based on user preferences and health data. + +# **Handles missing data cases:** +# - Required fields: user_id, age, gender, weight, height +# - Optional fields use smart defaults if not provided +# - Returns data quality information and warnings + +# **Future integration:** Will connect to user preferences table in database. +# """ +# ) +# async def get_meal_plan( +# user_id: str = Query(..., description="User ID from database"), +# age: Optional[int] = Query( +# None, ge=1, le=120, description="User age in years"), +# gender: Optional[Gender] = Query(None, description="User gender"), +# weight: Optional[float] = Query( +# None, ge=20, le=300, description="Weight in kg"), +# height: Optional[float] = Query( +# None, ge=50, le=250, description="Height in cm"), +# activity_level: Optional[ActivityLevel] = Query( +# None, description="Physical activity level"), +# dietary_preference: Optional[DietaryPreference] = Query( +# None, description="Dietary preference"), +# allergies: Optional[str] = Query( +# None, description="Comma-separated allergies (e.g., 'nuts,dairy,shellfish')"), +# health_goals: Optional[str] = Query( +# None, description="Health goals (e.g., 'weight_loss', 'muscle_gain')"), +# meals_per_day: Optional[int] = Query( +# None, ge=2, le=6, description="Number of meals per day") +# ): +# """ +# Generate personalized meal plan with error handling for missing data +# """ + +# try: +# # Collect all parameters +# params = { +# "user_id": user_id, +# "age": age, +# "gender": gender, +# "weight": weight, +# "height": height, +# "activity_level": activity_level, +# "dietary_preference": dietary_preference, +# "allergies": allergies, +# "health_goals": health_goals, +# "meals_per_day": meals_per_day +# } + +# # Validate and handle missing data +# is_valid, enriched_params, data_quality = MealPlanValidator.validate_and_enrich( +# params) + +# if not is_valid: +# # Cannot proceed - missing required fields +# return MealPlanResponse( +# success=False, +# message=f"Insufficient data to generate meal plan. Missing required fields: {', '.join(data_quality.missing_fields)}", +# meal_plan=None, +# data_quality=data_quality, +# recommendations=[ +# "Please provide all required fields to generate a personalized meal plan", +# "Required: user_id, age, gender, weight, height" +# ] +# ) + +# # Generate meal plan with enriched data +# meal_plan = generate_dummy_meal_plan(enriched_params) + +# # Build recommendations based on data quality +# recommendations = [] +# if data_quality.data_completeness < 100: +# recommendations.append( +# f"Meal plan generated with {data_quality.data_completeness}% data completeness. " +# "Provide more information for better personalization." +# ) + +# if enriched_params.get("allergies"): +# recommendations.append( +# f"Allergy restrictions applied: {enriched_params['allergies']}. " +# "All meals exclude these ingredients." +# ) + +# # Success response +# return MealPlanResponse( +# success=True, +# message="Meal plan generated successfully", +# meal_plan=meal_plan, +# data_quality=data_quality, +# recommendations=recommendations if recommendations else None +# ) + +# except ValueError as e: + # raise HTTPException(status_code=400, detail=f"Invalid input: {str(e)}") + +# except Exception as e: +# raise HTTPException( +# status_code=500, detail=f"Error generating meal plan: {str(e)}") + + +# # ============================ +# # Additional Endpoints +# # ============================ +# @router.get( +# "/preferences/{user_id}", +# summary="Get User Meal Preferences (Placeholder)", +# description="Future endpoint: Fetch user preferences from database" +# ) +# async def get_user_preferences(user_id: str): +# """ +# Placeholder for database integration +# Will fetch user preferences from DB in future sprint +# """ +# return { +# "message": "Database integration pending", +# "user_id": user_id, +# "note": "This will fetch stored preferences from user profile table" +# } + + +# nutrihelp_ai/routers/mealplan_api.py + +from fastapi import APIRouter, HTTPException, Query +from pydantic import BaseModel, Field +from typing import Optional, List, Dict, Any +from enum import Enum +# NutriHelp-AI\nutrihelp_ai\model\fetchAllHealthConditions.py +# ============================ +# DATABASE IMPORTS (UPDATE THIS SECTION) +# ============================ +from nutrihelp_ai.model.fetchUserProfile import fetch_user_profile +from nutrihelp_ai.model.fetchUserPreferences import fetch_user_preferences, fetch_user_allergies +from nutrihelp_ai.model.fetchAllHealthConditions import fetch_all_health_conditions +from nutrihelp_ai.model.fetchUserHealthConditions import fetch_user_health_conditions # ADD THIS LINE +import logging + +logger = logging.getLogger(__name__) + +router = APIRouter( + prefix="/ai/mealplan", + tags=["Meal Plan Generation"], +) + +# ============================ +# Enums for Valid Values +# ============================ + +class ActivityLevel(str, Enum): + sedentary = "sedentary" + light = "light" + moderate = "moderate" + active = "active" + very_active = "very_active" + + +class DietaryPreference(str, Enum): + none = "none" + vegetarian = "vegetarian" + vegan = "vegan" + keto = "keto" + paleo = "paleo" + mediterranean = "mediterranean" + + +class Gender(str, Enum): + male = "male" + female = "female" + other = "other" + +# ============================ +# Response Models +# ============================ + +class MealItem(BaseModel): + name: str + calories: int + protein: float + carbs: float + fat: float + description: str + + +class DailyMealPlan(BaseModel): + day: str + breakfast: MealItem + lunch: MealItem + dinner: MealItem + snacks: List[MealItem] + total_calories: int + total_protein: float + total_carbs: float + total_fat: float + + +class DataQualityInfo(BaseModel): + """Information about data quality and missing fields""" + missing_fields: List[str] = [] + warnings: List[str] = [] + fallbacks_applied: Dict[str, Any] = {} + data_completeness: float = Field( + ..., description="Percentage of required data provided (0-100)") + + +class MealPlanResponse(BaseModel): + success: bool + message: str + meal_plan: Optional[List[DailyMealPlan]] = None + data_quality: DataQualityInfo + recommendations: Optional[List[str]] = None + +# ============================ +# Data Validator +# ============================ + +class MealPlanValidator: + """Handles missing data cases for meal plan generation""" + + REQUIRED_FIELDS = { + "user_id": "User ID is required", + "age": "Age is required for calorie calculation", + "gender": "Gender is required for calorie calculation", + "weight": "Weight is required for calorie calculation", + "height": "Height is required for BMI calculation" + } + + OPTIONAL_FIELDS = { + "activity_level": "sedentary", + "dietary_preference": "none", + "allergies": "", + "health_goals": "maintain_weight", + "meals_per_day": 3 + } + + @staticmethod + def validate_and_enrich(params: Dict) -> tuple[bool, Dict, DataQualityInfo]: + """ + Validate parameters and apply fallbacks for missing optional data + Returns: (is_valid, enriched_params, data_quality_info) + """ + missing_fields = [] + warnings = [] + fallbacks_applied = {} + + # Check required fields + for field, error_msg in MealPlanValidator.REQUIRED_FIELDS.items(): + if field not in params or params[field] is None or params[field] == "": + missing_fields.append(field) + + # If required fields are missing, cannot proceed + if missing_fields: + data_quality = DataQualityInfo( + missing_fields=missing_fields, + warnings=[ + f"Cannot generate meal plan without: {', '.join(missing_fields)}"], + fallbacks_applied={}, + data_completeness=0.0 + ) + return False, params, data_quality + + # Apply fallbacks for optional fields + enriched_params = params.copy() + for field, default_value in MealPlanValidator.OPTIONAL_FIELDS.items(): + if field not in enriched_params or enriched_params[field] is None or enriched_params[field] == "": + enriched_params[field] = default_value + fallbacks_applied[field] = default_value + warnings.append( + f"'{field}' not provided - using default: '{default_value}'") + + # Calculate data completeness + total_fields = len(MealPlanValidator.REQUIRED_FIELDS) + \ + len(MealPlanValidator.OPTIONAL_FIELDS) + provided_fields = len(MealPlanValidator.REQUIRED_FIELDS) + provided_fields += sum( + 1 for f in MealPlanValidator.OPTIONAL_FIELDS if f in params and params[f]) + completeness = (provided_fields / total_fields) * 100 + + data_quality = DataQualityInfo( + missing_fields=[], + warnings=warnings, + fallbacks_applied=fallbacks_applied, + data_completeness=round(completeness, 1) + ) + + return True, enriched_params, data_quality + +# ============================ +# Dummy Meal Plan Generator (Enhanced) +# ============================ + +def generate_dummy_meal_plan(params: Dict) -> List[DailyMealPlan]: + """ + Generate dummy 3-day meal plan based on user preferences + This will be replaced with real logic in future sprints + """ + + # Calculate target calories (simplified Harris-Benedict) + age = params["age"] + weight = params["weight"] + height = params["height"] + gender = params["gender"] + activity_level = params["activity_level"] + dietary_pref = params["dietary_preference"] + + # BMR calculation + if gender == "male": + bmr = 88.362 + (13.397 * weight) + (4.799 * height) - (5.677 * age) + else: + bmr = 447.593 + (9.247 * weight) + (3.098 * height) - (4.330 * age) + + # Activity multiplier + activity_multipliers = { + "sedentary": 1.2, + "light": 1.375, + "moderate": 1.55, + "active": 1.725, + "very_active": 1.9 + } + + target_calories = int(bmr * activity_multipliers.get(activity_level, 1.2)) + + # Meal templates based on dietary preference + meal_templates = { + "none": { + "breakfast": [ + ("Scrambled Eggs with Whole Wheat Toast", + "3 eggs, 2 slices whole wheat bread, butter"), + ("Greek Yogurt Parfait", "Greek yogurt, granola, mixed berries, honey"), + ("Oatmeal with Banana", "Steel-cut oats, banana, almonds, cinnamon") + ], + "lunch": [ + ("Grilled Chicken Salad", + "Grilled chicken breast, mixed greens, olive oil dressing"), + ("Turkey Sandwich", "Whole grain bread, turkey, lettuce, tomato, avocado"), + ("Quinoa Buddha Bowl", + "Quinoa, roasted vegetables, chickpeas, tahini sauce") + ], + "dinner": [ + ("Baked Salmon with Veggies", + "Salmon fillet, steamed broccoli, sweet potato"), + ("Lean Beef Stir-fry", "Lean beef strips, mixed vegetables, brown rice"), + ("Chicken Breast with Quinoa", + "Grilled chicken, quinoa, roasted asparagus") + ] + }, + "vegetarian": { + "breakfast": [ + ("Veggie Omelette", "3 eggs, bell peppers, mushrooms, cheese"), + ("Avocado Toast", "Whole grain toast, avocado, cherry tomatoes, feta"), + ("Smoothie Bowl", "Banana, berries, protein powder, granola, chia seeds") + ], + "lunch": [ + ("Mediterranean Wrap", "Hummus, falafel, vegetables in whole wheat wrap"), + ("Caprese Panini", "Mozzarella, tomato, basil, pesto on ciabatta"), + ("Lentil Soup with Bread", + "Red lentil soup, whole grain roll, side salad") + ], + "dinner": [ + ("Vegetable Lasagna", "Lasagna with spinach, ricotta, marinara sauce"), + ("Stuffed Bell Peppers", "Peppers filled with quinoa, beans, cheese"), + ("Eggplant Parmesan", "Baked eggplant, marinara, mozzarella, pasta") + ] + }, + "vegan": { + "breakfast": [ + ("Tofu Scramble", "Tofu, turmeric, vegetables, nutritional yeast"), + ("Chia Pudding", "Chia seeds, almond milk, berries, maple syrup"), + ("Vegan Pancakes", "Whole wheat pancakes, banana, peanut butter") + ], + "lunch": [ + ("Buddha Bowl", "Quinoa, chickpeas, tahini, roasted vegetables"), + ("Vegan Burrito", "Black beans, rice, salsa, guacamole, lettuce"), + ("Lentil Curry", "Red lentils, coconut milk, vegetables, brown rice") + ], + "dinner": [ + ("Vegan Stir-fry", "Tofu, mixed vegetables, soy sauce, brown rice"), + ("Pasta Primavera", "Whole wheat pasta, vegetables, olive oil, herbs"), + ("Chickpea Curry", "Chickpeas, tomato sauce, spinach, naan bread") + ] + }, + "keto": { + "breakfast": [ + ("Keto Omelette", "3 eggs, cheese, bacon, avocado, butter"), + ("Bulletproof Coffee & Eggs", + "Coffee with MCT oil, scrambled eggs, bacon"), + ("Greek Yogurt Bowl", "Full-fat Greek yogurt, nuts, berries (limited)") + ], + "lunch": [ + ("Cobb Salad", "Mixed greens, chicken, bacon, eggs, cheese, ranch"), + ("Bunless Burger", "Beef patty, cheese, lettuce wrap, mayo, pickles"), + ("Salmon Avocado Salad", + "Grilled salmon, avocado, leafy greens, olive oil") + ], + "dinner": [ + ("Ribeye Steak", "Grilled ribeye, butter, asparagus, cauliflower mash"), + ("Keto Chicken Thighs", + "Crispy chicken thighs, green beans, butter sauce"), + ("Pork Chops", "Pan-seared pork chops, broccoli, cheese sauce") + ] + }, + "paleo": { + "breakfast": [ + ("Paleo Egg Scramble", "Eggs, sweet potato, vegetables, coconut oil"), + ("Berry Smoothie", "Berries, banana, almond butter, coconut milk"), + ("Breakfast Hash", "Sweet potato, sausage, peppers, onions") + ], + "lunch": [ + ("Grilled Chicken Bowl", "Chicken, roasted vegetables, olive oil"), + ("Tuna Salad", "Tuna, mixed greens, olive oil, nuts"), + ("Beef Lettuce Wraps", "Ground beef, lettuce, vegetables, avocado") + ], + "dinner": [ + ("Baked Cod", "Cod fillet, roasted vegetables, olive oil"), + ("Paleo Meatballs", "Grass-fed beef meatballs, marinara, zucchini noodles"), + ("Grilled Lamb Chops", "Lamb chops, roasted root vegetables") + ] + }, + "mediterranean": { + "breakfast": [ + ("Mediterranean Omelette", "Eggs, feta, tomatoes, olives, spinach"), + ("Greek Yogurt Bowl", "Greek yogurt, walnuts, honey, figs"), + ("Whole Grain Toast", "Toast, hummus, cucumber, tomato") + ], + "lunch": [ + ("Greek Salad", "Cucumbers, tomatoes, olives, feta, olive oil"), + ("Falafel Wrap", "Falafel, hummus, vegetables, whole wheat pita"), + ("Grilled Fish Plate", "White fish, lemon, vegetables, olive oil") + ], + "dinner": [ + ("Mediterranean Salmon", "Salmon, olives, tomatoes, capers, olive oil"), + ("Chicken Souvlaki", "Grilled chicken skewers, tzatziki, vegetables"), + ("Shrimp Pasta", "Whole wheat pasta, shrimp, garlic, olive oil, feta") + ] + } + } + + # Get templates for the selected diet + templates = meal_templates.get(dietary_pref, meal_templates["none"]) + + # Snack options + snack_options = [ + ("Mixed Nuts", "Almonds, walnuts, cashews"), + ("Apple with Peanut Butter", "Sliced apple, natural peanut butter"), + ("Protein Shake", "Protein powder, banana, almond milk"), + ("Greek Yogurt", "Plain Greek yogurt with berries"), + ("Veggie Sticks with Hummus", "Carrots, celery, cucumber, hummus") + ] + + # Generate 3-day plan + meal_plans = [] + for day_num in range(1, 4): + # Select meals for this day + breakfast_name, breakfast_desc = templates["breakfast"][( + day_num - 1) % len(templates["breakfast"])] + lunch_name, lunch_desc = templates["lunch"][( + day_num - 1) % len(templates["lunch"])] + dinner_name, dinner_desc = templates["dinner"][( + day_num - 1) % len(templates["dinner"])] + snack_name, snack_desc = snack_options[( + day_num - 1) % len(snack_options)] + + # Calculate calorie distribution + breakfast_cal = int(target_calories * 0.25) + lunch_cal = int(target_calories * 0.35) + dinner_cal = int(target_calories * 0.30) + snack_cal = int(target_calories * 0.10) + + # Adjust macros based on diet type + if dietary_pref == "keto": + # High fat, low carb + breakfast = MealItem( + name=breakfast_name, + calories=breakfast_cal, + protein=round(breakfast_cal * 0.25 / 4, 1), + carbs=round(breakfast_cal * 0.05 / 4, 1), + fat=round(breakfast_cal * 0.70 / 9, 1), + description=breakfast_desc + ) + lunch = MealItem( + name=lunch_name, + calories=lunch_cal, + protein=round(lunch_cal * 0.30 / 4, 1), + carbs=round(lunch_cal * 0.05 / 4, 1), + fat=round(lunch_cal * 0.65 / 9, 1), + description=lunch_desc + ) + dinner = MealItem( + name=dinner_name, + calories=dinner_cal, + protein=round(dinner_cal * 0.30 / 4, 1), + carbs=round(dinner_cal * 0.05 / 4, 1), + fat=round(dinner_cal * 0.65 / 9, 1), + description=dinner_desc + ) + else: + # Balanced macros + breakfast = MealItem( + name=breakfast_name, + calories=breakfast_cal, + protein=round(breakfast_cal * 0.20 / 4, 1), + carbs=round(breakfast_cal * 0.50 / 4, 1), + fat=round(breakfast_cal * 0.30 / 9, 1), + description=breakfast_desc + ) + lunch = MealItem( + name=lunch_name, + calories=lunch_cal, + protein=round(lunch_cal * 0.25 / 4, 1), + carbs=round(lunch_cal * 0.45 / 4, 1), + fat=round(lunch_cal * 0.30 / 9, 1), + description=lunch_desc + ) + dinner = MealItem( + name=dinner_name, + calories=dinner_cal, + protein=round(dinner_cal * 0.30 / 4, 1), + carbs=round(dinner_cal * 0.40 / 4, 1), + fat=round(dinner_cal * 0.30 / 9, 1), + description=dinner_desc + ) + + snack = MealItem( + name=snack_name, + calories=snack_cal, + protein=round(snack_cal * 0.20 / 4, 1), + carbs=round(snack_cal * 0.40 / 4, 1), + fat=round(snack_cal * 0.40 / 9, 1), + description=snack_desc + ) + + total_cal = breakfast.calories + lunch.calories + dinner.calories + snack.calories + + daily_plan = DailyMealPlan( + day=f"Day {day_num}", + breakfast=breakfast, + lunch=lunch, + dinner=dinner, + snacks=[snack], + total_calories=total_cal, + total_protein=round( + breakfast.protein + lunch.protein + dinner.protein + snack.protein, 1), + total_carbs=round(breakfast.carbs + lunch.carbs + + dinner.carbs + snack.carbs, 1), + total_fat=round(breakfast.fat + lunch.fat + + dinner.fat + snack.fat, 1) + ) + + meal_plans.append(daily_plan) + + return meal_plans + +# ============================ +# ENDPOINT 1: Manual Input (Original - No Database) +# ============================ + +@router.get( + "", + response_model=MealPlanResponse, + summary="Generate Personalized Meal Plan (Manual Input)", + description=""" + Generate a 3-day personalized meal plan based on user preferences and health data. + + **Handles missing data cases:** + - Required fields: user_id, age, gender, weight, height + - Optional fields use smart defaults if not provided + - Returns data quality information and warnings + + **Note:** This endpoint uses manual input. Use `/from-profile/{user_id}` to fetch from database. + """ +) +async def get_meal_plan( + user_id: str = Query(..., description="User ID from database"), + age: Optional[int] = Query( + None, ge=1, le=120, description="User age in years"), + gender: Optional[Gender] = Query(None, description="User gender"), + weight: Optional[float] = Query( + None, ge=20, le=300, description="Weight in kg"), + height: Optional[float] = Query( + None, ge=50, le=250, description="Height in cm"), + activity_level: Optional[ActivityLevel] = Query( + None, description="Physical activity level"), + dietary_preference: Optional[DietaryPreference] = Query( + None, description="Dietary preference"), + allergies: Optional[str] = Query( + None, description="Comma-separated allergies (e.g., 'nuts,dairy,shellfish')"), + health_goals: Optional[str] = Query( + None, description="Health goals (e.g., 'weight_loss', 'muscle_gain')"), + meals_per_day: Optional[int] = Query( + None, ge=2, le=6, description="Number of meals per day") +): + """ + Generate personalized meal plan with error handling for missing data + """ + + try: + # Collect all parameters + params = { + "user_id": user_id, + "age": age, + "gender": gender, + "weight": weight, + "height": height, + "activity_level": activity_level, + "dietary_preference": dietary_preference, + "allergies": allergies, + "health_goals": health_goals, + "meals_per_day": meals_per_day + } + + # Validate and handle missing data + is_valid, enriched_params, data_quality = MealPlanValidator.validate_and_enrich( + params) + + if not is_valid: + # Cannot proceed - missing required fields + return MealPlanResponse( + success=False, + message=f"Insufficient data to generate meal plan. Missing required fields: {', '.join(data_quality.missing_fields)}", + meal_plan=None, + data_quality=data_quality, + recommendations=[ + "Please provide all required fields to generate a personalized meal plan", + "Required: user_id, age, gender, weight, height" + ] + ) + + # Generate meal plan with enriched data + meal_plan = generate_dummy_meal_plan(enriched_params) + + # Build recommendations based on data quality + recommendations = [] + if data_quality.data_completeness < 100: + recommendations.append( + f"Meal plan generated with {data_quality.data_completeness}% data completeness. " + "Provide more information for better personalization." + ) + + if enriched_params.get("allergies"): + recommendations.append( + f"Allergy restrictions applied: {enriched_params['allergies']}. " + "All meals exclude these ingredients." + ) + + # Success response + return MealPlanResponse( + success=True, + message="Meal plan generated successfully", + meal_plan=meal_plan, + data_quality=data_quality, + recommendations=recommendations if recommendations else None + ) + + except ValueError as e: + raise HTTPException(status_code=400, detail=f"Invalid input: {str(e)}") + + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Error generating meal plan: {str(e)}") + + +# ============================ +# ENDPOINT 2: Database Integration with Health Conditions (ENHANCED) +# ============================ + +@router.get( + "/from-profile/{user_id}", + response_model=MealPlanResponse, + summary="Generate Meal Plan from Database Profile ", + description=""" + **DATABASE VERSION** - Fetch user data from Supabase and generate meal plan. + + **Fetches from database:** + - age, gender, weight, height from 'users' table + - activity_level, dietary_preference from 'user_preferences' table + - allergies from 'user_allergies' table + - health_conditions from 'user_health_conditions' table (NEW!) + + **Adjusts meal plan based on health conditions:** + - Diabetes: Lower carbs, focus on fiber + - Hypertension: Low sodium options + - Heart disease: Low fat, omega-3 rich + - Celiac: Gluten-free meals + + **Handles missing data with smart defaults** + """ +) +async def get_meal_plan_from_database(user_id: str): + """ + Generate meal plan using data from Supabase database + Now includes health condition adjustments! + """ + + try: + logger.info(f"š Fetching profile for user: {user_id}") + + # STEP 1: Fetch user profile from database (age, gender, weight, height) + user_profile = await fetch_user_profile(user_id) + + if not user_profile: + raise HTTPException( + status_code=404, + detail=f"User not found in database: {user_id}" + ) + + logger.info(f"š¤ User: Age={user_profile.get('age')}, Gender={user_profile.get('gender')}, Weight={user_profile.get('weight')}kg") + + # STEP 2: Fetch preferences from database + preferences = await fetch_user_preferences(user_id) + + # STEP 3: Fetch allergies from database + allergies_list = await fetch_user_allergies(user_id) + allergies_str = ",".join(allergies_list) if allergies_list else "" + + # STEP 4: Fetch user's health conditions (NEW!) + user_conditions = await fetch_user_health_conditions(user_id) + condition_names = [ + cond.get('health_conditions', {}).get('name', '') + for cond in user_conditions + if cond.get('health_conditions') + ] + condition_names = [name for name in condition_names if name] # Remove empty strings + + if allergies_str: + logger.info(f"ā ļø User allergies: {allergies_str}") + + if condition_names: + logger.info(f"š„ User health conditions: {', '.join(condition_names)}") + + # STEP 5: Build params from database data + params = { + "user_id": user_id, + "age": user_profile.get("age"), + "gender": user_profile.get("gender"), + "weight": user_profile.get("weight"), + "height": user_profile.get("height"), + "activity_level": preferences.get("activity_level") if preferences else None, + "dietary_preference": preferences.get("dietary_preference") if preferences else None, + "allergies": allergies_str, + "health_goals": preferences.get("health_goals") if preferences else None, + "meals_per_day": preferences.get("meals_per_day") if preferences else None, + "health_conditions": condition_names # NEW! + } + + # STEP 6: Validate and handle missing data + is_valid, enriched_params, data_quality = MealPlanValidator.validate_and_enrich(params) + + if not is_valid: + logger.warning(f"ā Incomplete profile - Missing: {', '.join(data_quality.missing_fields)}") + return MealPlanResponse( + success=False, + message=f"Incomplete user profile in database. Missing: {', '.join(data_quality.missing_fields)}", + meal_plan=None, + data_quality=data_quality, + recommendations=[ + "Please complete your profile in the app", + f"Missing fields: {', '.join(data_quality.missing_fields)}" + ] + ) + + # STEP 7: Generate meal plan with health condition adjustments + logger.info(f"š½ļø Generating meal plan ({data_quality.data_completeness}% complete)") + meal_plan = generate_dummy_meal_plan(enriched_params) + + # STEP 8: Build recommendations with health conditions + recommendations = [] + + if data_quality.data_completeness < 100: + recommendations.append( + f"Meal plan generated with {data_quality.data_completeness}% profile completeness. " + "Complete your profile for better personalization." + ) + + if allergies_str: + recommendations.append(f"ā ļø Allergy restrictions applied: {allergies_str}") + + # Add health condition recommendations (NEW!) + if condition_names: + recommendations.append(f"š„ Health conditions considered: {', '.join(condition_names)}") + + if "Diabetes" in condition_names or "Type 2 Diabetes" in condition_names: + recommendations.append("𩺠Diabetic-friendly: Low glycemic index foods, reduced sugar, high fiber") + + if "Hypertension" in condition_names or "High Blood Pressure" in condition_names: + recommendations.append("𩺠Heart-healthy: Low sodium, increased potassium, DASH diet principles") + + if "Celiac Disease" in condition_names or "Gluten Intolerance" in condition_names: + recommendations.append("𩺠Gluten-free: All meals exclude wheat, barley, rye") + + if "Heart Disease" in condition_names or "Cardiovascular Disease" in condition_names: + recommendations.append("𩺠Cardiac diet: Low saturated fat, omega-3 rich, increased fiber") + + if "Kidney Disease" in condition_names: + recommendations.append("𩺠Renal diet: Controlled protein, potassium, and sodium") + + if data_quality.warnings: + recommendations.extend(data_quality.warnings) + + logger.info(f"ā Meal plan generated successfully for user: {user_id}") + + # STEP 9: Success response + return MealPlanResponse( + success=True, + message="Meal plan generated from your database profile", + meal_plan=meal_plan, + data_quality=data_quality, + recommendations=recommendations if recommendations else None + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"ā Error: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"Error generating meal plan: {str(e)}" + ) + + +# ============================ +# ENDPOINT 5: Get User's Health Conditions (NEW) +# ============================ + +@router.get( + "/user-conditions/{user_id}", + summary="Get User's Health Conditions", + description="Fetch specific user's health conditions from database" +) +async def get_user_health_conditions_endpoint(user_id: str): + """ + Get health conditions for a specific user + """ + try: + conditions = await fetch_user_health_conditions(user_id) + + if not conditions: + return { + "success": True, + "user_id": user_id, + "count": 0, + "conditions": [], + "message": "No health conditions found for this user" + } + + # Extract condition details + condition_list = [ + { + "id": cond.get('health_conditions', {}).get('id'), + "name": cond.get('health_conditions', {}).get('name'), + "description": cond.get('health_conditions', {}).get('description'), + "severity": cond.get('severity') + } + for cond in conditions + if cond.get('health_conditions') + ] + + return { + "success": True, + "user_id": user_id, + "count": len(condition_list), + "conditions": condition_list + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/nutrihelp_ai/routers/multi_image_api.py b/nutrihelp_ai/routers/multi_image_api.py index a8b858f..64710b8 100644 --- a/nutrihelp_ai/routers/multi_image_api.py +++ b/nutrihelp_ai/routers/multi_image_api.py @@ -1,3 +1,36 @@ +# nutrihelp_ai/routers/multi_image_api.py +# from fastapi import APIRouter, UploadFile, File, HTTPException +# from typing import List +# from nutrihelp_ai.services.multi_image_pipeline import MultiImagePipelineService +# import logging + +# router = APIRouter() +# logger = logging.getLogger(__name__) + + +# pipeline = MultiImagePipelineService() + +# @router.post( +# "/multi-image-analysis", +# summary="Multi Image Classification", +# description="Analyze multiple images and return predicted labels and confidences" +# ) +# async def multi_image_analysis(files: List[UploadFile] = File(...)): +# """ +# Endpoint to analyze multiple images. +# Returns: +# - labels +# - confidences +# """ +# try: +# results = await pipeline.process_images(files) +# return {"predictions": results} + +# except Exception as e: +# logger.error(f"Multi-image analysis endpoint failed: {str(e)}", exc_info=True) +# raise HTTPException(status_code=500, detail="Internal server error") + + # nutrihelp_ai/routers/multi_image_api.py from fastapi import APIRouter, UploadFile, File, HTTPException, Query from typing import List @@ -9,6 +42,7 @@ pipeline = MultiImagePipelineService() + @router.post( "/multi-image-analysis", summary="Multi Image Classification", @@ -19,12 +53,14 @@ ) async def multi_image_analysis( files: List[UploadFile] = File(...), - topk: int = Query(5, ge=1, le=50, description="How many top classes to return per image"), + topk: int = Query( + 5, ge=1, le=50, description="How many top classes to return per image"), ): try: results = await pipeline.process_images(files, topk=topk) return {"predictions": results} except Exception as e: - logger.error(f"Multi-image analysis endpoint failed: {str(e)}", exc_info=True) + logger.error( + f"Multi-image analysis endpoint failed: {str(e)}", exc_info=True) raise HTTPException(status_code=500, detail="Internal server error") diff --git a/nutrihelp_ai/routers/test.py b/nutrihelp_ai/routers/test.py new file mode 100644 index 0000000..46ff3b2 --- /dev/null +++ b/nutrihelp_ai/routers/test.py @@ -0,0 +1,374 @@ +import json +import random +import re +import os + +ALLERGY_MAP = { + "nuts": ["contains_nuts", "contains_tree_nut", "contains_peanut"], + "peanut": ["contains_peanut"], + "tree_nut": ["contains_tree_nut"], + "dairy": ["contains_dairy", "contains_milk", "contains_cheese", "contains_yoghurt", "contains_butter", "contains_cream", "contains_whey", "contains_casein", "contains_lactose"], + "milk": ["contains_milk", "contains_dairy"], + "egg": ["contains_egg"], + "soy": ["contains_soy"], + "gluten": ["contains_gluten", "contains_wheat", "contains_barley", "contains_rye"], + "wheat": ["contains_wheat", "contains_gluten"], + "shellfish": ["contains_shellfish"], + "fish": ["contains_fish"], + "fruit": ["contains_fruit", "contains_apple", "contains_banana", "contains_citrus", "contains_orange", "contains_lemon", "contains_lime", "contains_grapefruit", "contains_strawberry", + "contains_kiwi", "contains_peach", "contains_mango", "contains_pineapple"], + "sesame": ["contains_sesame", "contains_tahini"], + "apple": ["contains_apple"], + "banana": ["contains_banana"], + "citrus": ["contains_citrus", "contains_orange", "contains_lemon", "contains_lime", "contains_grapefruit"], + "strawberry": ["contains_strawberry"], + "kiwi": ["contains_kiwi"], +} + +CONDITION_MAP = { + # Diabetes: avoid high sugar and high GI meals + "diabetes": ["high_sugar", "high_gi", "added_sugar", "refined_carb", "sugary_drink", "dessert", "fried", "high_saturated_fat", "processed_meat", "high_salt"], + # High cholesterol: avoid high fat (saturared and fried foods) + "cholesterol": ["high_fat", "fried", "high_saturated_fat"], + "high_cholesterol": ["high_fat", "fried", "high_saturated_fat"], + # Hypertension + "hypertension": ["high_salt", "very_high_salt", "processed_meat", "pickled", "soy_sauce_heavy"], + "high_blood_pressure": ["high_salt", "very_high_salt", "processed_meat", "pickled", "soy_sauce_heavy"], + # Kidney Disease + "kidney_disease": ["high_potassium", "high_phosphorus", "high_protein"], + "ckd": ["high_potassium", "high_phosphorus", "high_protein"], + # Elderly condition rules + "elderly": ["hard_food", "crunchy", "tough_meat", "very_high_salt", "fried", "very_spicy"] +} + + +def detect_allergens(text: str) -> list[str]: + if not text: + return [] + + t = text.lower() + + keywords = { + "dairy": ["milk", "cheese", "yoghurt", "yogurt", "butter", "cream", "whey", "casein", "lactose"], + "egg": ["egg", "eggs", "albumen", "ovalbumin", "egg white", "egg yolk", "mayonaise", "mayonnaise"], + "soy": ["soy", "soya", "soybean", "tofu", "edamame", "lecithin (soy)", "soy lecithin"], + "gluten": ["gluten", "wheat", "barley", "rye", "malt", "flour"], + "peanut": ["peanut", "groundnut", "peanut oil", "peanut butter"], + "nuts": ["nuts", "almond", "cashew", "walnut", "hazelnut", "pistachio", "pecan", "macadamia"], + "fish": ["fish", "salmon", "tuna", "cod", "anchovy", "sardine"], + "shellfish": ["shellfish", "shrimp", "prawn", "crab", "lobster", "clam", "clams", "mussel", "mussels", "oyster", "oysters", "scallop", "scallops"], + "sesame": ["sesame", "sesame seed", "sesame oil", "tahini", "hummus"], + "apple": ["apple", "apples", "apple juice", "apple puree"], + "banana": ["banana", "bananas", "banana powder"], + "citrus": ["orange", "oranges", "lemon", "lemons", "lime", "limes", "grapefruit", "mandarin", "citrus"], + "fruit": ["strawberry", "strawberries", "kiwi", "kiwi fruit", "peach", "peaches", "mango", "mangoes", "pineapple"] + } + + found = set() + for allergen, words in keywords.items(): + for w in words: + if re.search(rf"\b{re.escape(w)}\b", t): + found.add(allergen) + break + + if "citrus" in found: + found.add("fruit") + + found = [a for a in found if a in ALLERGY_MAP] + + return list(found) + +# Load meals form JSON file + + +def meal_library(): + base_dir = os.path.dirname(os.path.abspath(__file__)) + path = os.path.join(base_dir, "meal_library.json") + with open(path, "r", encoding="utf-8") as file: + return json.load(file) + + +def normalise_list_input(value): + # None -> [] + if value is None: + return [] + # list -> same list + if isinstance(value, list): + return value + # string "a,b" -> ["a", "b"] + # string "a" -> ["a"] + if isinstance(value, str): + # split by comma if needed + parts = [v.strip() for v in value.split(",")] + return [p for p in parts if p] # remove empty strings + # any other weird type + return [] + +# Filter by allergies + + +def filter_allergy(meals, allergies): + + result = [] + + allergies_lowercase = [] + for a in allergies: + allergies_lowercase.append(a.lower()) + + for meal in meals: + has_allergy = False + + for allergy in allergies_lowercase: + if allergy in ALLERGY_MAP: + to_avoid = ALLERGY_MAP[allergy] + else: + to_avoid = [] + + for tag in to_avoid: + if tag in meal["tags"]: + has_allergy = True + break + + if has_allergy: + break + + if not has_allergy: + result.append(meal) + return result + +# Filter by health condition + + +def filter_condition(meals, conditions): + result = [] + + conditions_lowercase = [] + for c in conditions: + conditions_lowercase.append(c.lower()) + + for meal in meals: + bad = False + + for condition in conditions_lowercase: + if condition in CONDITION_MAP: + bad_tag = CONDITION_MAP[condition] + else: + bad_tag = [] + + for tag in bad_tag: + if tag in meal["tags"]: + bad = True + break + + if bad: + break + + if not bad: + result.append(meal) + + return result + +# Filter by texture requirement + + +def filter_texture(meals, text): + if text is None: + text = "normal" + + text = text.lower() + + # We filter only if user needs soft food + if text != "soft": + return meals + + soft_meals = [] + for meal in meals: + if "soft_food" in meal["tags"]: + soft_meals.append(meal) + + return soft_meals + + +def filter_budget(meals, budget): + if budget is None: + budget = "medium" + budget = str(budget).lower() + + # preference order (fallback) + if budget == "low": + preferred_orders = [["cost_low"], ["cost_low", "cost_medium"], [ + "cost_low", "cost_medium", "cost_high"]] + elif budget == "medium": + preferred_orders = [["cost_medium"], ["cost_medium", "cost_low"], [ + "cost_medium", "cost_low", "cost_high"]] + else: + # high budget: no need to restrict + return meals + + # Try strict first, then relax + for allowed in preferred_orders: + filtered = [] + for meal in meals: + tags = meal.get("tags", []) + if any(cost_tag in tags for cost_tag in allowed): + filtered.append(meal) + if len(filtered) > 0: + return filtered + + # if no cost tags exist at all, return original + return meals + + +def pick_meal_under_calories(meals, remaining_calories): + valid_meals = [m for m in meals if m.get( + "calories", 0) <= remaining_calories] + if not valid_meals: + return None + return random.choice(valid_meals) + + +# Build the plan +def plan(user, all_meals): + if user is None: + user = {} + + # FALLBACK LOGIC FOR LIMITED INPUT + # allergies or conditions can be missing, string, or list + raw_allergies = user.get("allergies", []) + raw_conditions = user.get("conditions", []) + + allergies = normalise_list_input(raw_allergies) + conditions = normalise_list_input(raw_conditions) + + label_text = user.get("label_text", "") + detected_allergies = detect_allergens(label_text) + + # Combine user allergies + detected allergies + allergies = allergies + detected_allergies + allergies = list(set([a.lower() for a in allergies])) + + # texture: default to "normal" if missing or empty + texture = user.get("texture") or "normal" + + budget = user.get("budget") or "medium" + + # calories_target: default to 2000 if not given + calories = user.get("calories_target") + if calories is None: + calories = 2000 + + # Apply filters step by step + filtered = filter_allergy(all_meals, allergies) + filtered = filter_condition(filtered, conditions) + filtered = filter_texture(filtered, texture) + + conditions_lower = [str(c).lower() for c in conditions] + if "elderly" in conditions_lower: + soft_meals = [ + meal for meal in filtered if "soft_food" in meal.get("tags", [])] + if len(soft_meals) > 0: + filtered = soft_meals + + filtered = filter_budget(filtered, budget) + + # Split meals by type + breakfasts = [] + lunches = [] + dinners = [] + snacks = [] + + for meal in filtered: + if meal["meal_type"] == "breakfast": + breakfasts.append(meal) + elif meal["meal_type"] == "lunch": + lunches.append(meal) + elif meal["meal_type"] == "dinner": + dinners.append(meal) + elif meal["meal_type"] == "snack": + snacks.append(meal) + + # Pick one meal for each main meal, and up to 2 snacks + remaining_calories = calories + + plan = { + "breakfast": None, + "lunch": None, + "dinner": None, + "snacks": [] + } + + # Pick breakfast + plan["breakfast"] = pick_meal_under_calories( + breakfasts, remaining_calories) + if plan["breakfast"]: + remaining_calories -= plan["breakfast"]["calories"] + + # Pick lunch + plan["lunch"] = pick_meal_under_calories(lunches, remaining_calories) + if plan["lunch"]: + remaining_calories -= plan["lunch"]["calories"] + + # Pick dinner + plan["dinner"] = pick_meal_under_calories(dinners, remaining_calories) + if plan["dinner"]: + remaining_calories -= plan["dinner"]["calories"] + + # Dinner fallback ā MUST respect remaining calories + if plan["dinner"] is None: + soft_dinners = [m for m in dinners if "soft_food" in m.get("tags", [])] + plan["dinner"] = pick_meal_under_calories( + soft_dinners, remaining_calories) + + # Reuse lunch ONLY if it fits remaining calories + if plan["dinner"] is None and plan["lunch"] is not None: + if plan["lunch"]["calories"] <= remaining_calories: + plan["dinner"] = plan["lunch"] + remaining_calories -= plan["lunch"]["calories"] + + for snack in snacks: + if len(plan["snacks"]) >= 2: + break + if snack["calories"] <= remaining_calories: + plan["snacks"].append(snack) + remaining_calories -= snack["calories"] + + # Calculate total calories + total_calories = 0 + + if plan["breakfast"] is not None: + total_calories += plan["breakfast"]["calories"] + if plan["lunch"] is not None: + total_calories += plan["lunch"]["calories"] + if plan["dinner"] is not None: + total_calories += plan["dinner"]["calories"] + + for snack in plan["snacks"]: + total_calories += snack["calories"] + + plan["total_calories"] = total_calories + plan["target_calories"] = calories + + plan["allergies_used"] = allergies + plan["detected_allergies_from_label"] = detected_allergies + plan["budget_used"] = budget + + return plan + + +if __name__ == "__main__": + meals = meal_library() + + # Test 1: Dairy allergy + user_dairy_allergy = { + "name": "Test Dairy Allergy", + "label_text": "", + "allergies": [], + "conditions": [], + "budget": "medium", + "texture": "normal", + "calories_target": 1500 + } + + print("\n=== TEST 1: Dairy Allergy ===") + result1 = plan(user_dairy_allergy, meals) + print(json.dumps(result1, indent=2, ensure_ascii=False)) diff --git a/nutrihelp_ai/run_voice_app.bat b/nutrihelp_ai/run_voice_app.bat new file mode 100644 index 0000000..4ce8fe5 --- /dev/null +++ b/nutrihelp_ai/run_voice_app.bat @@ -0,0 +1,2 @@ +@echo off +start cmd /k "cd /d C:\Users\Naman Shah\OneDrive\Documents\Assignement_Deakin\T3\SIT764\Project\NutriHelp-AI\nutrihelp_ai && python app.py" diff --git a/nutrihelp_ai/services/multi_image_pipeline.py b/nutrihelp_ai/services/multi_image_pipeline.py index 9a80375..d41fc69 100644 --- a/nutrihelp_ai/services/multi_image_pipeline.py +++ b/nutrihelp_ai/services/multi_image_pipeline.py @@ -1,3 +1,60 @@ +# # nutrihelp_ai/services/multi_image_pipeline.py +# import asyncio +# import tempfile +# import os +# from typing import List, Dict, Any +# from fastapi import UploadFile +# from PIL import Image +# from nutrihelp_ai.services.multi_image_classifier.scripts.training.predict import Predictor +# import logging + +# logger = logging.getLogger(__name__) + +# class MultiImagePipelineService: +# def __init__(self): +# self.predictor = Predictor() + +# async def process_images(self, files: List[UploadFile]) -> List[Dict[str, Any]]: +# """ +# Process multiple UploadFile images and return predictions. +# Only labels and confidences are returned. +# """ +# results = [] +# loop = asyncio.get_running_loop() + +# for file in files: +# try: +# suffix = os.path.splitext(file.filename)[1] +# with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp: +# tmp.write(await file.read()) +# temp_path = tmp.name + +# print(f"[Stage 1] Image saved temporarily: {temp_path}") + +# prediction = await loop.run_in_executor( +# None, self.predictor.predict_paths, [temp_path] +# ) +# result = { +# "labels": prediction[0].get("labels", []), +# "confidences": prediction[0].get("confidences", []) +# } + +# print(f"[Stage 2] Prediction complete: {result['labels']}") + +# os.remove(temp_path) +# print(f"[Stage 3] Temporary file deleted: {temp_path}") + +# results.append(result) + +# except Exception as e: +# logger.error(f"Failed to process image {file.filename}: {e}", exc_info=True) +# results.append({ +# "error": "Failed to process this image" +# }) + +# return results + + # nutrihelp_ai/services/multi_image_pipeline.py import asyncio import tempfile @@ -14,7 +71,7 @@ # Internal threshold (not exposed to Swagger) UNCLEAR_THRESHOLD = 0.25 -# User-facing suggestion +# User-facing suggestion UNCLEAR_SUGGESTION = "Please upload a clearer image." diff --git a/nutrihelp_ai/tempCodeRunnerFile.py b/nutrihelp_ai/tempCodeRunnerFile.py new file mode 100644 index 0000000..ff3ab56 --- /dev/null +++ b/nutrihelp_ai/tempCodeRunnerFile.py @@ -0,0 +1 @@ +# gen \ No newline at end of file diff --git a/nutrihelp_ai/templates/index.html b/nutrihelp_ai/templates/index.html new file mode 100644 index 0000000..1f73665 --- /dev/null +++ b/nutrihelp_ai/templates/index.html @@ -0,0 +1,1975 @@ + + +
+ +
+ Jenny - Your AI Nutrition Assistant
+
+ Jenny - Your AI Nutrition Assistant
+