Le conseiller passe de planificateur a observateur
Idee du proprietaire, et elle est meilleure que la mienne : ce systeme n a pas besoin qu on lui suggere une strategie de plus, il a besoin que quelqu un VOIE que « je coupe EDF, non je remets, et hop je coupe » est absurde. Cinq automates ecrivent sur la meme installation, chacun localement correct, aucun ne voit les autres. Leurs pannes sont des CONTRADICTIONS, et elles ne se lisent que dans la SUITE des evenements — jamais dans un instantane. Le 12/09 au matin le contacteur reseau a bascule toutes les 4 minutes pendant dix minutes : une automatisation HA coupait a 35 % de SOC, l hysteresis rallumait. Aucun automate ne l a vu ; un humain oui. Sortie elargie : `constat`, `gravite` (aucune/faible/moyenne/haute) et `ce_qui_cloche`, valides strictement, publies dans trois helpers et dans le logbook. Le snapshot porte desormais l anciennete des cinq transitions cles (contacteur, verrou, takeover, etat EVCS, mode ACM). ⚠️ VALIDATION SCINDEE. Essai sur l oscillation reelle : le modele a produit un constat JUSTE et un `current_max_a` de 100, hors bornes. La validation tout-ou-rien jetait le diagnostic avec le champ fautif — c est-a-dire exactement ce qu on lui demande, perdu a cause de ce qu on ne lui demande plus. Les champs d ACTION gardent leurs bornes mais sont NEUTRALISES au lieu de faire echouer l observation ; `actionUtilisable` est le seul drapeau qui autoriserait un jour un cablage, et les compteurs ne comptent que lui. Tests advisor : +6. Deux attendus mis a la nouvelle semantique dans le meme commit (hors bornes -> observation gardee, action neutralisee). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
This commit is contained in:
@@ -54,14 +54,18 @@ const promptOut = run(BUILD_PROMPT, snapshot)[0].json;
|
||||
check('BUILD_PROMPT: enabled=true', promptOut.enabled === true);
|
||||
check('BUILD_PROMPT: 2 messages system+user', promptOut.messages?.length === 2 && promptOut.messages[0].role === 'system');
|
||||
check('BUILD_PROMPT: contexte transporté', promptOut.context?.soc === 80 && promptOut.context?.userStrategy === 'auto_solar_plus');
|
||||
check('BUILD_PROMPT: prompt < 6000 chars (modèle local)', (promptOut.messages[0].content.length + promptOut.messages[1].content.length) < 6000);
|
||||
// La borne de 6000 datait du modele local d aout (contexte etroit). Le modele
|
||||
// charge aujourd hui annonce 98 304 tokens ; la vraie contrainte est le cout
|
||||
// de latence, pas le contexte. On garde une borne pour eviter la derive du
|
||||
// briefing, elargie a la mission d observation et a l historique des evenements.
|
||||
check('BUILD_PROMPT: prompt < 9000 chars (garde-fou contre la derive)', (promptOut.messages[0].content.length + promptOut.messages[1].content.length) < 9000);
|
||||
|
||||
const killSwitchOut = run(BUILD_PROMPT, { data: JSON.stringify({ ...JSON.parse(snapshot.data), advisor_enabled: 'off' }) })[0].json;
|
||||
check('BUILD_PROMPT: kill-switch OFF → enabled=false', killSwitchOut.enabled === false);
|
||||
|
||||
// ---------- VALIDATE_PARSE : 7 réponses LLM simulées ----------
|
||||
const ctxNode = { BUILD_PROMPT: promptOut };
|
||||
const validAdvice = '{"strategy":"auto_solar_plus","night_target_soc":35,"current_max_a":22,"ev_charge_tonight":"oui","confidence":0.85,"reason":"Forecast fort demain, HC nuit pour la VW."}';
|
||||
const validAdvice = '{"constat":"Contacteur reseau stable, batterie en charge.","gravite":"aucune","ce_qui_cloche":"","strategy":"auto_solar_plus","night_target_soc":35,"current_max_a":22,"ev_charge_tonight":"oui","confidence":0.85,"reason":"Forecast fort demain, HC nuit pour la VW."}';
|
||||
|
||||
const cases = [
|
||||
{
|
||||
@@ -80,14 +84,19 @@ const cases = [
|
||||
expect: o => o.status === 'invalid' && o.advice === null,
|
||||
},
|
||||
{
|
||||
label: 'night_target_soc 15 (< plancher 22) → invalid',
|
||||
// ⚠️ SEMANTIQUE CHANGEE LE 12/09 : un champ d ACTION hors bornes ne fait
|
||||
// plus echouer la reponse — il est NEUTRALISE et l observation survit.
|
||||
// Le constat est devenu la sortie principale ; le perdre a cause d un
|
||||
// champ qu on ne demande plus serait absurde. `actionUtilisable` reste
|
||||
// le seul drapeau qui autoriserait un cablage.
|
||||
label: 'night_target_soc 15 (< plancher 22) → observation gardee, action neutralisee',
|
||||
resp: { model: 'x', choices: [{ message: { content: validAdvice.replace('"night_target_soc":35', '"night_target_soc":15') } }] },
|
||||
expect: o => o.status === 'invalid' && o.advice === null,
|
||||
expect: o => o.status === 'ok' && o.advice.nightTargetSoc === null && o.advice.actionUtilisable === false && o.advice.constat,
|
||||
},
|
||||
{
|
||||
label: 'current_max_a 40 (> 28) → invalid',
|
||||
label: 'current_max_a 40 (> 28) → observation gardee, action neutralisee',
|
||||
resp: { model: 'x', choices: [{ message: { content: validAdvice.replace('"current_max_a":22', '"current_max_a":40') } }] },
|
||||
expect: o => o.status === 'invalid' && o.advice === null,
|
||||
expect: o => o.status === 'ok' && o.advice.currentMaxA === null && o.advice.actionUtilisable === false,
|
||||
},
|
||||
{
|
||||
label: 'texte sans JSON → invalid',
|
||||
@@ -138,6 +147,41 @@ const sansBilan = compteur({ ...base, balanceStatus: 'degrade', balanceRequiredS
|
||||
check('COMPTEUR: bilan indisponible → on ne compte RIEN plutot que de compter faux',
|
||||
!sansBilan.accord && !sansBilan.divergence);
|
||||
|
||||
// ── OBSERVATION : le constat et la gravite sont obligatoires ───────────
|
||||
function valider(json) {
|
||||
return run(VALIDATE_PARSE, { model: 'x', choices: [{ message: { content: json } }] }, ctxNode)[0].json;
|
||||
}
|
||||
const osc = valider(validAdvice
|
||||
.replace('"constat":"Contacteur reseau stable, batterie en charge."', '"constat":"Le contacteur reseau a bascule 4 fois en 10 minutes."')
|
||||
.replace('"gravite":"aucune"', '"gravite":"haute"')
|
||||
.replace('"ce_qui_cloche":""', '"ce_qui_cloche":"deux ecrivains avec des seuils differents sur le meme actionneur"'));
|
||||
check('OBSERVATION: constat + gravite haute acceptes',
|
||||
osc.status === 'ok' && osc.advice.gravite === 'haute' && osc.advice.constat.includes('bascule'));
|
||||
|
||||
check('OBSERVATION: constat vide → REJET',
|
||||
valider(validAdvice.replace('"constat":"Contacteur reseau stable, batterie en charge."', '"constat":""')).status === 'invalid');
|
||||
check('OBSERVATION: gravite hors enum → REJET',
|
||||
valider(validAdvice.replace('"gravite":"aucune"', '"gravite":"catastrophique"')).status === 'invalid');
|
||||
|
||||
// Le constat doit survivre a un champ d action hors bornes.
|
||||
const horsBornes = valider(validAdvice
|
||||
.replace('"constat":"Contacteur reseau stable, batterie en charge."', '"constat":"Bascule du contacteur toutes les 1-2 min depuis 06:25."')
|
||||
.replace('"gravite":"aucune"', '"gravite":"haute"')
|
||||
.replace('"current_max_a":22', '"current_max_a":100'));
|
||||
check('OBSERVATION: champ action hors bornes → le constat SURVIT',
|
||||
horsBornes.status === 'ok' && horsBornes.advice.constat.includes('Bascule') && horsBornes.advice.actionUtilisable === false);
|
||||
const hbCalls = run(BUILD_HA_CALLS, horsBornes, ctxNode).map(i => i.json);
|
||||
check('OBSERVATION: action hors bornes → aucun helper numerique ecrit',
|
||||
hbCalls.some(c => (c.data.entity_id || '').includes('advisor_constat')) &&
|
||||
!hbCalls.some(c => (c.data.entity_id || '').includes('night_target_soc')) &&
|
||||
!hbCalls.some(c => (c.data.entity_id || '').includes('agree_count')));
|
||||
|
||||
const oscCalls = run(BUILD_HA_CALLS, osc, ctxNode).map(i => i.json);
|
||||
check('OBSERVATION: constat, gravite et diagnostic publies',
|
||||
['constat', 'gravite', 'ce_qui_cloche'].every(k => oscCalls.some(c => (c.data.entity_id || '').includes(`advisor_${k}`))));
|
||||
check('OMBRE: l observation ne sort pas du perimetre advisor_*',
|
||||
oscCalls.every(c => c.domain === 'logbook' || /^(input_text|input_number)\.evcs_advisor_/.test(c.data.entity_id || '')));
|
||||
|
||||
const offlineValidated = run(VALIDATE_PARSE, cases[6].resp, ctxNode)[0].json;
|
||||
const offlineCalls = run(BUILD_HA_CALLS, offlineValidated, ctxNode).map(i => i.json);
|
||||
check('OMBRE: llm_offline → statut écrit, pas de recommandation', offlineCalls.every(c => !(c.data.entity_id || '').includes('advisor_strategy')) && offlineCalls.some(c => (c.data.value || '').toString().startsWith('llm_offline')));
|
||||
|
||||
Reference in New Issue
Block a user