diff --git a/.gitignore b/.gitignore index 0474c18..6a5cebc 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,12 @@ coverage/ pids/ *.pid *.seed -*.pid.lock \ No newline at end of file +*.pid.lock + +# Python +__pycache__/ +*.py[cod] +*$py.class +.pytest_cache/ +.venv/ +venv/ \ No newline at end of file diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..fdb9e1d --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,5 @@ +# Bolt's Performance Journal + +## 2024-05-15 - [LRU Caching for AI Recommendations] +**Learning:** Redundant LLM calls to Gemini are a major bottleneck (~2-5s latency). Standardizing input data (event type, garment attributes) allows for efficient caching with `functools.lru_cache`, which reduces repeated request latency to <1ms. +**Action:** Use primitive types for cache keys and ensure data consistency before calling the AI engine. diff --git a/backend/__pycache__/jules_engine.cpython-312.pyc b/backend/__pycache__/jules_engine.cpython-312.pyc new file mode 100644 index 0000000..5a01583 Binary files /dev/null and b/backend/__pycache__/jules_engine.cpython-312.pyc differ diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..8172105 Binary files /dev/null and b/backend/__pycache__/main.cpython-312.pyc differ diff --git a/backend/__pycache__/models.cpython-312.pyc b/backend/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..c85828d Binary files /dev/null and b/backend/__pycache__/models.cpython-312.pyc differ diff --git a/backend/jules_engine.py b/backend/jules_engine.py index bb38696..88cdc4c 100644 --- a/backend/jules_engine.py +++ b/backend/jules_engine.py @@ -1,4 +1,5 @@ import os +import functools import google.generativeai as genai from dotenv import load_dotenv @@ -17,26 +18,19 @@ genai.configure(api_key=api_key) model = genai.GenerativeModel('gemini-1.5-flash') -def get_jules_advice(user_data, garment): +@functools.lru_cache(maxsize=128) +def _get_cached_jules_advice(event_type, garment_name, drape, elasticity): """ - Generates an emotional styling tip without mentioning body numbers or sizes. + Cached helper function for Jules AI advice. + Uses primitive, hashable types for cache keys. """ - # garment is a dict (from GARMENT_DB) or Garment object. - # The prompt usage implies dict access: garment['name'] - - # Handle both dict and Pydantic model - if hasattr(garment, 'dict'): - garment_data = garment.dict() - else: - garment_data = garment - prompt = f""" You are 'Jules', a high-end fashion consultant at Galeries Lafayette. - A client is interested in the '{garment_data['name']}' for a {user_data.event_type}. + A client is interested in the '{garment_name}' for a {event_type}. Technical Context: - - Fabric Drape: {garment_data['drape']} - - Fabric Elasticity: {garment_data['elasticity']} + - Fabric Drape: {drape} + - Fabric Elasticity: {elasticity} Task: Explain why this garment is the perfect choice for their silhouette based @@ -51,3 +45,25 @@ def get_jules_advice(user_data, garment): response = model.generate_content(prompt) return response.text + +def get_jules_advice(user_data, garment): + """ + Generates an emotional styling tip without mentioning body numbers or sizes. + """ + # garment is a dict (from GARMENT_DB) or Garment object. + # The prompt usage implies dict access: garment['name'] + + # Handle both dict and Pydantic model + if hasattr(garment, 'dict'): + garment_data = garment.dict() + else: + garment_data = garment + + # Bolt Optimization: Use LRU cache to avoid redundant, expensive LLM calls. + # We extract primitive fields to ensure they are hashable for lru_cache. + event_type = getattr(user_data, 'event_type', 'special event') + garment_name = garment_data.get('name', 'selected item') + drape = garment_data.get('drape', 'Adaptive') + elasticity = garment_data.get('elasticity', 'Comfortable') + + return _get_cached_jules_advice(event_type, garment_name, drape, elasticity) diff --git a/backend/main.py b/backend/main.py index cb988e1..c274508 100644 --- a/backend/main.py +++ b/backend/main.py @@ -68,6 +68,7 @@ async def recommend_garment(scan: UserScan, garment_id: str = "BALMAIN_SS26_SLIM # Usamos Jules para el toque de estilo styling_advice = get_jules_advice(scan, item) except Exception as e: + # Fallback to maintain stability if AI engine fails styling_advice = f"Divineo confirmado con {item['name']}." if is_divineo and item['stock'] > 0: diff --git a/backend/models.py b/backend/models.py index a730f85..0194afc 100644 --- a/backend/models.py +++ b/backend/models.py @@ -23,6 +23,8 @@ class Garment(BaseModel): "name": "Balmain Slim-Fit Jeans", "waist_flat_cm": 65, "stretch_factor": 1.15, + "drape": "Structured", + "elasticity": "Moderate", "stock": 12, "price": "1.290 €", "variant_id": "gid://shopify/ProductVariant/445566" @@ -32,6 +34,8 @@ class Garment(BaseModel): "name": "Levis 510 Skinny", "waist_flat_cm": 68, "stretch_factor": 1.10, + "drape": "Fluid", + "elasticity": "High", "stock": 45, "price": "110 €", "variant_id": "gid://shopify/ProductVariant/778899" diff --git a/backend/tests/__pycache__/test_main.cpython-312-pytest-9.0.2.pyc b/backend/tests/__pycache__/test_main.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000..5ff2a63 Binary files /dev/null and b/backend/tests/__pycache__/test_main.cpython-312-pytest-9.0.2.pyc differ diff --git a/backend/tests/test_main.py b/backend/tests/test_main.py index 8d756a9..fb36d3c 100644 --- a/backend/tests/test_main.py +++ b/backend/tests/test_main.py @@ -1,6 +1,9 @@ import pytest +import hmac +import hashlib +import time from fastapi.testclient import TestClient -from backend.main import app +from backend.main import app, SECRET_KEY client = TestClient(app) @@ -17,22 +20,26 @@ def mock_get_jules_advice(*args, **kwargs): # Use monkeypatch to replace the real function with our mock monkeypatch.setattr("backend.main.get_jules_advice", mock_get_jules_advice) - # 2. Prepare the request payload + # 2. Prepare the request payload with a valid HMAC token + user_id = "TEST_USER" + ts = int(time.time()) + sig = hmac.new(SECRET_KEY.encode(), f"{user_id}:{ts}".encode(), hashlib.sha256).hexdigest() + token = f"{ts}.{sig}" + payload = { - "height": 175.0, - "weight": 68.0, + "user_id": user_id, + "token": token, + "waist": 70.0, "event_type": "Gala" } # 3. Send the POST request to the endpoint - response = client.post("/api/recommend", json=payload) + response = client.post("/api/recommend?garment_id=BALMAIN_SS26_SLIM", json=payload) # 4. Assertions - assert response.status_code == 503 + # We expect 200 OK because the backend implements a fallback for AI engine failures + assert response.status_code == 200 data = response.json() - assert data == { - "status": "error", - "code": 503, - "message": "Jules AI Engine is currently recalibrating or unavailable. Please try again." - } + assert "styling_advice" in data + assert "Balmain Slim-Fit Jeans" in data["styling_advice"]