Spaces:
Sleeping
Sleeping
File size: 12,066 Bytes
5dadca5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 | """
Actor Pushback Engine β makes actors ACTIVE, not passive.
After the orchestrator submits a plan, each actor reviews it
against their own constraints and either ACCEPTS or REJECTS
with specific, actionable suggestions.
This transforms the environment from a "grader" into a
"negotiation arena" β the orchestrator must iterate with
actors until all accept.
Example episode flow:
Step 1: consult fitness_advisor β "use 10-16 sets, no barbell"
Step 2: consult nutrition_advisor β "target 2650 kcal"
Step 3: submit_plan β REJECTED by fitness_advisor:
"You prescribed 24 sets β max is 16 for a beginner.
Reduce to 14-16 sets. Also, 'Barbell Squat' needs
barbell which is not available. Use Dumbbell Squat."
Step 4: submit revised plan β ACCEPTED by all β reward 0.95
"""
from __future__ import annotations
import json
from typing import Optional
# Exercise alternatives for injury-safe substitutions
SAFE_ALTERNATIVES = {
"deadlift": ["hip thrust", "glute bridge", "cable pull-through", "goblet squat"],
"bent-over row": ["chest-supported row", "cable row", "dumbbell row (neutral spine)"],
"good morning": ["hip thrust", "glute bridge", "Romanian deadlift (light, neutral)"],
"overhead press": ["landmine press", "high incline press", "front raise"],
"upright row": ["lateral raise", "face pull", "rear delt fly"],
"lunge": ["leg press (shallow)", "step-up (low box)", "wall sit"],
"deep squat": ["goblet squat (parallel)", "leg press", "wall sit"],
"box jump": ["step-up", "sled push", "seated box jump"],
"leg extension": ["terminal knee extension (band)", "wall sit", "step-up"],
"barbell curl": ["dumbbell curl (neutral grip)", "hammer curl", "cable curl"],
}
# Equipment-safe alternatives
EQUIPMENT_SWAPS = {
"barbell squat": "dumbbell squat",
"barbell bench press": "dumbbell bench press",
"barbell row": "dumbbell row",
"barbell deadlift": "dumbbell Romanian deadlift",
"barbell curl": "dumbbell curl",
"lat pulldown": "pull-up (or resistance band pull-down)",
"cable row": "dumbbell row",
"leg press": "dumbbell squat",
"chest press machine":"dumbbell bench press",
}
def _get_exercises(workout: dict) -> list[dict]:
exercises = []
for day in workout.get("days", []):
exercises.extend(day.get("exercises", []))
return exercises
def fitness_pushback(
plan_workout: dict,
client: dict,
actor_response: dict,
) -> Optional[dict]:
"""
FitnessAdvisor reviews the submitted plan.
Returns None if ACCEPTED, or a rejection dict with suggestions.
"""
issues = []
suggestions = []
constraints = actor_response.get("constraints", {})
vol_min = constraints.get("weekly_sets_min", 0)
vol_max = constraints.get("weekly_sets_max", 999)
banned = constraints.get("must_avoid_exercises", [])
equipment = set(constraints.get("must_use_only_equipment", []))
exercises = _get_exercises(plan_workout)
agent_sets = int(plan_workout.get("weekly_volume_sets", 0))
if agent_sets == 0:
agent_sets = sum(int(ex.get("sets", 0) or 0) for ex in exercises)
# Check volume
if agent_sets < vol_min:
issues.append(f"Volume too low: {agent_sets} sets/week (minimum {vol_min})")
suggestions.append(f"Increase to {vol_min}-{vol_max} sets/week. Add 1-2 more exercises per day.")
elif agent_sets > vol_max:
issues.append(f"Volume too high: {agent_sets} sets/week (maximum {vol_max})")
suggestions.append(f"Reduce to {vol_min}-{vol_max} sets/week. Remove 1-2 exercises or reduce sets per exercise to 2.")
# Check banned exercises
for ex in exercises:
name_l = ex.get("name", "").lower()
for banned_move in banned:
if banned_move in name_l:
alts = SAFE_ALTERNATIVES.get(banned_move, ["a safe alternative"])
issues.append(f"'{ex['name']}' is BANNED due to injury")
suggestions.append(
f"Replace '{ex['name']}' with: {', '.join(alts[:3])}"
)
# Check equipment
EQUIPMENT_KEYWORDS = {
"barbell": {"barbell"},
"cables": {"cable", "lat pulldown"},
"machines": {"machine", "leg press", "leg curl", "leg extension"},
"kettlebell": {"kettlebell"},
}
for ex in exercises:
name_l = ex.get("name", "").lower()
for eq, keywords in EQUIPMENT_KEYWORDS.items():
if eq not in equipment and any(kw in name_l for kw in keywords):
swap = EQUIPMENT_SWAPS.get(name_l.strip(), f"a {list(equipment)[0] if equipment else 'bodyweight'} alternative")
issues.append(f"'{ex['name']}' requires {eq} (not available)")
suggestions.append(f"Replace '{ex['name']}' with {swap}")
break
if not issues:
return None # ACCEPTED
return {
"actor": "fitness_advisor",
"verdict": "REJECTED",
"issues": issues,
"suggestions": suggestions,
"message": (
f"ποΈ FITNESS ADVISOR REJECTS this plan ({len(issues)} issue(s)):\n"
+ "\n".join(f" β {iss}" for iss in issues)
+ "\n\nSuggested fixes:\n"
+ "\n".join(f" β {sug}" for sug in suggestions)
),
}
def nutrition_pushback(
plan_nutrition: dict,
client: dict,
actor_response: dict,
) -> Optional[dict]:
"""
NutritionAdvisor reviews the submitted plan.
Returns None if ACCEPTED, or a rejection dict with suggestions.
"""
issues = []
suggestions = []
constraints = actor_response.get("constraints", {})
target_cal = constraints.get("calories_target", 0)
target_pro = constraints.get("protein_minimum_g", 0)
banned_foods = constraints.get("banned_foods", [])
tolerance = constraints.get("tolerance_pct", 15) / 100
daily = plan_nutrition.get("daily_targets", {})
agent_cal = float(daily.get("calories", 0))
agent_pro = float(daily.get("protein_g", 0))
# Check macros
if target_cal > 0 and agent_cal > 0:
cal_err = abs(agent_cal - target_cal) / target_cal
if cal_err > tolerance:
direction = "increase" if agent_cal < target_cal else "decrease"
issues.append(
f"Calories off by {cal_err*100:.0f}%: {agent_cal:.0f} kcal "
f"(target {target_cal:.0f} Β± {tolerance*100:.0f}%)"
)
suggestions.append(
f"{direction.capitalize()} calories to {target_cal:.0f} kcal. "
f"Acceptable range: {target_cal*(1-tolerance):.0f}β{target_cal*(1+tolerance):.0f} kcal."
)
elif target_cal > 0 and agent_cal == 0:
issues.append("No calorie target specified in daily_targets")
suggestions.append(f"Set daily_targets.calories to {target_cal:.0f} kcal")
if target_pro > 0 and agent_pro > 0:
pro_err = abs(agent_pro - target_pro) / target_pro
if pro_err > tolerance:
issues.append(
f"Protein off by {pro_err*100:.0f}%: {agent_pro:.0f}g "
f"(target {target_pro:.0f}g)"
)
suggestions.append(
f"Adjust protein to {target_pro:.0f}g/day. "
f"Add high-protein foods: paneer (18g/100g), rajma (8.7g/100g), soya chunks (52g/100g)."
)
# Check banned foods
plan_text = json.dumps(plan_nutrition).lower()
for food in banned_foods:
if food.lower() in plan_text:
issues.append(f"'{food}' is banned for this client's dietary restrictions")
suggestions.append(f"Remove '{food}'. Use plant-based alternatives from IFCT 2017 database.")
break # one violation is enough
if not issues:
return None # ACCEPTED
return {
"actor": "nutrition_advisor",
"verdict": "REJECTED",
"issues": issues,
"suggestions": suggestions,
"message": (
f"π₯ NUTRITION ADVISOR REJECTS this plan ({len(issues)} issue(s)):\n"
+ "\n".join(f" β {iss}" for iss in issues)
+ "\n\nSuggested fixes:\n"
+ "\n".join(f" β {sug}" for sug in suggestions)
),
}
def progress_pushback(
plan_workout: dict,
plan_nutrition: dict,
client: dict,
progress: dict,
complications: list,
actor_response: dict,
) -> Optional[dict]:
"""
ProgressAnalyst reviews the submitted plan.
Returns None if ACCEPTED, or a rejection dict.
"""
issues = []
suggestions = []
constraints = actor_response.get("constraints", {})
must_adapt = constraints.get("must_adapt_if_plateau", False)
if not must_adapt:
return None # No plateau, auto-accept
# Check if the plan actually adapted
exercises = _get_exercises(plan_workout)
agent_sets = int(plan_workout.get("weekly_volume_sets", 0))
if agent_sets == 0:
agent_sets = sum(int(ex.get("sets", 0) or 0) for ex in exercises)
daily = plan_nutrition.get("daily_targets", {})
agent_cal = float(daily.get("calories", 0))
# Get baseline volume/calories from actor recommendations
recs = actor_response.get("recommendations", {})
plateau_status = recs.get("plateau_status", "")
if plateau_status not in ("plateau", "reversing"):
return None
signals = recs.get("signals", [])
plateau_signal = next((s for s in signals if s["type"] == "plateau"), None)
if not plateau_signal:
return None
# The agent should have done something β increased volume or adjusted calories
# We check loosely here since the grader does the precise check
goal = client.get("goal", "maintenance")
if goal == "weight_loss" and agent_cal > 1800:
# Probably didn't reduce calories enough β but let grader decide
pass
# Check if required actions were taken
required = plateau_signal.get("required_actions", [])
if required and agent_sets == 0 and agent_cal == 0:
issues.append("Plateau detected but plan has no volume or calorie data")
suggestions.append("You must either increase volume β₯10% or adjust calories β₯150 kcal")
if not issues:
return None
return {
"actor": "progress_analyst",
"verdict": "REJECTED",
"issues": issues,
"suggestions": suggestions,
"message": (
f"π PROGRESS ANALYST REJECTS this plan ({len(issues)} issue(s)):\n"
+ "\n".join(f" β {iss}" for iss in issues)
+ "\n\nSuggested fixes:\n"
+ "\n".join(f" β {sug}" for sug in suggestions)
),
}
def collect_actor_pushback(
workout: dict,
nutrition: dict,
client: dict,
progress: dict,
complications: list,
actor_responses: dict,
) -> list[dict]:
"""
Run all actor pushbacks and collect rejections.
Returns list of rejection dicts (empty if all accepted).
"""
rejections = []
if "fitness_advisor" in actor_responses:
fb = fitness_pushback(workout, client, actor_responses["fitness_advisor"])
if fb:
rejections.append(fb)
if "nutrition_advisor" in actor_responses:
nb = nutrition_pushback(nutrition, client, actor_responses["nutrition_advisor"])
if nb:
rejections.append(nb)
if "progress_analyst" in actor_responses:
pb = progress_pushback(
workout, nutrition, client, progress, complications,
actor_responses["progress_analyst"]
)
if pb:
rejections.append(pb)
return rejections |