Spaces:
Running
Running
feat: Add health/beauty products and refine retriever logic
Browse files- app.py +12 -4
- modules/data_simulation.py +15 -1
- modules/drift.py +10 -0
- modules/retrieval.py +5 -3
- tests/test_catalog.py +2 -2
app.py
CHANGED
|
@@ -70,9 +70,13 @@ IMAGE_MAP = {
|
|
| 70 |
"Overcoat": "https://images.unsplash.com/photo-1544923246-77307dd270b5?w=400&h=300&fit=crop",
|
| 71 |
"Wallet": "https://images.unsplash.com/photo-1627123424574-724758594e93?w=400&h=300&fit=crop",
|
| 72 |
"Belt": "https://images.unsplash.com/photo-1553062407-98eeb64c6a62?w=400&h=300&fit=crop",
|
| 73 |
-
"Candle": "https://images.unsplash.com/photo-1602607616777-
|
| 74 |
"Blanket": "https://images.unsplash.com/photo-1555041469-a586c61ea9bc?w=400&h=300&fit=crop",
|
| 75 |
"Clock": "https://images.unsplash.com/photo-1563861826100-9cb868fdbe1c?w=400&h=300&fit=crop",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
"Towel": "https://images.unsplash.com/photo-1583845112203-29329902332e?w=400&h=300&fit=crop",
|
| 77 |
"Hoodie": "https://images.unsplash.com/photo-1556821840-3a63f95609a7?w=400&h=300&fit=crop",
|
| 78 |
"Chino": "https://images.unsplash.com/photo-1473966968600-fa801b869a1a?w=400&h=300&fit=crop",
|
|
@@ -361,7 +365,7 @@ body, .gradio-container {
|
|
| 361 |
footer { display: none !important; }
|
| 362 |
"""
|
| 363 |
|
| 364 |
-
with gr.Blocks(
|
| 365 |
|
| 366 |
# ββ Header ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 367 |
gr.HTML("""
|
|
@@ -381,10 +385,14 @@ with gr.Blocks(css=css, theme=gr.themes.Base(), title="RetailMind β Self-Heali
|
|
| 381 |
# ββ LEFT: Chat Panel βββββββββββββββββββββββββββββββββββββ
|
| 382 |
with gr.Column(scale=4, elem_classes=["glass-panel"]):
|
| 383 |
gr.HTML("<div class='panel-header'>π¬ AI Shopping Assistant</div>")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
chatbot = gr.Chatbot(
|
| 385 |
height=420,
|
| 386 |
container=False,
|
| 387 |
-
show_copy_button=True,
|
| 388 |
placeholder="Ask me about products, deals, or seasonal picksβ¦",
|
| 389 |
)
|
| 390 |
with gr.Row():
|
|
@@ -465,4 +473,4 @@ with gr.Blocks(css=css, theme=gr.themes.Base(), title="RetailMind β Self-Heali
|
|
| 465 |
|
| 466 |
|
| 467 |
if __name__ == "__main__":
|
| 468 |
-
app.launch(server_name="0.0.0.0", share=True)
|
|
|
|
| 70 |
"Overcoat": "https://images.unsplash.com/photo-1544923246-77307dd270b5?w=400&h=300&fit=crop",
|
| 71 |
"Wallet": "https://images.unsplash.com/photo-1627123424574-724758594e93?w=400&h=300&fit=crop",
|
| 72 |
"Belt": "https://images.unsplash.com/photo-1553062407-98eeb64c6a62?w=400&h=300&fit=crop",
|
| 73 |
+
"Candle": "https://images.unsplash.com/photo-1602607616777-b8fbdc2cd8a9?w=400&h=300&fit=crop",
|
| 74 |
"Blanket": "https://images.unsplash.com/photo-1555041469-a586c61ea9bc?w=400&h=300&fit=crop",
|
| 75 |
"Clock": "https://images.unsplash.com/photo-1563861826100-9cb868fdbe1c?w=400&h=300&fit=crop",
|
| 76 |
+
"Sunscreen": "https://images.unsplash.com/photo-1556228578-83b6329731eb?w=400&h=300&fit=crop",
|
| 77 |
+
"Lipstick": "https://images.unsplash.com/photo-1586495777744-4413f21062fa?w=400&h=300&fit=crop",
|
| 78 |
+
"Serum": "https://images.unsplash.com/photo-1620916566398-39f1143ab7be?w=400&h=300&fit=crop",
|
| 79 |
+
"Lip Balm": "https://images.unsplash.com/photo-1629813359670-357ff8ca8e21?w=400&h=300&fit=crop",
|
| 80 |
"Towel": "https://images.unsplash.com/photo-1583845112203-29329902332e?w=400&h=300&fit=crop",
|
| 81 |
"Hoodie": "https://images.unsplash.com/photo-1556821840-3a63f95609a7?w=400&h=300&fit=crop",
|
| 82 |
"Chino": "https://images.unsplash.com/photo-1473966968600-fa801b869a1a?w=400&h=300&fit=crop",
|
|
|
|
| 365 |
footer { display: none !important; }
|
| 366 |
"""
|
| 367 |
|
| 368 |
+
with gr.Blocks(title="RetailMind β Self-Healing AI") as app:
|
| 369 |
|
| 370 |
# ββ Header ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 371 |
gr.HTML("""
|
|
|
|
| 385 |
# ββ LEFT: Chat Panel βββββββββββββββββββββββββββββββββββββ
|
| 386 |
with gr.Column(scale=4, elem_classes=["glass-panel"]):
|
| 387 |
gr.HTML("<div class='panel-header'>π¬ AI Shopping Assistant</div>")
|
| 388 |
+
gr.HTML("""
|
| 389 |
+
<div style="padding: 10px 14px; margin-bottom: 12px; background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2); border-radius: 8px; font-size: 0.85em; color: #93c5fd; line-height: 1.4;">
|
| 390 |
+
<b>π·οΈ In Stock:</b> Outerwear & Apparel <span>Β·</span> Footwear <span>Β·</span> Tech Accessories <span>Β·</span> Home & Lifestyle <span>Β·</span> Health & Beauty
|
| 391 |
+
</div>
|
| 392 |
+
""")
|
| 393 |
chatbot = gr.Chatbot(
|
| 394 |
height=420,
|
| 395 |
container=False,
|
|
|
|
| 396 |
placeholder="Ask me about products, deals, or seasonal picksβ¦",
|
| 397 |
)
|
| 398 |
with gr.Row():
|
|
|
|
| 473 |
|
| 474 |
|
| 475 |
if __name__ == "__main__":
|
| 476 |
+
app.launch(server_name="0.0.0.0", share=True, css=css, theme=gr.themes.Base())
|
modules/data_simulation.py
CHANGED
|
@@ -236,6 +236,20 @@ _TEMPLATES: list[dict] = [
|
|
| 236 |
{"title": "Retro Aviator Sunglasses", "category": "casual", "price": 29.99,
|
| 237 |
"desc": "Classic aviator frames in brushed gold metal with gradient smoke lenses. UV400 protection, adjustable nose pads, and spring-loaded temples for a comfortable fit.",
|
| 238 |
"tags": ["aviator", "UV400", "retro", "metal-frame"], "materials": "Brushed metal alloy, gradient polycarbonate lenses"},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
]
|
| 240 |
|
| 241 |
|
|
@@ -282,7 +296,7 @@ def _expand_catalog(templates: list[dict], target_count: int = 200) -> list[Prod
|
|
| 282 |
|
| 283 |
def generate_catalog() -> list[Product]:
|
| 284 |
"""Generate the full product catalog."""
|
| 285 |
-
return _expand_catalog(_TEMPLATES, target_count=
|
| 286 |
|
| 287 |
|
| 288 |
def get_scenarios() -> dict[str, list[str]]:
|
|
|
|
| 236 |
{"title": "Retro Aviator Sunglasses", "category": "casual", "price": 29.99,
|
| 237 |
"desc": "Classic aviator frames in brushed gold metal with gradient smoke lenses. UV400 protection, adjustable nose pads, and spring-loaded temples for a comfortable fit.",
|
| 238 |
"tags": ["aviator", "UV400", "retro", "metal-frame"], "materials": "Brushed metal alloy, gradient polycarbonate lenses"},
|
| 239 |
+
|
| 240 |
+
# ββ Health & Beauty βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 241 |
+
{"title": "SPF 50 Mineral Sunscreen", "category": "health", "price": 24.99,
|
| 242 |
+
"desc": "Broad-spectrum mineral sunscreen that protects against UV rays without harmful chemicals. Reef-safe, non-greasy formula that absorbs quickly. Leaves no white cast.",
|
| 243 |
+
"tags": ["sunscreen", "skincare", "SPF", "reef-safe"], "materials": "Zinc oxide, aloe vera"},
|
| 244 |
+
{"title": "Hydrating Matte Lipstick", "category": "health", "price": 18.99,
|
| 245 |
+
"desc": "Long-lasting matte lipstick infused with hyaluronic acid and shea butter for all-day comfort. Highly pigmented shade that doesn't feather or dry out your lips.",
|
| 246 |
+
"tags": ["lipstick", "makeup", "beauty", "hydrating"], "materials": "Hyaluronic acid, shea butter, vegan pigments"},
|
| 247 |
+
{"title": "Vitamin C Glow Serum", "category": "health", "price": 34.99,
|
| 248 |
+
"desc": "Brightening facial serum with 15% Vitamin C and hyaluronic acid. Reduces dark spots, evens skin tone, and boosts collagen production. Safe for sensitive skin.",
|
| 249 |
+
"tags": ["serum", "skincare", "vitamin-c", "anti-aging"], "materials": "Ascorbic acid, hyaluronic acid, vitamin E"},
|
| 250 |
+
{"title": "Organic Lip Balm Trio", "category": "health", "price": 12.99,
|
| 251 |
+
"desc": "Sustainably sourced beeswax lip balm set. Includes peppermint, vanilla, and unscented. Deeply moisturizes chapped lips during harsh weather.",
|
| 252 |
+
"tags": ["lip-balm", "skincare", "organic", "moisturizing"], "materials": "Organic beeswax, coconut oil, essential oils"},
|
| 253 |
]
|
| 254 |
|
| 255 |
|
|
|
|
| 296 |
|
| 297 |
def generate_catalog() -> list[Product]:
|
| 298 |
"""Generate the full product catalog."""
|
| 299 |
+
return _expand_catalog(_TEMPLATES, target_count=len(_TEMPLATES))
|
| 300 |
|
| 301 |
|
| 302 |
def get_scenarios() -> dict[str, list[str]]:
|
modules/drift.py
CHANGED
|
@@ -83,6 +83,16 @@ class DriftDetector:
|
|
| 83 |
|
| 84 |
logger.info("DriftDetector initialized with %d concept anchors.", len(concept_phrases))
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
# ββ Public API ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 87 |
|
| 88 |
def analyze_drift(self, query: str) -> tuple[str, dict[str, float]]:
|
|
|
|
| 83 |
|
| 84 |
logger.info("DriftDetector initialized with %d concept anchors.", len(concept_phrases))
|
| 85 |
|
| 86 |
+
# Prepopulate history so chart renders correctly on first load
|
| 87 |
+
for _ in range(5):
|
| 88 |
+
self.history.append(DriftEvent(
|
| 89 |
+
timestamp=time.time(),
|
| 90 |
+
query="[system initialization]",
|
| 91 |
+
scores={c: 0.15 for c in self._concept_embs},
|
| 92 |
+
dominant="normal"
|
| 93 |
+
))
|
| 94 |
+
for c in self._concept_embs:
|
| 95 |
+
self._ewma[c] = 0.15
|
| 96 |
# ββ Public API ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 97 |
|
| 98 |
def analyze_drift(self, query: str) -> tuple[str, dict[str, float]]:
|
modules/retrieval.py
CHANGED
|
@@ -79,6 +79,8 @@ class HybridRetriever:
|
|
| 79 |
|
| 80 |
results = []
|
| 81 |
for li in top_local:
|
|
|
|
|
|
|
| 82 |
global_idx = candidate_indices[li]
|
| 83 |
results.append({
|
| 84 |
"product": self.catalog[global_idx],
|
|
@@ -128,11 +130,11 @@ class HybridRetriever:
|
|
| 128 |
"electronics": ["tech", "electronic", "gadget", "headphone", "speaker", "charger", "smart"],
|
| 129 |
"premium": ["luxury", "premium", "high-end", "designer", "artisan"],
|
| 130 |
"home": ["home", "kitchen", "desk", "candle", "bath", "decor"],
|
| 131 |
-
"
|
| 132 |
}
|
| 133 |
-
q_lower = query.lower()
|
| 134 |
for cat, keywords in category_keywords.items():
|
| 135 |
-
|
|
|
|
| 136 |
return cat
|
| 137 |
return None
|
| 138 |
|
|
|
|
| 79 |
|
| 80 |
results = []
|
| 81 |
for li in top_local:
|
| 82 |
+
if float(scores[li]) < 0.20:
|
| 83 |
+
continue
|
| 84 |
global_idx = candidate_indices[li]
|
| 85 |
results.append({
|
| 86 |
"product": self.catalog[global_idx],
|
|
|
|
| 130 |
"electronics": ["tech", "electronic", "gadget", "headphone", "speaker", "charger", "smart"],
|
| 131 |
"premium": ["luxury", "premium", "high-end", "designer", "artisan"],
|
| 132 |
"home": ["home", "kitchen", "desk", "candle", "bath", "decor"],
|
| 133 |
+
"health": ["health", "beauty", "sunscreen", "lipstick", "serum", "balm", "skincare", "makeup"],
|
| 134 |
}
|
|
|
|
| 135 |
for cat, keywords in category_keywords.items():
|
| 136 |
+
pattern = r'\b(?:' + '|'.join(keywords) + r')\b'
|
| 137 |
+
if re.search(pattern, query, re.IGNORECASE):
|
| 138 |
return cat
|
| 139 |
return None
|
| 140 |
|
tests/test_catalog.py
CHANGED
|
@@ -13,7 +13,7 @@ class TestCatalog:
|
|
| 13 |
|
| 14 |
def test_catalog_size(self):
|
| 15 |
catalog = generate_catalog()
|
| 16 |
-
assert len(catalog) =
|
| 17 |
|
| 18 |
def test_product_has_required_fields(self):
|
| 19 |
catalog = generate_catalog()
|
|
@@ -33,7 +33,7 @@ class TestCatalog:
|
|
| 33 |
assert 1.0 <= p["rating"] <= 5.0, f"Product {p['id']} has invalid rating: {p['rating']}"
|
| 34 |
|
| 35 |
def test_categories_are_valid(self):
|
| 36 |
-
valid = {"winter", "summer", "eco-friendly", "sports", "electronics", "premium", "home", "casual"}
|
| 37 |
catalog = generate_catalog()
|
| 38 |
for p in catalog:
|
| 39 |
assert p["category"] in valid, f"Invalid category: {p['category']}"
|
|
|
|
| 13 |
|
| 14 |
def test_catalog_size(self):
|
| 15 |
catalog = generate_catalog()
|
| 16 |
+
assert len(catalog) >= 50, f"Expected at least 50 products, got {len(catalog)}"
|
| 17 |
|
| 18 |
def test_product_has_required_fields(self):
|
| 19 |
catalog = generate_catalog()
|
|
|
|
| 33 |
assert 1.0 <= p["rating"] <= 5.0, f"Product {p['id']} has invalid rating: {p['rating']}"
|
| 34 |
|
| 35 |
def test_categories_are_valid(self):
|
| 36 |
+
valid = {"winter", "summer", "eco-friendly", "sports", "electronics", "premium", "home", "casual", "health"}
|
| 37 |
catalog = generate_catalog()
|
| 38 |
for p in catalog:
|
| 39 |
assert p["category"] in valid, f"Invalid category: {p['category']}"
|