diff --git a/backend/alembic/versions/e4f5a6b7c8d9_drop_product_interaction_columns.py b/backend/alembic/versions/e4f5a6b7c8d9_drop_product_interaction_columns.py new file mode 100644 index 0000000..c09a6b2 --- /dev/null +++ b/backend/alembic/versions/e4f5a6b7c8d9_drop_product_interaction_columns.py @@ -0,0 +1,28 @@ +"""drop_product_interaction_columns + +Revision ID: e4f5a6b7c8d9 +Revises: d3e4f5a6b7c8 +Create Date: 2026-03-04 00:00:00.000000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +revision: str = "e4f5a6b7c8d9" +down_revision: Union[str, None] = "d3e4f5a6b7c8" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.drop_column("products", "incompatible_with") + op.drop_column("products", "synergizes_with") + + +def downgrade() -> None: + op.add_column("products", sa.Column("synergizes_with", sa.JSON(), nullable=True)) + op.add_column("products", sa.Column("incompatible_with", sa.JSON(), nullable=True)) diff --git a/backend/innercontext/api/products.py b/backend/innercontext/api/products.py index 743979b..e01780e 100644 --- a/backend/innercontext/api/products.py +++ b/backend/innercontext/api/products.py @@ -38,7 +38,6 @@ from innercontext.models.product import ( ActiveIngredient, ProductContext, ProductEffectProfile, - ProductInteraction, ) router = APIRouter() @@ -91,8 +90,6 @@ class ProductUpdate(SQLModel): ph_min: Optional[float] = None ph_max: Optional[float] = None - incompatible_with: Optional[list[ProductInteraction]] = None - synergizes_with: Optional[list[str]] = None context_rules: Optional[ProductContext] = None min_interval_hours: Optional[int] = None @@ -140,8 +137,6 @@ class ProductParseResponse(SQLModel): product_effect_profile: Optional[ProductEffectProfile] = None ph_min: Optional[float] = None ph_max: Optional[float] = None - incompatible_with: Optional[list[ProductInteraction]] = None - synergizes_with: Optional[list[str]] = None context_rules: Optional[ProductContext] = None min_interval_hours: Optional[int] = None max_frequency_per_week: Optional[int] = None @@ -347,8 +342,6 @@ actives[].functions (array, pick applicable): actives[].strength_level: 1 (low) | 2 (medium) | 3 (high) actives[].irritation_potential: 1 (low) | 2 (medium) | 3 (high) -incompatible_with[].scope: "same_step" | "same_day" | "same_period" - OUTPUT SCHEMA (all fields optional — omit what you cannot determine): { "name": string, @@ -402,10 +395,6 @@ OUTPUT SCHEMA (all fields optional — omit what you cannot determine): }, "ph_min": number, "ph_max": number, - "incompatible_with": [ - {"target": string, "scope": string, "reason": string} - ], - "synergizes_with": [string, ...], "context_rules": { "safe_after_shaving": boolean, "safe_after_acids": boolean, @@ -721,7 +710,6 @@ def _build_safety_rules_tool_handler(products: list[Product]): return { "id": pid, "name": product.name, - "incompatible_with": (ctx.get("incompatible_with") or [])[:24], "contraindications": (ctx.get("contraindications") or [])[:24], "context_rules": ctx.get("context_rules") or {}, "safety": ctx.get("safety") or {}, @@ -755,7 +743,7 @@ _SAFETY_RULES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration( name="get_product_safety_rules", description=( "Return safety and compatibility rules for selected product UUIDs, " - "including incompatible_with, contraindications, context_rules and safety flags." + "including contraindications, context_rules and safety flags." ), parameters=genai_types.Schema( type=genai_types.Type.OBJECT, diff --git a/backend/innercontext/api/routines.py b/backend/innercontext/api/routines.py index 503e0c9..0cd860e 100644 --- a/backend/innercontext/api/routines.py +++ b/backend/innercontext/api/routines.py @@ -378,8 +378,6 @@ def _build_products_context( notable = {k: v for k, v in profile.items() if v and v > 0} if notable: entry += f" effects={notable}" - if ctx.get("incompatible_with"): - entry += f" incompatible_with={ctx['incompatible_with']}" if ctx.get("contraindications"): entry += f" contraindications={ctx['contraindications']}" if ctx.get("context_rules"): @@ -501,7 +499,6 @@ def _build_safety_rules_tool_handler( return { "id": pid, "name": product.name, - "incompatible_with": (ctx.get("incompatible_with") or [])[:24], "contraindications": (ctx.get("contraindications") or [])[:24], "context_rules": ctx.get("context_rules") or {}, "safety": ctx.get("safety") or {}, @@ -630,7 +627,7 @@ _SAFETY_RULES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration( name="get_product_safety_rules", description=( "Return safety and compatibility rules for selected product UUIDs: " - "incompatible_with, contraindications, context_rules and safety flags." + "contraindications, context_rules and safety flags." ), parameters=genai_types.Schema( type=genai_types.Type.OBJECT, @@ -688,8 +685,7 @@ WYMAGANIA ODPOWIEDZI: ZASADY PLANOWANIA: - Kolejność warstw: cleanser -> toner -> essence -> serum -> moisturizer -> [SPF dla AM]. -- Respektuj: incompatible_with (same_step / same_day / same_period), context_rules, - min_interval_hours, max_frequency_per_week, usage_notes. +- Respektuj: context_rules, min_interval_hours, max_frequency_per_week, usage_notes. - Zarządzanie inwentarzem: - Preferuj produkty już otwarte (miękka preferencja). - Unikaj funkcjonalnej redundancji (np. wielokrotne źródła panthenolu, ceramidów lub niacynamidu w tej samej rutynie), diff --git a/backend/innercontext/models/__init__.py b/backend/innercontext/models/__init__.py index 5fa50b0..73939cc 100644 --- a/backend/innercontext/models/__init__.py +++ b/backend/innercontext/models/__init__.py @@ -7,7 +7,6 @@ from .enums import ( EvidenceLevel, GroomingAction, IngredientFunction, - InteractionScope, MedicationKind, OverallSkinState, PartOfDay, @@ -29,7 +28,6 @@ from .product import ( ProductBase, ProductContext, ProductEffectProfile, - ProductInteraction, ProductInventory, ProductPublic, ProductWithInventory, @@ -53,7 +51,6 @@ __all__ = [ "EvidenceLevel", "GroomingAction", "IngredientFunction", - "InteractionScope", "MedicationKind", "OverallSkinState", "PartOfDay", @@ -77,7 +74,6 @@ __all__ = [ "ProductBase", "ProductContext", "ProductEffectProfile", - "ProductInteraction", "ProductInventory", "ProductPublic", "ProductWithInventory", diff --git a/backend/innercontext/models/enums.py b/backend/innercontext/models/enums.py index 293e280..a18e85c 100644 --- a/backend/innercontext/models/enums.py +++ b/backend/innercontext/models/enums.py @@ -131,12 +131,6 @@ class EvidenceLevel(str, Enum): HIGH = "high" -class InteractionScope(str, Enum): - SAME_STEP = "same_step" - SAME_DAY = "same_day" - SAME_PERIOD = "same_period" - - # --------------------------------------------------------------------------- # Health # --------------------------------------------------------------------------- diff --git a/backend/innercontext/models/product.py b/backend/innercontext/models/product.py index bb5dc0b..f2881e6 100644 --- a/backend/innercontext/models/product.py +++ b/backend/innercontext/models/product.py @@ -12,7 +12,6 @@ from .enums import ( AbsorptionSpeed, DayTime, IngredientFunction, - InteractionScope, PriceTier, ProductCategory, SkinConcern, @@ -57,12 +56,6 @@ class ActiveIngredient(SQLModel): irritation_potential: StrengthLevel | None = None -class ProductInteraction(SQLModel): - target: str - scope: InteractionScope - reason: str | None = None - - class ProductContext(SQLModel): safe_after_shaving: bool | None = None safe_after_acids: bool | None = None @@ -128,8 +121,6 @@ class ProductBase(SQLModel): ph_min: float | None = Field(default=None, ge=0, le=14) ph_max: float | None = Field(default=None, ge=0, le=14) - incompatible_with: list[ProductInteraction] | None = None - synergizes_with: list[str] | None = None context_rules: ProductContext | None = None min_interval_hours: int | None = Field(default=None, ge=0) @@ -181,12 +172,6 @@ class Product(ProductBase, table=True): sa_column=Column(JSON, nullable=False), ) - incompatible_with: list[ProductInteraction] | None = Field( - default=None, sa_column=Column(JSON, nullable=True) - ) - synergizes_with: list[str] | None = Field( - default=None, sa_column=Column(JSON, nullable=True) - ) context_rules: ProductContext | None = Field( default=None, sa_column=Column(JSON, nullable=True) ) @@ -302,26 +287,6 @@ class Product(ProductBase, table=True): if nonzero: ctx["effect_profile"] = nonzero - if self.incompatible_with: - parts = [] - for inc in self.incompatible_with: - if isinstance(inc, dict): - scope = inc.get("scope", "") - target = inc.get("target", "") - reason = inc.get("reason") - else: - scope = _ev(inc.scope) - target = inc.target - reason = inc.reason - msg = f"avoid {target} ({scope})" - if reason: - msg += f": {reason}" - parts.append(msg) - ctx["incompatible_with"] = parts - - if self.synergizes_with: - ctx["synergizes_with"] = self.synergizes_with - if self.context_rules is not None: cr = self.context_rules if isinstance(cr, dict): diff --git a/backend/tests/test_product_model.py b/backend/tests/test_product_model.py index f2076b3..77290ac 100644 --- a/backend/tests/test_product_model.py +++ b/backend/tests/test_product_model.py @@ -7,14 +7,12 @@ from innercontext.models import Product from innercontext.models.enums import ( DayTime, IngredientFunction, - InteractionScope, ProductCategory, ) from innercontext.models.product import ( ActiveIngredient, ProductContext, ProductEffectProfile, - ProductInteraction, ) @@ -156,29 +154,6 @@ def test_effect_profile_nonzero_included(): assert "retinoid_strength" not in ctx["effect_profile"] -# --------------------------------------------------------------------------- -# Incompatible_with -# --------------------------------------------------------------------------- - - -def test_incompatible_with_pydantic_objects(): - inc = ProductInteraction( - target="AHA", scope=InteractionScope.SAME_DAY, reason="increases irritation" - ) - p = _make(incompatible_with=[inc]) - ctx = p.to_llm_context() - assert "incompatible_with" in ctx - assert ctx["incompatible_with"][0] == "avoid AHA (same_day): increases irritation" - - -def test_incompatible_with_raw_dicts(): - raw = {"target": "Vitamin C", "scope": "same_step", "reason": None} - p = _make(incompatible_with=[raw]) - ctx = p.to_llm_context() - assert "incompatible_with" in ctx - assert ctx["incompatible_with"][0] == "avoid Vitamin C (same_step)" - - # --------------------------------------------------------------------------- # Context rules # --------------------------------------------------------------------------- diff --git a/backend/tests/test_products_helpers.py b/backend/tests/test_products_helpers.py index 805f35f..c4046f9 100644 --- a/backend/tests/test_products_helpers.py +++ b/backend/tests/test_products_helpers.py @@ -136,7 +136,6 @@ def test_shopping_tool_handlers_return_payloads(session: Session): usage_notes="Use AM and PM on clean skin.", inci=["Water", "Niacinamide"], actives=[{"name": "Niacinamide", "percent": 5, "functions": ["niacinamide"]}], - incompatible_with=[{"target": "Vitamin C", "scope": "same_step"}], context_rules={"safe_after_shaving": True}, product_effect_profile={}, ) @@ -153,5 +152,4 @@ def test_shopping_tool_handlers_return_payloads(session: Session): assert notes_data["products"][0]["usage_notes"] == "Use AM and PM on clean skin." safety_data = _build_safety_rules_tool_handler([product])(payload) - assert "incompatible_with" in safety_data["products"][0] assert "context_rules" in safety_data["products"][0] diff --git a/backend/tests/test_routines_helpers.py b/backend/tests/test_routines_helpers.py index 5d78872..9e0ec09 100644 --- a/backend/tests/test_routines_helpers.py +++ b/backend/tests/test_routines_helpers.py @@ -184,7 +184,6 @@ def test_build_products_context(session: Session): recommended_time="am", pao_months=6, product_effect_profile={"hydration_immediate": 2, "exfoliation_strength": 0}, - incompatible_with=[{"target": "retinol", "scope": "same_routine"}], context_rules={"safe_after_shaving": False}, min_interval_hours=12, max_frequency_per_week=7, @@ -233,7 +232,6 @@ def test_build_products_context(session: Session): assert "nearest_open_pao_deadline=" in ctx assert "pao_months=6" in ctx assert "effects={'hydration_immediate': 2}" in ctx - assert "incompatible_with=['avoid retinol (same_routine)']" in ctx assert "context_rules={'safe_after_shaving': False}" in ctx assert "min_interval_hours=12" in ctx assert "max_frequency_per_week=7" in ctx @@ -390,7 +388,6 @@ def test_additional_tool_handlers_return_product_payloads(session: Session): leave_on=True, usage_notes="Apply morning and evening.", actives=[{"name": "Niacinamide", "percent": 5, "functions": ["niacinamide"]}], - incompatible_with=[{"target": "Retinol", "scope": "same_step"}], context_rules={"safe_after_shaving": True}, product_effect_profile={}, ) @@ -404,5 +401,4 @@ def test_additional_tool_handlers_return_product_payloads(session: Session): assert notes_out["products"][0]["usage_notes"] == "Apply morning and evening." safety_out = _build_safety_rules_tool_handler([p])(ids_payload) - assert "incompatible_with" in safety_out["products"][0] assert "context_rules" in safety_out["products"][0] diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 9948336..f247272 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -379,16 +379,6 @@ "productForm_activeIrritation": "Irritation", "productForm_activeFunctions": "Functions", "productForm_effectProfile": "Effect profile (0–5)", - "productForm_interactions": "Interactions", - "productForm_synergizesWith": "Synergizes with (one per line)", - "productForm_incompatibleWith": "Incompatible with", - "productForm_addIncompatibility": "+ Add incompatibility", - "productForm_noIncompatibilities": "No incompatibilities added.", - "productForm_incompTarget": "Target ingredient", - "productForm_incompScope": "Scope", - "productForm_incompReason": "Reason (optional)", - "productForm_incompReasonPlaceholder": "e.g. reduces efficacy", - "productForm_incompScopeSelect": "Select…", "productForm_contextRules": "Context rules", "productForm_ctxAfterShaving": "Safe after shaving", "productForm_ctxAfterAcids": "Safe after acids", @@ -495,10 +485,6 @@ "productForm_fnVitaminC": "vitamin C", "productForm_fnAntiAging": "anti-aging", - "productForm_scopeSameStep": "same step", - "productForm_scopeSameDay": "same day", - "productForm_scopeSamePeriod": "same period", - "productForm_strengthLow": "1 Low", "productForm_strengthMedium": "2 Medium", "productForm_strengthHigh": "3 High", diff --git a/frontend/messages/pl.json b/frontend/messages/pl.json index 42f2199..7a3e492 100644 --- a/frontend/messages/pl.json +++ b/frontend/messages/pl.json @@ -393,16 +393,6 @@ "productForm_activeIrritation": "Podrażnienie", "productForm_activeFunctions": "Funkcje", "productForm_effectProfile": "Profil działania (0–5)", - "productForm_interactions": "Interakcje", - "productForm_synergizesWith": "Synergizuje z (jedno na linię)", - "productForm_incompatibleWith": "Niekompatybilny z", - "productForm_addIncompatibility": "+ Dodaj niekompatybilność", - "productForm_noIncompatibilities": "Brak niekompatybilności.", - "productForm_incompTarget": "Składnik docelowy", - "productForm_incompScope": "Zakres", - "productForm_incompReason": "Powód (opcjonalny)", - "productForm_incompReasonPlaceholder": "np. zmniejsza skuteczność", - "productForm_incompScopeSelect": "Wybierz…", "productForm_contextRules": "Reguły kontekstu", "productForm_ctxAfterShaving": "Bezpieczny po goleniu", "productForm_ctxAfterAcids": "Bezpieczny po kwasach", @@ -509,10 +499,6 @@ "productForm_fnVitaminC": "witamina C", "productForm_fnAntiAging": "przeciwstarzeniowy", - "productForm_scopeSameStep": "ten sam krok", - "productForm_scopeSameDay": "ten sam dzień", - "productForm_scopeSamePeriod": "ten sam okres", - "productForm_strengthLow": "1 Niskie", "productForm_strengthMedium": "2 Średnie", "productForm_strengthHigh": "3 Wysokie", diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 8ca13ab..ddaac4a 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -11,7 +11,6 @@ import type { Product, ProductContext, ProductEffectProfile, - ProductInteraction, ProductInventory, Routine, RoutineSuggestion, @@ -127,8 +126,6 @@ export interface ProductParseResponse { product_effect_profile?: ProductEffectProfile; ph_min?: number; ph_max?: number; - incompatible_with?: ProductInteraction[]; - synergizes_with?: string[]; context_rules?: ProductContext; min_interval_hours?: number; max_frequency_per_week?: number; diff --git a/frontend/src/lib/components/ProductForm.svelte b/frontend/src/lib/components/ProductForm.svelte index b0cc24f..e2e730e 100644 --- a/frontend/src/lib/components/ProductForm.svelte +++ b/frontend/src/lib/components/ProductForm.svelte @@ -1,7 +1,7 @@