mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
feat(ai): integrate performance score for smarter analysis and parallelize batch processing
This commit is contained in:
1 parent
4dac21dfac
commit
5c8910825b
3 files changed
+37
-31
No files matched your search
@@ -10,18 +10,21 @@ const AnalyzeSchema = z.object({
|
|||||||
totalQuantite: z.number(),
|
totalQuantite: z.number(),
|
||||||
sales12m: z.record(z.string(), z.number()),
|
sales12m: z.record(z.string(), z.number()),
|
||||||
codeGamme: z.string().nullable(),
|
codeGamme: z.string().nullable(),
|
||||||
|
score: z.number().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
|
const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
|
||||||
const SYSTEM_PROMPT = `Tu es un expert en analyse de gammes de produits B2B pour un acheteur retail professionnel.
|
const SYSTEM_PROMPT = `Tu es un expert en analyse de gammes de produits B2B pour un acheteur retail professionnel.
|
||||||
|
|
||||||
En analysant les données de ventes fournies, génère une recommandation de gamme (A=Permanent, C=Saisonnier, Z=Sortie).
|
En analysant les données de ventes et le SCORE de performance relative (0-100), génère une recommandation de gamme (A=Permanent, C=Saisonnier, Z=Sortie).
|
||||||
- A : Produit permanent avec rotation régulière.
|
|
||||||
- C : Produit saisonnier (pics de ventes spécifiques).
|
RÈGLES CRITIQUES :
|
||||||
- Z : Produit en sortie (aucune vente ou rotation insignifiante).
|
- SCORE < 20 : Signal fort de "Sortie" (Z). Même si le volume semble correct dans l'absolu, un score faible signifie que le produit sous-performe par rapport à ses pairs.
|
||||||
|
- A (Permanent) : Uniquement si la rotation est régulière ET le score est satisfaisant.
|
||||||
|
- C (Saisonnier) : Uniquement si des pics clairs apparaissent dans l'historique.
|
||||||
|
|
||||||
Ta réponse doit être en 1-2 phrases maximum, en français, directe et actionnable.
|
Ta réponse doit être en 1-2 phrases maximum, en français, directe et actionnable.
|
||||||
Format: "[Recommandation]: [Justification courte basée sur les données]"`;
|
Format: "[Recommandation]: [Justification courte basée sur les données et le score]"`;
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
console.log("[AI] Analysis request received.");
|
console.log("[AI] Analysis request received.");
|
||||||
@@ -42,7 +45,7 @@ export async function POST(req: NextRequest) {
|
|||||||
return NextResponse.json({ error: "Invalid payload", details: parsed.error.flatten() }, { status: 400 });
|
return NextResponse.json({ error: "Invalid payload", details: parsed.error.flatten() }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { codein, libelle1, totalCa, tauxMarge, totalQuantite, sales12m, codeGamme } = parsed.data;
|
const { codein, libelle1, totalCa, tauxMarge, totalQuantite, sales12m, codeGamme, score } = parsed.data;
|
||||||
|
|
||||||
// Format monthly sales for the prompt
|
// Format monthly sales for the prompt
|
||||||
const monthlySummary = Object.entries(sales12m)
|
const monthlySummary = Object.entries(sales12m)
|
||||||
@@ -51,6 +54,7 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
const userMessage = `Produit: "${libelle1}" (Code: ${codein})
|
const userMessage = `Produit: "${libelle1}" (Code: ${codein})
|
||||||
Gamme actuelle: ${codeGamme ?? "Non définie"}
|
Gamme actuelle: ${codeGamme ?? "Non définie"}
|
||||||
|
Score de performance: ${score ?? "N/A"}/100
|
||||||
CA total 12m: ${totalCa.toFixed(0)}€ | Taux de marge: ${tauxMarge.toFixed(1)}% | Volume: ${Math.round(totalQuantite)} unités
|
CA total 12m: ${totalCa.toFixed(0)}€ | Taux de marge: ${tauxMarge.toFixed(1)}% | Volume: ${Math.round(totalQuantite)} unités
|
||||||
Historique mensuel: ${monthlySummary}
|
Historique mensuel: ${monthlySummary}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const BatchAnalyzeSchema = z.object({
|
|||||||
ca: z.number().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(),
|
||||||
|
score: z.number().nullable().optional(),
|
||||||
gammeInit: z.string().nullable().optional(),
|
gammeInit: z.string().nullable().optional(),
|
||||||
historique: z.string().nullable().optional(),
|
historique: z.string().nullable().optional(),
|
||||||
nomenclature: z.string().nullable().optional(),
|
nomenclature: z.string().nullable().optional(),
|
||||||
@@ -41,39 +42,40 @@ export async function POST(req: NextRequest) {
|
|||||||
// Optimized System Prompt: Merged from analyze/route.ts as requested by user
|
// Optimized System Prompt: Merged from analyze/route.ts as requested by user
|
||||||
const systemPrompt = `Tu es un expert en analyse de gammes de produits B2B pour un acheteur retail professionnel.
|
const systemPrompt = `Tu es un expert en analyse de gammes de produits B2B pour un acheteur retail professionnel.
|
||||||
|
|
||||||
En analysant les données de ventes fournies (CA, Marge, Volume, Historique), génère pour chaque produit une recommandation de gamme (A=Permanent, C=Saisonnier, Z=Sortie).
|
En analysant les données de ventes fournies (CA, Marge, Volume, Historique) et surtout le SCORE de performance relative (0-100), génère pour chaque produit une recommandation de gamme (A=Permanent, C=Saisonnier, Z=Sortie).
|
||||||
|
|
||||||
CRITÈRES :
|
CRITÈRES PRIORITAIRES :
|
||||||
- A (Permanent) : Produit permanent avec une rotation régulière et satisfaisante sur toute l'année.
|
- SCORE < 20 : Recommandation "Z" (Sortie) quasi-obligatoire. Même si le volume semble correct, le produit est un boulet par rapport au reste du fournisseur.
|
||||||
- C (Saisonnier) : Produit dont les ventes sont concentrées sur des périodes spécifiques. Détecte les pics dans l' 'historique' (ex: ventes en 202412 mais rien ailleurs). Aide-toi de la 'nomenclature' et du 'nom' pour confirmer l'usage saisonnier.
|
- A (Permanent) : Produit avec une rotation régulière ET un score satisfaisant (> 30-40).
|
||||||
- Z (Sortie) : Produit à arrêter car les ventes sont quasi nulles ou la rotation est insuffisante.
|
- C (Saisonnier) : Pics de ventes concentrés sur l'historique. Aide-toi de la nomenclature.
|
||||||
|
- Z (Sortie) : Ventes nulles, rotation insuffisante ou score médiocre.`;
|
||||||
|
|
||||||
DÉTECTION :
|
DÉTECTION:
|
||||||
Marque 'isDuplicate: true' si le produit semble être un doublon dans le lot.
|
Marque 'isDuplicate: true' si le produit semble être un doublon dans le lot.
|
||||||
|
|
||||||
IMPORTANT : RÉPONDS UNIQUEMENT EN JSON VALIDE.
|
IMPORTANT : RÉPONDS UNIQUEMENT EN JSON VALIDE.
|
||||||
Format:
|
Format:
|
||||||
{
|
{
|
||||||
"results": [
|
"results": [
|
||||||
{
|
{
|
||||||
"codein": "ID",
|
"codein": "ID",
|
||||||
"recommandationGamme": "A|C|Z",
|
"recommandationGamme": "A|C|Z",
|
||||||
"isDuplicate": boolean,
|
"isDuplicate": boolean,
|
||||||
"justificationCourte": "..."
|
"justificationCourte": "..."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
INTERDICTION FORMELLE : N'utilise jamais la phrase "Justification courte basée sur les données" comme réponse. Tu dois rédiger une analyse réelle.
|
INTERDICTION FORMELLE: N'utilise jamais la phrase "Justification courte basée sur les données" comme réponse. Tu dois rédiger une analyse réelle.
|
||||||
|
|
||||||
Données : codein, nom, ca (€), ventes (unités), marge (%), gammeInit, historique, nomenclature.`;
|
Données: codein, nom, ca(€), ventes(unités), marge(%), gammeInit, historique, nomenclature.`;
|
||||||
|
|
||||||
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) } `;
|
||||||
|
|
||||||
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": `Bearer ${apiKey}`,
|
"Authorization": `Bearer ${ apiKey } `,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -91,7 +93,7 @@ Données : codein, nom, ca (€), ventes (unités), marge (%), gammeInit, histor
|
|||||||
// Read the retry-after header and pass it back to the client
|
// Read the retry-after header and pass it back to the client
|
||||||
const retryAfter = response.headers.get("Retry-After") || response.headers.get("x-ratelimit-reset-requests");
|
const retryAfter = response.headers.get("Retry-After") || response.headers.get("x-ratelimit-reset-requests");
|
||||||
const waitSeconds = retryAfter ? parseInt(retryAfter, 10) : 60;
|
const waitSeconds = retryAfter ? parseInt(retryAfter, 10) : 60;
|
||||||
console.warn(`[batch-analyze] Rate limited. Retry after ${waitSeconds}s.`);
|
console.warn(`[batch - analyze] Rate limited.Retry after ${ waitSeconds } s.`);
|
||||||
return NextResponse.json({ error: "rate_limited", retryAfter: waitSeconds }, { status: 429 });
|
return NextResponse.json({ error: "rate_limited", retryAfter: waitSeconds }, { status: 429 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +114,7 @@ Données : codein, nom, ca (€), ventes (unités), marge (%), gammeInit, histor
|
|||||||
|
|
||||||
// Try to extract JSON block from the response (handles markdown code blocks and raw JSON)
|
// Try to extract JSON block from the response (handles markdown code blocks and raw JSON)
|
||||||
let jsonStr = content;
|
let jsonStr = content;
|
||||||
const jsonBlock = content.match(/```json\s*([\s\S]*?)\s*```/);
|
const jsonBlock = content.match(/```json\s * ([\s\S] *?) \s * ```/);
|
||||||
if (jsonBlock) {
|
if (jsonBlock) {
|
||||||
jsonStr = jsonBlock[1];
|
jsonStr = jsonBlock[1];
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const useAiCopilotStore = create<AiCopilotState>((set) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
analyzeProduct: async (payload) => {
|
analyzeProduct: async (payload) => {
|
||||||
const { codein } = payload;
|
const { codein, score } = payload;
|
||||||
|
|
||||||
// Mark as loading
|
// Mark as loading
|
||||||
set((s) => ({
|
set((s) => ({
|
||||||
|
|||||||
Reference in new issue
Block a user