mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-14 10:48:09 +02:00
feat: Introduce bulk AI product analysis, integrating with OpenRouter via a new API and UI.
This commit is contained in:
1 parent
04e6ed70f6
commit
3de16d15d7
5 files changed
+194
-106
No files matched your search
@@ -0,0 +1,51 @@
|
||||
import { ProductAnalysisInput } from "../models/ai-analysis.types";
|
||||
|
||||
export class AnalysisEngine {
|
||||
static generateSystemPrompt(): string {
|
||||
return `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) : Produit avec rotation régulière, CA stable ou en croissance.
|
||||
- C (Saisonnier) : Produit avec pics de ventes spécifiques (saisonnalité marquée).
|
||||
- Z (Sortie) : Produit en fin de vie, rotation insignifiante ou CA en chute libre.
|
||||
|
||||
Critères de pondération :
|
||||
1. Volume et CA : Priorité aux produits générant du flux.
|
||||
2. Volume Relatif : Compare le volume du produit à la moyenne du fournisseur (fournie). Un produit très au-dessus de la moyenne est un pilier (A).
|
||||
3. Marge : Un produit à forte marge mais faible rotation peut être maintenu en C.
|
||||
4. Régularité : La stabilité des ventes sur 12 mois favorise le score A.
|
||||
|
||||
Ta réponse doit être en 1-2 phrases maximum, en français, directe et actionnable.
|
||||
Format impératif : "[Recommandation]: [Justification courte]"`;
|
||||
}
|
||||
|
||||
static generateUserMessage(p: ProductAnalysisInput): string {
|
||||
const monthlySummary = Object.entries(p.sales12m)
|
||||
.map(([k, v]) => `${k}: ${Math.round(v)}u`)
|
||||
.join(", ");
|
||||
|
||||
const volumeContext = p.avgTotalQuantite
|
||||
? `- Volume vs Moyenne Fournisseur : ${Math.round(p.totalQuantite)}u (Moyenne: ${Math.round(p.avgTotalQuantite)}u)`
|
||||
: `- Volume total : ${Math.round(p.totalQuantite)} unités`;
|
||||
|
||||
return `Produit : "${p.libelle1}" (Ref: ${p.codein})
|
||||
Gamme actuelle : ${p.codeGamme ?? "Non définie"}
|
||||
Indicateurs 12m :
|
||||
- CA : ${p.totalCa.toFixed(2)}€
|
||||
- Marge : ${p.tauxMarge.toFixed(1)}%
|
||||
${volumeContext}
|
||||
- Historique mensuel : ${monthlySummary}
|
||||
|
||||
Quelle est ta recommandation (A, C ou Z) et pourquoi ?`;
|
||||
}
|
||||
|
||||
static extractRecommendation(content: string): "A" | "C" | "Z" | null {
|
||||
// Recherche une lettre A, C ou Z isolée (entourée de non-lettres ou début/fin)
|
||||
// On favorise le format [A] ou "A:" mais on accepte A seul.
|
||||
const match = content.match(/\b([ACZ])\b/i);
|
||||
if (match) {
|
||||
return match[1].toUpperCase() as "A" | "C" | "Z";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { AnalysisEngine } from "../business/analysis-engine";
|
||||
import { ProductAnalysisInput, AnalysisResult } from "../models/ai-analysis.types";
|
||||
|
||||
const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
|
||||
|
||||
export interface OpenRouterConfig {
|
||||
apiKey: string;
|
||||
model: string;
|
||||
}
|
||||
|
||||
export class OpenRouterClient {
|
||||
constructor(private config: OpenRouterConfig) { }
|
||||
|
||||
async analyzeProduct(p: ProductAnalysisInput): Promise<AnalysisResult> {
|
||||
const response = await fetch(OPENROUTER_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.config.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://collectflow.app",
|
||||
"X-Title": "CollectFlow AI Copilot",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: this.config.model,
|
||||
messages: [
|
||||
{ role: "system", content: AnalysisEngine.generateSystemPrompt() },
|
||||
{ role: "user", content: AnalysisEngine.generateUserMessage(p) },
|
||||
],
|
||||
max_tokens: 150,
|
||||
temperature: 0.3,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status === 429) {
|
||||
throw new Error("rate_limited");
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.text();
|
||||
throw new Error(`OpenRouter error: ${err}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const content = data.choices?.[0]?.message?.content ?? "";
|
||||
|
||||
return {
|
||||
insight: content,
|
||||
codein: p.codein,
|
||||
recommandation: AnalysisEngine.extractRecommendation(content)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export interface ProductAnalysisInput {
|
||||
codein: string;
|
||||
libelle1: string;
|
||||
totalCa: number;
|
||||
tauxMarge: number;
|
||||
totalQuantite: number;
|
||||
avgTotalQuantite?: number;
|
||||
sales12m: Record<string, number>;
|
||||
codeGamme: string | null;
|
||||
}
|
||||
|
||||
export interface AnalysisResult {
|
||||
insight: string;
|
||||
codein: string;
|
||||
recommandation: "A" | "C" | "Z" | null;
|
||||
}
|
||||
Reference in new issue
Block a user