refactor(products): remove obsolete interaction fields across stack
This commit is contained in:
parent
1d8a8eafb8
commit
c5ea38880c
16 changed files with 32 additions and 278 deletions
|
|
@ -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))
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue