Public Access
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:
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -37,6 +38,53 @@ class PlannerConfig:
|
||||
if abs(total - 1.0) > 1e-6:
|
||||
raise ValueError(f"weights must sum to 1.0, got {total}")
|
||||
|
||||
def merge(self, overrides: "dict[str, Any]") -> "PlannerConfig":
|
||||
"""Return a new PlannerConfig with overrides applied."""
|
||||
current = {
|
||||
"recency_weeks": self.recency_weeks,
|
||||
"calorie_tolerance_pct": self.calorie_tolerance_pct,
|
||||
"max_total_minutes": self.max_total_minutes,
|
||||
"max_meal_cost": self.max_meal_cost,
|
||||
"w_savings": self.w_savings,
|
||||
"w_coverage": self.w_coverage,
|
||||
"w_pantry": self.w_pantry,
|
||||
"w_time": self.w_time,
|
||||
"w_recency": self.w_recency,
|
||||
"time_ideal_minutes": self.time_ideal_minutes,
|
||||
"time_full_minutes": self.time_full_minutes,
|
||||
"recency_full_weeks": self.recency_full_weeks,
|
||||
"top_k": self.top_k,
|
||||
"set_size": self.set_size,
|
||||
"p_protein": self.p_protein,
|
||||
"p_cuisine": self.p_cuisine,
|
||||
}
|
||||
for key, value in overrides.items():
|
||||
if key in current:
|
||||
current[key] = value
|
||||
inst = PlannerConfig(**current)
|
||||
inst.validate()
|
||||
return inst
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"recency_weeks": self.recency_weeks,
|
||||
"calorie_tolerance_pct": self.calorie_tolerance_pct,
|
||||
"max_total_minutes": self.max_total_minutes,
|
||||
"max_meal_cost": self.max_meal_cost,
|
||||
"w_savings": self.w_savings,
|
||||
"w_coverage": self.w_coverage,
|
||||
"w_pantry": self.w_pantry,
|
||||
"w_time": self.w_time,
|
||||
"w_recency": self.w_recency,
|
||||
"time_ideal_minutes": self.time_ideal_minutes,
|
||||
"time_full_minutes": self.time_full_minutes,
|
||||
"recency_full_weeks": self.recency_full_weeks,
|
||||
"top_k": self.top_k,
|
||||
"set_size": self.set_size,
|
||||
"p_protein": self.p_protein,
|
||||
"p_cuisine": self.p_cuisine,
|
||||
}
|
||||
|
||||
|
||||
DEFAULT = PlannerConfig()
|
||||
DEFAULT.validate()
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user