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 {
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) ---
@@ -27,21 +28,21 @@ export class AnalysisEngine {
Si isLowContribution = true [poids CA < 0.5% ET poids QTÉ < 0.5% du fournisseur]
ET score composite < 35
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.
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.
— 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%.
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%.
Si poids < 0.5% et score < 35 → tendance Z.
— Quadrant WATCH ⚠️ : Signal négatif par défaut.
Si poids CA rayon > 5% ou poids QTÉ rayon > 5% → A ou C selon inactivité.
Sinon → Z si inactivité ≥ 2 mois, C si 1 mois, A si actif mais surveiller.
Si poids < 0.5% et score < 35 → Z.
— Quadrant WATCH ⚠️ : Signal négatif.
Si poids CA rayon > 5% ou poids QTÉ rayon > 5% → A (surveiller).
Sinon → Z si inactivité ≥ 2 mois, A si actif.
--- 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.
@@ -50,7 +51,7 @@ Ne mets jamais Z un produit si son percentile CA ET son percentile QTÉ sont tou
JSON uniquement, sans markdown.
{
"rule_applies": boolean,
"recommendation": "A" | "C" | "Z",
"recommendation": "A" | "Z",
"justification": "2 phrases max. Cite poids, percentile, quadrant, score."
}`;
}
@@ -181,9 +182,9 @@ Génère UNIQUEMENT le JSON :`;
// Utilitaires de parsing (inchangés)
// -----------------------------------------------------------------------
static extractRecommendation(content: string): "A" | "C" | "Z" | null {
const match = content.match(/\b([ACZ])\b/i);
if (match) return match[1].toUpperCase() as "A" | "C" | "Z";
static extractRecommendation(content: string): "A" | "Z" | null {
const match = content.match(/\b([AZ])\b/i);
if (match) return match[1].toUpperCase() as "A" | "Z";
return null;
}
@@ -35,7 +35,7 @@ export class OpenRouterClient {
{ role: "user", content: AnalysisEngine.generateUserMessage(p) },
],
response_format: { type: "json_object" },
max_tokens: 200,
max_tokens: 150,
temperature: 0.1,
}),
});
@@ -73,8 +73,12 @@ export class OpenRouterClient {
reco = parsed.recommendation as "A" | "C" | "Z";
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
if (!reco || !["A", "C", "Z"].includes(reco)) {
if (!reco || !["A", "Z"].includes(reco)) {
reco = AnalysisEngine.extractRecommendation(cleanInsight) || "A";
}
} catch (e) {
@@ -181,7 +181,7 @@ export function BulkAiAnalyzer() {
codeins.forEach(c => gammeChanges[c] = "Aucune");
batchSetDraftGamme(gammeChanges);
const CONCURRENCY = 3;
const CONCURRENCY = 2;
const remaining = [...productPayloads];
const processBatch = async () => {
@@ -298,9 +298,9 @@ export function BulkAiAnalyzer() {
) : (
<button
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
</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
- **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 :
- **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.
- **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é.