Agent IA planificateur EVCS en MODE OMBRE (Advisor Shadow V1)
- Workflow n8n tdfInZns7cqo8fU2 actif (15 min): snapshot HA compact -> prompt FR -> LLM local Spark (modèle dynamique via /v1/models) avec fallback OpenRouter claude-haiku-4.5 (credential chiffré, clé hors git) -> validation stricte (enums, bornes, rejet total si invalide) -> helpers advisor_* + divergence + compteurs accord/divergence + logbook. - GARANTIE OMBRE structurelle: écritures limitées à advisor_*/logbook, vérifiée par tests/advisor/run.mjs (16 tests, bloquants au push). - Kill-switch input_boolean.evcs_advisor_enabled. - Extension R2 multi-workflows (lib-node-map WORKFLOWS[], build/extract/push par fichier, n8n-create.mjs, gen-advisor-workflow.mjs). - Helpers HA via package NFS + carte dashboard EVCS Master (backup fait). - 1re reco réelle: auto_solar_plus, cible 35%, accord avec le réel. - Fix sécurité n8n-push: plus de fallback d'id implicite vers le workflow EVCS.
This commit is contained in:
94
src/advisor/VALIDATE_PARSE.js
Normal file
94
src/advisor/VALIDATE_PARSE.js
Normal file
@@ -0,0 +1,94 @@
|
||||
// =====================================================
|
||||
// EVCS ADVISOR SHADOW V1 — VALIDATE_PARSE
|
||||
// Valide STRICTEMENT la sortie LLM (locale OU cloud, format OpenAI).
|
||||
// Sortie invalide = REJET TOTAL (status invalid), jamais de clamp silencieux.
|
||||
// =====================================================
|
||||
|
||||
const resp = $input.first().json;
|
||||
const context = $('BUILD_PROMPT').first().json.context;
|
||||
|
||||
const STRATEGIES = ['auto_solar_plus', 'auto_solar', 'force_grid_now', 'force_charge_now', 'stop'];
|
||||
const TONIGHT = ['oui', 'non', 'sans_objet'];
|
||||
|
||||
function fail(status, detail) {
|
||||
return [{
|
||||
json: {
|
||||
status,
|
||||
detail: (detail || '').toString().slice(0, 200),
|
||||
advice: null,
|
||||
modelUsed: (resp?.model || 'aucun').toString().slice(0, 60),
|
||||
context,
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
// 1. Réponse LLM présente ? (les deux backends ont échoué => item d'erreur n8n)
|
||||
const content = resp?.choices?.[0]?.message?.content;
|
||||
if (typeof content !== 'string' || !content.trim()) {
|
||||
return fail('llm_offline', resp?.error?.message || resp?.error || 'aucune réponse LLM');
|
||||
}
|
||||
|
||||
// 2. Nettoyage : balises de raisonnement (<think>...</think>) et fences markdown
|
||||
let text = content
|
||||
.replace(/<think>[\s\S]*?<\/think>/gi, '')
|
||||
.replace(/```(?:json)?/gi, '')
|
||||
.trim();
|
||||
|
||||
// 3. Extraction du premier objet JSON équilibré
|
||||
const start = text.indexOf('{');
|
||||
if (start === -1) return fail('invalid', 'aucun JSON dans la réponse');
|
||||
let depth = 0, end = -1;
|
||||
for (let i = start; i < text.length; i++) {
|
||||
if (text[i] === '{') depth++;
|
||||
else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }
|
||||
}
|
||||
if (end === -1) return fail('invalid', 'JSON non terminé');
|
||||
|
||||
let obj;
|
||||
try {
|
||||
obj = JSON.parse(text.slice(start, end + 1));
|
||||
} catch (e) {
|
||||
return fail('invalid', `JSON invalide: ${e.message}`);
|
||||
}
|
||||
|
||||
// 4. Validation stricte du schéma (enums + bornes)
|
||||
const strategy = (obj.strategy ?? '').toString().trim().toLowerCase();
|
||||
if (!STRATEGIES.includes(strategy)) return fail('invalid', `strategy hors enum: ${strategy}`);
|
||||
|
||||
const nightTarget = Number(obj.night_target_soc);
|
||||
if (!Number.isFinite(nightTarget) || nightTarget < 22 || nightTarget > 90) {
|
||||
return fail('invalid', `night_target_soc hors bornes: ${obj.night_target_soc}`);
|
||||
}
|
||||
|
||||
const currentMax = Number(obj.current_max_a);
|
||||
if (!Number.isFinite(currentMax) || currentMax < 6 || currentMax > 28) {
|
||||
return fail('invalid', `current_max_a hors bornes: ${obj.current_max_a}`);
|
||||
}
|
||||
|
||||
const tonight = (obj.ev_charge_tonight ?? '').toString().trim().toLowerCase();
|
||||
if (!TONIGHT.includes(tonight)) return fail('invalid', `ev_charge_tonight hors enum: ${tonight}`);
|
||||
|
||||
const confidence = Number(obj.confidence);
|
||||
if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
|
||||
return fail('invalid', `confidence hors bornes: ${obj.confidence}`);
|
||||
}
|
||||
|
||||
const reason = (obj.reason ?? '').toString().trim();
|
||||
if (!reason) return fail('invalid', 'reason vide');
|
||||
|
||||
return [{
|
||||
json: {
|
||||
status: 'ok',
|
||||
detail: '',
|
||||
advice: {
|
||||
strategy,
|
||||
nightTargetSoc: Math.round(nightTarget),
|
||||
currentMaxA: Math.round(currentMax),
|
||||
evChargeTonight: tonight,
|
||||
confidence: Math.round(confidence * 100) / 100,
|
||||
reason: reason.slice(0, 250),
|
||||
},
|
||||
modelUsed: (resp.model || 'inconnu').toString().slice(0, 60),
|
||||
context,
|
||||
}
|
||||
}];
|
||||
Reference in New Issue
Block a user