mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
feat: implement AI Copilot for product analysis and recommendation, including a new analysis engine, OpenRouter client, and parser verification.
This commit is contained in:
1 parent
a008b40a59
commit
2edf6202ab
5 files changed
+134
-14
No files matched your search
@@ -31,10 +31,11 @@ IMPORTANT : La gamme C est RÉSERVÉE aux produits saisonniers et gérée MANUEL
|
||||
ET scoreCritique = true [score brut < 20]
|
||||
→ 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 la consigne À LA LETTRE.
|
||||
Si la règle ordonne explicitement une Gamme B, C ou D, TU DOIS SORTIR "B", "C" ou "D" dans la recommandation JSON. C'est la SEULE exception où tu es autorisé à utiliser B, C ou D. "rule_applies" doit être 'true'.
|
||||
|
||||
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. Par défaut, génère "A" ou "Z".
|
||||
|
||||
— Quadrant STAR ⭐ : Fort signal positif → A sauf inactivité ≥ 3 mois.
|
||||
— Quadrant TRAFIC 🚶 : Générateur de flux local.
|
||||
@@ -56,7 +57,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" | "Z",
|
||||
"recommendation": "A" | "B" | "C" | "D" | "Z",
|
||||
"justification": "2 phrases max. Cite poids, percentile, quadrant, score."
|
||||
}`;
|
||||
}
|
||||
@@ -187,15 +188,15 @@ Génère UNIQUEMENT le JSON :`;
|
||||
// Utilitaires de parsing (inchangés)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
static extractRecommendation(content: string): "A" | "Z" | null {
|
||||
const match = content.match(/\b([AZ])\b/i);
|
||||
if (match) return match[1].toUpperCase() as "A" | "Z";
|
||||
static extractRecommendation(content: string): "A" | "B" | "C" | "D" | "Z" | null {
|
||||
const match = content.match(/\b([ABCDZ])\b/i);
|
||||
if (match) return match[1].toUpperCase() as "A" | "B" | "C" | "D" | "Z";
|
||||
return null;
|
||||
}
|
||||
|
||||
static cleanInsight(content: string): string {
|
||||
let cleaned = content;
|
||||
cleaned = cleaned.replace(/^\[?[ACZ]\]?\s*[:\s-]+\s*/i, "");
|
||||
cleaned = cleaned.replace(/^\[?[ABCDZ]\]?\s*[:\s-]+\s*/i, "");
|
||||
cleaned = cleaned.replace(
|
||||
/^(justification|explication|pourquoi|justification courte|raison|avis)\s*[:\s-]+\s*/i,
|
||||
""
|
||||
|
||||
@@ -61,7 +61,7 @@ export class OpenRouterClient {
|
||||
const data = await response.json();
|
||||
const content = data.choices?.[0]?.message?.content ?? "";
|
||||
|
||||
let reco: "A" | "C" | "Z" | null = null;
|
||||
let reco: "A" | "B" | "C" | "D" | "Z" | null = null;
|
||||
let cleanInsight = "Erreur de génération.";
|
||||
|
||||
try {
|
||||
@@ -70,16 +70,21 @@ export class OpenRouterClient {
|
||||
const jsonText = jsonMatch ? jsonMatch[0] : content;
|
||||
const parsed = JSON.parse(jsonText);
|
||||
|
||||
reco = parsed.recommendation as "A" | "C" | "Z";
|
||||
reco = parsed.recommendation as "A" | "B" | "C" | "D" | "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";
|
||||
const ruleApplies = parsed.rule_applies === true;
|
||||
|
||||
// Garde-fou de sécurité : On n'autorise B, C ou D QUE si une règle manager s'applique.
|
||||
if (!ruleApplies && (reco === "B" || reco === "C" || reco === "D")) {
|
||||
reco = "A";
|
||||
}
|
||||
|
||||
// Override recommendation if it doesn't match extracted reco for safety
|
||||
if (!reco || !["A", "Z"].includes(reco)) {
|
||||
if (!reco || !["A", "B", "C", "D", "Z"].includes(reco)) {
|
||||
reco = AnalysisEngine.extractRecommendation(cleanInsight) || "A";
|
||||
// Ré-appliquer le garde-fou pour la reco extraite du texte.
|
||||
if (!ruleApplies && (reco === "B" || reco === "C" || reco === "D")) reco = "A";
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse AI JSON response:", content, e);
|
||||
|
||||
@@ -62,6 +62,6 @@ export interface ProductAnalysisInput {
|
||||
export interface AnalysisResult {
|
||||
insight: string;
|
||||
codein: string;
|
||||
recommandation: "A" | "C" | "Z" | null;
|
||||
recommandation: "A" | "B" | "C" | "D" | "Z" | null;
|
||||
scoring?: any; // On peut typer plus finement si nécessaire
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
const fs = require('fs');
|
||||
|
||||
function extractRecommendation(content) {
|
||||
const match = content.match(/\b([ABCDZ])\b/i);
|
||||
if (match) return match[1].toUpperCase();
|
||||
return null;
|
||||
}
|
||||
|
||||
function simulateParse(content) {
|
||||
let reco = null;
|
||||
let ruleApplies = false;
|
||||
|
||||
try {
|
||||
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
||||
const jsonText = jsonMatch ? jsonMatch[0] : content;
|
||||
const parsed = JSON.parse(jsonText);
|
||||
|
||||
reco = parsed.recommendation;
|
||||
ruleApplies = parsed.rule_applies === true;
|
||||
|
||||
if (!ruleApplies && (reco === "B" || reco === "C" || reco === "D")) reco = "A";
|
||||
|
||||
if (!reco || !["A", "B", "C", "D", "Z"].includes(reco)) {
|
||||
reco = extractRecommendation(parsed.justification || "") || "A";
|
||||
if (!ruleApplies && (reco === "B" || reco === "C" || reco === "D")) reco = "A";
|
||||
}
|
||||
} catch (e) {
|
||||
reco = extractRecommendation(content) || "A";
|
||||
if (!ruleApplies && (reco === "B" || reco === "C" || reco === "D")) reco = "A";
|
||||
}
|
||||
return reco;
|
||||
}
|
||||
|
||||
const tests = [
|
||||
{ name: "Valid Rule B", json: '{"rule_applies": true, "recommendation": "B", "justification": "blabla"}', expected: "B" },
|
||||
{ name: "Valid Rule C", json: '{"rule_applies": true, "recommendation": "C", "justification": "blabla"}', expected: "C" },
|
||||
{ name: "Valid Rule D", json: '{"rule_applies": true, "recommendation": "D", "justification": "blabla"}', expected: "D" },
|
||||
{ name: "Invalid Rule B (hallucinated)", json: '{"rule_applies": false, "recommendation": "B", "justification": "blabla"}', expected: "A" },
|
||||
{ name: "Invalid Rule D (hallucinated)", json: '{"rule_applies": false, "recommendation": "D", "justification": "blabla"}', expected: "A" },
|
||||
{ name: "Valid Rule A", json: '{"rule_applies": true, "recommendation": "A", "justification": "blabla"}', expected: "A" },
|
||||
{ name: "Valid Extraction D", json: '{"rule_applies": true, "recommendation": "INVALID", "justification": "Je choisis D."}', expected: "D" },
|
||||
{ name: "Invalid Extraction D", json: '{"rule_applies": false, "recommendation": "INVALID", "justification": "Je choisis D."}', expected: "A" }
|
||||
];
|
||||
|
||||
let allPassed = true;
|
||||
tests.forEach(t => {
|
||||
const res = simulateParse(t.json);
|
||||
const pass = res === t.expected;
|
||||
console.log(`${pass ? "✅" : "❌"} ${t.name}: expected ${t.expected}, got ${res}`);
|
||||
if (!pass) allPassed = false;
|
||||
});
|
||||
|
||||
if (allPassed) {
|
||||
console.log("All parsing tests passed.");
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log("Some tests failed.");
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
import { AnalysisEngine } from "./src/features/ai-copilot/business/analysis-engine";
|
||||
|
||||
console.log("--- Testing AI Parser Logic for B/C ---");
|
||||
|
||||
function simulateParse(content: string, simulateRuleResponse = false) {
|
||||
let reco: "A" | "B" | "C" | "Z" | null = null;
|
||||
let ruleApplies = simulateRuleResponse;
|
||||
|
||||
try {
|
||||
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
||||
const jsonText = jsonMatch ? jsonMatch[0] : content;
|
||||
const parsed = JSON.parse(jsonText);
|
||||
|
||||
reco = parsed.recommendation as "A" | "B" | "C" | "Z";
|
||||
ruleApplies = parsed.rule_applies === true;
|
||||
|
||||
if (!ruleApplies && (reco === "B" || reco === "C")) reco = "A";
|
||||
|
||||
if (!reco || !["A", "B", "C", "Z"].includes(reco)) {
|
||||
reco = AnalysisEngine.extractRecommendation(parsed.justification || "") || "A";
|
||||
if (!ruleApplies && (reco === "B" || reco === "C")) reco = "A";
|
||||
}
|
||||
} catch (e) {
|
||||
reco = AnalysisEngine.extractRecommendation(content);
|
||||
if (!ruleApplies && (reco === "B" || reco === "C")) reco = "A";
|
||||
}
|
||||
return reco;
|
||||
}
|
||||
|
||||
const tests = [
|
||||
{ name: "Valid Rule B", json: '{"rule_applies": true, "recommendation": "B", "justification": "blabla"}', expected: "B" },
|
||||
{ name: "Valid Rule C", json: '{"rule_applies": true, "recommendation": "C", "justification": "blabla"}', expected: "C" },
|
||||
{ name: "Invalid Rule B (hallucinated)", json: '{"rule_applies": false, "recommendation": "B", "justification": "blabla"}', expected: "A" },
|
||||
{ name: "Invalid Rule C (hallucinated)", json: '{"rule_applies": false, "recommendation": "C", "justification": "blabla"}', expected: "A" },
|
||||
{ name: "Valid Rule A", json: '{"rule_applies": true, "recommendation": "A", "justification": "blabla"}', expected: "A" },
|
||||
{ name: "Valid Extraction B", json: '{"rule_applies": true, "recommendation": "INVALID", "justification": "Je choisis B."}', expected: "B" },
|
||||
{ name: "Invalid Extraction B", json: '{"rule_applies": false, "recommendation": "INVALID", "justification": "Je choisis B."}', expected: "A" }
|
||||
];
|
||||
|
||||
let allPassed = true;
|
||||
tests.forEach(t => {
|
||||
const res = simulateParse(t.json);
|
||||
const pass = res === t.expected;
|
||||
console.log(`${pass ? "✅" : "❌"} ${t.name}: expected ${t.expected}, got ${res}`);
|
||||
if (!pass) allPassed = false;
|
||||
});
|
||||
|
||||
if (allPassed) {
|
||||
console.log("All parsing tests passed.");
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log("Some tests failed.");
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in new issue
Block a user