fix(api): tighten shopping suggestion response rules
Constrain shopping target concerns to SkinConcern enums and add a regression test for invalid values. Simplify the shopping prompt so repurchase suggestions stay practical, use shorter product types, and avoid leaking raw scoring/debug language into user-facing copy.
This commit is contained in:
parent
d91d06455b
commit
e20c18c2ee
2 changed files with 37 additions and 41 deletions
|
|
@ -416,7 +416,7 @@ class ProductSuggestion(PydanticBase):
|
||||||
product_type: str
|
product_type: str
|
||||||
priority: Literal["high", "medium", "low"]
|
priority: Literal["high", "medium", "low"]
|
||||||
key_ingredients: list[str]
|
key_ingredients: list[str]
|
||||||
target_concerns: list[str]
|
target_concerns: list[SkinConcern]
|
||||||
recommended_time: DayTime
|
recommended_time: DayTime
|
||||||
frequency: str
|
frequency: str
|
||||||
short_reason: str
|
short_reason: str
|
||||||
|
|
@ -440,7 +440,7 @@ class _ProductSuggestionOut(PydanticBase):
|
||||||
product_type: str
|
product_type: str
|
||||||
priority: Literal["high", "medium", "low"]
|
priority: Literal["high", "medium", "low"]
|
||||||
key_ingredients: list[str]
|
key_ingredients: list[str]
|
||||||
target_concerns: list[str]
|
target_concerns: list[SkinConcern]
|
||||||
recommended_time: DayTime
|
recommended_time: DayTime
|
||||||
frequency: str
|
frequency: str
|
||||||
short_reason: str
|
short_reason: str
|
||||||
|
|
@ -1189,48 +1189,24 @@ def _extract_requested_product_ids(
|
||||||
|
|
||||||
_SHOPPING_SYSTEM_PROMPT = """Jesteś asystentem zakupowym w dziedzinie pielęgnacji skóry.
|
_SHOPPING_SYSTEM_PROMPT = """Jesteś asystentem zakupowym w dziedzinie pielęgnacji skóry.
|
||||||
|
|
||||||
Twoim zadaniem jest ocenić dwie rzeczy:
|
Oceń dwie rzeczy: realne luki w rutynie oraz odkupy produktów, które warto uzupełnić teraz.
|
||||||
1. czy w rutynie użytkownika istnieją realne luki produktowe,
|
Działaj konserwatywnie: sugeruj tylko wtedy, gdy istnieje wyraźny powód praktyczny.
|
||||||
2. czy któryś z już posiadanych typów produktów warto odkupić teraz z powodu kończącego się zapasu.
|
Najpierw rozważ luki w rutynie, potem odkupy.
|
||||||
|
Traktuj `replenishment_score`, `replenishment_priority_hint`, `repurchase_candidate`, `stock_state`, `lowest_remaining_level`, `sealed_backup_count`, `last_used_on` i `days_since_last_used` jako główne sygnały decyzji zakupowej.
|
||||||
Masz działać konserwatywnie: nie sugeruj zakupu tylko dlatego, że coś mogłoby się przydać.
|
`sealed_backup_count` odnosi się do zapasu tego produktu lub bardzo zbliżonego typu; inny produkt z tej samej kategorii obniża pilność tylko wtedy, gdy realistycznie pełni podobną funkcję w rutynie.
|
||||||
Sugestia ma pojawić się tylko wtedy, gdy istnieje wyraźny powód praktyczny.
|
Jeśli zakup nie jest pilny dzięki alternatywie, wyjaśnij, czy chodzi o rzeczywisty zapas tego samego typu produktu, czy o funkcjonalny zamiennik z tej samej kategorii.
|
||||||
|
Jeśli istnieje sealed backup lub bardzo bliski funkcjonalny zamiennik, sugestia zwykle nie powinna mieć `priority=high`, chyba że potrzeba odkupu jest wyraźnie wyjątkowa.
|
||||||
WAŻNE POJĘCIA:
|
`product_type` ma być krótką nazwą typu produktu i opisywać funkcję produktu, a nie opis marketingowy lub pełną specyfikację składu.
|
||||||
- `stock_state` opisuje stan zapasu: `out_of_stock`, `healthy`, `monitor`, `low`, `urgent`
|
Przy odkupie możesz odwoływać się do konkretnych już posiadanych produktów, jeśli pomaga to uzasadnić decyzję. Przy lukach w rutynie sugeruj typy produktów, nie marki.
|
||||||
- `sealed_backup_count` > 0 zwykle oznacza, że odkup nie jest pilny
|
Uwzględniaj aktywne problemy skóry, miejsce produktu w rutynie, konflikty składników i bezpieczeństwo przy naruszonej barierze.
|
||||||
- `lowest_remaining_level` dotyczy tylko otwartych opakowań i może mieć wartość: `high`, `medium`, `low`, `nearly_empty`
|
Pisz po polsku, językiem praktycznym i zakupowym. Unikaj nadmiernie medycznego lub diagnostycznego tonu.
|
||||||
- `last_used_on` i `days_since_last_used` pomagają ocenić, czy produkt jest nadal faktycznie używany
|
Nie używaj w tekstach dla użytkownika surowych sygnałów systemowych ani dosłownych etykiet z warstwy danych, takich jak `id`, `score`, `status low` czy `poziom produktu jest niski`; opisuj naturalnie wniosek, np. jako kończący się zapas, niski zapas lub wysoką pilność odkupu.
|
||||||
- `replenishment_score`, `replenishment_priority_hint` i `repurchase_candidate` są backendowym sygnałem pilności odkupu; traktuj je jako mocną wskazówkę
|
Możesz zwrócić pustą listę `suggestions`, jeśli nie widzisz realnej potrzeby.
|
||||||
- `reason_codes` tłumaczą, dlaczego backend uznał odkup za pilny albo niepilny
|
`target_concerns` musi używać wyłącznie wartości enumu `SkinConcern` poniżej. `priority` ustawiaj jako: high = wyraźna luka lub pilna potrzeba, medium = sensowne uzupełnienie, low = opcjonalny upgrade.
|
||||||
|
|
||||||
JAK PODEJMOWAĆ DECYZJĘ:
|
|
||||||
0. Sugeruj tylko wtedy, gdy jest realna potrzeba - nie zwracaj stałej liczby produktów
|
|
||||||
1. Najpierw oceń, czy użytkownik ma lukę w rutynie
|
|
||||||
2. Potem oceń, czy któryś istniejący typ produktu jest wart odkupu teraz
|
|
||||||
3. Nie rekomenduj ponownie produktu, który ma zdrowy zapas
|
|
||||||
4. Nie rekomenduj odkupu tylko dlatego, że produkt jest niski, jeśli nie był używany od dawna i nie wygląda na nadal potrzebny
|
|
||||||
5. Dla kategorii podstawowych (`cleanser`, `moisturizer`, `spf`) niski stan i świeże użycie mają większe znaczenie niż dla produktów okazjonalnych
|
|
||||||
6. Dla produktów okazjonalnych (`exfoliant`, `mask`, `spot_treatment`) świeżość użycia jest ważniejsza niż sam niski stan
|
|
||||||
7. Jeśli `sealed_backup_count` > 0, zwykle nie rekomenduj odkupu
|
|
||||||
8. Sugeruj TYLKO typy produktów, NIGDY konkretne marki (np. "Salicylic Acid 2% Masque", nie "La Roche-Posay")
|
|
||||||
9. Bierz pod uwagę aktywne problemy skóry (acne, hyperpigmentacja, aging, etc.)
|
|
||||||
10. Sugeruj realistyczną częstotliwość użycia (dzienna, 2-3x tygodniowo, etc.)
|
|
||||||
11. Zachowaj kolejność warstw: cleanse → toner → serum → moisturizer → SPF
|
|
||||||
12. Jeśli użytkownik ma uszkodzoną barierę, unikaj silnych eksfoliantów i retinoidów
|
|
||||||
13. Zwracaj uwagę na ewentualne konflikty polecanych składników z tymi, które użytkownik już posiada
|
|
||||||
14. Odpowiadaj w języku polskim
|
|
||||||
15. Używaj wyłącznie dozwolonych wartości enumów poniżej - nie twórz synonimów typu "night", "evening" ani "treatment"
|
|
||||||
16. Możesz zwrócić pustą listę suggestions, jeśli nie widzisz realnej potrzeby zakupowej
|
|
||||||
17. Każda sugestia ma mieć charakter decision-support: konkretnie wyjaśnij, dlaczego warto kupić ją teraz, jak wpisuje się w obecną rutynę i jakie są ograniczenia
|
|
||||||
18. `short_reason` ma być krótkim, 1-zdaniowym skrótem decyzji zakupowej
|
|
||||||
19. `reason_to_buy_now` ma być konkretne i praktyczne, bez lania wody
|
|
||||||
20. `reason_not_needed_if_budget_tight` jest opcjonalne - uzupełniaj tylko wtedy, gdy zakup nie jest pilny lub istnieje rozsądny kompromis
|
|
||||||
21. `usage_cautions` ma być krótką listą praktycznych uwag; gdy brak istotnych zastrzeżeń zwróć pustą listę
|
|
||||||
22. `priority` ustawiaj jako: high = wyraźna luka lub pilna potrzeba, medium = sensowne uzupełnienie, low = opcjonalny upgrade
|
|
||||||
|
|
||||||
DOZWOLONE WARTOŚCI ENUMÓW:
|
DOZWOLONE WARTOŚCI ENUMÓW:
|
||||||
- category: "cleanser" | "toner" | "essence" | "serum" | "moisturizer" | "spf" | "mask" | "exfoliant" | "hair_treatment" | "tool" | "spot_treatment" | "oil"
|
- category: "cleanser" | "toner" | "essence" | "serum" | "moisturizer" | "spf" | "mask" | "exfoliant" | "hair_treatment" | "tool" | "spot_treatment" | "oil"
|
||||||
|
- target_concerns: "acne" | "rosacea" | "hyperpigmentation" | "aging" | "dehydration" | "redness" | "damaged_barrier" | "pore_visibility" | "uneven_texture" | "hair_growth" | "sebum_excess"
|
||||||
- recommended_time: "am" | "pm" | "both"
|
- recommended_time: "am" | "pm" | "both"
|
||||||
|
|
||||||
Format odpowiedzi - zwróć wyłącznie JSON zgodny z podanym schematem."""
|
Format odpowiedzi - zwróć wyłącznie JSON zgodny z podanym schematem."""
|
||||||
|
|
|
||||||
|
|
@ -267,6 +267,26 @@ def test_suggest_shopping_invalid_schema_returns_502(client):
|
||||||
assert "suggestions/0/priority" in r.json()["detail"]
|
assert "suggestions/0/priority" in r.json()["detail"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_suggest_shopping_invalid_target_concern_returns_502(client):
|
||||||
|
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": ["glycerin"], "target_concerns": ["inflammation"], "recommended_time": "am", "frequency": "daily", "short_reason": "Brakuje lagodnego kroku myjacego rano.", "reason_to_buy_now": "Obecnie nie masz delikatnego produktu do porannego oczyszczania i wsparcia bariery.", "fit_with_current_routine": "To domknie podstawowy krok cleanse bez dokladania agresywnych aktywow.", "usage_cautions": []}], "reasoning": "Test shopping"}'
|
||||||
|
},
|
||||||
|
)
|
||||||
|
mock_gemini.return_value = (mock_response, None)
|
||||||
|
|
||||||
|
r = client.post("/products/suggest")
|
||||||
|
|
||||||
|
assert r.status_code == 502
|
||||||
|
assert "LLM returned invalid shopping suggestion schema" in r.json()["detail"]
|
||||||
|
assert "suggestions/0/target_concerns/0" in r.json()["detail"]
|
||||||
|
|
||||||
|
|
||||||
def test_shopping_context_medication_skip(session: Session):
|
def test_shopping_context_medication_skip(session: Session):
|
||||||
p = Product(
|
p = Product(
|
||||||
id=uuid.uuid4(),
|
id=uuid.uuid4(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue