API_RAG / retriever.py
Aghode91's picture
rag
f3350ab
Raw
History Blame Contribute Delete
11.4 kB
import os
from pathlib import Path
from typing import Optional
import chromadb
from groq import Groq
from sentence_transformers import SentenceTransformer
CHROMA_DIR = Path(__file__).parent / "chroma_db"
COLLECTION_NAME = "skincare_products"
_model: Optional[SentenceTransformer] = None
_collection = None
ROUTINE_STEPS = {
"matin": ["cleanser", "toner", "serum", "moisturizer"],
"soir": ["cleanser", "serum", "moisturizer"],
}
SKIN_RULES = {
"oily": {
"avoid": ["oil", "heavy", "comedogenic"],
"prefer": ["oil-free", "non-comedogenic", "light", "gel"],
},
"dry": {
"avoid": ["alcohol"],
"prefer": ["hydrating", "ceramides", "rich"],
}
}
def _get_model() -> SentenceTransformer:
global _model
if _model is None:
_model = SentenceTransformer("all-MiniLM-L6-v2")
return _model
def _get_collection():
global _collection
if _collection is None:
client = chromadb.PersistentClient(path=str(CHROMA_DIR))
_collection = client.get_collection(COLLECTION_NAME)
return _collection
def _build_query(skin_type: str, acne: bool, preferences: dict) -> str:
parts = [f"skincare product for {skin_type} skin"]
if acne:
parts.append("acne-prone")
if pt := preferences.get("product_type"):
parts.append(pt)
for f in preferences.get("formulation", []):
parts.append(f)
if "french" in preferences.get("origin", []):
parts.append("French brand")
return ", ".join(parts)
def _build_where(preferences: dict) -> Optional[dict]:
conditions = []
for flag, key in [("vegan", "is_vegan"), ("clean", "is_clean"), ("bio", "is_bio")]:
if flag in preferences.get("formulation", []):
conditions.append({key: {"$eq": True}})
if "french" in preferences.get("origin", []):
conditions.append({"is_french": {"$eq": True}})
if (pb := preferences.get("price_band")) and pb not in ("any", None):
conditions.append({"price_band": {"$eq": pb}})
if not conditions:
return None
if len(conditions) == 1:
return conditions[0]
return {"$and": conditions}
def _query(query_text: str, where: Optional[dict], n_results: int = 5) -> dict:
collection = _get_collection()
model = _get_model() # ← ton SentenceTransformer, enfin utilisé
embedding = model.encode(query_text).tolist() # même modèle qu'ingest.py
try:
kwargs: dict = {
"query_embeddings": [embedding], # ← vecteur, plus query_texts
"n_results": n_results,
}
if where:
kwargs["where"] = where
return collection.query(**kwargs)
except Exception:
return {"ids": [[]], "documents": [[]], "metadatas": [[]]}
def _generate_explanation(product_doc: str, skin_type: str, acne: bool, preferences: dict) -> str:
prefs = []
for f in preferences.get("formulation", []):
prefs.append(f)
if "french" in preferences.get("origin", []):
prefs.append("marque française")
if pt := preferences.get("product_type"):
prefs.append(f"type : {pt}")
if pb := preferences.get("price_band"):
prefs.append(f"budget : {pb}")
profile = f"peau {skin_type}"
if acne:
profile += ", tendance acnéique"
if prefs:
profile += f", préférences : {', '.join(prefs)}"
client = Groq(api_key=os.environ["GROQ_API_KEY"])
response = client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=[
{
"role": "system",
"content": (
"Tu es un expert en soins de la peau. "
"En 2 à 3 phrases courtes en français, explique pourquoi ce produit "
"est adapté au profil de l'utilisateur. Sois précis et bienveillant. "
),
},
{
"role": "user",
"content": f"Profil : {profile}\n\nProduit : {product_doc}",
},
],
max_tokens=350,
)
return response.choices[0].message.content
def routine_with_llm(routine: dict, skin_type: str, acne: bool) -> str:
import os
from groq import Groq
client = Groq(api_key=os.environ["GROQ_API_KEY"])
# 1. FIX: On boucle sur la 'routine' envoyée, pas sur le dictionnaire brut ROUTINE_STEPS
routine_text = ""
for moment, steps in routine.items():
routine_text += f"\n{moment.upper()}:\n"
for step in steps:
if step["product_name"]:
routine_text += f"- {step['etape']}: {step['product_name']} ({step['brand']})\n"
else:
routine_text += f"- {step['etape']}: Aucun produit\n"
profile = f"peau {skin_type}"
if acne:
profile += ", tendance acnéique"
response = client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=[
{
"role": "system",
"content": (
"Tu es un influenceur beauté expert en skincare. "
"Propose ta routine skincare en suivant la routine skincare qui t'a été fournie. "
"1. Verifie que le produit recommandé est adapté et si c'est le cas propose le dans ta routine"
"2. Propose un produit adapte au resultat du type de peau. "
"3. Explique avec enthousiasme et brievement le rôle de chaque produit. "
"Réponds en français avec un ton chaleureux. "
"CRUCIAL : Tu dois ABSOLUMENT utiliser la syntaxe Markdown suivante pour structurer ta réponse.\n\n"
"Format OBLIGATOIRE:\n"
"Coucou ! Voici ta routine sur-mesure ✨ :\n\n"
"### 🌞 Matin\n"
"- **[product_type]** :[Nom du produit] [Ton explication...]\n\n"
"### 🌙 Soir\n"
"- **[product_type]** : [Nom du produit][Ton explication...]\n"
"J'espère que cette routine te conviendra 😎!"
),
},
{
"role": "user",
"content": f"Profil: {profile}\n\nRoutine exacte à présenter:\n{routine_text}",
},
],
max_tokens=550,
)
return response.choices[0].message.content
def is_compatible(meta: dict, skin_type: str) -> bool:
"""Vérifie que le produit ne contient pas d'ingrédients déconseillés."""
rules = SKIN_RULES.get(skin_type, {})
name = meta.get("name", "").lower()
return not any(word in name for word in rules.get("avoid", []))
# 2. FIX: Il FAUT garder cette fonction dé-commentée, c'est elle qui interroge ChromaDB !
def _build_routine_steps(
skin_type: str,
acne: bool,
main_product: dict,
preferences: dict
) -> dict:
routine = {}
for moment, steps in ROUTINE_STEPS.items():
routine[moment] = []
for step in steps:
# Produit principal = on prend direct
if main_product.get("product_type") == step:
routine[moment].append({
"etape": step,
"product_name": main_product["product_name"],
"brand": main_product["brand"],
"price_display": main_product["price_display"],
"is_vegan": main_product["is_vegan"],
"is_clean": main_product["is_clean"],
"is_main": True,
})
continue
# Sinon -> chercher un produit
query = f"{step} pour peau {skin_type}"
if acne and step in ("serum", "cleanser", "exfoliant"):
query += " acnéique"
if skin_type == "oily" and step == "serum":
query += " oil-free serum gel"
if skin_type == "oily" and step == "moisturizer":
query += " lightweight oil-free gel moisturizer"
if step == "toner":
query += " toner astringent niacinamide"
if step == "exfoliant":
if acne:
query += " salicylic acid bha exfoliant"
where = {"product_type": {"$eq": step}}
results = _query(query, where, n_results=5)
if results["ids"][0]:
candidates = results["metadatas"][0]
chosen = None
for m in candidates:
if is_compatible(m, skin_type):
chosen = m
break
if not chosen:
chosen = candidates[0]
routine[moment].append({
"etape": step,
"product_name": chosen["name"],
"brand": chosen["brand"],
"price_display": f"{chosen['price_eur']}€" if chosen["price_eur"] > 0 else "",
"is_vegan": chosen["is_vegan"],
"is_clean": chosen["is_clean"],
"is_main": False,
})
else:
routine[moment].append({
"etape": step,
"product_name": None,
"brand": None,
"is_main": False,
})
return routine
def recommend(skin_type: str, acne: bool, preferences: dict) -> dict:
query_text = _build_query(skin_type, acne, preferences)
where = _build_where(preferences)
results = _query(query_text, where, n_results=5)
if not results["ids"][0] and where:
results = _query(query_text, None, n_results=5)
if not results["ids"][0]:
results = _query(query_text, None, n_results=5)
if not results["ids"][0]:
return {"error": "Aucun produit trouvé"}
meta = results["metadatas"][0][0]
doc = results["documents"][0][0]
main_product_dict = {
"product_name": meta["name"],
"brand": meta["brand"],
"product_type": meta["product_type"],
"price_display": f"{meta['price_eur']}€" if meta["price_eur"] > 0 else "",
"is_vegan": meta["is_vegan"],
"is_clean": meta["is_clean"],
}
# 3. FIX: On dé-commente la création de la routine !
routine_complete = _build_routine_steps(skin_type, acne, main_product_dict, preferences)
# 4. FIX: On passe bien `routine_complete` à l'influenceur LLM
validated_routine = routine_with_llm(
routine=routine_complete,
skin_type=skin_type,
acne=acne
)
explanation = _generate_explanation(
doc,
skin_type,
acne,
preferences
)
return {
"product_name": meta["name"],
"brand": meta["brand"],
"product_type": meta["product_type"],
"price_display": f"{meta['price_eur']}€" if meta["price_eur"] > 0 else "",
"source": meta["source"],
"is_french": meta["is_french"],
"is_vegan": meta["is_vegan"],
"is_clean": meta["is_clean"],
"explanation": explanation,
"routine": routine_complete,
"routine_validated": validated_routine
}