refactor(products): remove usage notes and contraindications fields

This commit is contained in:
Piotr Oleszczyk 2026-03-05 10:11:24 +01:00
parent 9df241a6a9
commit 013492ec2b
16 changed files with 54 additions and 179 deletions

View file

@ -0,0 +1,37 @@
"""drop_usage_notes_and_contraindications_from_products
Revision ID: 8e4c1b7a9d2f
Revises: f1a2b3c4d5e6
Create Date: 2026-03-04 00:00:00.000000
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = "8e4c1b7a9d2f"
down_revision: Union[str, None] = "f1a2b3c4d5e6"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.drop_column("products", "contraindications")
op.drop_column("products", "usage_notes")
def downgrade() -> None:
op.add_column(
"products",
sa.Column(
"contraindications",
sa.JSON(),
nullable=False,
server_default=sa.text("'[]'::json"),
),
)
op.alter_column("products", "contraindications", server_default=None)
op.add_column("products", sa.Column("usage_notes", sa.String(), nullable=True))

View file

@ -87,8 +87,6 @@ class ProductUpdate(SQLModel):
recommended_for: Optional[list[SkinType]] = None
targets: Optional[list[SkinConcern]] = None
contraindications: Optional[list[str]] = None
usage_notes: Optional[str] = None
fragrance_free: Optional[bool] = None
essential_oils_free: Optional[bool] = None
@ -139,8 +137,6 @@ class ProductParseResponse(SQLModel):
actives: Optional[list[ActiveIngredient]] = None
recommended_for: Optional[list[SkinType]] = None
targets: Optional[list[SkinConcern]] = None
contraindications: Optional[list[str]] = None
usage_notes: Optional[str] = None
fragrance_free: Optional[bool] = None
essential_oils_free: Optional[bool] = None
alcohol_denat_free: Optional[bool] = None
@ -553,8 +549,6 @@ OUTPUT SCHEMA (all fields optional — omit what you cannot determine):
],
"recommended_for": [string, ...],
"targets": [string, ...],
"contraindications": [string, ...],
"usage_notes": string,
"fragrance_free": boolean,
"essential_oils_free": boolean,
"alcohol_denat_free": boolean,
@ -972,23 +966,12 @@ def _build_actives_tool_handler(products: list[Product]):
return _build_product_details_tool_handler(products, mapper=_mapper)
def _build_usage_notes_tool_handler(products: list[Product]):
def _mapper(product: Product, pid: str) -> dict[str, object]:
notes = " ".join(str(product.usage_notes or "").split())
if len(notes) > 500:
notes = notes[:497] + "..."
return {"id": pid, "name": product.name, "usage_notes": notes}
return _build_product_details_tool_handler(products, mapper=_mapper)
def _build_safety_rules_tool_handler(products: list[Product]):
def _mapper(product: Product, pid: str) -> dict[str, object]:
ctx = product.to_llm_context()
return {
"id": pid,
"name": product.name,
"contraindications": (ctx.get("contraindications") or [])[:24],
"context_rules": ctx.get("context_rules") or {},
"safety": ctx.get("safety") or {},
"min_interval_hours": ctx.get("min_interval_hours"),
@ -1020,8 +1003,12 @@ _INCI_FUNCTION_DECLARATION = genai_types.FunctionDeclaration(
_SAFETY_RULES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration(
name="get_product_safety_rules",
description=(
"Return safety and compatibility rules for selected product UUIDs, "
"including contraindications, context_rules and safety flags."
"Return structured safety metadata for selected product UUIDs. "
"context_rules explain when a product should be avoided in a routine "
"(for example after acids, after retinoids, or with a compromised barrier). "
"safety flags describe formulation-level constraints "
"(for example whether the formula is fragrance-free, whether it contains denatured alcohol, and whether it is pregnancy-safe). "
"Also includes min_interval_hours and max_frequency_per_week."
),
parameters=genai_types.Schema(
type=genai_types.Type.OBJECT,
@ -1055,26 +1042,6 @@ _ACTIVES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration(
),
)
_USAGE_NOTES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration(
name="get_product_usage_notes",
description=(
"Return compact usage notes for selected product UUIDs "
"(timing, application method and cautions)."
),
parameters=genai_types.Schema(
type=genai_types.Type.OBJECT,
properties={
"product_ids": genai_types.Schema(
type=genai_types.Type.ARRAY,
items=genai_types.Schema(type=genai_types.Type.STRING),
description="Product UUIDs from POSIADANE PRODUKTY.",
)
},
required=["product_ids"],
),
)
_SHOPPING_SYSTEM_PROMPT = """Jesteś asystentem zakupowym w dziedzinie pielęgnacji skóry.
Twoim zadaniem jest przeanalizować stan skóry użytkownika oraz produkty, które już posiada,
a następnie zasugerować TYPY produktów (bez marek), które mogłyby uzupełnić ich rutynę.
@ -1108,7 +1075,7 @@ def suggest_shopping(session: Session = Depends(get_session)):
f"mogłyby uzupełnić rutynę pielęgnacyjną użytkownika.\n\n"
f"{context}\n\n"
"NARZEDZIA:\n"
"- Masz dostep do funkcji: get_product_inci, get_product_safety_rules, get_product_actives, get_product_usage_notes.\n"
"- Masz dostep do funkcji: get_product_inci, get_product_safety_rules, get_product_actives.\n"
"- Wywoluj narzedzia tylko, gdy potrzebujesz detali do oceny konfliktow skladnikow lub ryzyka podraznien.\n"
"- Grupuj UUID: staraj sie pobierac dane dla wielu produktow jednym wywolaniem.\n"
f"Zwróć wyłącznie JSON zgodny ze schematem."
@ -1126,7 +1093,6 @@ def suggest_shopping(session: Session = Depends(get_session)):
_INCI_FUNCTION_DECLARATION,
_SAFETY_RULES_FUNCTION_DECLARATION,
_ACTIVES_FUNCTION_DECLARATION,
_USAGE_NOTES_FUNCTION_DECLARATION,
]
)
],
@ -1142,7 +1108,6 @@ def suggest_shopping(session: Session = Depends(get_session)):
"get_product_inci": _build_inci_tool_handler(shopping_products),
"get_product_safety_rules": _build_safety_rules_tool_handler(shopping_products),
"get_product_actives": _build_actives_tool_handler(shopping_products),
"get_product_usage_notes": _build_usage_notes_tool_handler(shopping_products),
}
try:

