feat(ai): smarter batch analysis with revenue, history, nomenclature and strict retail rules

This commit is contained in:
Michael committed 2026-02-21 22:09:28 +01:00
1 parent 8ad2894301
commit 816ec6e483
2 files changed
+32 -22

No files matched your search

+24 -19
View File
@@ -7,10 +7,13 @@ const BatchAnalyzeSchema = z.object({
rayon: z.string(), rayon: z.string(),
products: z.array(z.object({ products: z.array(z.object({
codein: z.string(), codein: z.string(),
gtin: z.string().nullable().optional(),
nom: z.string().nullable().optional(), nom: z.string().nullable().optional(),
ca: z.number().nullable().optional(),
ventes: z.number().nullable().optional(), ventes: z.number().nullable().optional(),
marge: z.number().nullable().optional(), marge: z.number().nullable().optional(),
gammeInit: z.string().nullable().optional(),
historique: z.string().nullable().optional(),
nomenclature: z.string().nullable().optional(),
})), })),
}); });
@@ -22,7 +25,7 @@ export async function POST(req: NextRequest) {
if (!apiKey) { if (!apiKey) {
console.error("[batch-analyze] API key manquante."); console.error("[batch-analyze] API key manquante.");
return NextResponse.json({ error: "Clé API OpenRouter manquante. Configurez-la dans les Paramètres." }, { status: 500 }); return NextResponse.json({ error: "Clé API OpenRouter manquante. Configurez-la dans les Paramètres." }, { status: 503 });
} }
try { try {
@@ -35,30 +38,32 @@ export async function POST(req: NextRequest) {
const { rayon, products } = parsed.data; const { rayon, products } = parsed.data;
// Prompt definition // Optimized System Prompt for professional retail analysis
const systemPrompt = `Tu es un expert en Retail et en gestion d'assortiment. const systemPrompt = `Tu es un expert en analyse de gammes B2B (Retail) pour un acheteur professionnel.
Ta mission est d'analyser un lot de produits appartenant au rayon "${rayon}". Ta mission est d'analyser un lot de produits du rayon "${rayon}".
Pour chaque produit, tu dois recommander une Gamme (A, B, C ou Z) basée sur ses performances de ventes (volume) et sa marge (%).
Tu dois aussi détecter les doublons évidents (même produit, GTIN similaire, ventes réparties) en mettant isDuplicate: true le cas échéant.
Règles de Gamme : CRITÈRES DE DÉCISION STRICTS :
- A : Produit phare, rotation forte, excellente marge. - A (Cœur) : Rotation forte (>50 unités/an) ET marge solide. C'est le top 20% des ventes.
- B : Produit cœur de gamme, rotation moyenne. - B (Complémentaire) : Rotation correcte ou nouveau produit prometteur.
- C : Dépannage ou niche, faible rotation mais potentiellement bonne marge. - C (Saisonnier/Niche) : Faible rotation mais marge élevée (>45%) ou historique saisonnier marqué.
- Z : Produit à déréférencer ou mort (ventes très faibles, marge mauvaise). - Z (Sortie - CRITIQUE) : AUCUNE VENTE sur les 12 derniers mois (ou < 3 unités) ET CA quasi nul. Si 'ventes' est proche de 0, il DOIT être en Z sauf si c'est une nouveauté flagrante.
TU DOIS REPONDRE UNIQUEMENT EN FORMAT JSON VALIDE. AUCUN TEXTE AVANT OU APRES. DÉTECTION DE DOUBLONS :
Format attendu: Si plusieurs produits ont des noms quasi identiques et des ventes faibles, marque 'isDuplicate: true' sur le moins performant.
FORMAT DE RÉPONSE (JSON STRICT) :
{ {
"results": [ "results": [
{ {
"codein": "123", "codein": "ID",
"recommandationGamme": "A", "recommandationGamme": "A|B|C|Z",
"isDuplicate": false, "isDuplicate": boolean,
"justificationCourte": "Forte rotation et excellente marge." "justificationCourte": "Directe, basée sur CA/Marge/Ventes (max 15 mots)"
} }
] ]
}`; }
Données fournies : codein, nom, ca (revenue €), ventes (unités), marge (%), gammeInit (actuelle), historique (12 mois), nomenclature (contexte).`;
const userPrompt = `Analyse cette liste de produits :\n${JSON.stringify(products, null, 2)}`; const userPrompt = `Analyse cette liste de produits :\n${JSON.stringify(products, null, 2)}`;
@@ -47,13 +47,18 @@ export function BulkAiAnalyzer() {
for (const chunk of chunks) { for (const chunk of chunks) {
setProgress(prev => ({ ...prev, current: completed, total: chunks.length, message: `Analyse du rayon: ${chunk.rayon}` })); setProgress(prev => ({ ...prev, current: completed, total: chunks.length, message: `Analyse du rayon: ${chunk.rayon}` }));
// Simplify payload to save tokens // Enrich payload with more context to improve AI decision quality
const payloadProducts = chunk.items.map(r => ({ const payloadProducts = chunk.items.map(r => ({
codein: r.codein, codein: r.codein,
gtin: r.gtin,
nom: r.libelle1, nom: r.libelle1,
ventes: r.totalQuantite, ca: r.totalCa || 0,
ventes: r.totalQuantite || 0,
marge: r.totalMarge ? parseFloat(((r.totalMarge / (r.totalCa || 1)) * 100).toFixed(1)) : 0, marge: r.totalMarge ? parseFloat(((r.totalMarge / (r.totalCa || 1)) * 100).toFixed(1)) : 0,
gammeInit: r.codeGammeInit || "N/A",
// Simplified history: last 12 values joined (e.g., "5,0,10...")
historique: Object.values(r.sales12m || {}).join(","),
// Contextual nomenclature
nomenclature: `${r.libelleNiveau1 || ""} > ${r.libelleNiveau2 || ""} > ${r.libelle3 || ""}`
})); }));
try { try {