feat(backend): tunable planner weights via family profile config

- Add planner_config JSONB to family_profile model + migration
- Add PlannerConfig.merge(overrides) + to_dict() for family-level override merging
- generate_meal_plan merges family.planner_config into DEFAULT before filtering/scoring/selection
- New endpoints on /api/profile:
  - GET /planner-config — returns merged effective config
  - PUT /planner-config — partial override validation + merge
  - DELETE /planner-config — reset to system defaults
- Schemas: PlannerConfigOverride, PlannerConfigResponse, PlannerConfigUpdateRequest
  with weight-sum validation (0.999–1.001)
- Export RecipeBase/Create/Read/Update from schemas/__init__ to resolve forward refs
This commit is contained in:
2026-05-25 16:03:46 -07:00
parent 86164e6dd3
commit 98d611d7b3
7 changed files with 288 additions and 59 deletions
+12 -3
View File
@@ -107,6 +107,15 @@ def generate_meal_plan(
if family is None:
raise ValueError(f"family_profile {family_id} not found")
# Merge family-level planner overrides if present
effective_config = config
if family.planner_config:
from app.services.planner.config import PlannerConfig
try:
effective_config = config.merge(family.planner_config)
except (ValueError, TypeError) as exc:
logger.warning("Invalid planner_config for family %s: %s", family_id, exc)
recipes = db.query(Recipe).all()
exclude_set = exclude_recipe_ids or set()
recipe_dicts = [
@@ -160,7 +169,7 @@ def generate_meal_plan(
blocked_recipe_ids=blocked_recipes,
last_cooked_at=last_cooked,
family_calorie_target=family.calorie_target,
config=config,
config=effective_config,
today=today,
)
@@ -169,10 +178,10 @@ def generate_meal_plan(
recipes=feasible_recipes,
recipe_costs=recipe_costs,
last_cooked_at=last_cooked,
config=config,
config=effective_config,
today=today,
)
chosen, set_score = select_set(scored, config)
chosen, set_score = select_set(scored, effective_config)
plan = MealPlan(
family_profile_id=family_id,