View file

@ -201,8 +201,6 @@ def _is_minoxidil_product(product: Product) -> bool:
return True
if _contains_minoxidil_text(product.line_name):
return True
if _contains_minoxidil_text(product.usage_notes):
return True
if any(_contains_minoxidil_text(i) for i in (product.inci or [])):
return True
@ -378,8 +376,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("contraindications"):
entry += f" contraindications={ctx['contraindications']}"
if ctx.get("context_rules"):
entry += f" context_rules={ctx['context_rules']}"
safety = ctx.get("safety") or {}
@ -475,22 +471,6 @@ def _build_actives_tool_handler(
return _build_product_details_tool_handler(products, mapper=_mapper)
def _build_usage_notes_tool_handler(
products: list[Product],
):
def _mapper(product: Product, pid: str) -> dict[str, object]:
notes = " ".join(str(product.usage_notes or "").split())
if len(notes) > 500:
notes = notes[:497] + "..."
return {
"id": pid,
"name": product.name,
"usage_notes": notes,
}
return _build_product_details_tool_handler(products, mapper=_mapper)
def _build_safety_rules_tool_handler(
products: list[Product],
):
@ -499,7 +479,6 @@ def _build_safety_rules_tool_handler(
return {
"id": pid,
"name": product.name,
"contraindications": (ctx.get("contraindications") or [])[:24],
"context_rules": ctx.get("context_rules") or {},
"safety": ctx.get("safety") or {},
"min_interval_hours": ctx.get("min_interval_hours"),
@ -604,30 +583,15 @@ _ACTIVES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration(
),
)
_USAGE_NOTES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration(
name="get_product_usage_notes",
description=(
"Return compact usage notes for selected product UUIDs (application method, "
"timing, and cautions)."
),
parameters=genai_types.Schema(
type=genai_types.Type.OBJECT,
properties={
"product_ids": genai_types.Schema(
type=genai_types.Type.ARRAY,
items=genai_types.Schema(type=genai_types.Type.STRING),
description="Product UUIDs from AVAILABLE PRODUCTS.",
)
},
required=["product_ids"],
),
)
_SAFETY_RULES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration(
name="get_product_safety_rules",
description=(
"Return safety and compatibility rules for selected product UUIDs: "
"contraindications, context_rules and safety flags."
"Return structured safety metadata for selected product UUIDs. "
"context_rules explain when a product should be avoided in a routine "
"(for example after acids, after retinoids, or with a compromised barrier). "
"safety flags describe formulation-level constraints "
"(for example whether the formula is fragrance-free, whether it contains denatured alcohol, and whether it is pregnancy-safe). "
"Also includes min_interval_hours and max_frequency_per_week."
),
parameters=genai_types.Schema(
type=genai_types.Type.OBJECT,
@ -685,7 +649,7 @@ WYMAGANIA ODPOWIEDZI:
ZASADY PLANOWANIA:
- Kolejność warstw: cleanser -> toner -> essence -> serum -> moisturizer -> [SPF dla AM].
- Respektuj: context_rules, min_interval_hours, max_frequency_per_week, usage_notes.
- Respektuj: context_rules, min_interval_hours, max_frequency_per_week.
- 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),
@ -821,7 +785,7 @@ def suggest_routine(
"INPUT DATA:\n"
f"{skin_ctx}\n{grooming_ctx}\n{history_ctx}\n{day_ctx}\n{products_ctx}\n{objectives_ctx}"
"\nNARZEDZIA:\n"
"- Masz dostep do funkcji: get_product_inci, get_product_safety_rules, get_product_actives, get_product_usage_notes.\n"
"- Masz dostep do funkcji: get_product_inci, get_product_safety_rules, get_product_actives.\n"
"- Wywoluj narzedzia tylko, gdy potrzebujesz detali do decyzji klinicznej/bezpieczenstwa.\n"
"- Staraj sie grupowac zapytania: podawaj wszystkie potrzebne UUID w jednym wywolaniu narzedzia.\n"
"- Nie zgaduj detali skladu i zasad bezpieczenstwa; jesli potrzebujesz szczegolow, wywolaj odpowiednie narzedzie.\n"
@ -842,7 +806,6 @@ def suggest_routine(
_INCI_FUNCTION_DECLARATION,
_SAFETY_RULES_FUNCTION_DECLARATION,
_ACTIVES_FUNCTION_DECLARATION,
_USAGE_NOTES_FUNCTION_DECLARATION,
],
)
],
@ -860,7 +823,6 @@ def suggest_routine(
available_products
),
"get_product_actives": _build_actives_tool_handler(available_products),
"get_product_usage_notes": _build_usage_notes_tool_handler(available_products),
}
try:

View file

@ -107,9 +107,6 @@ class ProductBase(SQLModel):
recommended_for: list[SkinType] = Field(default_factory=list)
targets: list[SkinConcern] = Field(default_factory=list)
contraindications: list[str] = Field(default_factory=list)
usage_notes: str | None = None
fragrance_free: bool | None = None
essential_oils_free: bool | None = None
alcohol_denat_free: bool | None = None
@ -161,9 +158,6 @@ class Product(ProductBase, table=True):
targets: list[SkinConcern] = Field(
default_factory=list, sa_column=Column(JSON, nullable=False)
)
contraindications: list[str] = Field(
default_factory=list, sa_column=Column(JSON, nullable=False)
)
product_effect_profile: ProductEffectProfile = Field(
default_factory=ProductEffectProfile,
@ -217,9 +211,6 @@ class Product(ProductBase, table=True):
if self.category == ProductCategory.SPF and not self.leave_on:
raise ValueError("SPF products must be leave-on")
if self.is_medication and not self.usage_notes:
raise ValueError("Medication products must have usage_notes")
if self.price_currency is not None:
self.price_currency = self.price_currency.upper()
@ -267,9 +258,6 @@ class Product(ProductBase, table=True):
ctx["recommended_for"] = [_ev(s) for s in self.recommended_for]
if self.targets:
ctx["targets"] = [_ev(s) for s in self.targets]
if self.contraindications:
ctx["contraindications"] = self.contraindications
if self.actives:
actives_ctx = []
for a in self.actives:
@ -337,9 +325,6 @@ class Product(ProductBase, table=True):
ctx["is_tool"] = True
if self.needle_length_mm is not None:
ctx["needle_length_mm"] = self.needle_length_mm
if self.usage_notes:
ctx["usage_notes"] = self.usage_notes
if self.personal_tolerance_notes:
ctx["personal_tolerance_notes"] = self.personal_tolerance_notes
if self.personal_repurchase_intent is not None:

View file

@ -95,14 +95,12 @@ def test_list_filter_is_medication(client):
"category": "serum",
}
client.post("/products", json={**base, "name": "Normal", "is_medication": False})
# is_medication=True requires usage_notes (model validator)
client.post(
"/products",
json={
**base,
"name": "Med",
"is_medication": True,
"usage_notes": "Apply pea-sized amount",
},
)

View file

@ -9,7 +9,6 @@ from innercontext.api.products import (
_build_inci_tool_handler,
_build_safety_rules_tool_handler,
_build_shopping_context,
_build_usage_notes_tool_handler,
_extract_requested_product_ids,
)
from innercontext.models import Product, ProductInventory, SkinConditionSnapshot
@ -96,7 +95,6 @@ def test_suggest_shopping(client, session):
assert "get_product_inci" in kwargs["function_handlers"]
assert "get_product_safety_rules" in kwargs["function_handlers"]
assert "get_product_actives" in kwargs["function_handlers"]
assert "get_product_usage_notes" in kwargs["function_handlers"]
def test_shopping_context_medication_skip(session: Session):
@ -133,7 +131,6 @@ def test_shopping_tool_handlers_return_payloads(session: Session):
category="serum",
recommended_time="both",
leave_on=True,
usage_notes="Use AM and PM on clean skin.",
inci=["Water", "Niacinamide"],
actives=[{"name": "Niacinamide", "percent": 5, "functions": ["niacinamide"]}],
context_rules={"safe_after_shaving": True},
@ -148,8 +145,5 @@ def test_shopping_tool_handlers_return_payloads(session: Session):
actives_data = _build_actives_tool_handler([product])(payload)
assert actives_data["products"][0]["actives"][0]["name"] == "Niacinamide"
notes_data = _build_usage_notes_tool_handler([product])(payload)
assert notes_data["products"][0]["usage_notes"] == "Use AM and PM on clean skin."
safety_data = _build_safety_rules_tool_handler([product])(payload)
assert "context_rules" in safety_data["products"][0]

View file

@ -252,7 +252,6 @@ def test_suggest_routine(client, session):
assert "get_product_inci" in kwargs["function_handlers"]
assert "get_product_safety_rules" in kwargs["function_handlers"]
assert "get_product_actives" in kwargs["function_handlers"]
assert "get_product_usage_notes" in kwargs["function_handlers"]
def test_suggest_batch(client, session):

View file

@ -13,7 +13,6 @@ from innercontext.api.routines import (
_build_recent_history,
_build_safety_rules_tool_handler,
_build_skin_context,
_build_usage_notes_tool_handler,
_contains_minoxidil_text,
_ev,
_extract_active_names,
@ -56,10 +55,6 @@ def test_is_minoxidil_product():
assert _is_minoxidil_product(p) is True
p.line_name = None
p.usage_notes = "Use minoxidil daily"
assert _is_minoxidil_product(p) is True
p.usage_notes = None
p.inci = ["water", "minoxidil"]
assert _is_minoxidil_product(p) is True
@ -386,7 +381,6 @@ def test_additional_tool_handlers_return_product_payloads(session: Session):
brand="Test",
recommended_time="both",
leave_on=True,
usage_notes="Apply morning and evening.",
actives=[{"name": "Niacinamide", "percent": 5, "functions": ["niacinamide"]}],
context_rules={"safe_after_shaving": True},
product_effect_profile={},
@ -397,8 +391,5 @@ def test_additional_tool_handlers_return_product_payloads(session: Session):
actives_out = _build_actives_tool_handler([p])(ids_payload)
assert actives_out["products"][0]["actives"][0]["name"] == "Niacinamide"
notes_out = _build_usage_notes_tool_handler([p])(ids_payload)
assert notes_out["products"][0]["usage_notes"] == "Apply morning and evening."
safety_out = _build_safety_rules_tool_handler([p])(ids_payload)
assert "context_rules" in safety_out["products"][0]