mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat(skills): add fitness-nutrition skill to optional-skills
Cherry-picked from PR #9177 by @haileymarshall. Adds a fitness and nutrition skill for gym-goers and health-conscious users: - Exercise search via wger API (690+ exercises, free, no auth) - Nutrition lookup via USDA FoodData Central (380K+ foods, DEMO_KEY fallback) - Offline body composition calculators (BMI, TDEE, 1RM, macros, body fat %) - Pure stdlib Python, no pip dependencies Changes from original PR: - Moved from skills/ to optional-skills/health/ (correct location) - Fixed BMR formula in FORMULAS.md (removed confusing -5+10, now just +5) - Fixed author attribution to match PR submitter - Marked USDA_API_KEY as optional (DEMO_KEY works without signup) Also adds optional env var support to the skill readiness checker: - New 'optional: true' field in required_environment_variables entries - Optional vars are preserved in metadata but don't block skill readiness - Optional vars skip the CLI capture prompt flow - Skills with only optional missing vars show as 'available' not 'setup_needed'
This commit is contained in:
parent
5621fc449a
commit
7354a2dc26
5 changed files with 658 additions and 1 deletions
255
optional-skills/health/fitness-nutrition/SKILL.md
Normal file
255
optional-skills/health/fitness-nutrition/SKILL.md
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
---
|
||||
name: fitness-nutrition
|
||||
description: >
|
||||
Gym workout planner and nutrition tracker. Search 690+ exercises by muscle,
|
||||
equipment, or category via wger. Look up macros and calories for 380,000+
|
||||
foods via USDA FoodData Central. Compute BMI, TDEE, one-rep max, macro
|
||||
splits, and body fat — pure Python, no pip installs. Built for anyone
|
||||
chasing gains, cutting weight, or just trying to eat better.
|
||||
version: 1.0.0
|
||||
authors:
|
||||
- haileymarshall
|
||||
license: MIT
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [health, fitness, nutrition, gym, workout, diet, exercise]
|
||||
category: health
|
||||
prerequisites:
|
||||
commands: [curl, python3]
|
||||
required_environment_variables:
|
||||
- name: USDA_API_KEY
|
||||
prompt: "USDA FoodData Central API key (free)"
|
||||
help: "Get one free at https://fdc.nal.usda.gov/api-key-signup/ — or skip to use DEMO_KEY with lower rate limits"
|
||||
required_for: "higher rate limits on food/nutrition lookups (DEMO_KEY works without signup)"
|
||||
optional: true
|
||||
---
|
||||
|
||||
# Fitness & Nutrition
|
||||
|
||||
Expert fitness coach and sports nutritionist skill. Two data sources
|
||||
plus offline calculators — everything a gym-goer needs in one place.
|
||||
|
||||
**Data sources (all free, no pip dependencies):**
|
||||
|
||||
- **wger** (https://wger.de/api/v2/) — open exercise database, 690+ exercises with muscles, equipment, images. Public endpoints need zero authentication.
|
||||
- **USDA FoodData Central** (https://api.nal.usda.gov/fdc/v1/) — US government nutrition database, 380,000+ foods. `DEMO_KEY` works instantly; free signup for higher limits.
|
||||
|
||||
**Offline calculators (pure stdlib Python):**
|
||||
|
||||
- BMI, TDEE (Mifflin-St Jeor), one-rep max (Epley/Brzycki/Lombardi), macro splits, body fat % (US Navy method)
|
||||
|
||||
---
|
||||
|
||||
## When to Use
|
||||
|
||||
Trigger this skill when the user asks about:
|
||||
- Exercises, workouts, gym routines, muscle groups, workout splits
|
||||
- Food macros, calories, protein content, meal planning, calorie counting
|
||||
- Body composition: BMI, body fat, TDEE, caloric surplus/deficit
|
||||
- One-rep max estimates, training percentages, progressive overload
|
||||
- Macro ratios for cutting, bulking, or maintenance
|
||||
|
||||
---
|
||||
|
||||
## Procedure
|
||||
|
||||
### Exercise Lookup (wger API)
|
||||
|
||||
All wger public endpoints return JSON and require no auth. Always add
|
||||
`format=json` and `language=2` (English) to exercise queries.
|
||||
|
||||
**Step 1 — Identify what the user wants:**
|
||||
|
||||
- By muscle → use `/api/v2/exercise/?muscles={id}&language=2&status=2&format=json`
|
||||
- By category → use `/api/v2/exercise/?category={id}&language=2&status=2&format=json`
|
||||
- By equipment → use `/api/v2/exercise/?equipment={id}&language=2&status=2&format=json`
|
||||
- By name → use `/api/v2/exercise/search/?term={query}&language=english&format=json`
|
||||
- Full details → use `/api/v2/exerciseinfo/{exercise_id}/?format=json`
|
||||
|
||||
**Step 2 — Reference IDs (so you don't need extra API calls):**
|
||||
|
||||
Exercise categories:
|
||||
|
||||
| ID | Category |
|
||||
|----|-------------|
|
||||
| 8 | Arms |
|
||||
| 9 | Legs |
|
||||
| 10 | Abs |
|
||||
| 11 | Chest |
|
||||
| 12 | Back |
|
||||
| 13 | Shoulders |
|
||||
| 14 | Calves |
|
||||
| 15 | Cardio |
|
||||
|
||||
Muscles:
|
||||
|
||||
| ID | Muscle | ID | Muscle |
|
||||
|----|---------------------------|----|-------------------------|
|
||||
| 1 | Biceps brachii | 2 | Anterior deltoid |
|
||||
| 3 | Serratus anterior | 4 | Pectoralis major |
|
||||
| 5 | Obliquus externus | 6 | Gastrocnemius |
|
||||
| 7 | Rectus abdominis | 8 | Gluteus maximus |
|
||||
| 9 | Trapezius | 10 | Quadriceps femoris |
|
||||
| 11 | Biceps femoris | 12 | Latissimus dorsi |
|
||||
| 13 | Brachialis | 14 | Triceps brachii |
|
||||
| 15 | Soleus | | |
|
||||
|
||||
Equipment:
|
||||
|
||||
| ID | Equipment |
|
||||
|----|----------------|
|
||||
| 1 | Barbell |
|
||||
| 3 | Dumbbell |
|
||||
| 4 | Gym mat |
|
||||
| 5 | Swiss Ball |
|
||||
| 6 | Pull-up bar |
|
||||
| 7 | none (bodyweight) |
|
||||
| 8 | Bench |
|
||||
| 9 | Incline bench |
|
||||
| 10 | Kettlebell |
|
||||
|
||||
**Step 3 — Fetch and present results:**
|
||||
|
||||
```bash
|
||||
# Search exercises by name
|
||||
QUERY="$1"
|
||||
ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$QUERY")
|
||||
curl -s "https://wger.de/api/v2/exercise/search/?term=${ENCODED}&language=english&format=json" \
|
||||
| python3 -c "
|
||||
import json,sys
|
||||
data=json.load(sys.stdin)
|
||||
for s in data.get('suggestions',[])[:10]:
|
||||
d=s.get('data',{})
|
||||
print(f\" ID {d.get('id','?'):>4} | {d.get('name','N/A'):<35} | Category: {d.get('category','N/A')}\")
|
||||
"
|
||||
```
|
||||
|
||||
```bash
|
||||
# Get full details for a specific exercise
|
||||
EXERCISE_ID="$1"
|
||||
curl -s "https://wger.de/api/v2/exerciseinfo/${EXERCISE_ID}/?format=json" \
|
||||
| python3 -c "
|
||||
import json,sys,html,re
|
||||
data=json.load(sys.stdin)
|
||||
trans=[t for t in data.get('translations',[]) if t.get('language')==2]
|
||||
t=trans[0] if trans else data.get('translations',[{}])[0]
|
||||
desc=re.sub('<[^>]+>','',html.unescape(t.get('description','N/A')))
|
||||
print(f\"Exercise : {t.get('name','N/A')}\")
|
||||
print(f\"Category : {data.get('category',{}).get('name','N/A')}\")
|
||||
print(f\"Primary : {', '.join(m.get('name_en','') for m in data.get('muscles',[])) or 'N/A'}\")
|
||||
print(f\"Secondary : {', '.join(m.get('name_en','') for m in data.get('muscles_secondary',[])) or 'none'}\")
|
||||
print(f\"Equipment : {', '.join(e.get('name','') for e in data.get('equipment',[])) or 'bodyweight'}\")
|
||||
print(f\"How to : {desc[:500]}\")
|
||||
imgs=data.get('images',[])
|
||||
if imgs: print(f\"Image : {imgs[0].get('image','')}\")
|
||||
"
|
||||
```
|
||||
|
||||
```bash
|
||||
# List exercises filtering by muscle, category, or equipment
|
||||
# Combine filters as needed: ?muscles=4&equipment=1&language=2&status=2
|
||||
FILTER="$1" # e.g. "muscles=4" or "category=11" or "equipment=3"
|
||||
curl -s "https://wger.de/api/v2/exercise/?${FILTER}&language=2&status=2&limit=20&format=json" \
|
||||
| python3 -c "
|
||||
import json,sys
|
||||
data=json.load(sys.stdin)
|
||||
print(f'Found {data.get(\"count\",0)} exercises.')
|
||||
for ex in data.get('results',[]):
|
||||
print(f\" ID {ex['id']:>4} | muscles: {ex.get('muscles',[])} | equipment: {ex.get('equipment',[])}\")
|
||||
"
|
||||
```
|
||||
|
||||
### Nutrition Lookup (USDA FoodData Central)
|
||||
|
||||
Uses `USDA_API_KEY` env var if set, otherwise falls back to `DEMO_KEY`.
|
||||
DEMO_KEY = 30 requests/hour. Free signup key = 1,000 requests/hour.
|
||||
|
||||
```bash
|
||||
# Search foods by name
|
||||
FOOD="$1"
|
||||
API_KEY="${USDA_API_KEY:-DEMO_KEY}"
|
||||
ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$FOOD")
|
||||
curl -s "https://api.nal.usda.gov/fdc/v1/foods/search?api_key=${API_KEY}&query=${ENCODED}&pageSize=5&dataType=Foundation,SR%20Legacy" \
|
||||
| python3 -c "
|
||||
import json,sys
|
||||
data=json.load(sys.stdin)
|
||||
foods=data.get('foods',[])
|
||||
if not foods: print('No foods found.'); sys.exit()
|
||||
for f in foods:
|
||||
n={x['nutrientName']:x.get('value','?') for x in f.get('foodNutrients',[])}
|
||||
cal=n.get('Energy','?'); prot=n.get('Protein','?')
|
||||
fat=n.get('Total lipid (fat)','?'); carb=n.get('Carbohydrate, by difference','?')
|
||||
print(f\"{f.get('description','N/A')}\")
|
||||
print(f\" Per 100g: {cal} kcal | {prot}g protein | {fat}g fat | {carb}g carbs\")
|
||||
print(f\" FDC ID: {f.get('fdcId','N/A')}\")
|
||||
print()
|
||||
"
|
||||
```
|
||||
|
||||
```bash
|
||||
# Detailed nutrient profile by FDC ID
|
||||
FDC_ID="$1"
|
||||
API_KEY="${USDA_API_KEY:-DEMO_KEY}"
|
||||
curl -s "https://api.nal.usda.gov/fdc/v1/food/${FDC_ID}?api_key=${API_KEY}" \
|
||||
| python3 -c "
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
print(f\"Food: {d.get('description','N/A')}\")
|
||||
print(f\"{'Nutrient':<40} {'Amount':>8} {'Unit'}\")
|
||||
print('-'*56)
|
||||
for x in sorted(d.get('foodNutrients',[]),key=lambda x:x.get('nutrient',{}).get('rank',9999)):
|
||||
nut=x.get('nutrient',{}); amt=x.get('amount',0)
|
||||
if amt and float(amt)>0:
|
||||
print(f\" {nut.get('name',''):<38} {amt:>8} {nut.get('unitName','')}\")
|
||||
"
|
||||
```
|
||||
|
||||
### Offline Calculators
|
||||
|
||||
Use the helper scripts in `scripts/` for batch operations,
|
||||
or run inline for single calculations:
|
||||
|
||||
- `python3 scripts/body_calc.py bmi <weight_kg> <height_cm>`
|
||||
- `python3 scripts/body_calc.py tdee <weight_kg> <height_cm> <age> <M|F> <activity 1-5>`
|
||||
- `python3 scripts/body_calc.py 1rm <weight> <reps>`
|
||||
- `python3 scripts/body_calc.py macros <tdee_kcal> <cut|maintain|bulk>`
|
||||
- `python3 scripts/body_calc.py bodyfat <M|F> <neck_cm> <waist_cm> [hip_cm] <height_cm>`
|
||||
|
||||
See `references/FORMULAS.md` for the science behind each formula.
|
||||
|
||||
---
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- wger exercise endpoint returns **all languages by default** — always add `language=2` for English
|
||||
- wger includes **unverified user submissions** — add `status=2` to only get approved exercises
|
||||
- USDA `DEMO_KEY` has **30 req/hour** — add `sleep 2` between batch requests or get a free key
|
||||
- USDA data is **per 100g** — remind users to scale to their actual portion size
|
||||
- BMI does not distinguish muscle from fat — high BMI in muscular people is not necessarily unhealthy
|
||||
- Body fat formulas are **estimates** (±3-5%) — recommend DEXA scans for precision
|
||||
- 1RM formulas lose accuracy above 10 reps — use sets of 3-5 for best estimates
|
||||
- wger's `exercise/search` endpoint uses `term` not `query` as the parameter name
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
After running exercise search: confirm results include exercise names, muscle groups, and equipment.
|
||||
After nutrition lookup: confirm per-100g macros are returned with kcal, protein, fat, carbs.
|
||||
After calculators: sanity-check outputs (e.g. TDEE should be 1500-3500 for most adults).
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Task | Source | Endpoint |
|
||||
|------|--------|----------|
|
||||
| Search exercises by name | wger | `GET /api/v2/exercise/search/?term=&language=english` |
|
||||
| Exercise details | wger | `GET /api/v2/exerciseinfo/{id}/` |
|
||||
| Filter by muscle | wger | `GET /api/v2/exercise/?muscles={id}&language=2&status=2` |
|
||||
| Filter by equipment | wger | `GET /api/v2/exercise/?equipment={id}&language=2&status=2` |
|
||||
| List categories | wger | `GET /api/v2/exercisecategory/` |
|
||||
| List muscles | wger | `GET /api/v2/muscle/` |
|
||||
| Search foods | USDA | `GET /fdc/v1/foods/search?query=&dataType=Foundation,SR Legacy` |
|
||||
| Food details | USDA | `GET /fdc/v1/food/{fdcId}` |
|
||||
| BMI / TDEE / 1RM / macros | offline | `python3 scripts/body_calc.py` |
|
||||
100
optional-skills/health/fitness-nutrition/references/FORMULAS.md
Normal file
100
optional-skills/health/fitness-nutrition/references/FORMULAS.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# Formulas Reference
|
||||
|
||||
Scientific references for all calculators used in the fitness-nutrition skill.
|
||||
|
||||
## BMI (Body Mass Index)
|
||||
|
||||
**Formula:** BMI = weight (kg) / height (m)²
|
||||
|
||||
| Category | BMI Range |
|
||||
|-------------|------------|
|
||||
| Underweight | < 18.5 |
|
||||
| Normal | 18.5 – 24.9 |
|
||||
| Overweight | 25.0 – 29.9 |
|
||||
| Obese | 30.0+ |
|
||||
|
||||
**Limitation:** BMI does not distinguish muscle from fat. A muscular person
|
||||
can have a high BMI while being lean. Use body fat % for a better picture.
|
||||
|
||||
Reference: Quetelet, A. (1832). Keys et al., Int J Obes (1972).
|
||||
|
||||
## TDEE (Total Daily Energy Expenditure)
|
||||
|
||||
Uses the **Mifflin-St Jeor equation** — the most accurate BMR predictor for
|
||||
the general population according to the ADA (2005).
|
||||
|
||||
**BMR formulas:**
|
||||
|
||||
- Male: BMR = 10 × weight(kg) + 6.25 × height(cm) − 5 × age + 5
|
||||
- Female: BMR = 10 × weight(kg) + 6.25 × height(cm) − 5 × age − 161
|
||||
|
||||
**Activity multipliers:**
|
||||
|
||||
| Level | Description | Multiplier |
|
||||
|-------|--------------------------------|------------|
|
||||
| 1 | Sedentary (desk job) | 1.200 |
|
||||
| 2 | Lightly active (1-3 days/wk) | 1.375 |
|
||||
| 3 | Moderately active (3-5 days) | 1.550 |
|
||||
| 4 | Very active (6-7 days) | 1.725 |
|
||||
| 5 | Extremely active (2x/day) | 1.900 |
|
||||
|
||||
Reference: Mifflin et al., Am J Clin Nutr 51, 241-247 (1990).
|
||||
|
||||
## One-Rep Max (1RM)
|
||||
|
||||
Three validated formulas. Average of all three is most reliable.
|
||||
|
||||
- **Epley:** 1RM = w × (1 + r/30)
|
||||
- **Brzycki:** 1RM = w × 36 / (37 − r)
|
||||
- **Lombardi:** 1RM = w × r^0.1
|
||||
|
||||
All formulas are most accurate for r ≤ 10. Above 10 reps, error increases.
|
||||
|
||||
Reference: LeSuer et al., J Strength Cond Res 11(4), 211-213 (1997).
|
||||
|
||||
## Macro Splits
|
||||
|
||||
Recommended splits based on goal:
|
||||
|
||||
| Goal | Protein | Fat | Carbs | Calorie Offset |
|
||||
|-------------|---------|------|-------|----------------|
|
||||
| Fat loss | 40% | 30% | 30% | −500 kcal |
|
||||
| Maintenance | 30% | 30% | 40% | 0 |
|
||||
| Lean bulk | 30% | 25% | 45% | +400 kcal |
|
||||
|
||||
Protein targets for muscle growth: 1.6–2.2 g/kg body weight per day.
|
||||
Minimum fat intake: 0.5 g/kg to support hormone production.
|
||||
|
||||
Conversion: Protein = 4 kcal/g, Fat = 9 kcal/g, Carbs = 4 kcal/g.
|
||||
|
||||
Reference: Morton et al., Br J Sports Med 52, 376–384 (2018).
|
||||
|
||||
## Body Fat % (US Navy Method)
|
||||
|
||||
**Male:**
|
||||
|
||||
BF% = 86.010 × log₁₀(waist − neck) − 70.041 × log₁₀(height) + 36.76
|
||||
|
||||
**Female:**
|
||||
|
||||
BF% = 163.205 × log₁₀(waist + hip − neck) − 97.684 × log₁₀(height) − 78.387
|
||||
|
||||
All measurements in centimeters.
|
||||
|
||||
| Category | Male | Female |
|
||||
|--------------|--------|--------|
|
||||
| Essential | 2-5% | 10-13% |
|
||||
| Athletic | 6-13% | 14-20% |
|
||||
| Fitness | 14-17% | 21-24% |
|
||||
| Average | 18-24% | 25-31% |
|
||||
| Obese | 25%+ | 32%+ |
|
||||
|
||||
Accuracy: ±3-5% compared to DEXA. Measure at the navel (waist),
|
||||
at the Adam's apple (neck), and widest point (hip, females only).
|
||||
|
||||
Reference: Hodgdon & Beckett, Naval Health Research Center (1984).
|
||||
|
||||
## APIs
|
||||
|
||||
- wger: https://wger.de/api/v2/ — AGPL-3.0, exercise data is CC-BY-SA 3.0
|
||||
- USDA FoodData Central: https://api.nal.usda.gov/fdc/v1/ — public domain (CC0 1.0)
|
||||
210
optional-skills/health/fitness-nutrition/scripts/body_calc.py
Normal file
210
optional-skills/health/fitness-nutrition/scripts/body_calc.py
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
body_calc.py — All-in-one fitness calculator.
|
||||
|
||||
Subcommands:
|
||||
bmi <weight_kg> <height_cm>
|
||||
tdee <weight_kg> <height_cm> <age> <M|F> <activity 1-5>
|
||||
1rm <weight> <reps>
|
||||
macros <tdee_kcal> <cut|maintain|bulk>
|
||||
bodyfat <M|F> <neck_cm> <waist_cm> [hip_cm] <height_cm>
|
||||
|
||||
No external dependencies — stdlib only.
|
||||
"""
|
||||
import sys
|
||||
import math
|
||||
|
||||
|
||||
def bmi(weight_kg, height_cm):
|
||||
h = height_cm / 100
|
||||
val = weight_kg / (h * h)
|
||||
if val < 18.5:
|
||||
cat = "Underweight"
|
||||
elif val < 25:
|
||||
cat = "Normal weight"
|
||||
elif val < 30:
|
||||
cat = "Overweight"
|
||||
else:
|
||||
cat = "Obese"
|
||||
print(f"BMI: {val:.1f} — {cat}")
|
||||
print()
|
||||
print("Ranges:")
|
||||
print(f" Underweight : < 18.5")
|
||||
print(f" Normal : 18.5 – 24.9")
|
||||
print(f" Overweight : 25.0 – 29.9")
|
||||
print(f" Obese : 30.0+")
|
||||
|
||||
|
||||
def tdee(weight_kg, height_cm, age, sex, activity):
|
||||
if sex.upper() == "M":
|
||||
bmr = 10 * weight_kg + 6.25 * height_cm - 5 * age + 5
|
||||
else:
|
||||
bmr = 10 * weight_kg + 6.25 * height_cm - 5 * age - 161
|
||||
|
||||
multipliers = {
|
||||
1: ("Sedentary (desk job, no exercise)", 1.2),
|
||||
2: ("Lightly active (1-3 days/week)", 1.375),
|
||||
3: ("Moderately active (3-5 days/week)", 1.55),
|
||||
4: ("Very active (6-7 days/week)", 1.725),
|
||||
5: ("Extremely active (athlete + physical job)", 1.9),
|
||||
}
|
||||
|
||||
label, mult = multipliers.get(activity, ("Moderate", 1.55))
|
||||
total = bmr * mult
|
||||
|
||||
print(f"BMR (Mifflin-St Jeor): {bmr:.0f} kcal/day")
|
||||
print(f"Activity: {label} (x{mult})")
|
||||
print(f"TDEE: {total:.0f} kcal/day")
|
||||
print()
|
||||
print("Calorie targets:")
|
||||
print(f" Aggressive cut (-750): {total - 750:.0f} kcal/day")
|
||||
print(f" Fat loss (-500): {total - 500:.0f} kcal/day")
|
||||
print(f" Mild cut (-250): {total - 250:.0f} kcal/day")
|
||||
print(f" Maintenance : {total:.0f} kcal/day")
|
||||
print(f" Lean bulk (+250): {total + 250:.0f} kcal/day")
|
||||
print(f" Bulk (+500): {total + 500:.0f} kcal/day")
|
||||
|
||||
|
||||
def one_rep_max(weight, reps):
|
||||
if reps < 1:
|
||||
print("Error: reps must be at least 1.")
|
||||
sys.exit(1)
|
||||
if reps == 1:
|
||||
print(f"1RM = {weight:.1f} (actual single)")
|
||||
return
|
||||
|
||||
epley = weight * (1 + reps / 30)
|
||||
brzycki = weight * (36 / (37 - reps)) if reps < 37 else 0
|
||||
lombardi = weight * (reps ** 0.1)
|
||||
avg = (epley + brzycki + lombardi) / 3
|
||||
|
||||
print(f"Estimated 1RM ({weight} x {reps} reps):")
|
||||
print(f" Epley : {epley:.1f}")
|
||||
print(f" Brzycki : {brzycki:.1f}")
|
||||
print(f" Lombardi : {lombardi:.1f}")
|
||||
print(f" Average : {avg:.1f}")
|
||||
print()
|
||||
print("Training percentages off average 1RM:")
|
||||
for pct, rep_range in [
|
||||
(100, "1"), (95, "1-2"), (90, "3-4"), (85, "4-6"),
|
||||
(80, "6-8"), (75, "8-10"), (70, "10-12"),
|
||||
(65, "12-15"), (60, "15-20"),
|
||||
]:
|
||||
print(f" {pct:>3}% = {avg * pct / 100:>7.1f} (~{rep_range} reps)")
|
||||
|
||||
|
||||
def macros(tdee_kcal, goal):
|
||||
goal = goal.lower()
|
||||
if goal in ("cut", "lose", "deficit"):
|
||||
cals = tdee_kcal - 500
|
||||
p, f, c = 0.40, 0.30, 0.30
|
||||
label = "Fat Loss (-500 kcal)"
|
||||
elif goal in ("bulk", "gain", "surplus"):
|
||||
cals = tdee_kcal + 400
|
||||
p, f, c = 0.30, 0.25, 0.45
|
||||
label = "Lean Bulk (+400 kcal)"
|
||||
else:
|
||||
cals = tdee_kcal
|
||||
p, f, c = 0.30, 0.30, 0.40
|
||||
label = "Maintenance"
|
||||
|
||||
prot_g = cals * p / 4
|
||||
fat_g = cals * f / 9
|
||||
carb_g = cals * c / 4
|
||||
|
||||
print(f"Goal: {label}")
|
||||
print(f"Daily calories: {cals:.0f} kcal")
|
||||
print()
|
||||
print(f" Protein : {prot_g:>6.0f}g ({p * 100:.0f}%) = {prot_g * 4:.0f} kcal")
|
||||
print(f" Fat : {fat_g:>6.0f}g ({f * 100:.0f}%) = {fat_g * 9:.0f} kcal")
|
||||
print(f" Carbs : {carb_g:>6.0f}g ({c * 100:.0f}%) = {carb_g * 4:.0f} kcal")
|
||||
print()
|
||||
print(f"Per meal (3 meals): P {prot_g / 3:.0f}g | F {fat_g / 3:.0f}g | C {carb_g / 3:.0f}g")
|
||||
print(f"Per meal (4 meals): P {prot_g / 4:.0f}g | F {fat_g / 4:.0f}g | C {carb_g / 4:.0f}g")
|
||||
|
||||
|
||||
def bodyfat(sex, neck_cm, waist_cm, hip_cm, height_cm):
|
||||
sex = sex.upper()
|
||||
if sex == "M":
|
||||
if waist_cm <= neck_cm:
|
||||
print("Error: waist must be larger than neck."); sys.exit(1)
|
||||
bf = 86.010 * math.log10(waist_cm - neck_cm) - 70.041 * math.log10(height_cm) + 36.76
|
||||
else:
|
||||
if (waist_cm + hip_cm) <= neck_cm:
|
||||
print("Error: waist + hip must be larger than neck."); sys.exit(1)
|
||||
bf = 163.205 * math.log10(waist_cm + hip_cm - neck_cm) - 97.684 * math.log10(height_cm) - 78.387
|
||||
|
||||
print(f"Estimated body fat: {bf:.1f}%")
|
||||
|
||||
if sex == "M":
|
||||
ranges = [
|
||||
(6, "Essential fat (2-5%)"),
|
||||
(14, "Athletic (6-13%)"),
|
||||
(18, "Fitness (14-17%)"),
|
||||
(25, "Average (18-24%)"),
|
||||
]
|
||||
default = "Obese (25%+)"
|
||||
else:
|
||||
ranges = [
|
||||
(14, "Essential fat (10-13%)"),
|
||||
(21, "Athletic (14-20%)"),
|
||||
(25, "Fitness (21-24%)"),
|
||||
(32, "Average (25-31%)"),
|
||||
]
|
||||
default = "Obese (32%+)"
|
||||
|
||||
cat = default
|
||||
for threshold, label in ranges:
|
||||
if bf < threshold:
|
||||
cat = label
|
||||
break
|
||||
|
||||
print(f"Category: {cat}")
|
||||
print(f"Method: US Navy circumference formula")
|
||||
|
||||
|
||||
def usage():
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
usage()
|
||||
|
||||
cmd = sys.argv[1].lower()
|
||||
|
||||
try:
|
||||
if cmd == "bmi":
|
||||
bmi(float(sys.argv[2]), float(sys.argv[3]))
|
||||
|
||||
elif cmd == "tdee":
|
||||
tdee(
|
||||
float(sys.argv[2]), float(sys.argv[3]),
|
||||
int(sys.argv[4]), sys.argv[5], int(sys.argv[6]),
|
||||
)
|
||||
|
||||
elif cmd in ("1rm", "orm"):
|
||||
one_rep_max(float(sys.argv[2]), int(sys.argv[3]))
|
||||
|
||||
elif cmd == "macros":
|
||||
macros(float(sys.argv[2]), sys.argv[3])
|
||||
|
||||
elif cmd == "bodyfat":
|
||||
sex = sys.argv[2]
|
||||
if sex.upper() == "M":
|
||||
bodyfat(sex, float(sys.argv[3]), float(sys.argv[4]), 0, float(sys.argv[5]))
|
||||
else:
|
||||
bodyfat(sex, float(sys.argv[3]), float(sys.argv[4]), float(sys.argv[5]), float(sys.argv[6]))
|
||||
|
||||
else:
|
||||
print(f"Unknown command: {cmd}")
|
||||
usage()
|
||||
|
||||
except (IndexError, ValueError) as e:
|
||||
print(f"Error: {e}")
|
||||
usage()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
nutrition_search.py — Search USDA FoodData Central for nutrition info.
|
||||
|
||||
Usage:
|
||||
python3 nutrition_search.py "chicken breast"
|
||||
python3 nutrition_search.py "rice" "eggs" "broccoli"
|
||||
echo -e "oats\\nbanana\\nwhey protein" | python3 nutrition_search.py -
|
||||
|
||||
Reads USDA_API_KEY from environment, falls back to DEMO_KEY.
|
||||
No external dependencies.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import urllib.error
|
||||
|
||||
API_KEY = os.environ.get("USDA_API_KEY", "DEMO_KEY")
|
||||
BASE = "https://api.nal.usda.gov/fdc/v1"
|
||||
|
||||
|
||||
def search(query, max_results=3):
|
||||
encoded = urllib.parse.quote(query)
|
||||
url = (
|
||||
f"{BASE}/foods/search?api_key={API_KEY}"
|
||||
f"&query={encoded}&pageSize={max_results}"
|
||||
f"&dataType=Foundation,SR%20Legacy"
|
||||
)
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"Accept": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
return json.loads(r.read())
|
||||
except Exception as e:
|
||||
print(f" API error: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def display(food):
|
||||
nutrients = {n["nutrientName"]: n.get("value", "?") for n in food.get("foodNutrients", [])}
|
||||
cal = nutrients.get("Energy", "?")
|
||||
prot = nutrients.get("Protein", "?")
|
||||
fat = nutrients.get("Total lipid (fat)", "?")
|
||||
carb = nutrients.get("Carbohydrate, by difference", "?")
|
||||
fib = nutrients.get("Fiber, total dietary", "?")
|
||||
sug = nutrients.get("Sugars, total including NLEA", "?")
|
||||
|
||||
print(f" {food.get('description', 'N/A')}")
|
||||
print(f" Calories : {cal} kcal")
|
||||
print(f" Protein : {prot}g")
|
||||
print(f" Fat : {fat}g")
|
||||
print(f" Carbs : {carb}g (fiber: {fib}g, sugar: {sug}g)")
|
||||
print(f" FDC ID : {food.get('fdcId', 'N/A')}")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
|
||||
if sys.argv[1] == "-":
|
||||
queries = [line.strip() for line in sys.stdin if line.strip()]
|
||||
else:
|
||||
queries = sys.argv[1:]
|
||||
|
||||
for query in queries:
|
||||
print(f"\n--- {query.upper()} (per 100g) ---")
|
||||
data = search(query, max_results=2)
|
||||
if not data or not data.get("foods"):
|
||||
print(" No results found.")
|
||||
else:
|
||||
for food in data["foods"]:
|
||||
display(food)
|
||||
print()
|
||||
if len(queries) > 1:
|
||||
time.sleep(1) # respect rate limits
|
||||
|
||||
if API_KEY == "DEMO_KEY":
|
||||
print("\nTip: using DEMO_KEY (30 req/hr). Set USDA_API_KEY for 1000 req/hr.")
|
||||
print("Free signup: https://fdc.nal.usda.gov/api-key-signup/")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -245,6 +245,9 @@ def _get_required_environment_variables(
|
|||
if isinstance(required_for, str) and required_for.strip():
|
||||
normalized["required_for"] = required_for.strip()
|
||||
|
||||
if entry.get("optional"):
|
||||
normalized["optional"] = True
|
||||
|
||||
seen.add(env_name)
|
||||
required.append(normalized)
|
||||
|
||||
|
|
@ -378,6 +381,8 @@ def _remaining_required_environment_names(
|
|||
remaining = []
|
||||
for entry in required_env_vars:
|
||||
name = entry["name"]
|
||||
if entry.get("optional"):
|
||||
continue
|
||||
if name in missing_names or not _is_env_var_persisted(name, env_snapshot):
|
||||
remaining.append(name)
|
||||
return remaining
|
||||
|
|
@ -1042,7 +1047,8 @@ def skill_view(name: str, file_path: str = None, task_id: str = None) -> str:
|
|||
missing_required_env_vars = [
|
||||
e
|
||||
for e in required_env_vars
|
||||
if not _is_env_var_persisted(e["name"], env_snapshot)
|
||||
if not e.get("optional")
|
||||
and not _is_env_var_persisted(e["name"], env_snapshot)
|
||||
]
|
||||
capture_result = _capture_required_environment_variables(
|
||||
skill_name,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue