Beable commited on
Commit
a144435
·
verified ·
1 Parent(s): a27c14f

Upload 6 files

Browse files
Files changed (6) hide show
  1. README.md +168 -13
  2. diet_tool.py +508 -0
  3. foods_DB_Example.json +0 -0
  4. gradio_app_simple.py +772 -0
  5. main3.py +477 -0
  6. requirements.txt +6 -0
README.md CHANGED
@@ -1,13 +1,168 @@
1
- ---
2
- title: RAG DietAssistant
3
- emoji: 😻
4
- colorFrom: pink
5
- colorTo: indigo
6
- sdk: gradio
7
- sdk_version: 5.44.1
8
- app_file: app.py
9
- pinned: false
10
- license: mit
11
- ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: AI Diet Planner & Nutrition Assistant
3
+ emoji: 🍽️
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: gradio
7
+ sdk_version: 4.44.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ short_description: AI-powered diet planning with nutrition analysis
12
+ ---
13
+
14
+ # 🍽️ AI Diet Planner & Nutrition Assistant
15
+
16
+ An intelligent diet planning application that uses RAG (Retrieval-Augmented Generation) technology to provide personalized nutrition advice, meal planning, and dietary recommendations.
17
+
18
+ ## ✨ Features
19
+
20
+ ### 🎯 **Personalized Diet Planning**
21
+ - **Weekly Meal Plans**: Complete 7-day diet plans with detailed meal breakdowns
22
+ - **Daily Nutrition**: Individual day meal planning with macro and micronutrient analysis
23
+ - **Customizable Parameters**: Age, weight, height, activity level, and dietary preferences
24
+
25
+ ### 📊 **Nutrition Analysis**
26
+ - **Comprehensive Nutrition Facts**: Detailed breakdown of calories, macronutrients, vitamins, and minerals
27
+ - **USDA Food Database**: Access to extensive food nutrition information
28
+ - **BMR & TDEE Calculations**: Accurate metabolic rate calculations using Mifflin-St Jeor formula
29
+
30
+ ### 🤖 **AI-Powered Recommendations**
31
+ - **Smart Suggestions**: Personalized dietary recommendations based on your profile
32
+ - **Goal-Oriented Planning**: Weight loss, muscle gain, or maintenance-focused plans
33
+ - **Interactive Chat**: Natural language interface for easy interaction
34
+
35
+ ### 📋 **Advanced Features**
36
+ - **JSON Export**: All plans and analyses saved as structured JSON files
37
+ - **Table Formatting**: Beautiful markdown tables for easy reading
38
+ - **Multi-language Support**: English interface with comprehensive food database
39
+
40
+ ## 🚀 How to Use
41
+
42
+ ### 1. **Create a Diet Plan**
43
+ ```
44
+ "Create a weekly diet plan for a 25-year-old male, 70kg, 175cm, moderate activity"
45
+ ```
46
+
47
+ ### 2. **Calculate Nutrition**
48
+ ```
49
+ "Calculate nutrition for 200g chicken breast and 150g rice"
50
+ ```
51
+
52
+ ### 3. **Get Recommendations**
53
+ ```
54
+ "Get diet recommendations for weight loss"
55
+ ```
56
+
57
+ ### 4. **Vegetarian Options**
58
+ ```
59
+ "Create a vegetarian meal plan for 7 days"
60
+ ```
61
+
62
+ ## 🔧 Technical Details
63
+
64
+ ### **Architecture**
65
+ - **RAG System**: Retrieval-Augmented Generation for intelligent responses
66
+ - **Groq API**: Fast LLM inference for real-time responses
67
+ - **Sentence Transformers**: Semantic search for relevant nutrition data
68
+ - **Gradio**: Modern web interface for easy interaction
69
+
70
+ ### **Data Sources**
71
+ - **USDA FoodData Central**: Comprehensive nutrition database
72
+ - **Custom Food Database**: Curated collection of common foods
73
+ - **Nutritional Algorithms**: BMR, TDEE, and macro distribution calculations
74
+
75
+ ### **Output Formats**
76
+ - **JSON Files**: Structured data for all plans and analyses
77
+ - **Markdown Tables**: Human-readable formatted output
78
+ - **Interactive Chat**: Real-time conversation interface
79
+
80
+ ## 📁 File Structure
81
+
82
+ ```
83
+ ├── app.py # Main Gradio application
84
+ ├── main3.py # RAG system and tool router
85
+ ├── diet_tool.py # Diet planning algorithms
86
+ ├── usda_provider.py # USDA API integration
87
+ ├── requirements.txt # Python dependencies
88
+ ├── README.md # This file
89
+ └── outputs/ # Generated JSON files
90
+ ├── diet_plan_weekly_*.json
91
+ ├── diet_plan_daily_*.json
92
+ └── diet_recommendations_*.json
93
+ ```
94
+
95
+ ## 🛠️ Setup Instructions
96
+
97
+ ### **For Hugging Face Spaces**
98
+
99
+ 1. **Create a new Space** on Hugging Face
100
+ 2. **Upload all files** to your Space repository
101
+ 3. **Set Environment Variables**:
102
+ - Go to Settings → Secrets
103
+ - Add `GROQ_API_KEY` with your Groq API key
104
+ 4. **Deploy** - The Space will automatically build and deploy
105
+
106
+ ### **For Local Development**
107
+
108
+ 1. **Install Dependencies**:
109
+ ```bash
110
+ pip install -r requirements.txt
111
+ ```
112
+
113
+ 2. **Set Environment Variable**:
114
+ ```bash
115
+ export GROQ_API_KEY="your_groq_api_key_here"
116
+ ```
117
+
118
+ 3. **Run the Application**:
119
+ ```bash
120
+ python app.py
121
+ ```
122
+
123
+ ## 🔑 API Keys Required
124
+
125
+ - **Groq API Key**: Get your free API key from [console.groq.com](https://console.groq.com)
126
+
127
+ ## 📊 Example Outputs
128
+
129
+ ### **Weekly Diet Plan**
130
+ - 7-day meal breakdown
131
+ - Daily calorie and macro targets
132
+ - Detailed food lists with portions
133
+ - Nutritional analysis per meal
134
+
135
+ ### **Nutrition Analysis**
136
+ - Complete macronutrient breakdown
137
+ - Vitamin and mineral content
138
+ - Calorie calculations
139
+ - Serving size recommendations
140
+
141
+ ### **Dietary Recommendations**
142
+ - Personalized advice based on profile
143
+ - Goal-specific suggestions
144
+ - Macro distribution guidance
145
+ - Lifestyle integration tips
146
+
147
+ ## 🤝 Contributing
148
+
149
+ Feel free to contribute to this project by:
150
+ - Adding new food items to the database
151
+ - Improving nutrition calculation algorithms
152
+ - Enhancing the user interface
153
+ - Adding new dietary patterns (keto, paleo, etc.)
154
+
155
+ ## 📄 License
156
+
157
+ This project is licensed under the MIT License - see the LICENSE file for details.
158
+
159
+ ## 🙏 Acknowledgments
160
+
161
+ - **USDA FoodData Central** for comprehensive nutrition data
162
+ - **Groq** for fast LLM inference
163
+ - **Gradio** for the beautiful web interface
164
+ - **Sentence Transformers** for semantic search capabilities
165
+
166
+ ---
167
+
168
+ **Made with ❤️ for better nutrition and healthier living!**
diet_tool.py ADDED
@@ -0,0 +1,508 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import random
3
+ import math
4
+ from typing import Dict, List, Any, Optional
5
+ from dataclasses import dataclass
6
+ from datetime import datetime, timedelta
7
+ from usda_provider import fetch_nutrition_usda
8
+
9
+ @dataclass
10
+ class UserProfile:
11
+ """User profile information"""
12
+ age: int
13
+ sex: str # "male" or "female"
14
+ height_cm: float
15
+ weight_kg: float
16
+ activity_level: str # "sedentary", "light", "moderate", "active", "very_active"
17
+ goal: str # "weight_loss", "muscle_gain", "maintenance", "keto", "mediterranean"
18
+ dietary_restrictions: List[str] = None
19
+ allergies: List[str] = None
20
+ preferences: List[str] = None
21
+
22
+ @dataclass
23
+ class MealPlan:
24
+ """Meal plan"""
25
+ meal_name: str
26
+ time_range: str
27
+ foods: List[Dict[str, Any]]
28
+ total_calories: float
29
+ total_protein: float
30
+ total_carbs: float
31
+ total_fat: float
32
+ total_fiber: float
33
+
34
+ @dataclass
35
+ class DailyPlan:
36
+ """Daily diet plan"""
37
+ date: str
38
+ meals: List[MealPlan]
39
+ total_calories: float
40
+ total_protein: float
41
+ total_carbs: float
42
+ total_fat: float
43
+ total_fiber: float
44
+
45
+ class DietPlanner:
46
+ """Diet planner class"""
47
+
48
+ def __init__(self, diet_data_file: str = "foods_DB_Example.json"):
49
+ """Load diet data"""
50
+ with open(diet_data_file, 'r', encoding='utf-8') as f:
51
+ self.diet_data = json.load(f)
52
+
53
+ # Meal name translation mapping
54
+ self.meal_name_translation = {
55
+ "Kahvaltı": "Breakfast",
56
+ "Kuşluk": "Morning Snack",
57
+ "Öğle Yemeği": "Lunch",
58
+ "İkindi": "Afternoon Snack",
59
+ "Akşam Yemeği": "Dinner",
60
+ "Gece Atıştırması": "Evening Snack"
61
+ }
62
+
63
+ def calculate_bmr(self, profile: UserProfile) -> float:
64
+ """Calculate basal metabolic rate (Mifflin-St Jeor formula)"""
65
+ if profile.sex.lower() == "male":
66
+ bmr = 10 * profile.weight_kg + 6.25 * profile.height_cm - 5 * profile.age + 5
67
+ else:
68
+ bmr = 10 * profile.weight_kg + 6.25 * profile.height_cm - 5 * profile.age - 161
69
+ return bmr
70
+
71
+ def calculate_tdee(self, profile: UserProfile) -> float:
72
+ """Calculate total daily energy expenditure"""
73
+ bmr = self.calculate_bmr(profile)
74
+
75
+ activity_multipliers = {
76
+ "sedentary": 1.2, # Sedentary
77
+ "light": 1.375, # Lightly active
78
+ "moderate": 1.55, # Moderately active
79
+ "active": 1.725, # Active
80
+ "very_active": 1.9 # Very active
81
+ }
82
+
83
+ multiplier = activity_multipliers.get(profile.activity_level.lower(), 1.2)
84
+ return bmr * multiplier
85
+
86
+ def calculate_target_calories(self, profile: UserProfile) -> float:
87
+ """Calculate target calorie amount"""
88
+ tdee = self.calculate_tdee(profile)
89
+
90
+ if profile.goal == "weight_loss":
91
+ # 15-20% calorie deficit
92
+ deficit = random.uniform(0.15, 0.20)
93
+ return tdee * (1 - deficit)
94
+ elif profile.goal == "muscle_gain":
95
+ # 10-15% calorie surplus
96
+ surplus = random.uniform(0.10, 0.15)
97
+ return tdee * (1 + surplus)
98
+ elif profile.goal == "keto":
99
+ # 10-15% calorie deficit
100
+ deficit = random.uniform(0.10, 0.15)
101
+ return tdee * (1 - deficit)
102
+ else:
103
+ # Maintenance and Mediterranean
104
+ return tdee
105
+
106
+ def get_macro_ratios(self, profile: UserProfile) -> Dict[str, float]:
107
+ """Get macronutrient ratios"""
108
+ diet_type = self.diet_data["diet_types"].get(profile.goal, {})
109
+
110
+ if profile.goal == "weight_loss":
111
+ return {
112
+ "protein": random.uniform(0.25, 0.30),
113
+ "carbs": random.uniform(0.40, 0.45),
114
+ "fat": random.uniform(0.25, 0.30)
115
+ }
116
+ elif profile.goal == "muscle_gain":
117
+ return {
118
+ "protein": random.uniform(0.25, 0.30),
119
+ "carbs": random.uniform(0.45, 0.55),
120
+ "fat": random.uniform(0.20, 0.25)
121
+ }
122
+ elif profile.goal == "keto":
123
+ return {
124
+ "protein": random.uniform(0.20, 0.25),
125
+ "carbs": random.uniform(0.05, 0.10),
126
+ "fat": random.uniform(0.70, 0.75)
127
+ }
128
+ else:
129
+ return {
130
+ "protein": random.uniform(0.20, 0.25),
131
+ "carbs": random.uniform(0.45, 0.55),
132
+ "fat": random.uniform(0.25, 0.30)
133
+ }
134
+
135
+ def get_available_foods(self, profile: UserProfile) -> Dict[str, List[Dict]]:
136
+ """Filter foods that the user can eat"""
137
+ available_foods = {}
138
+
139
+ for category, subcategories in self.diet_data["foods"].items():
140
+ available_foods[category] = []
141
+
142
+ for subcategory, foods in subcategories.items():
143
+ for food in foods:
144
+ # Allergy check
145
+ if profile.allergies:
146
+ food_name_lower = food["name"].lower()
147
+ if any(allergy.lower() in food_name_lower for allergy in profile.allergies):
148
+ continue
149
+
150
+ # Dietary restrictions check
151
+ if profile.dietary_restrictions:
152
+ if "vegetarian" in profile.dietary_restrictions and category == "protein_sources":
153
+ if subcategory in ["lean_meats", "fish_seafood"]:
154
+ continue
155
+ if "vegan" in profile.dietary_restrictions and category in ["protein_sources", "dairy"]:
156
+ if subcategory in ["lean_meats", "fish_seafood", "eggs", "milk_products", "cheese"]:
157
+ continue
158
+
159
+ # Calorie value check (prevent division by zero)
160
+ if food.get("calories_per_100g", 0) <= 0:
161
+ continue
162
+
163
+ available_foods[category].append(food)
164
+
165
+ return available_foods
166
+
167
+ def select_foods_for_meal(self, meal_type: str, target_calories: float,
168
+ macro_ratios: Dict[str, float], available_foods: Dict[str, List[Dict]]) -> List[Dict[str, Any]]:
169
+ """Select foods for meal"""
170
+ meal_info = self.diet_data["meals"][meal_type]
171
+ food_categories = meal_info["food_categories"]
172
+
173
+ selected_foods = []
174
+ remaining_calories = target_calories
175
+
176
+ # Select food for each category
177
+ for category in food_categories:
178
+ if category not in available_foods or not available_foods[category]:
179
+ continue
180
+
181
+ # Calorie target for category
182
+ category_calories = remaining_calories * 0.3 # 30% for each category
183
+
184
+ if category_calories < 50: # If very few calories left
185
+ continue
186
+
187
+ # Randomly select food from category
188
+ food = random.choice(available_foods[category])
189
+
190
+ # Calculate portion amount (with division by zero check)
191
+ if food["calories_per_100g"] <= 0:
192
+ continue # Skip foods with 0 or negative calorie values
193
+
194
+ portion_grams = min(category_calories / (food["calories_per_100g"] / 100), 200)
195
+
196
+ # Minimum portion check
197
+ if portion_grams < 20:
198
+ continue
199
+
200
+ selected_food = {
201
+ "name": food["name"],
202
+ "portion_grams": round(portion_grams, 1),
203
+ "calories": round(food["calories_per_100g"] * portion_grams / 100, 1),
204
+ "protein": round(food["protein_per_100g"] * portion_grams / 100, 1),
205
+ "carbs": round(food["carbs_per_100g"] * portion_grams / 100, 1),
206
+ "fat": round(food["fat_per_100g"] * portion_grams / 100, 1),
207
+ "fiber": round(food["fiber_per_100g"] * portion_grams / 100, 1),
208
+ "vitamins": food["vitamins"],
209
+ "minerals": food["minerals"]
210
+ }
211
+
212
+ selected_foods.append(selected_food)
213
+ remaining_calories -= selected_food["calories"]
214
+
215
+ if remaining_calories < 100: # Stop if very few calories left
216
+ break
217
+
218
+ return selected_foods
219
+
220
+ def create_meal_plan(self, meal_type: str, target_calories: float,
221
+ macro_ratios: Dict[str, float], available_foods: Dict[str, List[Dict]]) -> MealPlan:
222
+ """Create meal plan"""
223
+ meal_info = self.diet_data["meals"][meal_type]
224
+ foods = self.select_foods_for_meal(meal_type, target_calories, macro_ratios, available_foods)
225
+
226
+ # Calculate total nutritional values
227
+ total_calories = sum(food["calories"] for food in foods)
228
+ total_protein = sum(food["protein"] for food in foods)
229
+ total_carbs = sum(food["carbs"] for food in foods)
230
+ total_fat = sum(food["fat"] for food in foods)
231
+ total_fiber = sum(food["fiber"] for food in foods)
232
+
233
+ # Translate meal name to English
234
+ meal_name = self.meal_name_translation.get(meal_info["name"], meal_info["name"])
235
+
236
+ return MealPlan(
237
+ meal_name=meal_name,
238
+ time_range=meal_info["time_range"],
239
+ foods=foods,
240
+ total_calories=round(total_calories, 1),
241
+ total_protein=round(total_protein, 1),
242
+ total_carbs=round(total_carbs, 1),
243
+ total_fat=round(total_fat, 1),
244
+ total_fiber=round(total_fiber, 1)
245
+ )
246
+
247
+ def generate_daily_plan(self, profile: UserProfile) -> DailyPlan:
248
+ """Generate daily diet plan"""
249
+ target_calories = self.calculate_target_calories(profile)
250
+ macro_ratios = self.get_macro_ratios(profile)
251
+ available_foods = self.get_available_foods(profile)
252
+
253
+ # Determine meal types
254
+ meal_types = ["breakfast", "lunch", "dinner"]
255
+
256
+ # Add snacks
257
+ if profile.goal == "muscle_gain":
258
+ meal_types.extend(["morning_snack", "afternoon_snack", "evening_snack"])
259
+ elif profile.goal == "weight_loss":
260
+ meal_types.append("morning_snack")
261
+ else:
262
+ meal_types.extend(["morning_snack", "afternoon_snack"])
263
+
264
+ meals = []
265
+ total_calories = 0
266
+ total_protein = 0
267
+ total_carbs = 0
268
+ total_fat = 0
269
+ total_fiber = 0
270
+
271
+ for meal_type in meal_types:
272
+ # Calorie target for meal
273
+ meal_calories = target_calories * float(self.diet_data["meals"][meal_type]["calorie_target"].split("-")[0]) / 100
274
+
275
+ meal_plan = self.create_meal_plan(meal_type, meal_calories, macro_ratios, available_foods)
276
+ meals.append(meal_plan)
277
+
278
+ total_calories += meal_plan.total_calories
279
+ total_protein += meal_plan.total_protein
280
+ total_carbs += meal_plan.total_carbs
281
+ total_fat += meal_plan.total_fat
282
+ total_fiber += meal_plan.total_fiber
283
+
284
+ return DailyPlan(
285
+ date=datetime.now().strftime("%Y-%m-%d"),
286
+ meals=meals,
287
+ total_calories=round(total_calories, 1),
288
+ total_protein=round(total_protein, 1),
289
+ total_carbs=round(total_carbs, 1),
290
+ total_fat=round(total_fat, 1),
291
+ total_fiber=round(total_fiber, 1)
292
+ )
293
+
294
+ def generate_weekly_plan(self, profile: UserProfile) -> List[DailyPlan]:
295
+ """Generate weekly diet plan"""
296
+ weekly_plan = []
297
+
298
+ for i in range(7):
299
+ # Create different variations for each day
300
+ daily_plan = self.generate_daily_plan(profile)
301
+ daily_plan.date = (datetime.now() + timedelta(days=i)).strftime("%Y-%m-%d")
302
+ weekly_plan.append(daily_plan)
303
+
304
+ return weekly_plan
305
+
306
+ def plan_to_dict(self, plan: DailyPlan) -> Dict[str, Any]:
307
+ """Convert plan to dictionary format"""
308
+ return {
309
+ "date": plan.date,
310
+ "meals": [
311
+ {
312
+ "meal_name": meal.meal_name,
313
+ "time_range": meal.time_range,
314
+ "foods": meal.foods,
315
+ "nutrition": {
316
+ "calories": meal.total_calories,
317
+ "protein": meal.total_protein,
318
+ "carbs": meal.total_carbs,
319
+ "fat": meal.total_fat,
320
+ "fiber": meal.total_fiber
321
+ }
322
+ }
323
+ for meal in plan.meals
324
+ ],
325
+ "daily_totals": {
326
+ "calories": plan.total_calories,
327
+ "protein": plan.total_protein,
328
+ "carbs": plan.total_carbs,
329
+ "fat": plan.total_fat,
330
+ "fiber": plan.total_fiber
331
+ }
332
+ }
333
+
334
+ def generate_diet_plan(user_profile: Dict[str, Any], plan_type: str = "daily") -> Dict[str, Any]:
335
+ """
336
+ Create diet plan
337
+
338
+ Args:
339
+ user_profile: User profile information
340
+ plan_type: "daily" or "weekly"
341
+
342
+ Returns:
343
+ Diet plan dictionary
344
+ """
345
+ try:
346
+ # Create UserProfile object
347
+ profile = UserProfile(
348
+ age=user_profile["age"],
349
+ sex=user_profile["sex"],
350
+ height_cm=user_profile["height_cm"],
351
+ weight_kg=user_profile["weight_kg"],
352
+ activity_level=user_profile["activity_level"],
353
+ goal=user_profile["goal"],
354
+ dietary_restrictions=user_profile.get("dietary_restrictions", []),
355
+ allergies=user_profile.get("allergies", []),
356
+ preferences=user_profile.get("preferences", [])
357
+ )
358
+
359
+ # Create DietPlanner
360
+ planner = DietPlanner()
361
+
362
+ if plan_type == "weekly":
363
+ plans = planner.generate_weekly_plan(profile)
364
+ return {
365
+ "plan_type": "weekly",
366
+ "plans": [planner.plan_to_dict(plan) for plan in plans]
367
+ }
368
+ else:
369
+ plan = planner.generate_daily_plan(profile)
370
+ return {
371
+ "plan_type": "daily",
372
+ "plan": planner.plan_to_dict(plan)
373
+ }
374
+
375
+ except Exception as e:
376
+ return {
377
+ "error": f"Error while creating diet plan: {str(e)}"
378
+ }
379
+
380
+ def calculate_nutrition_info(food_name: str, portion_grams: float) -> Dict[str, Any]:
381
+ """
382
+ Calculate nutritional values for a specific food
383
+
384
+ Args:
385
+ food_name: Food name
386
+ portion_grams: Portion amount (grams)
387
+
388
+ Returns:
389
+ Nutritional values dictionary
390
+ """
391
+ try:
392
+ planner = DietPlanner()
393
+
394
+ # Find the food
395
+ for category, subcategories in planner.diet_data["foods"].items():
396
+ for subcategory, foods in subcategories.items():
397
+ for food in foods:
398
+ if food["name"].lower() == food_name.lower():
399
+ return {
400
+ "food_name": food["name"],
401
+ "portion_grams": portion_grams,
402
+ "calories": round(food["calories_per_100g"] * portion_grams / 100, 1),
403
+ "protein": round(food["protein_per_100g"] * portion_grams / 100, 1),
404
+ "carbs": round(food["carbs_per_100g"] * portion_grams / 100, 1),
405
+ "fat": round(food["fat_per_100g"] * portion_grams / 100, 1),
406
+ "fiber": round(food["fiber_per_100g"] * portion_grams / 100, 1),
407
+ "vitamins": food["vitamins"],
408
+ "minerals": food["minerals"]
409
+ }
410
+ # If not found locally, get from USDA
411
+ usda = fetch_nutrition_usda(food_name, portion_grams)
412
+ if usda:
413
+ return {
414
+ "food_name": food_name,
415
+ "portion_grams": usda.get("portion_grams", portion_grams),
416
+ "calories": usda.get("calories"),
417
+ "protein": usda.get("protein"),
418
+ "carbs": usda.get("carbs"),
419
+ "fat": usda.get("fat"),
420
+ "fiber": usda.get("fiber"),
421
+ "vitamins": usda.get("vitamins", {}),
422
+ "minerals": usda.get("minerals", {}),
423
+ "source": "USDA",
424
+ "fdc_id": usda.get("fdc_id"),
425
+ "description": usda.get("description"),
426
+ }
427
+ return {"error": f"'{food_name}' not found (local and USDA)"}
428
+
429
+ except Exception as e:
430
+ return {"error": f"Error while calculating nutritional value: {str(e)}"}
431
+
432
+ def get_diet_recommendations(user_profile: Dict[str, Any]) -> Dict[str, Any]:
433
+ """
434
+ Create diet recommendations for user
435
+
436
+ Args:
437
+ user_profile: User profile information
438
+
439
+ Returns:
440
+ Diet recommendations dictionary
441
+ """
442
+ try:
443
+ profile = UserProfile(
444
+ age=user_profile["age"],
445
+ sex=user_profile["sex"],
446
+ height_cm=user_profile["height_cm"],
447
+ weight_kg=user_profile["weight_kg"],
448
+ activity_level=user_profile["activity_level"],
449
+ goal=user_profile["goal"],
450
+ dietary_restrictions=user_profile.get("dietary_restrictions", []),
451
+ allergies=user_profile.get("allergies", []),
452
+ preferences=user_profile.get("preferences", [])
453
+ )
454
+
455
+ planner = DietPlanner()
456
+
457
+ bmr = planner.calculate_bmr(profile)
458
+ tdee = planner.calculate_tdee(profile)
459
+ target_calories = planner.calculate_target_calories(profile)
460
+ macro_ratios = planner.get_macro_ratios(profile)
461
+
462
+ return {
463
+ "bmr": round(bmr, 1),
464
+ "tdee": round(tdee, 1),
465
+ "target_calories": round(target_calories, 1),
466
+ "macro_ratios": {
467
+ "protein": f"{macro_ratios['protein']*100:.1f}%",
468
+ "carbs": f"{macro_ratios['carbs']*100:.1f}%",
469
+ "fat": f"{macro_ratios['fat']*100:.1f}%"
470
+ },
471
+ "macro_grams": {
472
+ "protein": round(target_calories * macro_ratios["protein"] / 4, 1),
473
+ "carbs": round(target_calories * macro_ratios["carbs"] / 4, 1),
474
+ "fat": round(target_calories * macro_ratios["fat"] / 9, 1)
475
+ },
476
+ "recommendations": {
477
+ "meal_frequency": planner.diet_data["diet_types"][profile.goal]["meal_frequency"],
478
+ "fiber_target": planner.diet_data["diet_types"][profile.goal]["fiber_target"]
479
+ }
480
+ }
481
+
482
+ except Exception as e:
483
+ return {"error": f"Error while creating recommendations: {str(e)}"}
484
+
485
+ # Test function
486
+ if __name__ == "__main__":
487
+ # Example user profile
488
+ test_profile = {
489
+ "age": 30,
490
+ "sex": "male",
491
+ "height_cm": 175,
492
+ "weight_kg": 75,
493
+ "activity_level": "moderate",
494
+ "goal": "weight_loss",
495
+ "dietary_restrictions": [],
496
+ "allergies": [],
497
+ "preferences": []
498
+ }
499
+
500
+ # Create daily plan
501
+ daily_plan = generate_diet_plan(test_profile, "daily")
502
+ print("Daily Diet Plan:")
503
+ print(json.dumps(daily_plan, ensure_ascii=False, indent=2))
504
+
505
+ # Get recommendations
506
+ recommendations = get_diet_recommendations(test_profile)
507
+ print("\nDiet Recommendations:")
508
+ print(json.dumps(recommendations, ensure_ascii=False, indent=2))
foods_DB_Example.json ADDED
The diff for this file is too large to render. See raw diff
 
gradio_app_simple.py ADDED
@@ -0,0 +1,772 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import json
3
+ import os
4
+ import pandas as pd
5
+ import re
6
+ import glob
7
+ from typing import Dict, List, Any
8
+ from main3 import run_rag_tool_router, execute_diet_tool
9
+ from diet_tool import generate_diet_plan, get_diet_recommendations, calculate_nutrition_info
10
+
11
+ # Global chat history
12
+ chat_history = []
13
+
14
+ def format_json_to_table(json_data: Any, title: str = "") -> str:
15
+ """Convert JSON data to a formatted table string"""
16
+ if not json_data:
17
+ return ""
18
+
19
+ try:
20
+ if isinstance(json_data, str):
21
+ json_data = json.loads(json_data)
22
+
23
+ if isinstance(json_data, dict):
24
+ return format_dict_to_table(json_data, title)
25
+ elif isinstance(json_data, list):
26
+ return format_list_to_table(json_data, title)
27
+ else:
28
+ return str(json_data)
29
+ except:
30
+ return str(json_data)
31
+
32
+ def format_dict_to_table(data: dict, title: str = "") -> str:
33
+ """Format dictionary as a table"""
34
+ if not data:
35
+ return ""
36
+
37
+ result = f"## {title}\n\n" if title else ""
38
+
39
+ # Check if it's a simple key-value dict
40
+ if all(not isinstance(v, (dict, list)) for v in data.values()):
41
+ result += "| Key | Value |\n"
42
+ result += "|-----|-------|\n"
43
+ for key, value in data.items():
44
+ result += f"| **{key}** | {value} |\n"
45
+ else:
46
+ # Complex nested structure
47
+ for key, value in data.items():
48
+ if isinstance(value, dict):
49
+ result += f"### {key}\n"
50
+ result += format_dict_to_table(value)
51
+ elif isinstance(value, list):
52
+ result += f"### {key}\n"
53
+ result += format_list_to_table(value)
54
+ else:
55
+ result += f"**{key}**: {value}\n"
56
+
57
+ return result
58
+
59
+ def format_list_to_table(data: list, title: str = "") -> str:
60
+ """Format list as a table"""
61
+ if not data:
62
+ return ""
63
+
64
+ result = f"## {title}\n\n" if title else ""
65
+
66
+ if not data:
67
+ return result
68
+
69
+ # Check if all items are dictionaries with similar keys
70
+ if all(isinstance(item, dict) for item in data):
71
+ # Get all unique keys
72
+ all_keys = set()
73
+ for item in data:
74
+ all_keys.update(item.keys())
75
+
76
+ if all_keys:
77
+ # Create table header
78
+ result += "| " + " | ".join(all_keys) + " |\n"
79
+ result += "|" + "|".join(["---"] * len(all_keys)) + "|\n"
80
+
81
+ # Add rows
82
+ for item in data:
83
+ row = []
84
+ for key in all_keys:
85
+ value = item.get(key, "")
86
+ if isinstance(value, (dict, list)):
87
+ value = json.dumps(value, ensure_ascii=False)[:50] + "..."
88
+ row.append(str(value))
89
+ result += "| " + " | ".join(row) + " |\n"
90
+ else:
91
+ result += "Empty list\n"
92
+ else:
93
+ # Simple list
94
+ for i, item in enumerate(data):
95
+ result += f"{i+1}. {item}\n"
96
+
97
+ return result
98
+
99
+ def format_diet_plan_result(result: dict) -> str:
100
+ """Format diet plan result specifically"""
101
+ if not result or "error" in result:
102
+ return f"❌ Error: {result.get('error', 'Unknown error')}"
103
+
104
+ formatted = "## 🍽️ Diet Plan\n\n"
105
+
106
+ if "plan_type" in result:
107
+ formatted += f"**Plan Type**: {result['plan_type']}\n\n"
108
+
109
+ if "plan" in result:
110
+ plan = result["plan"]
111
+
112
+ # Daily totals
113
+ if "daily_totals" in plan:
114
+ totals = plan["daily_totals"]
115
+ formatted += "### 📊 Daily Nutrition Summary\n\n"
116
+ formatted += "| Nutrient | Amount |\n"
117
+ formatted += "|----------|--------|\n"
118
+ formatted += f"| **Calories** | {totals.get('calories', 0)} kcal |\n"
119
+ formatted += f"| **Protein** | {totals.get('protein', 0)} g |\n"
120
+ formatted += f"| **Carbs** | {totals.get('carbs', 0)} g |\n"
121
+ formatted += f"| **Fat** | {totals.get('fat', 0)} g |\n"
122
+ formatted += f"| **Fiber** | {totals.get('fiber', 0)} g |\n\n"
123
+
124
+ # Meals
125
+ if "meals" in plan:
126
+ formatted += "### 🍽️ Meals\n\n"
127
+ for meal in plan["meals"]:
128
+ formatted += f"#### {meal.get('meal_name', 'Meal')} ({meal.get('time_range', '')})\n\n"
129
+
130
+ # Meal nutrition
131
+ if "nutrition" in meal:
132
+ nutrition = meal["nutrition"]
133
+ formatted += "**Nutrition**: "
134
+ formatted += f"{nutrition.get('calories', 0)} kcal, "
135
+ formatted += f"{nutrition.get('protein', 0)}g protein, "
136
+ formatted += f"{nutrition.get('carbs', 0)}g carbs, "
137
+ formatted += f"{nutrition.get('fat', 0)}g fat\n\n"
138
+
139
+ # Foods
140
+ if "foods" in meal:
141
+ formatted += "**Foods**:\n"
142
+ for food in meal["foods"]:
143
+ formatted += f"- **{food.get('name', 'Unknown')}** "
144
+ formatted += f"({food.get('portion_grams', 0)}g) - "
145
+ formatted += f"{food.get('calories', 0)} kcal\n"
146
+ formatted += "\n"
147
+
148
+ elif "plans" in result:
149
+ # Weekly plan
150
+ plans = result["plans"]
151
+ formatted += f"### 📅 Weekly Plan ({len(plans)} days)\n\n"
152
+
153
+ for i, plan in enumerate(plans[:3]): # Show first 3 days
154
+ formatted += f"#### Day {i+1} - {plan.get('date', '')}\n"
155
+ if "daily_totals" in plan:
156
+ totals = plan["daily_totals"]
157
+ formatted += f"**Total**: {totals.get('calories', 0)} kcal, "
158
+ formatted += f"{totals.get('protein', 0)}g protein\n\n"
159
+
160
+ if len(plans) > 3:
161
+ formatted += f"... and {len(plans) - 3} more days\n\n"
162
+
163
+ return formatted
164
+
165
+ def format_recommendations_result(result: dict) -> str:
166
+ """Format recommendations result specifically"""
167
+ if not result or "error" in result:
168
+ return f"❌ Error: {result.get('error', 'Unknown error')}"
169
+
170
+ formatted = "## 📊 Diet Recommendations\n\n"
171
+
172
+ # Basic metrics
173
+ if "bmr" in result:
174
+ formatted += f"**BMR (Basal Metabolic Rate)**: {result['bmr']} kcal/day\n"
175
+ if "tdee" in result:
176
+ formatted += f"**TDEE (Total Daily Energy Expenditure)**: {result['tdee']} kcal/day\n"
177
+ if "target_calories" in result:
178
+ formatted += f"**Target Calories**: {result['target_calories']} kcal/day\n\n"
179
+
180
+ # Macro ratios
181
+ if "macro_ratios" in result:
182
+ ratios = result["macro_ratios"]
183
+ formatted += "### 🎯 Macro Ratios\n\n"
184
+ formatted += "| Macronutrient | Percentage |\n"
185
+ formatted += "|---------------|------------|\n"
186
+ for macro, ratio in ratios.items():
187
+ formatted += f"| **{macro.title()}** | {ratio} |\n"
188
+ formatted += "\n"
189
+
190
+ # Macro grams
191
+ if "macro_grams" in result:
192
+ grams = result["macro_grams"]
193
+ formatted += "### ⚖️ Daily Macro Targets (grams)\n\n"
194
+ formatted += "| Macronutrient | Grams |\n"
195
+ formatted += "|---------------|-------|\n"
196
+ for macro, grams_val in grams.items():
197
+ formatted += f"| **{macro.title()}** | {grams_val} g |\n"
198
+ formatted += "\n"
199
+
200
+ # Recommendations
201
+ if "recommendations" in result:
202
+ recs = result["recommendations"]
203
+ formatted += "### 💡 Recommendations\n\n"
204
+ for key, value in recs.items():
205
+ formatted += f"**{key.replace('_', ' ').title()}**: {value}\n"
206
+
207
+ return formatted
208
+
209
+ def format_nutrition_result(result: dict) -> str:
210
+ """Format nutrition calculation result specifically"""
211
+ if not result or "error" in result:
212
+ return f"❌ Error: {result.get('error', 'Unknown error')}"
213
+
214
+ formatted = "## 🧮 Nutrition Information\n\n"
215
+
216
+ if "food_name" in result:
217
+ formatted += f"**Food**: {result['food_name']}\n"
218
+ if "portion_grams" in result:
219
+ formatted += f"**Portion**: {result['portion_grams']} grams\n\n"
220
+
221
+ # Basic nutrition
222
+ formatted += "### 📊 Nutritional Values\n\n"
223
+ formatted += "| Nutrient | Amount |\n"
224
+ formatted += "|----------|--------|\n"
225
+
226
+ nutrients = ["calories", "protein", "carbs", "fat", "fiber"]
227
+ for nutrient in nutrients:
228
+ if nutrient in result:
229
+ unit = "kcal" if nutrient == "calories" else "g"
230
+ formatted += f"| **{nutrient.title()}** | {result[nutrient]} {unit} |\n"
231
+
232
+ # Vitamins and minerals
233
+ if "vitamins" in result and result["vitamins"]:
234
+ formatted += "\n### 🥕 Vitamins\n\n"
235
+ if isinstance(result["vitamins"], list):
236
+ for vitamin in result["vitamins"]:
237
+ formatted += f"- {vitamin}\n"
238
+ else:
239
+ formatted += str(result["vitamins"])
240
+
241
+ if "minerals" in result and result["minerals"]:
242
+ formatted += "\n### ⚡ Minerals\n\n"
243
+ if isinstance(result["minerals"], list):
244
+ for mineral in result["minerals"]:
245
+ formatted += f"- {mineral}\n"
246
+ else:
247
+ formatted += str(result["minerals"])
248
+
249
+ return formatted
250
+
251
+ def load_latest_json_output() -> str:
252
+ """Load and format the latest JSON output file"""
253
+ outputs_dir = "outputs"
254
+ if not os.path.exists(outputs_dir):
255
+ return "❌ No outputs directory found"
256
+
257
+ # Get all JSON files in outputs directory
258
+ json_files = glob.glob(os.path.join(outputs_dir, "*.json"))
259
+ if not json_files:
260
+ return "❌ No JSON files found in outputs directory"
261
+
262
+ # Sort by modification time and get the latest
263
+ latest_file = max(json_files, key=os.path.getmtime)
264
+
265
+ try:
266
+ with open(latest_file, 'r', encoding='utf-8') as f:
267
+ data = json.load(f)
268
+
269
+ # Extract filename for title
270
+ filename = os.path.basename(latest_file)
271
+
272
+ # Format based on file type
273
+ if "diet_plan" in filename:
274
+ if "weekly" in filename:
275
+ return format_weekly_diet_plan(data, filename)
276
+ else:
277
+ return format_daily_diet_plan(data, filename)
278
+ elif "recommendations" in filename:
279
+ return format_recommendations_result(data, filename)
280
+ elif "nutrition" in filename:
281
+ return format_nutrition_result(data, filename)
282
+ else:
283
+ return format_json_data_to_table(data, f"Latest Output: {filename}")
284
+
285
+ except Exception as e:
286
+ return f"❌ Error reading {latest_file}: {str(e)}"
287
+
288
+ def format_weekly_diet_plan(data: dict, filename: str) -> str:
289
+ """Format weekly diet plan from JSON file"""
290
+ if not data or "result" not in data:
291
+ return f"❌ Invalid data in {filename}"
292
+
293
+ result = data["result"]
294
+ if "plans" not in result:
295
+ return f"❌ No plans found in {filename}"
296
+
297
+ plans = result["plans"]
298
+ formatted = f"## 📅 Weekly Diet Plan ({len(plans)} days)\n\n"
299
+ formatted += f"**File**: {filename}\n\n"
300
+
301
+ # Summary table for all days
302
+ formatted += "### 📊 Weekly Summary\n\n"
303
+ formatted += "| Day | Date | Calories | Protein | Carbs | Fat | Fiber |\n"
304
+ formatted += "|-----|------|----------|---------|-------|-----|-------|\n"
305
+
306
+ for i, plan in enumerate(plans):
307
+ totals = plan.get("daily_totals", {})
308
+ date = plan.get("date", f"Day {i+1}")
309
+ formatted += f"| {i+1} | {date} | {totals.get('calories', 0)} | {totals.get('protein', 0)}g | {totals.get('carbs', 0)}g | {totals.get('fat', 0)}g | {totals.get('fiber', 0)}g |\n"
310
+
311
+ # Detailed view for all days
312
+ formatted += "\n### 🍽️ Detailed Meals (All Days)\n\n"
313
+
314
+ for i, plan in enumerate(plans):
315
+ date = plan.get("date", f"Day {i+1}")
316
+ formatted += f"#### Day {i+1} - {date}\n\n"
317
+
318
+ meals = plan.get("meals", [])
319
+ for meal in meals:
320
+ meal_name = meal.get("meal_name", "Meal")
321
+ time_range = meal.get("time_range", "")
322
+ nutrition = meal.get("nutrition", {})
323
+
324
+ formatted += f"**{meal_name}** ({time_range})\n"
325
+ formatted += f"- Calories: {nutrition.get('calories', 0)} | Protein: {nutrition.get('protein', 0)}g | Carbs: {nutrition.get('carbs', 0)}g | Fat: {nutrition.get('fat', 0)}g\n"
326
+
327
+ # Foods
328
+ foods = meal.get("foods", [])
329
+ if foods:
330
+ formatted += " - Foods: "
331
+ food_names = [f.get("name", "Unknown") for f in foods[:3]] # Show first 3 foods
332
+ formatted += ", ".join(food_names)
333
+ if len(foods) > 3:
334
+ formatted += f" (+{len(foods)-3} more)"
335
+ formatted += "\n"
336
+ formatted += "\n"
337
+
338
+ return formatted
339
+
340
+ def format_daily_diet_plan(data: dict, filename: str) -> str:
341
+ """Format daily diet plan from JSON file"""
342
+ if not data or "result" not in data:
343
+ return f"❌ Invalid data in {filename}"
344
+
345
+ result = data["result"]
346
+ if "plan" not in result:
347
+ return f"❌ No plan found in {filename}"
348
+
349
+ plan = result["plan"]
350
+ formatted = f"## 🍽️ Daily Diet Plan\n\n"
351
+ formatted += f"**File**: {filename}\n\n"
352
+
353
+ # Daily totals
354
+ if "daily_totals" in plan:
355
+ totals = plan["daily_totals"]
356
+ formatted += "### 📊 Daily Nutrition Summary\n\n"
357
+ formatted += "| Nutrient | Amount |\n"
358
+ formatted += "|----------|--------|\n"
359
+ formatted += f"| **Calories** | {totals.get('calories', 0)} kcal |\n"
360
+ formatted += f"| **Protein** | {totals.get('protein', 0)} g |\n"
361
+ formatted += f"| **Carbs** | {totals.get('carbs', 0)} g |\n"
362
+ formatted += f"| **Fat** | {totals.get('fat', 0)} g |\n"
363
+ formatted += f"| **Fiber** | {totals.get('fiber', 0)} g |\n\n"
364
+
365
+ # Meals
366
+ if "meals" in plan:
367
+ formatted += "### 🍽️ Meals\n\n"
368
+ for meal in plan["meals"]:
369
+ meal_name = meal.get("meal_name", "Meal")
370
+ time_range = meal.get("time_range", "")
371
+ nutrition = meal.get("nutrition", {})
372
+
373
+ formatted += f"#### {meal_name} ({time_range})\n\n"
374
+ formatted += f"**Nutrition**: {nutrition.get('calories', 0)} kcal, {nutrition.get('protein', 0)}g protein, {nutrition.get('carbs', 0)}g carbs, {nutrition.get('fat', 0)}g fat\n\n"
375
+
376
+ # Foods
377
+ foods = meal.get("foods", [])
378
+ if foods:
379
+ formatted += "**Foods**:\n"
380
+ for food in foods:
381
+ name = food.get("name", "Unknown")
382
+ portion = food.get("portion_grams", 0)
383
+ calories = food.get("calories", 0)
384
+ formatted += f"- **{name}** ({portion}g) - {calories} kcal\n"
385
+ formatted += "\n"
386
+
387
+ return formatted
388
+
389
+ def list_all_json_outputs() -> str:
390
+ """List all JSON output files with their details"""
391
+ outputs_dir = "outputs"
392
+ if not os.path.exists(outputs_dir):
393
+ return "❌ No outputs directory found"
394
+
395
+ json_files = glob.glob(os.path.join(outputs_dir, "*.json"))
396
+ if not json_files:
397
+ return "❌ No JSON files found in outputs directory"
398
+
399
+ # Sort by modification time (newest first)
400
+ json_files.sort(key=os.path.getmtime, reverse=True)
401
+
402
+ formatted = "## 📁 Available JSON Outputs\n\n"
403
+ formatted += "| File | Type | Size | Modified |\n"
404
+ formatted += "|------|------|------|----------|\n"
405
+
406
+ for file_path in json_files:
407
+ filename = os.path.basename(file_path)
408
+ file_type = "Unknown"
409
+
410
+ if "diet_plan_weekly" in filename:
411
+ file_type = "Weekly Diet Plan"
412
+ elif "diet_plan_daily" in filename:
413
+ file_type = "Daily Diet Plan"
414
+ elif "recommendations" in filename:
415
+ file_type = "Recommendations"
416
+ elif "nutrition" in filename:
417
+ file_type = "Nutrition Info"
418
+
419
+ file_size = os.path.getsize(file_path)
420
+ mod_time = os.path.getmtime(file_path)
421
+ mod_date = pd.Timestamp.fromtimestamp(mod_time).strftime("%Y-%m-%d %H:%M")
422
+
423
+ formatted += f"| {filename} | {file_type} | {file_size:,} bytes | {mod_date} |\n"
424
+
425
+ return formatted
426
+
427
+ def extract_and_format_json_in_text(text: str) -> str:
428
+ """Extract JSON objects from text and format them as tables"""
429
+ if not text:
430
+ return text
431
+
432
+ formatted_text = text
433
+
434
+ # First, try to find JSON in code blocks
435
+ code_block_pattern = r'```(?:json)?\s*(\{.*?\})\s*```'
436
+ matches = list(re.finditer(code_block_pattern, formatted_text, re.DOTALL | re.IGNORECASE))
437
+
438
+ for match in reversed(matches):
439
+ json_str = match.group(1).strip()
440
+ try:
441
+ json_data = json.loads(json_str)
442
+ table = format_json_data_to_table(json_data)
443
+ formatted_text = formatted_text[:match.start()] + table + formatted_text[match.end():]
444
+ except:
445
+ continue
446
+
447
+ # Then, try to find standalone JSON objects
448
+ # Look for patterns that start with { and end with }
449
+ json_pattern = r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}'
450
+ matches = list(re.finditer(json_pattern, formatted_text, re.DOTALL))
451
+
452
+ for match in reversed(matches):
453
+ json_str = match.group(0).strip()
454
+
455
+ # Skip if it's already been processed or is too short
456
+ if len(json_str) < 10 or '|' in json_str:
457
+ continue
458
+
459
+ try:
460
+ json_data = json.loads(json_str)
461
+ # Only format if it looks like structured data
462
+ if isinstance(json_data, (dict, list)) and len(str(json_data)) > 20:
463
+ table = format_json_data_to_table(json_data)
464
+ formatted_text = formatted_text[:match.start()] + table + formatted_text[match.end():]
465
+ except:
466
+ continue
467
+
468
+ return formatted_text
469
+
470
+ def format_json_data_to_table(json_data: Any) -> str:
471
+ """Format JSON data to a table string"""
472
+ if isinstance(json_data, dict):
473
+ return format_dict_to_table(json_data, "Data")
474
+ elif isinstance(json_data, list):
475
+ return format_list_to_table(json_data, "Data")
476
+ else:
477
+ return str(json_data)
478
+
479
+ def process_user_input(user_input: str, history: List) -> tuple:
480
+ """Process user input through RAG system"""
481
+ global chat_history
482
+
483
+ if not user_input.strip():
484
+ return history, ""
485
+
486
+ # Convert Gradio history format to our format
487
+ rag_history = []
488
+ for msg in history:
489
+ if isinstance(msg, dict):
490
+ rag_history.append(msg)
491
+ else:
492
+ # Handle old format
493
+ user_msg, bot_msg = msg
494
+ rag_history.append({"role": "user", "content": user_msg})
495
+ rag_history.append({"role": "assistant", "content": bot_msg})
496
+
497
+ # Process through RAG system
498
+ result = run_rag_tool_router(user_input, rag_history)
499
+
500
+ # Update our chat history
501
+ chat_history.append({"role": "user", "content": user_input})
502
+
503
+ # Handle different response types
504
+ if isinstance(result, dict) and "final" in result:
505
+ response = result["final"]
506
+ # Format any JSON in the response
507
+ formatted_response = extract_and_format_json_in_text(response)
508
+ chat_history.append({"role": "assistant", "content": formatted_response})
509
+ history.append({"role": "user", "content": user_input})
510
+ history.append({"role": "assistant", "content": formatted_response})
511
+ return history, ""
512
+
513
+ elif isinstance(result, dict) and "action" in result:
514
+ action = result["action"]
515
+ args = result.get("args", {})
516
+
517
+ # Show the action being taken
518
+ action_response = f"**Executing**: {action}\n\n**Arguments**:\n```json\n{json.dumps(args, indent=2)}\n```"
519
+
520
+ # Execute diet tools
521
+ if action in ["generate_diet_plan", "get_diet_recommendations", "calculate_nutrition_info", "expand_diet_database"]:
522
+ diet_result = execute_diet_tool(action, args)
523
+
524
+ if diet_result.get("success"):
525
+ final_response = f"✅ **{diet_result.get('message', 'Operation completed successfully')}**\n\n"
526
+ if "result" in diet_result:
527
+ # Format the result based on action type
528
+ if action == "generate_diet_plan":
529
+ formatted_result = format_diet_plan_result(diet_result['result'])
530
+ elif action == "get_diet_recommendations":
531
+ formatted_result = format_recommendations_result(diet_result['result'])
532
+ elif action == "calculate_nutrition_info":
533
+ formatted_result = format_nutrition_result(diet_result['result'])
534
+ else:
535
+ formatted_result = format_json_to_table(diet_result['result'], "Result")
536
+
537
+ final_response += formatted_result
538
+ else:
539
+ final_response = f"❌ **Error**: {diet_result.get('error', 'Unknown error occurred')}"
540
+
541
+ # Add to history
542
+ history.append({"role": "user", "content": user_input})
543
+ history.append({"role": "assistant", "content": action_response + "\n\n" + final_response})
544
+ chat_history.append({"role": "assistant", "content": final_response})
545
+ else:
546
+ # For other tools, show placeholder
547
+ final_response = "🔄 This tool is not yet implemented in the web interface."
548
+ history.append({"role": "user", "content": user_input})
549
+ history.append({"role": "assistant", "content": action_response + "\n\n" + final_response})
550
+ chat_history.append({"role": "assistant", "content": final_response})
551
+
552
+ return history, ""
553
+
554
+ else:
555
+ # Handle errors or other responses
556
+ error_response = f"❌ **Error**: {json.dumps(result, indent=2, ensure_ascii=False)}"
557
+ chat_history.append({"role": "assistant", "content": error_response})
558
+ history.append({"role": "user", "content": user_input})
559
+ history.append({"role": "assistant", "content": error_response})
560
+ return history, ""
561
+
562
+ def create_diet_plan_interface(age: int, sex: str, height_cm: float, weight_kg: float,
563
+ activity_level: str, goal: str, plan_type: str) -> str:
564
+ """Create diet plan using the interface"""
565
+ user_profile = {
566
+ "age": age,
567
+ "sex": sex,
568
+ "height_cm": height_cm,
569
+ "weight_kg": weight_kg,
570
+ "activity_level": activity_level,
571
+ "goal": goal,
572
+ "dietary_restrictions": [],
573
+ "allergies": [],
574
+ "preferences": []
575
+ }
576
+
577
+ try:
578
+ result = generate_diet_plan(user_profile, plan_type)
579
+ return format_diet_plan_result(result)
580
+ except Exception as e:
581
+ return f"❌ Error: {str(e)}"
582
+
583
+ def get_recommendations_interface(age: int, sex: str, height_cm: float, weight_kg: float,
584
+ activity_level: str, goal: str) -> str:
585
+ """Get diet recommendations using the interface"""
586
+ user_profile = {
587
+ "age": age,
588
+ "sex": sex,
589
+ "height_cm": height_cm,
590
+ "weight_kg": weight_kg,
591
+ "activity_level": activity_level,
592
+ "goal": goal,
593
+ "dietary_restrictions": [],
594
+ "allergies": [],
595
+ "preferences": []
596
+ }
597
+
598
+ try:
599
+ result = get_diet_recommendations(user_profile)
600
+ return format_recommendations_result(result)
601
+ except Exception as e:
602
+ return f"❌ Error: {str(e)}"
603
+
604
+ def calculate_nutrition_interface(food_name: str, portion_grams: float) -> str:
605
+ """Calculate nutrition info using the interface"""
606
+ try:
607
+ result = calculate_nutrition_info(food_name, portion_grams)
608
+ return format_nutrition_result(result)
609
+ except Exception as e:
610
+ return f"❌ Error: {str(e)}"
611
+
612
+ # Create Gradio interface
613
+ with gr.Blocks(title="RAG Diet Assistant") as app:
614
+ gr.Markdown("# 🍽️ RAG Diet Assistant")
615
+ gr.Markdown("Your intelligent nutrition planning assistant powered by RAG (Retrieval-Augmented Generation)")
616
+
617
+ with gr.Tabs():
618
+ # Chat Interface Tab
619
+ with gr.Tab("💬 Chat Assistant"):
620
+ gr.Markdown("Ask me anything about diet planning, nutrition, or health!")
621
+
622
+ chatbot = gr.Chatbot(
623
+ label="Conversation",
624
+ height=500,
625
+ type="messages",
626
+ render_markdown=True
627
+ )
628
+
629
+ with gr.Row():
630
+ msg = gr.Textbox(
631
+ label="Your message",
632
+ placeholder="Ask me to create a diet plan, calculate nutrition, or get recommendations...",
633
+ lines=2,
634
+ scale=4
635
+ )
636
+ send_btn = gr.Button("Send", variant="primary", scale=1)
637
+
638
+ # Action buttons
639
+ with gr.Row():
640
+ clear_btn = gr.Button("Clear Chat", variant="secondary")
641
+ load_latest_btn = gr.Button("📄 Load Latest JSON", variant="secondary")
642
+ list_outputs_btn = gr.Button("📁 List All Outputs", variant="secondary")
643
+
644
+ # Event handlers
645
+ msg.submit(process_user_input, [msg, chatbot], [chatbot, msg])
646
+ send_btn.click(process_user_input, [msg, chatbot], [chatbot, msg])
647
+ clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg])
648
+ load_latest_btn.click(lambda: ([{"role": "assistant", "content": load_latest_json_output()}], ""), outputs=[chatbot, msg])
649
+ list_outputs_btn.click(lambda: ([{"role": "assistant", "content": list_all_json_outputs()}], ""), outputs=[chatbot, msg])
650
+
651
+ # Diet Plan Creator Tab
652
+ with gr.Tab("📋 Diet Plan Creator"):
653
+ gr.Markdown("Create personalized diet plans based on your profile")
654
+
655
+ with gr.Row():
656
+ with gr.Column():
657
+ gr.Markdown("### Personal Information")
658
+ age = gr.Number(label="Age", value=30, minimum=15, maximum=100)
659
+ sex = gr.Dropdown(label="Gender", choices=["male", "female"], value="male")
660
+ height_cm = gr.Number(label="Height (cm)", value=175, minimum=100, maximum=250)
661
+ weight_kg = gr.Number(label="Weight (kg)", value=75, minimum=30, maximum=300)
662
+
663
+ gr.Markdown("### Activity & Goals")
664
+ activity_level = gr.Dropdown(
665
+ label="Activity Level",
666
+ choices=["sedentary", "light", "moderate", "active", "very_active"],
667
+ value="moderate"
668
+ )
669
+ goal = gr.Dropdown(
670
+ label="Goal",
671
+ choices=["weight_loss", "muscle_gain", "maintenance", "keto", "mediterranean"],
672
+ value="weight_loss"
673
+ )
674
+ plan_type = gr.Dropdown(
675
+ label="Plan Type",
676
+ choices=["daily", "weekly"],
677
+ value="daily"
678
+ )
679
+
680
+ create_plan_btn = gr.Button("Create Diet Plan", variant="primary")
681
+
682
+ with gr.Column():
683
+ plan_output = gr.Textbox(
684
+ label="Diet Plan",
685
+ lines=20,
686
+ max_lines=30
687
+ )
688
+
689
+ create_plan_btn.click(
690
+ create_diet_plan_interface,
691
+ inputs=[age, sex, height_cm, weight_kg, activity_level, goal, plan_type],
692
+ outputs=plan_output
693
+ )
694
+
695
+ # Recommendations Tab
696
+ with gr.Tab("📊 Diet Recommendations"):
697
+ gr.Markdown("Get personalized nutrition recommendations")
698
+
699
+ with gr.Row():
700
+ with gr.Column():
701
+ gr.Markdown("### Personal Information")
702
+ rec_age = gr.Number(label="Age", value=30, minimum=15, maximum=100)
703
+ rec_sex = gr.Dropdown(label="Gender", choices=["male", "female"], value="male")
704
+ rec_height_cm = gr.Number(label="Height (cm)", value=175, minimum=100, maximum=250)
705
+ rec_weight_kg = gr.Number(label="Weight (kg)", value=75, minimum=30, maximum=300)
706
+
707
+ gr.Markdown("### Activity & Goals")
708
+ rec_activity_level = gr.Dropdown(
709
+ label="Activity Level",
710
+ choices=["sedentary", "light", "moderate", "active", "very_active"],
711
+ value="moderate"
712
+ )
713
+ rec_goal = gr.Dropdown(
714
+ label="Goal",
715
+ choices=["weight_loss", "muscle_gain", "maintenance", "keto", "mediterranean"],
716
+ value="weight_loss"
717
+ )
718
+
719
+ get_rec_btn = gr.Button("Get Recommendations", variant="primary")
720
+
721
+ with gr.Column():
722
+ rec_output = gr.Textbox(
723
+ label="Recommendations",
724
+ lines=15,
725
+ max_lines=20
726
+ )
727
+
728
+ get_rec_btn.click(
729
+ get_recommendations_interface,
730
+ inputs=[rec_age, rec_sex, rec_height_cm, rec_weight_kg, rec_activity_level, rec_goal],
731
+ outputs=rec_output
732
+ )
733
+
734
+ # Nutrition Calculator Tab
735
+ with gr.Tab("🧮 Nutrition Calculator"):
736
+ gr.Markdown("Calculate nutritional values for specific foods")
737
+
738
+ with gr.Row():
739
+ with gr.Column():
740
+ food_name = gr.Textbox(
741
+ label="Food Name",
742
+ placeholder="e.g., Chicken Breast, Apple, Brown Rice...",
743
+ value="Chicken Breast"
744
+ )
745
+ portion_grams = gr.Number(
746
+ label="Portion (grams)",
747
+ value=150,
748
+ minimum=1,
749
+ maximum=1000
750
+ )
751
+
752
+ calc_nutrition_btn = gr.Button("Calculate Nutrition", variant="primary")
753
+
754
+ with gr.Column():
755
+ nutrition_output = gr.Textbox(
756
+ label="Nutritional Information",
757
+ lines=15,
758
+ max_lines=20
759
+ )
760
+
761
+ calc_nutrition_btn.click(
762
+ calculate_nutrition_interface,
763
+ inputs=[food_name, portion_grams],
764
+ outputs=nutrition_output
765
+ )
766
+
767
+ if __name__ == "__main__":
768
+ app.launch(
769
+ server_name="127.0.0.1",
770
+ server_port=7860,
771
+ share=False
772
+ )
main3.py ADDED
@@ -0,0 +1,477 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # main.py
2
+ # Takes multi-turn chat from console → Selects most relevant tool cards with RAG →
3
+ # Directs Groq model with single "JSON-only" contract →
4
+ # If model args are missing, generates QUESTION in format { "final": "<which fields are needed?>" } →
5
+ # When args are complete, returns { "action": "<tool>", "args": {...} }.
6
+ # NOTE: Existing logic is preserved; only new tool (generate_workout_plan) is integrated.
7
+
8
+ import os
9
+ import json
10
+ import numpy as np
11
+ from typing import Dict, Any, List
12
+ from sentence_transformers import SentenceTransformer
13
+ from groq import Client # Groq Python SDK
14
+ from diet_tool import generate_diet_plan, get_diet_recommendations, calculate_nutrition_info
15
+ # 1) Your Groq API key - for Hugging Face Spaces, set this as a secret
16
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY")
17
+ if not GROQ_API_KEY:
18
+ raise ValueError("GROQ_API_KEY environment variable is required. Please set it in your Hugging Face Space secrets.")
19
+ client = Client(api_key=GROQ_API_KEY)
20
+
21
+ # 2) Embedding model and tool cards (RAG)
22
+ embed_model = SentenceTransformer("all-MiniLM-L6-v2")
23
+
24
+ tool_cards = [
25
+ {
26
+ "name": "one_rm_calculator",
27
+ "description": (
28
+ "Calculate 1-rep-max (1RM) from a lifted weight (kg) and reps using the "
29
+ "Wathan equation, and return JSON with the 1RM plus predicted weights for 1-10 reps."
30
+ )
31
+ },
32
+ {
33
+ "name": "get_user_profile",
34
+ "description": "Fetch the current user's profile summary."
35
+ },
36
+ {
37
+ "name": "analyze_exercise_video",
38
+ "description": "Analyze a given exercise video and return coaching feedback."
39
+ },
40
+ {
41
+ "name": "calculate_body_fat",
42
+ "description": "Calculates user's body fat percentage using U.S. Navy formula. Arguments: user_id (string), sex, height_cm, weight_kg, neck_cm, waist_cm, hip_cm."
43
+ },
44
+ {
45
+ "name": "upsert_profile",
46
+ "description": "Creates or updates user profile in database. Arguments: user_id, sex, height_cm, weight_kg, neck_cm, waist_cm, hip_cm."
47
+ },
48
+ {
49
+ "name": "generate_workout_plan",
50
+ "description": "Creates block-periodized, auto-regulated, VBT/HRV compatible, multi-disciplinary training plan (with user profile + goal + weekly days count)."
51
+ },
52
+ {
53
+ "name": "generate_diet_plan",
54
+ "description": "Creates personalized diet plan based on user profile and goals (daily/weekly plan, calorie calculation, macronutrient distribution)."
55
+ },
56
+ {
57
+ "name": "get_diet_recommendations",
58
+ "description": "Creates diet recommendations and nutrition advice for user profile (BMR, TDEE, macro targets)."
59
+ },
60
+ {
61
+ "name": "calculate_nutrition_info",
62
+ "description": "Calculates nutritional values for a specific food (calories, protein, carbs, fat, fiber)."
63
+ }
64
+ ]
65
+
66
+ # 2.b) Tool parameter schemas (to properly direct and validate the model)
67
+ tool_param_schemas: Dict[str, Dict[str, Any]] = {
68
+ "one_rm_calculator": {
69
+ "type": "object",
70
+ "properties": {
71
+ "weight_kg": {
72
+ "type": "number",
73
+ "description": "Weight lifted in kilograms."
74
+ },
75
+ "reps": {
76
+ "type": "integer",
77
+ "minimum": 1,
78
+ "maximum": 10,
79
+ "description": "Number of repetitions (1-10)."
80
+ },
81
+ "exercise": { # 🆕 nullable
82
+ "oneOf": [
83
+ {
84
+ "type": "string",
85
+ "enum": [
86
+ "Bench Press", "Squat", "Deadlift", "Overhead Press",
87
+ "Barbell Row", "Weighted Dips", "Weighted Pull Ups"
88
+ ]
89
+ },
90
+ { "type": "null" }
91
+ ],
92
+ "description": (
93
+ "Exercise name. Leave null or omit if the user did not specify. Only Bench Press, Squat, "
94
+ "Deadlift, Overhead Press, Barbell Row, Weighted Dips, and Weighted Pull Ups are supported. "
95
+ "You can translate the user-provided exercise name if needed, e.g. Barfiks -> Weighted Pull Ups, Bench -> Bench Press."
96
+ )
97
+ },
98
+ },
99
+ "required": ["weight_kg", "reps"]
100
+ },
101
+ "get_user_profile": {
102
+ "type": "object",
103
+ "properties": {},
104
+ "required": []
105
+ },
106
+ "analyze_exercise_video": {
107
+ "type": "object",
108
+ "properties": {
109
+ "videoUrl": {"type": "string", "description": "Publicly accessible video URL"},
110
+ "exercise": {"type": "string", "description": "Exercise name, e.g., 'Squat'"}
111
+ },
112
+ "required": ["videoUrl", "exercise"]
113
+ },
114
+ "calculate_body_fat": {
115
+ "type": "object",
116
+ "properties": {
117
+ "user_id": {"type": "string", "description": "User identifier"},
118
+ "sex": {"type": "string"},
119
+ "height_cm": {"type": "number"},
120
+ "weight_kg": {"type": "number"},
121
+ "neck_cm": {"type": "number"},
122
+ "waist_cm": {"type": "number"},
123
+ "hip_cm": {"type": "number"}
124
+ },
125
+ "required": ["user_id", "height_cm", "weight_kg", "neck_cm", "waist_cm", "hip_cm", "sex"]
126
+ },
127
+ "upsert_profile": {
128
+ "type": "object",
129
+ "properties": {
130
+ "user_id": {"type": "string"},
131
+ "sex": {"type": "string"},
132
+ "height_cm": {"type": "number"},
133
+ "weight_kg": {"type": "number"},
134
+ "neck_cm": {"type": "number"},
135
+ "waist_cm": {"type": "number"},
136
+ "hip_cm": {"type": "number"}
137
+ },
138
+ "required": ["user_id", "sex", "height_cm", "weight_kg", "neck_cm", "waist_cm"] # hip_cm optional
139
+ },
140
+ "generate_workout_plan": {
141
+ "type": "object",
142
+ "properties": {
143
+ "user_profile": {"type": "object", "description": "User profile/summary; may include injuries, recent_1RM etc."},
144
+ "goal": {"type": "string", "enum": ["hypertrophy", "strength", "fat_loss", "general_fitness"]},
145
+ "days_per_week": {"type": "integer", "minimum": 1, "maximum": 7},
146
+ "sport": {"type": "string",
147
+ "enum": ["general", "powerlifting", "olympic_weightlifting", "crossfit",
148
+ "bodybuilding", "endurance", "strongman", "calisthenics", "combat_sports"]},
149
+ "training_level": {"type": "string", "enum": ["novice", "intermediate", "advanced"]},
150
+ "sex": {"type": "string", "enum": ["male", "female"]},
151
+ "cycle_phase": {"type": "string", "enum": ["follicular", "luteal", "na"]},
152
+ "weekly_volume_pref": {"type": "string", "enum": ["low", "moderate", "high"]},
153
+ "block_type": {"type": "string", "enum": ["accumulation", "intensification", "peaking", "deload"]},
154
+ "mesocycle_length": {"type": "integer"},
155
+ "equipment": {"type": "array", "items": {"type": "string"}},
156
+ "weak_points": {"type": "array", "items": {"type": "string"}},
157
+ "sticking_points": {"type": "object"},
158
+ "auto_accessories": {"type": "boolean"},
159
+ "vbt_available": {"type": "boolean"},
160
+ "readiness_score": {"type": "number", "minimum": 0, "maximum": 10},
161
+ "constraints": {"type": "object"},
162
+ "swap_exercise_if_unavailable": {"type": "boolean"}
163
+ },
164
+ "required": ["user_profile", "goal", "days_per_week"]
165
+ },
166
+ "generate_diet_plan": {
167
+ "type": "object",
168
+ "properties": {
169
+ "user_profile": {
170
+ "type": "object",
171
+ "properties": {
172
+ "age": {"type": "integer", "minimum": 15, "maximum": 100},
173
+ "sex": {"type": "string", "enum": ["male", "female"]},
174
+ "height_cm": {"type": "number", "minimum": 100, "maximum": 250},
175
+ "weight_kg": {"type": "number", "minimum": 30, "maximum": 300},
176
+ "activity_level": {"type": "string", "enum": ["sedentary", "light", "moderate", "active", "very_active"]},
177
+ "goal": {"type": "string", "enum": ["weight_loss", "muscle_gain", "maintenance", "keto", "mediterranean"]},
178
+ "dietary_restrictions": {"type": "array", "items": {"type": "string"}},
179
+ "allergies": {"type": "array", "items": {"type": "string"}},
180
+ "preferences": {"type": "array", "items": {"type": "string"}}
181
+ },
182
+ "required": ["age", "sex", "height_cm", "weight_kg", "activity_level", "goal"]
183
+ },
184
+ "plan_type": {"type": "string", "enum": ["daily", "weekly"], "default": "daily"}
185
+ },
186
+ "required": ["user_profile"]
187
+ },
188
+ "get_diet_recommendations": {
189
+ "type": "object",
190
+ "properties": {
191
+ "user_profile": {
192
+ "type": "object",
193
+ "properties": {
194
+ "age": {"type": "integer", "minimum": 15, "maximum": 100},
195
+ "sex": {"type": "string", "enum": ["male", "female"]},
196
+ "height_cm": {"type": "number", "minimum": 100, "maximum": 250},
197
+ "weight_kg": {"type": "number", "minimum": 30, "maximum": 300},
198
+ "activity_level": {"type": "string", "enum": ["sedentary", "light", "moderate", "active", "very_active"]},
199
+ "goal": {"type": "string", "enum": ["weight_loss", "muscle_gain", "maintenance", "keto", "mediterranean"]},
200
+ "dietary_restrictions": {"type": "array", "items": {"type": "string"}},
201
+ "allergies": {"type": "array", "items": {"type": "string"}},
202
+ "preferences": {"type": "array", "items": {"type": "string"}}
203
+ },
204
+ "required": ["age", "sex", "height_cm", "weight_kg", "activity_level", "goal"]
205
+ }
206
+ },
207
+ "required": ["user_profile"]
208
+ },
209
+ "calculate_nutrition_info": {
210
+ "type": "object",
211
+ "properties": {
212
+ "food_name": {"type": "string", "description": "Food name"},
213
+ "portion_grams": {"type": "number", "minimum": 1, "maximum": 1000, "description": "Portion amount (grams)"}
214
+ },
215
+ "required": ["food_name", "portion_grams"]
216
+ },
217
+ "expand_diet_database": {
218
+ "type": "object",
219
+ "properties": {
220
+ "max_per_category": {"type": "integer", "minimum": 1, "maximum": 20, "description": "Maximum number of foods per category", "default": 5},
221
+ "output_filename": {"type": "string", "description": "Output filename (saved to outputs/ folder)", "default": "usda_foods_database.json"}
222
+ },
223
+ "required": []
224
+ }
225
+ }
226
+
227
+ descs = [c["description"] for c in tool_cards]
228
+ embs = embed_model.encode(descs)
229
+
230
+ # 3) Simple cosine-similarity top-K retrieval (RAG)
231
+ def retrieve_tools(query: str, k: int = 5):
232
+ q_emb = embed_model.encode([query])[0]
233
+ sims = (embs @ q_emb) / (np.linalg.norm(embs, axis=1) * np.linalg.norm(q_emb) + 1e-10)
234
+ idxs = np.argsort(-sims)[:k]
235
+ return [tool_cards[i] for i in idxs]
236
+
237
+ # 3.b) Find missing fields according to schema
238
+ def find_missing_required(tool_name: str, args: Dict[str, Any]) -> List[str]:
239
+ schema = tool_param_schemas.get(tool_name)
240
+ if not schema:
241
+ return []
242
+ required = schema.get("required", [])
243
+ missing = []
244
+ for key in required:
245
+ if key not in args or args[key] in ("", None):
246
+ missing.append(key)
247
+ return missing
248
+
249
+ # 3.c) Diet tool functions
250
+ def execute_diet_tool(action: str, args: Dict[str, Any]) -> Dict[str, Any]:
251
+ """Execute diet tool functions and save outputs to outputs folder"""
252
+ try:
253
+ # Create outputs folder (if it doesn't exist)
254
+ os.makedirs("outputs", exist_ok=True)
255
+
256
+ # Create timestamp
257
+ from datetime import datetime
258
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
259
+
260
+ if action == "generate_diet_plan":
261
+ user_profile = args["user_profile"]
262
+ plan_type = args.get("plan_type", "daily")
263
+
264
+ result = generate_diet_plan(user_profile, plan_type)
265
+
266
+ # Save output to file
267
+ filename = f"outputs/diet_plan_{plan_type}_{timestamp}.json"
268
+ with open(filename, 'w', encoding='utf-8') as f:
269
+ json.dump({
270
+ "action": action,
271
+ "user_profile": user_profile,
272
+ "plan_type": plan_type,
273
+ "result": result,
274
+ "timestamp": timestamp
275
+ }, f, ensure_ascii=False, indent=2)
276
+
277
+ return {
278
+ "success": True,
279
+ "result": result,
280
+ "message": f"{plan_type.capitalize()} diet plan successfully created and saved to {filename} file."
281
+ }
282
+
283
+ elif action == "get_diet_recommendations":
284
+ user_profile = args["user_profile"]
285
+ result = get_diet_recommendations(user_profile)
286
+
287
+ # Save output to file
288
+ filename = f"outputs/diet_recommendations_{timestamp}.json"
289
+ with open(filename, 'w', encoding='utf-8') as f:
290
+ json.dump({
291
+ "action": action,
292
+ "user_profile": user_profile,
293
+ "result": result,
294
+ "timestamp": timestamp
295
+ }, f, ensure_ascii=False, indent=2)
296
+
297
+ return {
298
+ "success": True,
299
+ "result": result,
300
+ "message": f"Diet recommendations successfully created and saved to {filename} file."
301
+ }
302
+
303
+ elif action == "calculate_nutrition_info":
304
+ food_name = args["food_name"]
305
+ portion_grams = args["portion_grams"]
306
+ result = calculate_nutrition_info(food_name, portion_grams)
307
+
308
+ # Save output to file
309
+ filename = f"outputs/nutrition_info_{timestamp}.json"
310
+ with open(filename, 'w', encoding='utf-8') as f:
311
+ json.dump({
312
+ "action": action,
313
+ "food_name": food_name,
314
+ "portion_grams": portion_grams,
315
+ "result": result,
316
+ "timestamp": timestamp
317
+ }, f, ensure_ascii=False, indent=2)
318
+
319
+ return {
320
+ "success": True,
321
+ "result": result,
322
+ "message": f"Nutrition calculation completed and saved to {filename} file."
323
+ }
324
+
325
+ elif action == "expand_diet_database":
326
+ max_per_category = args.get("max_per_category", 5)
327
+ output_filename = args.get("output_filename", "usda_foods_database.json")
328
+
329
+ # Import and run expand_diet_data.py
330
+ try:
331
+ from expand_diet_data import expand_diet_data_from_api
332
+ output_path = os.path.join("outputs", output_filename)
333
+ expand_diet_data_from_api(output_path, max_per_category)
334
+
335
+ return {
336
+ "success": True,
337
+ "result": {
338
+ "max_per_category": max_per_category,
339
+ "output_path": output_path
340
+ },
341
+ "message": f"USDA nutrition database successfully created: {output_path}"
342
+ }
343
+ except Exception as e:
344
+ return {
345
+ "success": False,
346
+ "error": f"Error while expanding database: {str(e)}"
347
+ }
348
+
349
+ else:
350
+ return {
351
+ "success": False,
352
+ "error": f"Unknown diet tool: {action}"
353
+ }
354
+
355
+ except Exception as e:
356
+ return {
357
+ "success": False,
358
+ "error": f"Error while running diet tool: {str(e)}"
359
+ }
360
+
361
+ # 4) RAG + Groq tool-call router (single turn)
362
+ def _build_messages(user_query: str, cards: List[Dict[str, str]], chat_history: List[Dict[str, str]]) -> List[Dict[str, str]]:
363
+ # Explicitly provide tool + schema information to the model
364
+ tools_lines = []
365
+ for c in cards:
366
+ name = c["name"]
367
+ schema = tool_param_schemas.get(name, {})
368
+ tools_lines.append(
369
+ f"{name}: {c['description']}\n"
370
+ f"PARAMETERS(JSON Schema): {json.dumps(schema, ensure_ascii=False)}"
371
+ )
372
+ tools_block = "\n\n".join(tools_lines)
373
+
374
+ system_prompt = (
375
+ "You are an assistant that only returns JSON. Do not write any other explanations.\n"
376
+ "IF TOOL IS NEEDED: produce exactly this schema → {\"action\":\"<tool_name>\",\"args\":{...}}\n"
377
+ "IF TOOL IS NOT NEEDED: {\"final\":\"...\"}\n"
378
+ "IF ARGUMENTS ARE MISSING: never make up; {\"final\":\"<ask user for required fields in ENGLISH, short and clear>\"}\n"
379
+ "Do not go outside JSON, do not add text before/after.\n"
380
+ "Tool name must be one from the list and argument names must exactly match PARAMETER schema."
381
+ )
382
+
383
+ # Clean chat history to remove unsupported fields like 'metadata'
384
+ cleaned_history = []
385
+ for msg in chat_history:
386
+ if isinstance(msg, dict) and "role" in msg and "content" in msg:
387
+ cleaned_msg = {"role": msg["role"], "content": msg["content"]}
388
+ cleaned_history.append(cleaned_msg)
389
+
390
+ messages = [
391
+ {"role": "system", "content": system_prompt},
392
+ {"role": "system", "content": tools_block},
393
+ *cleaned_history, # previous turns (cleaned)
394
+ {"role": "user", "content": user_query},
395
+ ]
396
+ return messages
397
+
398
+ def run_rag_tool_router(user_query: str, chat_history: List[Dict[str, str]]):
399
+ cards = retrieve_tools(user_query, k=5)
400
+ messages = _build_messages(user_query, cards, chat_history)
401
+
402
+ resp = client.chat.completions.create(
403
+ model="openai/gpt-oss-120b",
404
+ messages=messages,
405
+ max_tokens=240,
406
+ temperature=0.0
407
+ )
408
+ text = resp.choices[0].message.content.strip()
409
+ try:
410
+ obj = json.loads(text)
411
+ except json.JSONDecodeError:
412
+ return {"error": "invalid_json", "raw": text}
413
+
414
+ # If it's final (question/message)
415
+ if isinstance(obj, dict) and "final" in obj:
416
+ return obj
417
+
418
+ # Otherwise we expect action+args
419
+ if not isinstance(obj, dict) or "action" not in obj or "args" not in obj:
420
+ return {"error": "invalid_shape", "raw": obj}
421
+
422
+ action = obj["action"]
423
+ args = obj.get("args", {})
424
+ if not isinstance(action, str) or not isinstance(args, dict):
425
+ return {"error": "invalid_types", "raw": obj}
426
+
427
+ # Missing field check (additional security – model should have already asked)
428
+ missing = find_missing_required(action, args)
429
+ if missing:
430
+ need = ", ".join(missing)
431
+ return {"final": f"Please provide the following fields: {need}"}
432
+
433
+ # Here you can add type validation/type conversion if you want
434
+ return {"action": action, "args": args}
435
+
436
+ # 5) Multi-turn chat loop
437
+ if __name__ == "__main__":
438
+ chat_history: List[Dict[str, str]] = []
439
+ print("Multi-turn RAG+Tool Router. Leave empty line to exit.\n")
440
+ while True:
441
+ user_msg = input("Enter your prompt: ").strip()
442
+ if not user_msg:
443
+ break
444
+
445
+ result = run_rag_tool_router(user_msg, chat_history)
446
+
447
+ # Update history
448
+ chat_history.append({"role": "user", "content": user_msg})
449
+
450
+ # Assistant response
451
+ if isinstance(result, dict) and "final" in result:
452
+ print(json.dumps(result, ensure_ascii=False, indent=2))
453
+ chat_history.append({"role": "assistant", "content": json.dumps(result, ensure_ascii=False)})
454
+ continue
455
+
456
+ if isinstance(result, dict) and "action" in result:
457
+ print(json.dumps(result, ensure_ascii=False, indent=2))
458
+ chat_history.append({"role": "assistant", "content": json.dumps(result, ensure_ascii=False)})
459
+
460
+ # Diet tool check and execution
461
+ action = result["action"]
462
+ args = result.get("args", {})
463
+
464
+ if action in ["generate_diet_plan", "get_diet_recommendations", "calculate_nutrition_info", "expand_diet_database"]:
465
+ diet_result = execute_diet_tool(action, args)
466
+ print(f"\nDiet Tool Result:")
467
+ print(json.dumps(diet_result, ensure_ascii=False, indent=2))
468
+ chat_history.append({"role": "assistant", "content": json.dumps(diet_result, ensure_ascii=False)})
469
+ else:
470
+ # Placeholder for other tools
471
+ print("(Implementation needed for other tools)")
472
+
473
+ continue
474
+
475
+ # Error/raw response
476
+ print(json.dumps(result, ensure_ascii=False, indent=2))
477
+ chat_history.append({"role": "assistant", "content": json.dumps(result, ensure_ascii=False)})
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ groq>=0.4.0
3
+ sentence-transformers>=2.2.0
4
+ numpy>=1.21.0
5
+ pandas>=1.3.0
6
+ requests>=2.25.0