[ { "id": "tab_classifier", "type": "tab", "label": "veille-ia : classifier", "disabled": false, "info": "PRD §5.3 — Classifieur LLM : prompt système figé (PRD §6, embarqué tel quel) → Ollama /api/chat (format json) → POST /api/alerts de veille-ia → si urgence haute, notification.\n\nVariables d'environnement :\n- OLLAMA_URL (défaut http://127.0.0.1:11434)\n- CLASSIFIER_MODEL (défaut qwen3:32b)\n- VEILLE_IA_URL (défaut http://127.0.0.1:3000 — avec sous-chemin si applicable, ex. https://domaine.tld/veille)\n- ALERTS_TOKEN (obligatoire — visible dans /var/www/veille-ia/app/.env)\n- NOTIFY_URL (défaut http://127.0.0.1:1880/veille-ia/notify)" }, { "id": "cl_http_in", "type": "http in", "z": "tab_classifier", "name": "POST classifier", "url": "/veille-ia/classifier", "method": "post", "upload": false, "swaggerDoc": "", "x": 140, "y": 120, "wires": [["cl_requete_ollama"]] }, { "id": "cl_requete_ollama", "type": "function", "z": "tab_classifier", "name": "Requête Ollama (prompt PRD §6)", "func": "// Prompt système FIGÉ du PRD §6 — ne pas modifier sans versionner le PRD.\nconst SYSTEM_PROMPT = `Tu es un classifieur d'événements IA pour un créateur indépendant.\nRéponds UNIQUEMENT en JSON valide respectant ce schéma :\n{type, modele, editeur, modalites[], licence: {nom, commercial_ok(nullable),\nseuil(nullable), attribution_requise(nullable), poids_ouverts(nullable)},\nnsfw_ok(nullable), vram_gb(nullable), comfyui_natif(nullable), statut,\nimpact_profils[], resume(2 phrases max), sources[], confiance: haute|moyenne|faible}\nRÈGLES STRICTES : si une information n'est pas explicitement présente dans le\ntexte source, mets null. N'invente jamais une licence, un prix ou un benchmark.\nSignale les benchmarks auto-rapportés par l'éditeur (confiance: moyenne max).`;\n\nconst ollama = (env.get('OLLAMA_URL') || 'http://127.0.0.1:11434').replace(/\\/$/, '');\nconst modele = env.get('CLASSIFIER_MODEL') || 'qwen3:32b';\n\nmsg.method = 'POST';\nmsg.url = `${ollama}/api/chat`;\nmsg.headers = { 'content-type': 'application/json' };\nmsg.payload = {\n model: modele,\n stream: false,\n format: 'json',\n messages: [\n { role: 'system', content: SYSTEM_PROMPT },\n {\n role: 'user',\n content: [\n `Titre : ${msg.titreOriginal || '(sans titre)'}`,\n `Type suggéré : ${msg.typeSuggere || 'nouvelle_sortie'}`,\n '',\n 'Texte source :',\n msg.texte || msg.payload || ''\n ].join('\\n')\n }\n ]\n};\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 410, "y": 120, "wires": [["cl_ollama"]] }, { "id": "cl_ollama", "type": "http request", "z": "tab_classifier", "name": "POST Ollama /api/chat", "method": "use", "ret": "obj", "paytoqs": "ignore", "url": "", "tls": "", "persist": false, "proxy": "", "insecureHTTPParser": false, "authType": "", "senderr": true, "headers": [], "x": 660, "y": 120, "wires": [["cl_parser"]] }, { "id": "cl_parser", "type": "function", "z": "tab_classifier", "name": "Parser + mapper alerte", "func": "// Parse la réponse du classifieur et la mappe sur le schéma d'alerte du PRD §4.4.\n// Règle anti-hallucination : rien n'est inventé — les champs absents restent null/absents,\n// et une alerte sans aucune source est rejetée (l'app les refuserait de toute façon).\nconst TYPES = ['nouvelle_sortie', 'changement_licence', 'changement_classement', 'comfyui_support'];\nconst brut = msg.payload?.message?.content;\nlet ev;\ntry {\n ev = JSON.parse(brut);\n} catch (e) {\n node.error(`Réponse Ollama non JSON : ${String(brut).slice(0, 200)}`, msg);\n return null;\n}\n\nfunction slug(texte) {\n return typeof texte === 'string' && texte\n ? texte.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || null\n : null;\n}\n\nconst sources = Array.isArray(ev.sources) ? ev.sources.filter((s) => typeof s === 'string' && s.startsWith('http')) : [];\nif (sources.length === 0 && msg.lien) sources.push(msg.lien);\nif (sources.length === 0) {\n node.error('Alerte rejetée : aucune source (règle anti-hallucination)', msg);\n return null;\n}\n\nconst confiance = ['haute', 'moyenne', 'faible'].includes(ev.confiance) ? ev.confiance : 'faible';\n\nmsg.alerte = {\n type: TYPES.includes(ev.type) ? ev.type : (msg.typeSuggere || 'nouvelle_sortie'),\n modele_id: slug(ev.modele),\n titre: msg.titreOriginal || (ev.modele ? `Événement : ${ev.modele}` : 'Événement IA'),\n resume: typeof ev.resume === 'string' && ev.resume\n ? `[confiance ${confiance}] ${ev.resume}`\n : `[confiance ${confiance}] (pas de résumé)`,\n impact_profils: Array.isArray(ev.impact_profils)\n ? ev.impact_profils.filter((p) => typeof p === 'string')\n : [],\n urgence: msg.urgenceSuggeree || 'moyenne',\n sources,\n date: new Date().toISOString().slice(0, 10)\n};\n\nconst veille = (env.get('VEILLE_IA_URL') || 'http://127.0.0.1:3000').replace(/\\/$/, '');\nconst token = env.get('ALERTS_TOKEN');\nif (!token) {\n node.error('ALERTS_TOKEN non défini dans l\\'environnement Node-RED', msg);\n return null;\n}\nmsg.method = 'POST';\nmsg.url = `${veille}/api/alerts`;\nmsg.headers = {\n 'content-type': 'application/json',\n 'authorization': `Bearer ${token}`\n};\nmsg.payload = msg.alerte;\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 890, "y": 120, "wires": [["cl_vers_app"]] }, { "id": "cl_vers_app", "type": "http request", "z": "tab_classifier", "name": "POST veille-ia /api/alerts", "method": "use", "ret": "obj", "paytoqs": "ignore", "url": "", "tls": "", "persist": false, "proxy": "", "insecureHTTPParser": false, "authType": "", "senderr": true, "headers": [], "x": 1140, "y": 120, "wires": [["cl_urgence"]] }, { "id": "cl_urgence", "type": "switch", "z": "tab_classifier", "name": "urgence haute ?", "property": "alerte.urgence", "propertyType": "msg", "rules": [ { "t": "eq", "v": "haute", "vt": "str" }, { "t": "else" } ], "checkall": "true", "repair": false, "outputs": 2, "x": 1130, "y": 200, "wires": [["cl_vers_notify"], ["cl_response"]] }, { "id": "cl_vers_notify", "type": "http request", "z": "tab_classifier", "name": "POST notify", "method": "POST", "ret": "txt", "paytoqs": "ignore", "url": "http://127.0.0.1:1880/veille-ia/notify", "tls": "", "persist": false, "proxy": "", "insecureHTTPParser": false, "authType": "", "senderr": false, "headers": [], "x": 1330, "y": 180, "wires": [["cl_response"]] }, { "id": "cl_response", "type": "http response", "z": "tab_classifier", "name": "200", "statusCode": "200", "headers": {}, "x": 1350, "y": 240, "wires": [] } ]