mirror of
https://github.com/escalante29/healthy-fit.git
synced 2026-03-21 13:48:46 +01:00
Add AI-powered nutrition and plan modules
Introduces DSPy-based nutrition and plan generation modules, including image analysis for nutritional info and personalized diet/exercise plans. Adds new API endpoints for health metrics/goals, nutrition image analysis, and plan management. Updates models, schemas, and backend structure to support these features, and includes initial training data and configuration for prompt optimization.
This commit is contained in:
@@ -1,25 +1,107 @@
|
||||
import base64
|
||||
|
||||
import dspy
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.config import settings
|
||||
|
||||
|
||||
class NutritionalInfo(BaseModel):
|
||||
name: str
|
||||
calories: float
|
||||
protein: float
|
||||
carbs: float
|
||||
fats: float
|
||||
reasoning: str = Field(description="Step-by-step reasoning for the nutritional estimates")
|
||||
name: str = Field(description="Name of the food item")
|
||||
calories: float = Field(description="Estimated calories")
|
||||
protein: float = Field(description="Estimated protein in grams")
|
||||
carbs: float = Field(description="Estimated carbohydrates in grams")
|
||||
fats: float = Field(description="Estimated fats in grams")
|
||||
micros: dict | None = None
|
||||
|
||||
|
||||
class ExtractNutrition(dspy.Signature):
|
||||
"""Extract nutritional information from a food description."""
|
||||
"""Extract nutritional information from a food description.
|
||||
|
||||
You must first provide a detailed step-by-step reasoning analysis of the ingredients,
|
||||
portions, AND preparation methods (cooking oils, butter, sauces) before estimating values.
|
||||
Verify if the caloric totals match the sum of macros (multiplying protein/carbs by 4, fats by 9).
|
||||
"""
|
||||
|
||||
description: str = dspy.InputField(desc="Description of the food or meal")
|
||||
nutritional_info: NutritionalInfo = dspy.OutputField(desc="Nutritional information as a structured object")
|
||||
nutritional_info: NutritionalInfo = dspy.OutputField(desc="Nutritional information with reasoning")
|
||||
|
||||
|
||||
class AnalyzeFoodImage(dspy.Signature):
|
||||
"""Analyze the food image to estimate nutritional content.
|
||||
|
||||
1. Identify all food items and estimated portion sizes.
|
||||
2. CRITICAL: Account for hidden calories from cooking fats, oils, and sauces (searing, frying).
|
||||
3. Reason step-by-step about the total composition before summing macros.
|
||||
"""
|
||||
|
||||
image: dspy.Image = dspy.InputField(desc="The food image")
|
||||
description: str = dspy.InputField(desc="Additional user description", default="")
|
||||
nutritional_info: NutritionalInfo = dspy.OutputField(desc="Nutritional information with reasoning")
|
||||
|
||||
|
||||
class NutritionModule(dspy.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.extract = dspy.ChainOfThought(ExtractNutrition)
|
||||
self.analyze_image = dspy.ChainOfThought(AnalyzeFoodImage)
|
||||
|
||||
# Load optimized prompts if available
|
||||
import os
|
||||
|
||||
compiled_path = os.path.join(os.path.dirname(__file__), "nutrition_compiled.json")
|
||||
if os.path.exists(compiled_path):
|
||||
self.load(compiled_path)
|
||||
print(f"Loaded optimized DSPy prompts from {compiled_path}")
|
||||
else:
|
||||
print("No optimized prompts found, using default zero-shot.")
|
||||
|
||||
def forward(self, description: str):
|
||||
return self.extract(description=description)
|
||||
pred = self.extract(description=description)
|
||||
|
||||
# Assertion: Check Macro Consistency
|
||||
calc_cals = (
|
||||
(pred.nutritional_info.protein * 4) + (pred.nutritional_info.carbs * 4) + (pred.nutritional_info.fats * 9)
|
||||
)
|
||||
|
||||
# dspy.Suggest is not available in dspy>=3.1.0
|
||||
# dspy.Suggest(
|
||||
# abs(calc_cals - pred.nutritional_info.calories) < (pred.nutritional_info.calories * 0.20),
|
||||
# f"The sum of macros ({calc_cals:.1f}) should match the total calories "
|
||||
# f"({pred.nutritional_info.calories}). Check your math.",
|
||||
# )
|
||||
return pred
|
||||
|
||||
def forward_image(self, image_url: str, description: str = ""):
|
||||
image = dspy.Image(image_url)
|
||||
pred = self.analyze_image(image=image, description=description)
|
||||
|
||||
# Assertion: Check Macro Consistency
|
||||
calc_cals = (
|
||||
(pred.nutritional_info.protein * 4) + (pred.nutritional_info.carbs * 4) + (pred.nutritional_info.fats * 9)
|
||||
)
|
||||
|
||||
# dspy.Suggest is not available in dspy>=3.1.0
|
||||
# dspy.Suggest(
|
||||
# abs(calc_cals - pred.nutritional_info.calories) < (pred.nutritional_info.calories * 0.20),
|
||||
# f"The sum of macros ({calc_cals:.1f}) should match the total calories "
|
||||
# f"({pred.nutritional_info.calories}). Check your math.",
|
||||
# )
|
||||
return pred
|
||||
|
||||
|
||||
nutrition_module = NutritionModule()
|
||||
|
||||
|
||||
def analyze_nutrition_from_image(image_bytes: bytes, description: str = "") -> NutritionalInfo:
|
||||
if not settings.OPENAI_API_KEY:
|
||||
raise ValueError("OpenAI API Key not set")
|
||||
|
||||
# Convert to base64 data URI
|
||||
base64_image = base64.b64encode(image_bytes).decode("utf-8")
|
||||
image_url = f"data:image/jpeg;base64,{base64_image}"
|
||||
|
||||
# Use DSPy module
|
||||
result = nutrition_module.forward_image(image_url=image_url, description=description)
|
||||
return result.nutritional_info
|
||||
|
||||
147
backend/app/ai/nutrition_compiled.json
Normal file
147
backend/app/ai/nutrition_compiled.json
Normal file
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"extract.predict": {
|
||||
"traces": [],
|
||||
"train": [],
|
||||
"demos": [
|
||||
{
|
||||
"augmented": true,
|
||||
"description": "Blueberry Muffin (Bakery size)",
|
||||
"reasoning": "A typical bakery-sized blueberry muffin is generally larger than a standard homemade muffin and is made from ingredients such as flour, sugar, butter, eggs, milk, blueberries, and baking powder. The estimated calorie count for a large blueberry muffin is about 400-500 calories, primarily derived from carbohydrates (mainly from flour and sugar), fats (from butter), and a moderate amount of protein. \n\nFor nutritional breakdown:\n- Carbs: Approximately 60g derived from the flour and sugar.\n- Fats: Approximately 20g from the butter.\n- Protein: Roughly 6g from the flour and egg content.\n- The muffin may also contain vitamins and minerals from the blueberries.\n\nOverall, a bakery-sized blueberry muffin is calorie-dense due to the combination of ingredients used, especially sugar and butter.",
|
||||
"nutritional_info": {
|
||||
"reasoning": "Bakery-sized muffin has ingredients like flour, sugar, butter, and blueberries. Estimated 450 cal, with ~60g carbs, ~20g fat, and ~6g protein.",
|
||||
"name": "Blueberry Muffin",
|
||||
"calories": 450.0,
|
||||
"protein": 6.0,
|
||||
"carbs": 60.0,
|
||||
"fats": 20.0,
|
||||
"micros": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Philly Cheesesteak",
|
||||
"nutritional_info": {
|
||||
"reasoning": "Roll (250 cal). Fatty steak (400 cal). Cheese whiz/provolone (150 cal). Oil (100 cal).",
|
||||
"name": "Cheesesteak",
|
||||
"calories": 900.0,
|
||||
"protein": 40.0,
|
||||
"carbs": 50.0,
|
||||
"fats": 55.0,
|
||||
"micros": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Grilled salmon with asparagus and roasted potatoes",
|
||||
"nutritional_info": {
|
||||
"reasoning": "6oz Salmon fillet (350 cal). Oil for cooking (60 cal). Asparagus (30 cal) + oil (30 cal). 1 cup roasted potatoes (150 cal) + oil (60 cal). Total ~680 cal.",
|
||||
"name": "Salmon Dinner",
|
||||
"calories": 680.0,
|
||||
"protein": 40.0,
|
||||
"carbs": 25.0,
|
||||
"fats": 45.0,
|
||||
"micros": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Tacos - 3 beef tacos with cheese and sour cream",
|
||||
"nutritional_info": {
|
||||
"reasoning": "3 corn tortillas (150 cal). Ground beef filling (250 cal - cooked with fat). Cheese (110 cal). Sour cream (60 cal). Total ~570 cal.",
|
||||
"name": "Beef Tacos",
|
||||
"calories": 570.0,
|
||||
"protein": 25.0,
|
||||
"carbs": 45.0,
|
||||
"fats": 30.0,
|
||||
"micros": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"signature": {
|
||||
"instructions": "Extract nutritional information from a food description.\n\nYou must first provide a detailed step-by-step reasoning analysis of the ingredients,\nportions, AND preparation methods (cooking oils, butter, sauces) before estimating values.\nVerify if the caloric totals match the sum of macros (multiplying protein/carbs by 4, fats by 9).",
|
||||
"fields": [
|
||||
{
|
||||
"prefix": "Description:",
|
||||
"description": "Description of the food or meal"
|
||||
},
|
||||
{
|
||||
"prefix": "Reasoning: Let's think step by step in order to",
|
||||
"description": "${reasoning}"
|
||||
},
|
||||
{
|
||||
"prefix": "Nutritional Info:",
|
||||
"description": "Nutritional information with reasoning"
|
||||
}
|
||||
]
|
||||
},
|
||||
"lm": null
|
||||
},
|
||||
"analyze_image.predict": {
|
||||
"traces": [],
|
||||
"train": [],
|
||||
"demos": [
|
||||
{
|
||||
"description": "Philly Cheesesteak",
|
||||
"nutritional_info": {
|
||||
"reasoning": "Roll (250 cal). Fatty steak (400 cal). Cheese whiz/provolone (150 cal). Oil (100 cal).",
|
||||
"name": "Cheesesteak",
|
||||
"calories": 900.0,
|
||||
"protein": 40.0,
|
||||
"carbs": 50.0,
|
||||
"fats": 55.0,
|
||||
"micros": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Grilled salmon with asparagus and roasted potatoes",
|
||||
"nutritional_info": {
|
||||
"reasoning": "6oz Salmon fillet (350 cal). Oil for cooking (60 cal). Asparagus (30 cal) + oil (30 cal). 1 cup roasted potatoes (150 cal) + oil (60 cal). Total ~680 cal.",
|
||||
"name": "Salmon Dinner",
|
||||
"calories": 680.0,
|
||||
"protein": 40.0,
|
||||
"carbs": 25.0,
|
||||
"fats": 45.0,
|
||||
"micros": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Tacos - 3 beef tacos with cheese and sour cream",
|
||||
"nutritional_info": {
|
||||
"reasoning": "3 corn tortillas (150 cal). Ground beef filling (250 cal - cooked with fat). Cheese (110 cal). Sour cream (60 cal). Total ~570 cal.",
|
||||
"name": "Beef Tacos",
|
||||
"calories": 570.0,
|
||||
"protein": 25.0,
|
||||
"carbs": 45.0,
|
||||
"fats": 30.0,
|
||||
"micros": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"signature": {
|
||||
"instructions": "Analyze the food image to estimate nutritional content.\n\n1. Identify all food items and estimated portion sizes.\n2. CRITICAL: Account for hidden calories from cooking fats, oils, and sauces (searing, frying).\n3. Reason step-by-step about the total composition before summing macros.",
|
||||
"fields": [
|
||||
{
|
||||
"prefix": "Image:",
|
||||
"description": "The food image"
|
||||
},
|
||||
{
|
||||
"prefix": "Description:",
|
||||
"description": "Additional user description"
|
||||
},
|
||||
{
|
||||
"prefix": "Reasoning: Let's think step by step in order to",
|
||||
"description": "${reasoning}"
|
||||
},
|
||||
{
|
||||
"prefix": "Nutritional Info:",
|
||||
"description": "Nutritional information with reasoning"
|
||||
}
|
||||
]
|
||||
},
|
||||
"lm": null
|
||||
},
|
||||
"metadata": {
|
||||
"dependency_versions": {
|
||||
"python": "3.11",
|
||||
"dspy": "3.1.0",
|
||||
"cloudpickle": "3.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
34
backend/app/ai/plans.py
Normal file
34
backend/app/ai/plans.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import dspy
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class PlanOutput(BaseModel):
|
||||
reasoning: str = Field(description="Reasoning behind the selected plan based on user goals")
|
||||
title: str = Field(description="Title of the plan")
|
||||
summary: str = Field(description="Brief summary of the plan")
|
||||
diet_plan: list[str] = Field(description="List of daily diet recommendations")
|
||||
exercise_plan: list[str] = Field(description="List of daily exercise routines")
|
||||
tips: list[str] = Field(description="Additional health tips")
|
||||
|
||||
|
||||
class GeneratePlan(dspy.Signature):
|
||||
"""Generate a personalized diet and exercise plan based on user goal and details.
|
||||
|
||||
Analyze the user's profile and goal, explain your reasoning, and then generate the plan.
|
||||
"""
|
||||
|
||||
user_profile: str = dspy.InputField(desc="User details (age, weight, height, etc)")
|
||||
goal: str = dspy.InputField(desc="Specific user goal")
|
||||
plan: PlanOutput = dspy.OutputField(desc="Structured plan with reasoning")
|
||||
|
||||
|
||||
class PlanModule(dspy.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.generate = dspy.ChainOfThought(GeneratePlan)
|
||||
|
||||
def forward(self, user_profile: str, goal: str):
|
||||
return self.generate(user_profile=user_profile, goal=goal)
|
||||
|
||||
|
||||
plan_module = PlanModule()
|
||||
Reference in New Issue
Block a user