feat: Implement bulk AI product analysis, including algorithmic scoring and context profiling, with a new OpenRouter client.

This commit is contained in:
Michael committed 2026-02-27 10:44:02 +01:00
1 parent 02fdd18a52
commit b0bb8cc09a
4 files changed
+24 -19

No files matched your search

@@ -17,7 +17,8 @@ export class AnalysisEngine {
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
static generateSystemPrompt(): string { static generateSystemPrompt(): string {
return `Tu es Mary, Senior Retail Strategist. Tu analyses des produits pour recommander A (garder), C (saisonnier), ou Z (sortir). return `Tu es Mary, Senior Retail Strategist. Tu analyses des produits pour recommander A (garder) ou Z (sortir).
IMPORTANT : La gamme C est RÉSERVÉE aux produits saisonniers et gérée MANUELLEMENT par l'acheteur. Tu ne dois JAMAIS recommander C.
--- ORDRE DE PRIORITÉ (STRICT) --- --- ORDRE DE PRIORITÉ (STRICT) ---
@@ -27,21 +28,21 @@ export class AnalysisEngine {
Si isLowContribution = true [poids CA < 0.5% ET poids QTÉ < 0.5% du fournisseur] Si isLowContribution = true [poids CA < 0.5% ET poids QTÉ < 0.5% du fournisseur]
ET score composite < 35 ET score composite < 35
ET scoreCritique = true [score brut < 20] ET scoreCritique = true [score brut < 20]
→ Z DIRECT. Ce produit est marginal et sous-performant. Aucun signal ne peut l'annuler. → Z DIRECT. Ce produit est marginal et sous-performant.
3. RÈGLE MANAGER : Si le manager a défini une consigne ET que le produit est concerné → Appliquer. 3. RÈGLE MANAGER : Si le manager a défini une consigne ET que le produit est concerné → Appliquer.
4. ANALYSE CONTEXTUELLE (si aucune règle ci-dessus ne s'applique) : 4. ANALYSE CONTEXTUELLE (si aucune règle ci-dessus ne s'applique) :
Utilise les données de la fiche pour raisonner. Les quadrants sont des INDICES, pas des verdicts. Utilise les données de la fiche pour raisonner. Les quadrants sont des INDICES, pas des verdicts.
— Quadrant STAR ⭐ : Fort signal positif. A sauf inactivité ≥ 3 mois. — Quadrant STAR ⭐ : Fort signal positif → A sauf inactivité ≥ 3 mois.
— Quadrant TRAFIC 🚶 : Signal positif SEULEMENT si poids QTÉ fournisseur > 1%. — Quadrant TRAFIC 🚶 : Signal positif SEULEMENT si poids QTÉ fournisseur > 1%.
Si poids < 1% et score < 40 → tendance Z ou C. Si poids < 1% et score < 40 → Z.
— Quadrant MARGE 💎 : Signal positif SEULEMENT si poids CA fournisseur > 0.5%. — Quadrant MARGE 💎 : Signal positif SEULEMENT si poids CA fournisseur > 0.5%.
Si poids < 0.5% et score < 35 → tendance Z. Si poids < 0.5% et score < 35 → Z.
— Quadrant WATCH ⚠️ : Signal négatif par défaut. — Quadrant WATCH ⚠️ : Signal négatif.
Si poids CA rayon > 5% ou poids QTÉ rayon > 5% → A ou C selon inactivité. Si poids CA rayon > 5% ou poids QTÉ rayon > 5% → A (surveiller).
Sinon → Z si inactivité ≥ 2 mois, C si 1 mois, A si actif mais surveiller. Sinon → Z si inactivité ≥ 2 mois, A si actif.
--- COHÉRENCE INTER-PRODUITS --- --- COHÉRENCE INTER-PRODUITS ---
Ne mets jamais Z un produit si son percentile CA ET son percentile QTÉ sont tous les deux supérieurs à un autre produit déjà classé A dans ce lot. Ne mets jamais Z un produit si son percentile CA ET son percentile QTÉ sont tous les deux supérieurs à un autre produit déjà classé A dans ce lot.
@@ -50,7 +51,7 @@ Ne mets jamais Z un produit si son percentile CA ET son percentile QTÉ sont tou
JSON uniquement, sans markdown. JSON uniquement, sans markdown.
{ {
"rule_applies": boolean, "rule_applies": boolean,
"recommendation": "A" | "C" | "Z", "recommendation": "A" | "Z",
"justification": "2 phrases max. Cite poids, percentile, quadrant, score." "justification": "2 phrases max. Cite poids, percentile, quadrant, score."
}`; }`;
} }
@@ -181,9 +182,9 @@ Génère UNIQUEMENT le JSON :`;
// Utilitaires de parsing (inchangés) // Utilitaires de parsing (inchangés)
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
static extractRecommendation(content: string): "A" | "C" | "Z" | null { static extractRecommendation(content: string): "A" | "Z" | null {
const match = content.match(/\b([ACZ])\b/i); const match = content.match(/\b([AZ])\b/i);
if (match) return match[1].toUpperCase() as "A" | "C" | "Z"; if (match) return match[1].toUpperCase() as "A" | "Z";
return null; return null;
} }
@@ -35,7 +35,7 @@ export class OpenRouterClient {
{ role: "user", content: AnalysisEngine.generateUserMessage(p) }, { role: "user", content: AnalysisEngine.generateUserMessage(p) },
], ],
response_format: { type: "json_object" }, response_format: { type: "json_object" },
max_tokens: 200, max_tokens: 150,
temperature: 0.1, temperature: 0.1,
}), }),
}); });
@@ -73,8 +73,12 @@ export class OpenRouterClient {
reco = parsed.recommendation as "A" | "C" | "Z"; reco = parsed.recommendation as "A" | "C" | "Z";
cleanInsight = parsed.justification || ""; cleanInsight = parsed.justification || "";
// Garde-fou : C est réservé aux saisonniers (gestion manuelle).
// Si l'IA recommande C malgré l'interdiction dans le prompt → forcer A.
if (reco === "C") reco = "A";
// Override recommendation if it doesn't match extracted reco for safety // Override recommendation if it doesn't match extracted reco for safety
if (!reco || !["A", "C", "Z"].includes(reco)) { if (!reco || !["A", "Z"].includes(reco)) {
reco = AnalysisEngine.extractRecommendation(cleanInsight) || "A"; reco = AnalysisEngine.extractRecommendation(cleanInsight) || "A";
} }
} catch (e) { } catch (e) {
@@ -181,7 +181,7 @@ export function BulkAiAnalyzer() {
codeins.forEach(c => gammeChanges[c] = "Aucune"); codeins.forEach(c => gammeChanges[c] = "Aucune");
batchSetDraftGamme(gammeChanges); batchSetDraftGamme(gammeChanges);
const CONCURRENCY = 3; const CONCURRENCY = 2;
const remaining = [...productPayloads]; const remaining = [...productPayloads];
const processBatch = async () => { const processBatch = async () => {
@@ -298,9 +298,9 @@ export function BulkAiAnalyzer() {
) : ( ) : (
<button <button
onClick={handleAnalyze} onClick={handleAnalyze}
className="apple-btn-ai" className="apple-btn-secondary"
> >
<Sparkles className="w-4 h-4" /> <Sparkles className="w-4 h-4" style={{ color: "var(--accent)" }} />
Analyse IA Analyse IA
</button> </button>
)} )}
+2 -2
View File
@@ -23,9 +23,9 @@ Mary (IA) donnait des recommandations basées sur le score brut sans analyse con
## Progress Log ## Progress Log
- **Correction IA** : Algorithme Mary v4 implémenté avec profilage contextuel et groupement N2. Le bug de classification 'A' systématique est résolu.
- **Refonte Esthétique** : Bouton de sauvegarde et design system harmonisés en style Apple Glossy/Premium avec micro-animations et feedback visuel.
- **Agent Architecte (Timeout)** : Intervention structurelle sur l'entonnoir d'analyse : - **Agent Architecte (Timeout)** : Intervention structurelle sur l'entonnoir d'analyse :
- **Client-Side (AbortController)** : Timeout poussé de 25s à 50s pour supporter les requêtes des LLM denses (ex: M37). - **Client-Side (AbortController)** : Timeout poussé de 25s à 50s pour supporter les requêtes des LLM denses (ex: M37).
- **Server-Side (N+1 Query)** : Suppression d'une requête BDD redondante invoquée par chaque produit, éliminant ~100 appels BDD par lot. - **Server-Side (N+1 Query)** : Suppression d'une requête BDD redondante invoquée par chaque produit, éliminant ~100 appels BDD par lot.
- **Agent Architecte (Gamme C)** : Prompt v5 → Mary ne recommande plus que A ou Z. Gamme C interdite (réservée saisonniers). Garde-fou C→A côté client. Concurrence réduite à 2 workers, max_tokens réduit à 150.
- **Bouton Analyse IA** : Refactorisé en `apple-btn-secondary` (sobre, conforme PRD Apple-style) avec icône Sparkles colorée en accent.
- **Vérification** : Walkthrough documenté et système fluidifié. - **Vérification** : Walkthrough documenté et système fluidifié.