Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,14 @@ coverage/
pids/
*.pid
*.seed
*.pid.lock
*.pid.lock

# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.pytest_cache/
.venv/
venv/
env/
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## 2025-05-15 - [Add LRU caching to AI recommendation engine]
**Learning:** LLM API calls are a significant performance bottleneck in the recommendation pipeline. Since fashion advice for specific garment/event combinations is often static, caching these results provides a massive performance boost (from ~1s to <0.1ms). Using primitive, hashable types for cache keys is essential when working with complex objects like Pydantic models.

**Action:** Always wrap expensive, repeatable AI logic with `functools.lru_cache` using primitive keys to maximize hits and minimize latency.
50 changes: 50 additions & 0 deletions backend/benchmark_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import time
from unittest.mock import MagicMock
import jules_engine

def benchmark():
print("--- ⚡ BOLT CACHE BENCHMARK (MOCKED LLM) ---")

# Mock the LLM call to simulate a real, slow API response
original_model = jules_engine.model
jules_engine.model = MagicMock()

def slow_generate_content(prompt):
time.sleep(1.0) # Simulate 1s network latency
mock_response = MagicMock()
mock_response.text = f"Mocked advice for: {prompt[:50]}..."
return mock_response

jules_engine.model.generate_content.side_effect = slow_generate_content

# Clear cache for a clean run
jules_engine._get_cached_advice.cache_clear()

test_args = ("Gala", "Balmain Slim-Fit Jeans", "Architectural and structured", "Minimal with memory retention")

print(f"Executing first call (uncached)...")
start_time = time.time()
advice1 = jules_engine._get_cached_advice(*test_args)
end_time = time.time()
uncached_duration = end_time - start_time
print(f"Uncached duration: {uncached_duration:.4f} seconds")

print(f"\nExecuting second call (cached)...")
start_time = time.time()
advice2 = jules_engine._get_cached_advice(*test_args)
end_time = time.time()
cached_duration = end_time - start_time
print(f"Cached duration: {cached_duration:.4f} seconds")

# Restore original model
jules_engine.model = original_model

if advice1 == advice2:
print("\n[SUCCESS] Cache returned identical result.")
speedup = uncached_duration / cached_duration if cached_duration > 0 else float('inf')
print(f"Performance Gain: {speedup:.1f}x faster")
else:
print("\n[ERROR] Cache mismatch!")

if __name__ == "__main__":
benchmark()
41 changes: 27 additions & 14 deletions backend/jules_engine.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
from functools import lru_cache
import google.generativeai as genai
from dotenv import load_dotenv

Expand All @@ -17,26 +18,19 @@
genai.configure(api_key=api_key)
model = genai.GenerativeModel('gemini-1.5-flash')

def get_jules_advice(user_data, garment):
@lru_cache(maxsize=128)
def _get_cached_advice(event_type, garment_name, drape, elasticity):
"""
Generates an emotional styling tip without mentioning body numbers or sizes.
Internal cached function to generate styling tips.
Uses primitive types as keys for the LRU cache.
"""
# 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
Expand All @@ -51,3 +45,22 @@ 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.
Utilizes LRU caching to avoid redundant expensive LLM calls.
"""
# Handle both dict and Pydantic model for garment
if hasattr(garment, 'dict'):
garment_data = garment.dict()
else:
garment_data = garment

# Extract primitive types for the cache key
event_type = getattr(user_data, 'event_type', 'Casual')
garment_name = garment_data.get('name', 'Unknown Garment')
drape = garment_data.get('drape', 'Unknown')
elasticity = garment_data.get('elasticity', 'Unknown')

return _get_cached_advice(event_type, garment_name, drape, elasticity)
10 changes: 8 additions & 2 deletions backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class Garment(BaseModel):
stock: int
price: str
variant_id: str
drape: str
elasticity: str

# 👗 Catálogo Shopify (Divineo Bunker)
SHOPIFY_INVENTORY = {
Expand All @@ -25,7 +27,9 @@ class Garment(BaseModel):
"stretch_factor": 1.15,
"stock": 12,
"price": "1.290 €",
"variant_id": "gid://shopify/ProductVariant/445566"
"variant_id": "gid://shopify/ProductVariant/445566",
"drape": "Architectural and structured",
"elasticity": "Minimal with memory retention"
},
"LEVIS_510_STRETCH": {
"id": "LEVIS_510_STRETCH",
Expand All @@ -34,7 +38,9 @@ class Garment(BaseModel):
"stretch_factor": 1.10,
"stock": 45,
"price": "110 €",
"variant_id": "gid://shopify/ProductVariant/778899"
"variant_id": "gid://shopify/ProductVariant/778899",
"drape": "Fluid and adaptive",
"elasticity": "High performance stretch"
}
}

Expand Down
37 changes: 23 additions & 14 deletions backend/tests/test_main.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

This change introduces a dependency on SECRET_KEY. Upon inspection of the full file (backend/main.py, line 22), the secret key is hardcoded: SECRET_KEY = "LVT_SECRET_PROD_091228222". Hardcoding secrets is a critical security vulnerability as it exposes sensitive credentials directly in the source code. This key should be managed securely by loading it from an environment variable or a secret management service, similar to how GEMINI_API_KEY is handled in the project.


client = TestClient(app)

def test_recommend_garment_engine_failure(monkeypatch):
"""
Test that the /api/recommend endpoint correctly handles failures
from the Jules AI engine (get_jules_advice) and returns a 503
Service Unavailable with a gracefully structured JSON error.
from the Jules AI engine (get_jules_advice) by returning a 200 OK
with a fallback recommendation string.
"""
# 1. Mock the get_jules_advice function to raise an exception
def mock_get_jules_advice(*args, **kwargs):
Expand All @@ -17,22 +20,28 @@ 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 valid authentication
user_id = "test_user_123"
ts = int(time.time())
sig = hmac.new(SECRET_KEY.encode(), f"{user_id}:{ts}".encode(), hashlib.sha256).hexdigest()
token = f"{ts}.{sig}"

# 3. Prepare the request payload matching UserScan model
payload = {
"height": 175.0,
"weight": 68.0,
"user_id": user_id,
"token": token,
"waist": 74.75, # 74.75 / (65 * 1.15) = 1.0 (Perfect fit for BALMAIN_SS26_SLIM)
"event_type": "Gala"
}

# 3. Send the POST request to the endpoint
# 4. Send the POST request to the endpoint
# Note: the endpoint also takes a garment_id as a query param, defaults to "BALMAIN_SS26_SLIM"
response = client.post("/api/recommend", json=payload)

# 4. Assertions
assert response.status_code == 503
# 5. Assertions
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 data["status"] == "SUCCESS"
assert "styling_advice" in data
assert "Balmain Slim-Fit Jeans" in data["styling_advice"]