Revue : etat des leviers joint a chaque constat, transmis au conseiller et historise
This commit is contained in:
16
HANDOFF.md
16
HANDOFF.md
@@ -2216,5 +2216,19 @@ approximations successives ») ; cette nuit la VW a charge A FOND de 05:17 a 07:
|
|||||||
Carte « Arbitrage » (EVCS Master). Cote SPARK : envoi WhatsApp et ecriture de la reponse, a cabler.
|
Carte « Arbitrage » (EVCS Master). Cote SPARK : envoi WhatsApp et ecriture de la reponse, a cabler.
|
||||||
- Service redemarre 07:25.
|
- Service redemarre 07:25.
|
||||||
|
|
||||||
|
### 08/10 ~08:00 — Premiere revue reelle, et le contexte du conseiller enrichi
|
||||||
|
- 07:30 revue publiee (3 points : midi 35 % pour 43,6 % ; EDF HP 18:06-01:24 8,3 kWh ; VW bloquee 01:25-05:15) ;
|
||||||
|
07:30 pistes du conseiller (Spark, 0 ecartee) **globalement fausses** (midi : « filtre de demande vehicule » ;
|
||||||
|
VW : « session non autorisee par le bilan ») : il ne voyait que la raison du controleur.
|
||||||
|
- **Enrichissement (accord du proprietaire)** : chaque constat porte `etat` = releve des leviers a l'instant
|
||||||
|
(SOC, maison, reseau, consigne reseau, courant ACM, borne, session HC, mode de nuit, budget voiture, SOC requis,
|
||||||
|
cible du matin, etat/source du controleur ; milieu de fenetre pour les objectifs). Transmis au conseiller,
|
||||||
|
admis par l'anti-invention, historise (`revue_constat.etat`). Tests revue 14/14, conseiller 29/29.
|
||||||
|
- Le releve revele la cause de midi qu'on n'avait pas nommee : **a 13:24 la VW tirait 3 497 W** en HC de midi
|
||||||
|
(maison 6 632 W), ACM a 30 A.
|
||||||
|
- **Rejeu du conseiller sur le 08/10 avec ce contexte** (Spark, sans ecriture) : VW **juste** (« sans_edf avec
|
||||||
|
budget 0 kWh … activer complete ») ; EDF HP **partiel** ; midi **toujours faux** (« consigne inferieure au
|
||||||
|
solaire »). 1 juste / 1 partiel / 1 faux, contre 0 juste avant. Pas encore un role actif.
|
||||||
|
|
||||||
---
|
---
|
||||||
*Document mis a jour le : 08/10/2026 (~07:30) — conseiller relecteur de la journee (revue calculee + pistes) ; nuit : voiture qui profite d'EDF ferme pour la batterie, nuit qui prepare le soir les jours annonces faibles ; 08/10/2026 (~00:15) — jour faible : charge maximale en HC de midi (bilan 100 %, ACM plafond + solaire) ; 07/10/2026 (~22:45) — reprise de 16 h verifiee, EDF en HP a 18:03, bilan : reprise du soir et prevision recalee sur la matinee ; bilan HC de midi, reprise du soir apres coupure en pleine chauffe, mesure ECS sur 10 min ; recharge reseau HC : courant calcule (12 kVA) et consigne non tenue ; HC de midi : AUTO ne chauffe pas (BOOST), alerte v2, Jeep (contact, relecture, zone), recharge batterie bridee a 30 A par l'ACM ; jour faible = EDF les deux fenetres HC pour le chauffe-eau, alerte BOOST, Victron Mobile refait ; 1re decision jour faible en reel, mesure ECS reparee, douches 06:00/20:00, Grafana chauffe-eau, tableaux ECS ; chauffe-eau en HC de nuit les jours faibles (decision du bilan, workflow ECS sous controle du depot, VRM, V2.3 desactive) ; garde batterie reseau : voiture bridee a 6 A au lieu d'un arret ; garde-fou concierge 2 h ; allers-retours brefs de la borne retrouves dans l'historique HA (echange VW -> JEEP de 20 s), production 14,5 kWh contre 22 prevus, defaut garde batterie en mode reseau a trancher ; voiture a midi : la batterie n'est plus comptee comme du solaire (EDF automatique les jours de depart si le surplus ne couvre pas) ; nuit 05->06 : JEEP chargee sur EDF en complete (6,32 kWh), budget 0 du a la prevision, Windy/ECMWF contre VRM, a comparer ce soir ; concierge aligne sur la preuve fin de trajet ; a verifier : nuit 05->06 et mardi faible, comparaison VRM/Forecast.Solar ; JEEP identifiee aussi par sa fin de trajet, prevision de la nuit 05->06 ; identification JEEP reussie en reel, correction humaine qui prime (refus supprime), session du 04/10 coupee ; EVCS Master en quatre onglets ; Jeep lue par Uconnect et identifiee seule, capacite JEEP 10 kWh, concierge variante (a) ; releve par photo (SPARK), hass-uconnect en attente ; ecart semaine/week-end mesure (pas de profil week-end la nuit) ; nuit du budget : budget borne au plancher prudent, sans EDF = voiture bloquee meme EDF ferme (batterie seule) ; Energie : sauts corriges ; tableau Energie complete (Sud/Ouest, batterie par integration, borne ; aucun redemarrage HA) ; kilometrage des vehicules (km/kWh, capacite, releves de reference, agent cable 15/15) ; a verifier le 04/10 au matin : nuit du budget JEEP ; concierge : vehicule non confirme demande, heure de saisie ; Grafana vehicules en tableau ; Grafana vehicules : ZERO et Invite ajoutes, session 16:46 reetiquetee ZERO ; prevision SOC 10 h ~35 % ; concierge fiable : niveau note par le script, une ligne, dedoublonnage, simulation par drapeau ; agent : message incompris = question, bourse sur demande explicite (banc final 6/6) ; a verifier : nuit du samedi 03 au dimanche 04/10 (budget < besoin JEEP), cron orange-cloture lundi 05/10 apres 17:40, comparaison VRM / Forecast.Solar lundi 05/10*
|
*Document mis a jour le : 08/10/2026 (~08:00) — conseiller relecteur de la journee (revue calculee + pistes) ; nuit : voiture qui profite d'EDF ferme pour la batterie, nuit qui prepare le soir les jours annonces faibles ; 08/10/2026 (~00:15) — jour faible : charge maximale en HC de midi (bilan 100 %, ACM plafond + solaire) ; 07/10/2026 (~22:45) — reprise de 16 h verifiee, EDF en HP a 18:03, bilan : reprise du soir et prevision recalee sur la matinee ; bilan HC de midi, reprise du soir apres coupure en pleine chauffe, mesure ECS sur 10 min ; recharge reseau HC : courant calcule (12 kVA) et consigne non tenue ; HC de midi : AUTO ne chauffe pas (BOOST), alerte v2, Jeep (contact, relecture, zone), recharge batterie bridee a 30 A par l'ACM ; jour faible = EDF les deux fenetres HC pour le chauffe-eau, alerte BOOST, Victron Mobile refait ; 1re decision jour faible en reel, mesure ECS reparee, douches 06:00/20:00, Grafana chauffe-eau, tableaux ECS ; chauffe-eau en HC de nuit les jours faibles (decision du bilan, workflow ECS sous controle du depot, VRM, V2.3 desactive) ; garde batterie reseau : voiture bridee a 6 A au lieu d'un arret ; garde-fou concierge 2 h ; allers-retours brefs de la borne retrouves dans l'historique HA (echange VW -> JEEP de 20 s), production 14,5 kWh contre 22 prevus, defaut garde batterie en mode reseau a trancher ; voiture a midi : la batterie n'est plus comptee comme du solaire (EDF automatique les jours de depart si le surplus ne couvre pas) ; nuit 05->06 : JEEP chargee sur EDF en complete (6,32 kWh), budget 0 du a la prevision, Windy/ECMWF contre VRM, a comparer ce soir ; concierge aligne sur la preuve fin de trajet ; a verifier : nuit 05->06 et mardi faible, comparaison VRM/Forecast.Solar ; JEEP identifiee aussi par sa fin de trajet, prevision de la nuit 05->06 ; identification JEEP reussie en reel, correction humaine qui prime (refus supprime), session du 04/10 coupee ; EVCS Master en quatre onglets ; Jeep lue par Uconnect et identifiee seule, capacite JEEP 10 kWh, concierge variante (a) ; releve par photo (SPARK), hass-uconnect en attente ; ecart semaine/week-end mesure (pas de profil week-end la nuit) ; nuit du budget : budget borne au plancher prudent, sans EDF = voiture bloquee meme EDF ferme (batterie seule) ; Energie : sauts corriges ; tableau Energie complete (Sud/Ouest, batterie par integration, borne ; aucun redemarrage HA) ; kilometrage des vehicules (km/kWh, capacite, releves de reference, agent cable 15/15) ; a verifier le 04/10 au matin : nuit du budget JEEP ; concierge : vehicule non confirme demande, heure de saisie ; Grafana vehicules en tableau ; Grafana vehicules : ZERO et Invite ajoutes, session 16:46 reetiquetee ZERO ; prevision SOC 10 h ~35 % ; concierge fiable : niveau note par le script, une ligne, dedoublonnage, simulation par drapeau ; agent : message incompris = question, bourse sur demande explicite (banc final 6/6) ; a verifier : nuit du samedi 03 au dimanche 04/10 (budget < besoin JEEP), cron orange-cloture lundi 05/10 apres 17:40, comparaison VRM / Forecast.Solar lundi 05/10*
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const ctx = {
|
|||||||
jour: jourParis,
|
jour: jourParis,
|
||||||
revueDuJour,
|
revueDuJour,
|
||||||
dejaRepondu,
|
dejaRepondu,
|
||||||
constats: aExaminer.map((c) => ({ code: c.code, texte: String(c.texte), contexte: String(c.contexte || '') })),
|
constats: aExaminer.map((c) => ({ code: c.code, texte: String(c.texte), contexte: String(c.contexte || ''), etat: String(c.etat || '') })),
|
||||||
};
|
};
|
||||||
|
|
||||||
const systemPrompt = [
|
const systemPrompt = [
|
||||||
@@ -56,6 +56,11 @@ const systemPrompt = [
|
|||||||
'- Si tu ne vois pas de cause, dis « cause non identifiable avec ces donnees ».',
|
'- Si tu ne vois pas de cause, dis « cause non identifiable avec ces donnees ».',
|
||||||
'- Une piste fait au plus 280 caracteres, en francais, sans jargon inutile.',
|
'- Une piste fait au plus 280 caracteres, en francais, sans jargon inutile.',
|
||||||
'- Le contexte est la raison publiee par le controleur au debut de l episode.',
|
'- Le contexte est la raison publiee par le controleur au debut de l episode.',
|
||||||
|
'- L etat donne les leviers a ce moment-la : consigne reseau, courant de charge max fixe',
|
||||||
|
' par l ACM, mode de nuit (sans_edf : la voiture ne fait pas fermer EDF ; complete : elle',
|
||||||
|
' charge sur EDF en HC), budget voiture sans EDF, SOC requis, cible du matin. Cherche la',
|
||||||
|
' cause d abord dans ces leviers (un courant ACM bas, une consigne egale a la maison, un',
|
||||||
|
' mode sans_edf avec budget 0 expliquent souvent le constat).',
|
||||||
'',
|
'',
|
||||||
'Reponds UNIQUEMENT en JSON : {"pistes":[{"code":"<code du constat>","piste":"<texte>"}]}',
|
'Reponds UNIQUEMENT en JSON : {"pistes":[{"code":"<code du constat>","piste":"<texte>"}]}',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
@@ -63,7 +68,7 @@ const systemPrompt = [
|
|||||||
const userPrompt = [
|
const userPrompt = [
|
||||||
`Revue du ${jourParis}. ${aExaminer.length} constat(s) a examiner :`,
|
`Revue du ${jourParis}. ${aExaminer.length} constat(s) a examiner :`,
|
||||||
'',
|
'',
|
||||||
...aExaminer.map((c, i) => `${i + 1}. code=${c.code}\n constat : ${c.texte}\n contexte : ${String(c.contexte || '(aucun)').slice(0, 400)}`),
|
...aExaminer.map((c, i) => `${i + 1}. code=${c.code}\n constat : ${c.texte}\n contexte : ${String(c.contexte || '(aucun)').slice(0, 400)}\n etat : ${String(c.etat || '(inconnu)').slice(0, 600)}`),
|
||||||
'',
|
'',
|
||||||
'JSON uniquement.',
|
'JSON uniquement.',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ for (const p of obj.pistes) {
|
|||||||
const c = parCode.get(code);
|
const c = parCode.get(code);
|
||||||
if (!piste) { ecarter(code, '', 'piste vide'); continue; }
|
if (!piste) { ecarter(code, '', 'piste vide'); continue; }
|
||||||
if (!c) { ecarter(code, piste, 'code inconnu ou constat non a examiner'); continue; }
|
if (!c) { ecarter(code, piste, 'code inconnu ou constat non a examiner'); continue; }
|
||||||
const permis = new Set(nombres(`${c.texte} ${c.contexte}`));
|
const permis = new Set(nombres(`${c.texte} ${c.contexte} ${c.etat || ''}`)
|
||||||
|
.flatMap((n) => [n, String(Math.round(Number(n))), String(Math.round(Number(n) * 10) / 10)]));
|
||||||
const inventes = nombres(piste).filter((n) => !permis.has(n));
|
const inventes = nombres(piste).filter((n) => !permis.has(n));
|
||||||
if (inventes.length) { ecarter(code, piste, `chiffre absent du constat : ${inventes.join(', ')}`); continue; }
|
if (inventes.length) { ecarter(code, piste, `chiffre absent du constat : ${inventes.join(', ')}`); continue; }
|
||||||
if (pistes.some((x) => x.code === code)) { ecarter(code, piste, 'doublon'); continue; }
|
if (pistes.some((x) => x.code === code)) { ecarter(code, piste, 'doublon'); continue; }
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function run(code, inputJson, nodeOutputs = {}, D = DateFixe) {
|
|||||||
|
|
||||||
// Constats reels du 07-08/10 (sortie de tools/revue-jour.mjs sur la base).
|
// Constats reels du 07-08/10 (sortie de tools/revue-jour.mjs sur la base).
|
||||||
const CONSTATS = [
|
const CONSTATS = [
|
||||||
{ code: 'batterie_sous_objectif_midi', gravite: 'a_examiner', texte: 'Fin des heures creuses de midi (14:24) : batterie a 35 % pour 43,6 % vises, il manquait 2,6 kWh.', contexte: 'Maintien du courant' },
|
{ code: 'batterie_sous_objectif_midi', gravite: 'a_examiner', texte: 'Fin des heures creuses de midi (14:24) : batterie a 35 % pour 43,6 % vises, il manquait 2,6 kWh.', contexte: 'Maintien du courant', etat: "a 13:24 : SOC 33 %, consigne reseau 5198 W, courant de charge max fixe par l'ACM 30 A, mode de nuit sans_edf" },
|
||||||
{ code: 'edf_heures_pleines', gravite: 'a_examiner', texte: 'EDF ferme en heures pleines de 18:06 a 01:24 (7 h 18), 8,3 kWh achetes, batterie a 21 % a la fermeture ; ferme par Victron - Grid ON si SoC < 22%.', contexte: 'Conditions de démarrage non réunies' },
|
{ code: 'edf_heures_pleines', gravite: 'a_examiner', texte: 'EDF ferme en heures pleines de 18:06 a 01:24 (7 h 18), 8,3 kWh achetes, batterie a 21 % a la fermeture ; ferme par Victron - Grid ON si SoC < 22%.', contexte: 'Conditions de démarrage non réunies' },
|
||||||
{ code: 'voiture_bloquee_session_hc', gravite: 'a_examiner', texte: "Voiture branchee mais bloquee de 01:24 a 05:14 (3 h 50) alors qu'EDF etait ferme en heures creuses (etat night_no_grid_hc_lock).", contexte: 'Nuit sans autorisation HC réseau | Nuit EVCS: target=38.8% (BILAN)' },
|
{ code: 'voiture_bloquee_session_hc', gravite: 'a_examiner', texte: "Voiture branchee mais bloquee de 01:24 a 05:14 (3 h 50) alors qu'EDF etait ferme en heures creuses (etat night_no_grid_hc_lock).", contexte: 'Nuit sans autorisation HC réseau | Nuit EVCS: target=38.8% (BILAN)' },
|
||||||
{ code: 'charge_max_midi', gravite: 'info', texte: 'info', contexte: '' },
|
{ code: 'charge_max_midi', gravite: 'info', texte: 'info', contexte: '' },
|
||||||
@@ -52,6 +52,8 @@ check('PROMPT: rien a examiner -> inactif (pas d appel au modele)', run(BUILD_PR
|
|||||||
const D11 = class extends Date { constructor(...a) { super(...(a.length ? a : [Date.parse('2026-10-08T11:00:00+02:00')])); } };
|
const D11 = class extends Date { constructor(...a) { super(...(a.length ? a : [Date.parse('2026-10-08T11:00:00+02:00')])); } };
|
||||||
check('PROMPT: pas de relance apres 10 h', run(BUILD_PROMPT, snap(), {}, D11)[0].json.enabled === false);
|
check('PROMPT: pas de relance apres 10 h', run(BUILD_PROMPT, snap(), {}, D11)[0].json.enabled === false);
|
||||||
|
|
||||||
|
check('PROMPT: l etat des leviers est transmis', /consigne reseau 5198 W/.test(p.messages[1].content) && /ACM 30 A/.test(p.messages[1].content));
|
||||||
|
|
||||||
// ---------- VALIDATE_PARSE ----------
|
// ---------- VALIDATE_PARSE ----------
|
||||||
const ctxNode = { BUILD_PROMPT: p };
|
const ctxNode = { BUILD_PROMPT: p };
|
||||||
const rep = (obj) => ({ model: 'qwen3-coder', choices: [{ message: { content: typeof obj === 'string' ? obj : JSON.stringify(obj) } }] });
|
const rep = (obj) => ({ model: 'qwen3-coder', choices: [{ message: { content: typeof obj === 'string' ? obj : JSON.stringify(obj) } }] });
|
||||||
@@ -68,6 +70,7 @@ const o2 = v({ pistes: [{ code: 'edf_heures_pleines', piste: 'Il aurait fallu 60
|
|||||||
check('VALIDATE: ANTI-INVENTION — chiffre absent du constat (60) -> piste ecartee', o2.pistes.length === 0 && o2.rejetees === 1, JSON.stringify(o2));
|
check('VALIDATE: ANTI-INVENTION — chiffre absent du constat (60) -> piste ecartee', o2.pistes.length === 0 && o2.rejetees === 1, JSON.stringify(o2));
|
||||||
const o3 = v({ pistes: [{ code: 'edf_heures_pleines', piste: 'Fermeture a 21 %, 8,3 kWh : revoir la recharge de midi.' }] });
|
const o3 = v({ pistes: [{ code: 'edf_heures_pleines', piste: 'Fermeture a 21 %, 8,3 kWh : revoir la recharge de midi.' }] });
|
||||||
check('VALIDATE: chiffres du constat (21, 8,3) acceptes', o3.pistes.length === 1, JSON.stringify(o3));
|
check('VALIDATE: chiffres du constat (21, 8,3) acceptes', o3.pistes.length === 1, JSON.stringify(o3));
|
||||||
|
check('VALIDATE: chiffres de l etat acceptes (30 A, 5198 W)', v({ pistes: [{ code: 'batterie_sous_objectif_midi', piste: 'ACM a 30 A et consigne tenue a 5198 W.' }] }).pistes.length === 1);
|
||||||
check('VALIDATE: code inconnu -> ecarte', v({ pistes: [{ code: 'invente', piste: 'x' }] }).rejetees === 1);
|
check('VALIDATE: code inconnu -> ecarte', v({ pistes: [{ code: 'invente', piste: 'x' }] }).rejetees === 1);
|
||||||
check('VALIDATE: un constat info n est pas relu', v({ pistes: [{ code: 'charge_max_midi', piste: 'x' }] }).pistes.length === 0);
|
check('VALIDATE: un constat info n est pas relu', v({ pistes: [{ code: 'charge_max_midi', piste: 'x' }] }).pistes.length === 0);
|
||||||
check('VALIDATE: doublon de code -> un seul garde', v({ pistes: [bon.pistes[0], bon.pistes[0]] }).pistes.length === 1);
|
check('VALIDATE: doublon de code -> un seul garde', v({ pistes: [bon.pistes[0], bon.pistes[0]] }).pistes.length === 1);
|
||||||
|
|||||||
@@ -78,5 +78,12 @@ const sessionMidi = (d, requis) => {
|
|||||||
const r = constatsRevue(d); const c = r.constats.find((x) => x.code === 'charge_max_midi');
|
const r = constatsRevue(d); const c = r.constats.find((x) => x.code === 'charge_max_midi');
|
||||||
ok('midi-charge-max-info', !!c && c.gravite === 'info' && /31 % -> 35 % \(\+1,2 kWh\)/.test(c.texte) && !codes(r).includes('sous_objectif'), JSON.stringify(r.constats)); }
|
ok('midi-charge-max-info', !!c && c.gravite === 'info' && /31 % -> 35 % \(\+1,2 kWh\)/.test(c.texte) && !codes(r).includes('sous_objectif'), JSON.stringify(r.constats)); }
|
||||||
|
|
||||||
|
{ const d = base(); sessionMidi(d, 43.6);
|
||||||
|
d.acmA = [{ ts: T('07T12:00:00'), v: 30 }]; d.consigneW = [{ ts: T('07T12:30:00'), v: 5198 }];
|
||||||
|
d.modeNuit = [{ ts: T('07T12:00:00'), v: 'sans_edf' }];
|
||||||
|
const c = constatsRevue(d).constats.find((x) => x.code === 'batterie_sous_objectif_midi');
|
||||||
|
ok('etat-joint-au-constat', !!c && /a 13:24/.test(c.etat) && /ACM 30 A/.test(c.etat) && /consigne reseau 5198 W/.test(c.etat)
|
||||||
|
&& /mode de nuit sans_edf/.test(c.etat) && /session HC engagee oui/.test(c.etat) && /budget voiture sans EDF inconnu/.test(c.etat), c && c.etat); }
|
||||||
|
|
||||||
console.log(echecs ? `\n${echecs} ECHEC(S)` : '\nREVUE OK');
|
console.log(echecs ? `\n${echecs} ECHEC(S)` : '\nREVUE OK');
|
||||||
process.exit(echecs ? 1 : 0);
|
process.exit(echecs ? 1 : 0);
|
||||||
|
|||||||
@@ -796,6 +796,7 @@ async function migrer(client) {
|
|||||||
chiffres jsonb,
|
chiffres jsonb,
|
||||||
PRIMARY KEY (jour, code, debut)
|
PRIMARY KEY (jour, code, debut)
|
||||||
)`);
|
)`);
|
||||||
|
await client.query('ALTER TABLE revue_constat ADD COLUMN IF NOT EXISTS etat text');
|
||||||
await client.query(`
|
await client.query(`
|
||||||
CREATE TABLE IF NOT EXISTS revue_piste (
|
CREATE TABLE IF NOT EXISTS revue_piste (
|
||||||
jour date NOT NULL,
|
jour date NOT NULL,
|
||||||
@@ -1195,13 +1196,13 @@ async function revueDeLaJournee(sql, client, st) {
|
|||||||
await publierCapteur('sensor.evcs_revue_jour', jour, {
|
await publierCapteur('sensor.evcs_revue_jour', jour, {
|
||||||
friendly_name: 'Revue de la journee',
|
friendly_name: 'Revue de la journee',
|
||||||
a_examiner: aExaminer,
|
a_examiner: aExaminer,
|
||||||
constats: r.constats.map((c) => ({ code: c.code, gravite: c.gravite, texte: c.texte, contexte: c.contexte })),
|
constats: r.constats.map((c) => ({ code: c.code, gravite: c.gravite, texte: c.texte, contexte: c.contexte, etat: c.etat || '' })),
|
||||||
texte: r.texte,
|
texte: r.texte,
|
||||||
});
|
});
|
||||||
for (const c of r.constats) {
|
for (const c of r.constats) {
|
||||||
await sql(`INSERT INTO revue_constat (jour, code, gravite, debut, fin, texte, contexte, chiffres)
|
await sql(`INSERT INTO revue_constat (jour, code, gravite, debut, fin, texte, contexte, chiffres, etat)
|
||||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT DO NOTHING`,
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) ON CONFLICT DO NOTHING`,
|
||||||
[jour, c.code, c.gravite, new Date(c.debut), new Date(c.fin), c.texte, c.contexte, JSON.stringify(c.chiffres || {})]);
|
[jour, c.code, c.gravite, new Date(c.debut), new Date(c.fin), c.texte, c.contexte, JSON.stringify(c.chiffres || {}), c.etat || null]);
|
||||||
}
|
}
|
||||||
await journaliser(sql, 'revue', `Revue de la journee : ${aExaminer} point(s) a examiner`, { jour, texte: r.texte });
|
await journaliser(sql, 'revue', `Revue de la journee : ${aExaminer} point(s) a examiner`, { jour, texte: r.texte });
|
||||||
await publierRevueHistorique(client);
|
await publierRevueHistorique(client);
|
||||||
|
|||||||
@@ -65,6 +65,33 @@ function plages(debut, fin, pred) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RELEVE DE L'ETAT a un instant (08/10, proprietaire) : le conseiller ne voyait
|
||||||
|
// que la raison du controleur au debut de l'episode, et ses pistes du 08/10
|
||||||
|
// etaient fausses faute de voir l'ACM a 30 A, la consigne tenue ou le mode
|
||||||
|
// « sans EDF ». Chaque constat porte donc l'etat des leviers a ce moment-la.
|
||||||
|
function etatA(d, t) {
|
||||||
|
const v = (serie, unite, dec = 0) => {
|
||||||
|
const x = serie ? aInstant(serie, t) : null;
|
||||||
|
return x === null || !Number.isFinite(Number(x)) ? 'inconnu' : `${Math.round(Number(x) * 10 ** dec) / 10 ** dec}${unite}`;
|
||||||
|
};
|
||||||
|
let x = null; for (const r of d.decisions || []) { if (r.ts <= t) x = r; else break; }
|
||||||
|
const mode = d.modeNuit ? aInstant(d.modeNuit, t) : null;
|
||||||
|
return `a ${hhmm(t)} : ` + [
|
||||||
|
`SOC ${v(d.soc, ' %')}`,
|
||||||
|
`maison ${v(d.maisonW, ' W')}`,
|
||||||
|
`reseau ${v(d.reseauW, ' W')}`,
|
||||||
|
`consigne reseau ${v(d.consigneW, ' W')}`,
|
||||||
|
`courant de charge max fixe par l'ACM ${v(d.acmA, ' A')}`,
|
||||||
|
`borne ${v(d.borneW, ' W')}`,
|
||||||
|
`session HC engagee ${x ? (x.session_engagee ? 'oui' : 'non') : 'inconnu'}`,
|
||||||
|
`mode de nuit ${mode || 'inconnu'}`,
|
||||||
|
`budget voiture sans EDF ${v(d.budgetKwh, ' kWh', 2)}`,
|
||||||
|
`SOC requis (bilan) ${v(d.requis, ' %', 1)}`,
|
||||||
|
`cible du matin ${x && Number.isFinite(Number(x.cible_matin_pct)) ? `${x.cible_matin_pct} %` : 'inconnu'}`,
|
||||||
|
`controleur ${x ? `${x.etat} / ${x.source_mode}` : 'inconnu'}`,
|
||||||
|
].join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object} d
|
* @param {object} d
|
||||||
* debut, fin : ms
|
* debut, fin : ms
|
||||||
@@ -107,6 +134,7 @@ export function constatsRevue(d) {
|
|||||||
+ (socDebut !== null ? `, batterie a ${Math.round(socDebut)} % a la fermeture` : '')
|
+ (socDebut !== null ? `, batterie a ${Math.round(socDebut)} % a la fermeture` : '')
|
||||||
+ (ouverture?.auteur ? ` ; ferme par ${ouverture.auteur}` : '') + '.',
|
+ (ouverture?.auteur ? ` ; ferme par ${ouverture.auteur}` : '') + '.',
|
||||||
chiffres: { minutes: Math.round(min), kwh: kwh === null ? null : Math.round(kwh * 100) / 100, soc: socDebut },
|
chiffres: { minutes: Math.round(min), kwh: kwh === null ? null : Math.round(kwh * 100) / 100, soc: socDebut },
|
||||||
|
etat: etatA(d, p.debut),
|
||||||
contexte: (aInstant(d.decisions.map((x) => ({ ts: x.ts, v: x.raison })), p.debut) || '').slice(0, 400),
|
contexte: (aInstant(d.decisions.map((x) => ({ ts: x.ts, v: x.raison })), p.debut) || '').slice(0, 400),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -127,6 +155,7 @@ export function constatsRevue(d) {
|
|||||||
debut: p.debut, fin: p.fin,
|
debut: p.debut, fin: p.fin,
|
||||||
texte: `Voiture branchee mais bloquee de ${hhmm(p.debut)} a ${hhmm(p.fin)} (${duree(min)}) alors qu'EDF etait ferme en heures creuses (etat ${x.etat}).`,
|
texte: `Voiture branchee mais bloquee de ${hhmm(p.debut)} a ${hhmm(p.fin)} (${duree(min)}) alors qu'EDF etait ferme en heures creuses (etat ${x.etat}).`,
|
||||||
chiffres: { minutes: Math.round(min) },
|
chiffres: { minutes: Math.round(min) },
|
||||||
|
etat: etatA(d, p.debut + MIN),
|
||||||
contexte: (x.raison || '').slice(0, 400),
|
contexte: (x.raison || '').slice(0, 400),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -156,6 +185,7 @@ export function constatsRevue(d) {
|
|||||||
texte: `Heures creuses de midi en charge maximale (jour faible) : batterie ${soc0 === null ? '?' : Math.round(soc0)} % -> ${Math.round(soc)} %`
|
texte: `Heures creuses de midi en charge maximale (jour faible) : batterie ${soc0 === null ? '?' : Math.round(soc0)} % -> ${Math.round(soc)} %`
|
||||||
+ (soc0 === null ? '.' : ` (+${f1((soc - soc0) * KWH_PAR_POINT)} kWh).`),
|
+ (soc0 === null ? '.' : ` (+${f1((soc - soc0) * KWH_PAR_POINT)} kWh).`),
|
||||||
chiffres: { socDebut: soc0, socFin: soc },
|
chiffres: { socDebut: soc0, socFin: soc },
|
||||||
|
etat: etatA(d, t - 60 * MIN),
|
||||||
contexte: (x.raison || '').slice(0, 400),
|
contexte: (x.raison || '').slice(0, 400),
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
@@ -168,6 +198,8 @@ export function constatsRevue(d) {
|
|||||||
debut: t, fin: t,
|
debut: t, fin: t,
|
||||||
texte: `Fin des heures creuses de ${f.nom} (${hhmm(t)}) : batterie a ${Math.round(soc)} % pour ${f1(objectif)} % vises, il manquait ${f1(manque * KWH_PAR_POINT)} kWh.`,
|
texte: `Fin des heures creuses de ${f.nom} (${hhmm(t)}) : batterie a ${Math.round(soc)} % pour ${f1(objectif)} % vises, il manquait ${f1(manque * KWH_PAR_POINT)} kWh.`,
|
||||||
chiffres: { soc, objectif, manqueKwh: Math.round(manque * KWH_PAR_POINT * 10) / 10 },
|
chiffres: { soc, objectif, manqueKwh: Math.round(manque * KWH_PAR_POINT * 10) / 10 },
|
||||||
|
// Milieu de la fenetre : la ou la recharge aurait du se faire.
|
||||||
|
etat: etatA(d, t - (f.nom === 'nuit' ? 3 * 60 : 60) * MIN),
|
||||||
contexte: (x.raison || '').slice(0, 400),
|
contexte: (x.raison || '').slice(0, 400),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -212,6 +244,15 @@ export async function donneesRevue(client, fenetres, finMs) {
|
|||||||
debut, fin, fenetres, contacteur, decisions,
|
debut, fin, fenetres, contacteur, decisions,
|
||||||
edfKwh: await serie('sensor.edf_general_ok_energie_totale'),
|
edfKwh: await serie('sensor.edf_general_ok_energie_totale'),
|
||||||
reseauW: await serie('sensor.victron_grid_power_l1'),
|
reseauW: await serie('sensor.victron_grid_power_l1'),
|
||||||
|
maisonW: await serie('sensor.victron_consumption_power_l1'),
|
||||||
|
borneW: await serie('sensor.victron_evcs_ac_power'),
|
||||||
|
consigneW: await serie('number.gx_device_ac_power_setpoint'),
|
||||||
|
acmA: await serie('sensor.victron_max_charge_current_setting'),
|
||||||
|
budgetKwh: await serie('input_number.evcs_balance_night_vehicle_budget_kwh'),
|
||||||
|
modeNuit: (await q(`SELECT ts, v FROM (
|
||||||
|
(SELECT ts, detail->>'apres' v FROM evenement WHERE detail->>'entite'='input_select.victron_evcs_night_charge_mode' AND ts < $1 ORDER BY ts DESC LIMIT 1)
|
||||||
|
UNION ALL (SELECT ts, detail->>'apres' v FROM evenement WHERE detail->>'entite'='input_select.victron_evcs_night_charge_mode' AND ts >= $1 AND ts < $2)) s ORDER BY ts`, [d0, d1]))
|
||||||
|
.map((r) => ({ ts: ms(r), v: r.v })),
|
||||||
soc: await serie('sensor.victron_battery_soc'),
|
soc: await serie('sensor.victron_battery_soc'),
|
||||||
requis: await serie('input_number.evcs_balance_required_soc'),
|
requis: await serie('input_number.evcs_balance_required_soc'),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"jsCode": "// =====================================================\n// EVCS ADVISOR — BUILD_PROMPT (RELECTEUR DE LA JOURNEE, 08/10/2026)\n//\n// Le conseiller n'observe plus l'instantane toutes les 15 min : 1 500 « rien a\n// signaler » contre 871 signalements sans suite, et aucun des rates du 07/10\n// (HC de midi perdue, EDF en heures pleines a 18:03, VW bloquee de 01:24 a\n// 05:14 avec EDF ferme) n'avait ete vu. Il RELIT desormais, une fois par jour,\n// les constats CALCULES par le service d'historisation (tools/revue-jour.mjs,\n// sensor.evcs_revue_jour) et propose pour chacun une piste a examiner.\n// Doctrine §79 : les chiffres viennent du code, le modele ne fait que relire.\n// Il ne pilote rien.\n//\n// Il tourne quand : le conseiller est actif, la revue du jour est publiee, il\n// y a au moins un point a examiner, et il n'a pas deja repondu aujourd'hui.\n// =====================================================\n\nconst raw = $input.first().json;\nconst input = typeof raw.data === 'string' ? JSON.parse(raw.data) : (raw.data || raw);\nconst onOff = (v) => (v ?? 'off').toString().trim().toLowerCase() === 'on';\n\nconst jourParis = new Date().toLocaleDateString('sv-SE', { timeZone: 'Europe/Paris' });\nlet constats = input.revue_jour_constats;\nif (typeof constats === 'string') { try { constats = JSON.parse(constats); } catch { constats = []; } }\nif (!Array.isArray(constats)) constats = [];\nconst aExaminer = constats.filter((c) => c && c.gravite === 'a_examiner' && c.code && c.texte);\n\nconst revueDuJour = (input.revue_jour_date || '').toString().trim() === jourParis;\nconst dejaRepondu = (input.advisor_revue_date || '').toString().trim() === jourParis;\n// Pas de relance au-dela de 10 h : un modele qui echoue ne doit pas etre\n// rappele toute la journee (le courriel part a 08:15 sans lui).\nconst heureParis = Number(new Intl.DateTimeFormat('en-GB', { timeZone: 'Europe/Paris', hour: '2-digit', hour12: false }).format(new Date()));\n\nconst ctx = {\n advisorEnabled: onOff(input.advisor_enabled),\n jour: jourParis,\n revueDuJour,\n dejaRepondu,\n constats: aExaminer.map((c) => ({ code: c.code, texte: String(c.texte), contexte: String(c.contexte || '') })),\n};\n\nconst systemPrompt = [\n \"Tu es le RELECTEUR du systeme energie d'une maison en Bretagne : 16 kWc de solaire,\",\n 'batterie de ~23 kWh utiles (0,30 kWh par point de SOC), Victron ESS SANS injection',\n '(le solaire est bride quand personne ne le prend), abonnement EDF 12 kVA, contacteur',\n 'EDF ouvert la plupart du temps, heures creuses 01:24-07:24 et 12:24-14:24, borne de',\n 'recharge pour trois vehicules. Des automates decident : le controleur de la borne,',\n \"le bilan energetique (SOC requis), l'ACM (courant de charge), l'hysteresis EDF, des\",\n 'automatisations Home Assistant.',\n '',\n 'On te donne des CONSTATS MESURES sur les dernieres 24 h. Ce sont des FAITS : ne les',\n 'conteste pas et ne les recalcule pas. Pour chacun, propose UNE piste : la cause la',\n 'plus probable et ce qu il faut verifier ou corriger. Tu ne pilotes rien.',\n '',\n 'REGLES ABSOLUES :',\n '- N ecris AUCUN chiffre qui ne figure pas dans le constat ou son contexte.',\n '- Si tu ne vois pas de cause, dis « cause non identifiable avec ces donnees ».',\n '- Une piste fait au plus 280 caracteres, en francais, sans jargon inutile.',\n '- Le contexte est la raison publiee par le controleur au debut de l episode.',\n '',\n 'Reponds UNIQUEMENT en JSON : {\"pistes\":[{\"code\":\"<code du constat>\",\"piste\":\"<texte>\"}]}',\n].join('\\n');\n\nconst userPrompt = [\n `Revue du ${jourParis}. ${aExaminer.length} constat(s) a examiner :`,\n '',\n ...aExaminer.map((c, i) => `${i + 1}. code=${c.code}\\n constat : ${c.texte}\\n contexte : ${String(c.contexte || '(aucun)').slice(0, 400)}`),\n '',\n 'JSON uniquement.',\n].join('\\n');\n\nreturn [{\n json: {\n enabled: ctx.advisorEnabled && revueDuJour && !dejaRepondu && aExaminer.length > 0 && heureParis < 10,\n messages: [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: userPrompt },\n ],\n context: ctx,\n },\n}];\n"
|
"jsCode": "// =====================================================\n// EVCS ADVISOR — BUILD_PROMPT (RELECTEUR DE LA JOURNEE, 08/10/2026)\n//\n// Le conseiller n'observe plus l'instantane toutes les 15 min : 1 500 « rien a\n// signaler » contre 871 signalements sans suite, et aucun des rates du 07/10\n// (HC de midi perdue, EDF en heures pleines a 18:03, VW bloquee de 01:24 a\n// 05:14 avec EDF ferme) n'avait ete vu. Il RELIT desormais, une fois par jour,\n// les constats CALCULES par le service d'historisation (tools/revue-jour.mjs,\n// sensor.evcs_revue_jour) et propose pour chacun une piste a examiner.\n// Doctrine §79 : les chiffres viennent du code, le modele ne fait que relire.\n// Il ne pilote rien.\n//\n// Il tourne quand : le conseiller est actif, la revue du jour est publiee, il\n// y a au moins un point a examiner, et il n'a pas deja repondu aujourd'hui.\n// =====================================================\n\nconst raw = $input.first().json;\nconst input = typeof raw.data === 'string' ? JSON.parse(raw.data) : (raw.data || raw);\nconst onOff = (v) => (v ?? 'off').toString().trim().toLowerCase() === 'on';\n\nconst jourParis = new Date().toLocaleDateString('sv-SE', { timeZone: 'Europe/Paris' });\nlet constats = input.revue_jour_constats;\nif (typeof constats === 'string') { try { constats = JSON.parse(constats); } catch { constats = []; } }\nif (!Array.isArray(constats)) constats = [];\nconst aExaminer = constats.filter((c) => c && c.gravite === 'a_examiner' && c.code && c.texte);\n\nconst revueDuJour = (input.revue_jour_date || '').toString().trim() === jourParis;\nconst dejaRepondu = (input.advisor_revue_date || '').toString().trim() === jourParis;\n// Pas de relance au-dela de 10 h : un modele qui echoue ne doit pas etre\n// rappele toute la journee (le courriel part a 08:15 sans lui).\nconst heureParis = Number(new Intl.DateTimeFormat('en-GB', { timeZone: 'Europe/Paris', hour: '2-digit', hour12: false }).format(new Date()));\n\nconst ctx = {\n advisorEnabled: onOff(input.advisor_enabled),\n jour: jourParis,\n revueDuJour,\n dejaRepondu,\n constats: aExaminer.map((c) => ({ code: c.code, texte: String(c.texte), contexte: String(c.contexte || ''), etat: String(c.etat || '') })),\n};\n\nconst systemPrompt = [\n \"Tu es le RELECTEUR du systeme energie d'une maison en Bretagne : 16 kWc de solaire,\",\n 'batterie de ~23 kWh utiles (0,30 kWh par point de SOC), Victron ESS SANS injection',\n '(le solaire est bride quand personne ne le prend), abonnement EDF 12 kVA, contacteur',\n 'EDF ouvert la plupart du temps, heures creuses 01:24-07:24 et 12:24-14:24, borne de',\n 'recharge pour trois vehicules. Des automates decident : le controleur de la borne,',\n \"le bilan energetique (SOC requis), l'ACM (courant de charge), l'hysteresis EDF, des\",\n 'automatisations Home Assistant.',\n '',\n 'On te donne des CONSTATS MESURES sur les dernieres 24 h. Ce sont des FAITS : ne les',\n 'conteste pas et ne les recalcule pas. Pour chacun, propose UNE piste : la cause la',\n 'plus probable et ce qu il faut verifier ou corriger. Tu ne pilotes rien.',\n '',\n 'REGLES ABSOLUES :',\n '- N ecris AUCUN chiffre qui ne figure pas dans le constat ou son contexte.',\n '- Si tu ne vois pas de cause, dis « cause non identifiable avec ces donnees ».',\n '- Une piste fait au plus 280 caracteres, en francais, sans jargon inutile.',\n '- Le contexte est la raison publiee par le controleur au debut de l episode.',\n '- L etat donne les leviers a ce moment-la : consigne reseau, courant de charge max fixe',\n ' par l ACM, mode de nuit (sans_edf : la voiture ne fait pas fermer EDF ; complete : elle',\n ' charge sur EDF en HC), budget voiture sans EDF, SOC requis, cible du matin. Cherche la',\n ' cause d abord dans ces leviers (un courant ACM bas, une consigne egale a la maison, un',\n ' mode sans_edf avec budget 0 expliquent souvent le constat).',\n '',\n 'Reponds UNIQUEMENT en JSON : {\"pistes\":[{\"code\":\"<code du constat>\",\"piste\":\"<texte>\"}]}',\n].join('\\n');\n\nconst userPrompt = [\n `Revue du ${jourParis}. ${aExaminer.length} constat(s) a examiner :`,\n '',\n ...aExaminer.map((c, i) => `${i + 1}. code=${c.code}\\n constat : ${c.texte}\\n contexte : ${String(c.contexte || '(aucun)').slice(0, 400)}\\n etat : ${String(c.etat || '(inconnu)').slice(0, 600)}`),\n '',\n 'JSON uniquement.',\n].join('\\n');\n\nreturn [{\n json: {\n enabled: ctx.advisorEnabled && revueDuJour && !dejaRepondu && aExaminer.length > 0 && heureParis < 10,\n messages: [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: userPrompt },\n ],\n context: ctx,\n },\n}];\n"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -235,7 +235,7 @@
|
|||||||
-30
|
-30
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"jsCode": "// =====================================================\n// EVCS ADVISOR — VALIDATE_PARSE (relecteur, 08/10/2026)\n// Valide STRICTEMENT la sortie LLM (locale OU cloud, format OpenAI).\n// - JSON {\"pistes\":[{code, piste}]} ; sinon REJET TOTAL.\n// - une piste dont le code n'est pas celui d'un constat est ecartee ;\n// - ANTI-INVENTION : une piste qui contient un nombre absent du constat et de\n// son contexte est ecartee (le modele ne produit aucun chiffre, §79).\n// =====================================================\n\nconst resp = $input.first().json;\nconst context = $('BUILD_PROMPT').first().json.context;\n\n// QUI A REPONDU (08/10, proprietaire : « OpenRouter, garde-le mais affiche-le\n// clairement »). Le repli cloud n'est appele que si le modele local de la Spark\n// echoue ; dans ce cas les constats ont quitte la maison et il faut le dire.\nconst viaCloud = (() => { try { return $('LLM_CLOUD').isExecuted === true; } catch { return false; } })()\n || typeof resp?.provider === 'string' || /^gen-/.test(String(resp?.id || ''));\nconst source = viaCloud ? 'openrouter' : 'local';\n\nfunction fail(status, detail) {\n return [{ json: { status, detail: (detail || '').toString().slice(0, 200), pistes: null, rejetees: 0, ecartees: [],\n source, modelUsed: (resp?.model || 'aucun').toString().slice(0, 60), context } }];\n}\n\nconst content = resp?.choices?.[0]?.message?.content;\nif (typeof content !== 'string' || !content.trim()) {\n return fail('llm_offline', resp?.error?.message || resp?.error || 'aucune réponse LLM');\n}\nconst text = content.replace(/<think>[\\s\\S]*?<\\/think>/gi, '').replace(/```(?:json)?/gi, '').trim();\nconst start = text.indexOf('{');\nif (start === -1) return fail('invalid', 'aucun JSON dans la réponse');\nlet depth = 0, end = -1;\nfor (let i = start; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n}\nif (end === -1) return fail('invalid', 'JSON non terminé');\nlet obj;\ntry { obj = JSON.parse(text.slice(start, end + 1)); } catch (e) { return fail('invalid', `JSON invalide: ${e.message}`); }\nif (!Array.isArray(obj.pistes)) return fail('invalid', 'champ pistes absent');\n\n// Nombres d'un texte, normalises (virgule decimale -> point, sans zeros inutiles).\nconst nombres = (s) => (String(s).match(/\\d+(?:[.,]\\d+)?/g) || []).map((x) => String(Number(x.replace(',', '.'))));\n\nconst parCode = new Map(context.constats.map((c) => [c.code, c]));\n// Les pistes ECARTEES sont gardees avec leur motif : elles sont historisees\n// pour rejouer le conseiller plus tard (regression) et juger s'il progresse.\nconst pistes = [];\nconst ecartees = [];\nconst ecarter = (code, piste, motif) => ecartees.push({ code, piste: piste.slice(0, 280), motif });\nfor (const p of obj.pistes) {\n const code = (p?.code ?? '').toString().trim();\n const piste = (p?.piste ?? '').toString().trim();\n const c = parCode.get(code);\n if (!piste) { ecarter(code, '', 'piste vide'); continue; }\n if (!c) { ecarter(code, piste, 'code inconnu ou constat non a examiner'); continue; }\n const permis = new Set(nombres(`${c.texte} ${c.contexte}`));\n const inventes = nombres(piste).filter((n) => !permis.has(n));\n if (inventes.length) { ecarter(code, piste, `chiffre absent du constat : ${inventes.join(', ')}`); continue; }\n if (pistes.some((x) => x.code === code)) { ecarter(code, piste, 'doublon'); continue; }\n pistes.push({ code, piste: piste.slice(0, 280) });\n}\n\nreturn [{ json: { status: 'ok', detail: '', pistes, rejetees: ecartees.length, ecartees, source,\n modelUsed: (resp.model || 'inconnu').toString().slice(0, 60), context } }];\n"
|
"jsCode": "// =====================================================\n// EVCS ADVISOR — VALIDATE_PARSE (relecteur, 08/10/2026)\n// Valide STRICTEMENT la sortie LLM (locale OU cloud, format OpenAI).\n// - JSON {\"pistes\":[{code, piste}]} ; sinon REJET TOTAL.\n// - une piste dont le code n'est pas celui d'un constat est ecartee ;\n// - ANTI-INVENTION : une piste qui contient un nombre absent du constat et de\n// son contexte est ecartee (le modele ne produit aucun chiffre, §79).\n// =====================================================\n\nconst resp = $input.first().json;\nconst context = $('BUILD_PROMPT').first().json.context;\n\n// QUI A REPONDU (08/10, proprietaire : « OpenRouter, garde-le mais affiche-le\n// clairement »). Le repli cloud n'est appele que si le modele local de la Spark\n// echoue ; dans ce cas les constats ont quitte la maison et il faut le dire.\nconst viaCloud = (() => { try { return $('LLM_CLOUD').isExecuted === true; } catch { return false; } })()\n || typeof resp?.provider === 'string' || /^gen-/.test(String(resp?.id || ''));\nconst source = viaCloud ? 'openrouter' : 'local';\n\nfunction fail(status, detail) {\n return [{ json: { status, detail: (detail || '').toString().slice(0, 200), pistes: null, rejetees: 0, ecartees: [],\n source, modelUsed: (resp?.model || 'aucun').toString().slice(0, 60), context } }];\n}\n\nconst content = resp?.choices?.[0]?.message?.content;\nif (typeof content !== 'string' || !content.trim()) {\n return fail('llm_offline', resp?.error?.message || resp?.error || 'aucune réponse LLM');\n}\nconst text = content.replace(/<think>[\\s\\S]*?<\\/think>/gi, '').replace(/```(?:json)?/gi, '').trim();\nconst start = text.indexOf('{');\nif (start === -1) return fail('invalid', 'aucun JSON dans la réponse');\nlet depth = 0, end = -1;\nfor (let i = start; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n}\nif (end === -1) return fail('invalid', 'JSON non terminé');\nlet obj;\ntry { obj = JSON.parse(text.slice(start, end + 1)); } catch (e) { return fail('invalid', `JSON invalide: ${e.message}`); }\nif (!Array.isArray(obj.pistes)) return fail('invalid', 'champ pistes absent');\n\n// Nombres d'un texte, normalises (virgule decimale -> point, sans zeros inutiles).\nconst nombres = (s) => (String(s).match(/\\d+(?:[.,]\\d+)?/g) || []).map((x) => String(Number(x.replace(',', '.'))));\n\nconst parCode = new Map(context.constats.map((c) => [c.code, c]));\n// Les pistes ECARTEES sont gardees avec leur motif : elles sont historisees\n// pour rejouer le conseiller plus tard (regression) et juger s'il progresse.\nconst pistes = [];\nconst ecartees = [];\nconst ecarter = (code, piste, motif) => ecartees.push({ code, piste: piste.slice(0, 280), motif });\nfor (const p of obj.pistes) {\n const code = (p?.code ?? '').toString().trim();\n const piste = (p?.piste ?? '').toString().trim();\n const c = parCode.get(code);\n if (!piste) { ecarter(code, '', 'piste vide'); continue; }\n if (!c) { ecarter(code, piste, 'code inconnu ou constat non a examiner'); continue; }\n const permis = new Set(nombres(`${c.texte} ${c.contexte} ${c.etat || ''}`)\n .flatMap((n) => [n, String(Math.round(Number(n))), String(Math.round(Number(n) * 10) / 10)]));\n const inventes = nombres(piste).filter((n) => !permis.has(n));\n if (inventes.length) { ecarter(code, piste, `chiffre absent du constat : ${inventes.join(', ')}`); continue; }\n if (pistes.some((x) => x.code === code)) { ecarter(code, piste, 'doublon'); continue; }\n pistes.push({ code, piste: piste.slice(0, 280) });\n}\n\nreturn [{ json: { status: 'ok', detail: '', pistes, rejetees: ecartees.length, ecartees, source,\n modelUsed: (resp.model || 'inconnu').toString().slice(0, 60), context } }];\n"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user