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:
@@ -1,8 +1,26 @@
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from typing import Optional, List, Any
|
||||
from uuid import UUID
|
||||
from datetime import date, datetime
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
# Re-export from submodules so forward references resolve
|
||||
from .recipe import (
|
||||
RecipeBase,
|
||||
RecipeCreate,
|
||||
RecipeIngredientRef,
|
||||
RecipeRead,
|
||||
RecipeUpdate,
|
||||
ResolveIngredientCandidate,
|
||||
ResolveIngredientRequest,
|
||||
ResolveIngredientResponse,
|
||||
)
|
||||
|
||||
# These may be referenced by other models; re-export as aliases if needed.
|
||||
RecipeResponse = RecipeRead
|
||||
|
||||
|
||||
class FamilyMemberRole(str, Enum):
|
||||
@@ -97,16 +115,66 @@ class FamilyProfileBase(BaseModel):
|
||||
budget_per_meal: float = 50.00
|
||||
|
||||
|
||||
class FamilyProfileResponse(FamilyProfileBase):
|
||||
class FamilyProfileResponse(BaseModel):
|
||||
id: UUID
|
||||
name: str
|
||||
household_size: int
|
||||
adult_count: int
|
||||
child_count: int
|
||||
dietary_notes: Optional[str] = None
|
||||
budget_per_meal: float = 50.00
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
members: List[FamilyMemberResponse] = []
|
||||
planner_config: Optional[dict] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PlannerConfigOverride(BaseModel):
|
||||
recency_weeks: Optional[int] = Field(default=None, ge=0)
|
||||
calorie_tolerance_pct: Optional[int] = Field(default=None, ge=0, le=100)
|
||||
max_total_minutes: Optional[int] = Field(default=None, ge=0)
|
||||
max_meal_cost: Optional[float] = Field(default=None, ge=0)
|
||||
w_savings: Optional[float] = Field(default=None, ge=0, le=1)
|
||||
w_coverage: Optional[float] = Field(default=None, ge=0, le=1)
|
||||
w_pantry: Optional[float] = Field(default=None, ge=0, le=1)
|
||||
w_time: Optional[float] = Field(default=None, ge=0, le=1)
|
||||
w_recency: Optional[float] = Field(default=None, ge=0, le=1)
|
||||
time_ideal_minutes: Optional[int] = Field(default=None, ge=0)
|
||||
time_full_minutes: Optional[int] = Field(default=None, ge=0)
|
||||
recency_full_weeks: Optional[int] = Field(default=None, ge=0)
|
||||
top_k: Optional[int] = Field(default=None, ge=1)
|
||||
set_size: Optional[int] = Field(default=None, ge=1)
|
||||
p_protein: Optional[float] = Field(default=None, ge=0)
|
||||
p_cuisine: Optional[float] = Field(default=None, ge=0)
|
||||
|
||||
|
||||
class PlannerConfigResponse(BaseModel):
|
||||
recency_weeks: int
|
||||
calorie_tolerance_pct: int
|
||||
max_total_minutes: int
|
||||
max_meal_cost: float
|
||||
w_savings: float
|
||||
w_coverage: float
|
||||
w_pantry: float
|
||||
w_time: float
|
||||
w_recency: float
|
||||
time_ideal_minutes: int
|
||||
time_full_minutes: int
|
||||
recency_full_weeks: int
|
||||
top_k: int
|
||||
set_size: int
|
||||
p_protein: float
|
||||
p_cuisine: float
|
||||
source: str = "default"
|
||||
|
||||
|
||||
class PlannerConfigUpdateRequest(BaseModel):
|
||||
planner_config: PlannerConfigOverride
|
||||
|
||||
|
||||
class FamilyProfileCreate(FamilyProfileBase):
|
||||
pass
|
||||
|
||||
@@ -118,56 +186,7 @@ class FamilyProfileUpdate(BaseModel):
|
||||
child_count: Optional[int] = None
|
||||
dietary_notes: Optional[str] = None
|
||||
budget_per_meal: Optional[float] = None
|
||||
|
||||
|
||||
class RecipeIngredient(BaseModel):
|
||||
ingredient_id: Optional[UUID] = None
|
||||
name: Optional[str] = None
|
||||
quantity: Optional[float] = None
|
||||
unit: Optional[str] = None
|
||||
is_optional: bool = False
|
||||
notes: Optional[str] = None
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _normalize(cls, data):
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
# JSONB stores qty; schema uses quantity
|
||||
if "qty" in data and "quantity" not in data:
|
||||
data["quantity"] = data.pop("qty")
|
||||
return data
|
||||
|
||||
|
||||
class RecipeBase(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
image_url: Optional[str] = None
|
||||
image_source: Optional[str] = None
|
||||
prep_time_minutes: Optional[int] = None
|
||||
cook_time_minutes: Optional[int] = None
|
||||
servings: int
|
||||
servings_scaled: Optional[int] = None
|
||||
cuisine_tags: Optional[List[str]] = []
|
||||
dietary_tags: Optional[List[str]] = []
|
||||
protein_type: Optional[str] = None
|
||||
spice_level: Optional[int] = None
|
||||
ingredients: List[RecipeIngredient] = []
|
||||
instructions: List[str] = []
|
||||
source_url: Optional[str] = None
|
||||
is_manually_added: bool = False
|
||||
|
||||
|
||||
class RecipeResponse(RecipeBase):
|
||||
id: UUID
|
||||
family_profile_id: Optional[UUID] = None
|
||||
scraped_at: Optional[datetime] = None
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
total_time_minutes: Optional[int] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
planner_config: Optional[dict] = None
|
||||
|
||||
|
||||
class RecipeCreate(RecipeBase):
|
||||
|
||||
Reference in New Issue
Block a user