Keep /products/suggest lean by exposing product UUIDs and fetching INCI, safety rules, actives, and usage notes on demand through Gemini function tools. Add conservative fallback behavior for tool roundtrip limits and expand helper tests to cover tool wiring and payload handlers.
157 lines
5.1 KiB
Python
157 lines
5.1 KiB
Python
import uuid
|
|
from datetime import date
|
|
from unittest.mock import patch
|
|
|
|
from sqlmodel import Session
|
|
|
|
from innercontext.api.products import (
|
|
_build_actives_tool_handler,
|
|
_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
|
|
|
|
|
|
def test_build_shopping_context(session: Session):
|
|
# Empty context
|
|
ctx = _build_shopping_context(session)
|
|
assert "(brak danych)" in ctx
|
|
assert "POSIADANE PRODUKTY" in ctx
|
|
|
|
# Add snapshot
|
|
snap = SkinConditionSnapshot(
|
|
id=uuid.uuid4(),
|
|
snapshot_date=date.today(),
|
|
overall_state="fair",
|
|
skin_type="combination",
|
|
hydration_level=3,
|
|
sensitivity_level=4,
|
|
barrier_state="mildly_compromised",
|
|
active_concerns=["redness"],
|
|
priorities=["soothing"],
|
|
)
|
|
session.add(snap)
|
|
|
|
# Add product
|
|
p = Product(
|
|
id=uuid.uuid4(),
|
|
name="Soothing Serum",
|
|
brand="BrandX",
|
|
category="serum",
|
|
recommended_time="both",
|
|
leave_on=True,
|
|
targets=["redness"],
|
|
product_effect_profile={"soothing_strength": 4, "hydration_immediate": 1},
|
|
actives=[{"name": "Centella"}],
|
|
)
|
|
session.add(p)
|
|
session.commit()
|
|
|
|
# Add inventory
|
|
inv = ProductInventory(id=uuid.uuid4(), product_id=p.id, is_opened=True)
|
|
session.add(inv)
|
|
session.commit()
|
|
|
|
ctx = _build_shopping_context(session)
|
|
assert "Typ skóry: combination" in ctx
|
|
assert "Nawilżenie: 3/5" in ctx
|
|
assert "Wrażliwość: 4/5" in ctx
|
|
assert "Aktywne problemy: redness" in ctx
|
|
assert "Priorytety: soothing" in ctx
|
|
|
|
# Check product
|
|
assert "[✓] id=" in ctx
|
|
assert "Soothing Serum" in ctx
|
|
assert f"id={p.id}" in ctx
|
|
assert "BrandX" in ctx
|
|
assert "targets: ['redness']" in ctx
|
|
assert "actives: ['Centella']" in ctx
|
|
assert "effects: {'soothing': 4}" in ctx
|
|
|
|
|
|
def test_suggest_shopping(client, session):
|
|
with patch(
|
|
"innercontext.api.products.call_gemini_with_function_tools"
|
|
) as mock_gemini:
|
|
mock_response = type(
|
|
"Response",
|
|
(),
|
|
{
|
|
"text": '{"suggestions": [{"category": "cleanser", "product_type": "cleanser", "priority": "high", "key_ingredients": [], "target_concerns": [], "why_needed": "reason", "recommended_time": "am", "frequency": "daily"}], "reasoning": "Test shopping"}'
|
|
},
|
|
)
|
|
mock_gemini.return_value = mock_response
|
|
|
|
r = client.post("/products/suggest")
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
assert len(data["suggestions"]) == 1
|
|
assert data["suggestions"][0]["product_type"] == "cleanser"
|
|
assert data["reasoning"] == "Test shopping"
|
|
kwargs = mock_gemini.call_args.kwargs
|
|
assert "function_handlers" in kwargs
|
|
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):
|
|
p = Product(
|
|
id=uuid.uuid4(),
|
|
name="Epiduo",
|
|
brand="Galderma",
|
|
category="serum",
|
|
recommended_time="pm",
|
|
leave_on=True,
|
|
is_medication=True,
|
|
product_effect_profile={},
|
|
)
|
|
session.add(p)
|
|
session.commit()
|
|
|
|
ctx = _build_shopping_context(session)
|
|
assert "Epiduo" not in ctx
|
|
|
|
|
|
def test_extract_requested_product_ids_dedupes_and_limits():
|
|
ids = _extract_requested_product_ids(
|
|
{"product_ids": ["a", "b", "a", 1, "c", "d"]},
|
|
max_ids=3,
|
|
)
|
|
assert ids == ["a", "b", "c"]
|
|
|
|
|
|
def test_shopping_tool_handlers_return_payloads(session: Session):
|
|
product = Product(
|
|
id=uuid.uuid4(),
|
|
name="Test Product",
|
|
brand="Brand",
|
|
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"]}],
|
|
incompatible_with=[{"target": "Vitamin C", "scope": "same_step"}],
|
|
context_rules={"safe_after_shaving": True},
|
|
product_effect_profile={},
|
|
)
|
|
|
|
payload = {"product_ids": [str(product.id)]}
|
|
|
|
inci_data = _build_inci_tool_handler([product])(payload)
|
|
assert inci_data["products"][0]["inci"] == ["Water", "Niacinamide"]
|
|
|
|
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 "incompatible_with" in safety_data["products"][0]
|
|
assert "context_rules" in safety_data["products"][0]
|