feat(assistant): Do Stuff — outil device_action (OLT) dans le copilote STAFF
Le chat STAFF peut désormais lancer les 7 opérations OLT à distance (Do Stuff)
par langage naturel : reboot · speed · suspend · unsuspend · activate · replace
· remove. Un seul outil device_action(action, serial|customer_id, [profileid],
[new_sn]) → résout l'ONU du client au besoin, APERÇU via olt-ops.plan (effet,
cible, avertissements, can_run), puis exécution via olt-ops.run.
Sécurité (écriture réseau conséquente) :
- staged comme WRITE → aperçu + bouton « Confirmer et exécuter » (jamais auto).
- double-verrou : confirm humain + run({confirm:true}) du module + idempotencyKey
(fixé au plan → un double-clic rejoue la même clé) + acteur journalisé.
- permission reboot_provision (capacité « Équipement » existante, assignable).
- can_run honoré : une action non branchée pour la techno de l'ONU (ex. reboot
tech-3 = TR-069) n'est PAS proposée — l'assistant l'explique.
- prompt : APPELER l'outil EST la proposition (pas de « voulez-vous ? » en texte).
Vérifié live : NL « suspends l'internet de C-LPB4 » → device_action(suspend)
staged, severity high, cible TPLGC4160688/OLT résolue. GET /staff-agent/tools
liste device_action (15 outils).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
19e2c1fea0
commit
473b360c4c
|
|
@ -27,5 +27,6 @@
|
|||
{ "audience": "staff", "mode": "write", "permission": "assign_jobs", "title": "Reporter le rendez-vous", "type": "function", "function": { "name": "reschedule_job", "description": "STAFF/dispatch. Reporte un Dispatch Job à une nouvelle date (et heure). Conserve le technicien assigné si possible. ÉCRITURE — confirmée avant exécution.", "parameters": { "type": "object", "properties": { "job": { "type": "string", "description": "Identifiant du Dispatch Job" }, "date": { "type": "string", "description": "Nouvelle date AAAA-MM-JJ" }, "start": { "type": "string", "description": "Nouvelle heure HH:MM (optionnel)" } }, "required": ["job", "date"] } } },
|
||||
{ "audience": "staff", "mode": "write", "permission": "assign_jobs", "title": "Proposer un RDV au client", "type": "function", "function": { "name": "proposer_rdv_client", "description": "STAFF/dispatch. Envoie au CLIENT un lien (par texto) pour qu'il choisisse LUI-MÊME son rendez-vous : soit il prend un créneau proposé (réservé 5 min), soit il propose ses 3 disponibilités. À utiliser après un diagnostic quand une intervention est requise mais qu'on laisse le client choisir l'heure, plutôt que d'assigner soi-même un technicien. ÉCRITURE — confirmée avant exécution. Requiert un Dispatch Job OUVERT : fournis job, ou ticket (Issue source), ou customer_id pour le retrouver.", "parameters": { "type": "object", "properties": { "job": { "type": "string", "description": "Identifiant du Dispatch Job (optionnel si ticket/customer_id fourni)" }, "ticket": { "type": "string", "description": "Ticket/Issue source (optionnel) — retrouve le job ouvert lié" }, "customer_id": { "type": "string", "description": "ID client (optionnel) — retrouve son dernier job ouvert" }, "message": { "type": "string", "description": "Message texto personnalisé (optionnel)" } }, "required": [] } } },
|
||||
{ "audience": "staff", "mode": "write", "permission": "assign_jobs", "title": "Créer un horaire récurrent", "type": "function", "function": { "name": "create_recurring_shift", "description": "STAFF/planification. Définit le QUART RÉCURRENT (horaire hebdomadaire) d'un technicien — matérialisé automatiquement en assignations sur l'horizon. ÉCRITURE — confirmée avant exécution. days = jours travaillés parmi mon,tue,wed,thu,fri,sat,sun. « jours de semaine » = mon,tue,wed,thu,fri ; retire les exceptions demandées (ex. « sauf le vendredi » → mon,tue,wed,thu). « fin de semaine » = sat,sun. Convertis « 8-16h » en start 08:00 / end 16:00.", "parameters": { "type": "object", "properties": { "technicien_id": { "type": "string", "description": "id du technicien (TECH-xxxx), via list_technicians" }, "days": { "type": "array", "items": { "type": "string" }, "description": "Jours travaillés: mon,tue,wed,thu,fri,sat,sun" }, "start": { "type": "string", "description": "Heure de début HH:MM, ex. 08:00" }, "end": { "type": "string", "description": "Heure de fin HH:MM, ex. 16:00" } }, "required": ["technicien_id", "days", "start", "end"] } } },
|
||||
{ "audience": "staff", "mode": "write", "title": "Suivre", "type": "function", "function": { "name": "follow_doc", "description": "STAFF. (Dé)suivre un job ou un ticket pour recevoir les notifications de mise à jour (l'utilisateur connecté devient abonné). ÉCRITURE légère — confirmée avant exécution.", "parameters": { "type": "object", "properties": { "doctype": { "type": "string", "enum": ["Dispatch Job", "Issue"], "description": "Type de document à suivre" }, "name": { "type": "string", "description": "Identifiant du job ou du ticket" }, "follow": { "type": "boolean", "description": "true = suivre (défaut), false = ne plus suivre" } }, "required": ["doctype", "name"] } } }
|
||||
{ "audience": "staff", "mode": "write", "title": "Suivre", "type": "function", "function": { "name": "follow_doc", "description": "STAFF. (Dé)suivre un job ou un ticket pour recevoir les notifications de mise à jour (l'utilisateur connecté devient abonné). ÉCRITURE légère — confirmée avant exécution.", "parameters": { "type": "object", "properties": { "doctype": { "type": "string", "enum": ["Dispatch Job", "Issue"], "description": "Type de document à suivre" }, "name": { "type": "string", "description": "Identifiant du job ou du ticket" }, "follow": { "type": "boolean", "description": "true = suivre (défaut), false = ne plus suivre" } }, "required": ["doctype", "name"] } } },
|
||||
{ "audience": "staff", "mode": "write", "permission": "reboot_provision", "title": "Action équipement (OLT)", "type": "function", "function": { "name": "device_action", "description": "STAFF/réseau (Do Stuff). Opération à distance sur l'ONU/modem d'un client via l'OLT. ÉCRITURE RÉSEAU — proposée avec un APERÇU (effet, cible, avertissements) puis CONFIRMÉE avant exécution ; idempotente ; l'acteur est journalisé. À utiliser après check_service pour un CORRECTIF À DISTANCE avant d'envoyer un technicien (ex. redémarrer un modem en ligne). Actions : reboot (redémarrer l'ONU) · speed (changer le forfait / profil de ligne, l'ONU redémarre) · suspend (couper l'internet, bascule VLAN) · unsuspend (rétablir l'internet) · activate (provisionner l'ONU) · replace (remplacer le n° de série) · remove (retirer / désactiver l'ONU). Fournis serial (n° de série ONU) OU customer_id (on résout l'ONU du client). profileid requis pour activate / speed ; new_sn requis pour replace.", "parameters": { "type": "object", "properties": { "action": { "type": "string", "enum": ["reboot", "speed", "suspend", "unsuspend", "activate", "replace", "remove"], "description": "Opération à exécuter sur l'ONU" }, "serial": { "type": "string", "description": "Numéro de série de l'ONU (ex. TPLGxxxx, RCMGxxxx)" }, "customer_id": { "type": "string", "description": "ID client (C-xxxx) — si le serial est inconnu, on résout l'ONU du client" }, "profileid": { "type": "string", "description": "N° de profil de ligne (requis pour activate et speed)" }, "new_sn": { "type": "string", "description": "Nouveau numéro de série (requis pour replace)" } }, "required": ["action"] } } }
|
||||
]
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ for (const t of STAFF_TOOLS_RAW) META[t.function.name] = { mode: t.mode || 'read
|
|||
// require paresseux (évite les cycles + n'alourdit pas le boot) — mêmes modules que l'UI appelle.
|
||||
const roster = () => require('./roster')
|
||||
const dispatch = () => require('./dispatch')
|
||||
const oltOps = () => require('./olt-ops') // Do Stuff : opérations OLT à distance (plan/run, confirm obligatoire)
|
||||
|
||||
function todayET () { return new Date().toLocaleDateString('en-CA', { timeZone: 'America/Toronto' }) }
|
||||
|
||||
|
|
@ -71,6 +72,20 @@ async function resolveTechName (techId) {
|
|||
catch { return techId }
|
||||
}
|
||||
|
||||
// Résout l'ONU (n° de série) d'un client pour device_action — préfère un ONU/fibre non retiré ; ambigu si plusieurs.
|
||||
async function resolveCustomerOnu (customerId) {
|
||||
const rows = await erp.list('Service Equipment', {
|
||||
filters: [['customer', '=', String(customerId)]],
|
||||
fields: ['name', 'serial_number', 'equipment_type', 'olt_ip', 'status'], limit: 20,
|
||||
}).catch(() => [])
|
||||
const onus = (rows || []).filter(r => r.serial_number && (r.olt_ip || /onu|ont|fib|gpon/i.test(String(r.equipment_type || ''))))
|
||||
if (!onus.length) return { error: `Aucun ONU trouvé pour le client ${customerId} — fournis le numéro de série.` }
|
||||
const active = onus.filter(o => String(o.status || '').toLowerCase() !== 'retiré')
|
||||
const pick = active.length ? active : onus
|
||||
if (pick.length > 1) return { error: `Plusieurs ONU pour ${customerId} (${pick.map(o => o.serial_number).join(', ')}) — précise le numéro de série.` }
|
||||
return { serial: pick[0].serial_number }
|
||||
}
|
||||
|
||||
// ── Outils LECTURE (exécutés pendant le PLAN — sans effet de bord) ───────────
|
||||
async function read_list_technicians ({ query, skill } = {}) {
|
||||
let techs
|
||||
|
|
@ -267,6 +282,42 @@ const WRITES = {
|
|||
return hubResult(await hubCall('POST', '/conversations/follow', { doctype: p.doctype, name: p.name, follow: p.follow !== false }, email), (p.follow !== false) ? 'Suivi activé' : 'Suivi retiré')
|
||||
},
|
||||
},
|
||||
// Do Stuff : opération à distance sur l'ONU via l'OLT (olt-ops). build = APERÇU via plan() (lecture seule) ;
|
||||
// exec = run({confirm:true}) — double-verrou (confirm humain + confirm:true module) + idempotence + acteur journalisé.
|
||||
device_action: {
|
||||
permission: 'reboot_provision', // capacité « Équipement › Reboot/provisionner » (déjà au catalogue, assignable par groupe)
|
||||
async build (p) {
|
||||
const action = String(p.action || '').trim()
|
||||
if (!oltOps().ACTIONS[action]) throw new Error(`action inconnue : « ${action} » (reboot, speed, suspend, unsuspend, activate, replace, remove)`)
|
||||
let serial = String(p.serial || '').trim()
|
||||
if (!serial && p.customer_id) { const r = await resolveCustomerOnu(p.customer_id); if (r.error) throw new Error(r.error); serial = r.serial }
|
||||
if (!serial) throw new Error('serial (n° de série ONU) ou customer_id requis')
|
||||
const opts = {}
|
||||
if (p.profileid != null && p.profileid !== '') opts.profileid = String(p.profileid)
|
||||
if (p.new_sn) opts.new_sn = String(p.new_sn)
|
||||
// APERÇU = plan olt-ops (LECTURE SEULE) : effet, cible réseau, avertissements, exécutable ?
|
||||
const pl = await oltOps().plan({ serial, action, ...opts })
|
||||
if (!pl.can_run) throw new Error(`« ${pl.label || action} » non exécutable pour ${serial}${(pl.warnings && pl.warnings.length) ? ' — ' + pl.warnings.join(' ') : ''}`)
|
||||
const who = pl.customer ? ` · client ${pl.customer}` : ''
|
||||
const warn = (pl.warnings && pl.warnings.length) ? ' ⚠ ' + pl.warnings.join(' ') : ''
|
||||
const preview = `${pl.label} — ONU ${pl.target.serial}${pl.target.olt ? ' · OLT ' + pl.target.olt : ''}${who}${(pl.effects && pl.effects.length) ? ' · ' + pl.effects.join(' ') : ''}${warn}`
|
||||
const severity = /^(suspend|remove|replace|activate)$/.test(action) ? 'high' : 'normal'
|
||||
const idempotencyKey = `sa-${action}-${serial}-${Date.now()}` // fixé au plan → un double-clic Confirmer rejoue la MÊME clé (dédup module)
|
||||
return { title: META.device_action.title, preview, severity, params: { serial, action, profileid: opts.profileid || '', new_sn: opts.new_sn || '', idempotencyKey } }
|
||||
},
|
||||
async exec (p, email) {
|
||||
const opts = {}
|
||||
if (p.profileid) opts.profileid = p.profileid
|
||||
if (p.new_sn) opts.new_sn = p.new_sn
|
||||
const r = await oltOps().run({ serial: p.serial, action: p.action, confirm: true, idempotencyKey: p.idempotencyKey, actor: email || 'ops', ...opts })
|
||||
const label = (oltOps().ACTIONS[p.action] && oltOps().ACTIONS[p.action].label) || p.action
|
||||
if (r && r.ok) {
|
||||
const extra = (r.mirrored && r.mirrored.length) ? ' · ' + r.mirrored.join(', ') : ''
|
||||
return { ok: true, message: `✅ ${label} — ${p.serial} (${r.method || '?'})${r.idempotent ? ' [déjà exécuté]' : ''}${extra}`, data: r }
|
||||
}
|
||||
return { ok: false, message: `échec « ${label} » : ${(r && (r.net_error || r.error)) || 'erreur réseau'}`, data: r }
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ── Runtime Gemini (function-calling) — même config/boucle que lib/agent.js & roster-assistant.js ──
|
||||
|
|
@ -281,9 +332,11 @@ Aujourd'hui = ${todayET()}. Tu agis AU NOM de l'utilisateur connecté${email ? `
|
|||
Tu transformes une commande en langage naturel en APPELS D'OUTILS.
|
||||
RÈGLES :
|
||||
- Utilise d'abord les outils de LECTURE (list_technicians, get_job, resolve_skill, resource_availability, find_slot, check_service) pour RÉSOUDRE et DIAGNOSTIQUER. Résous toujours un technicien nommé (« Simon ») en tech_id via list_technicians avant d'agir.
|
||||
- PROBLÈME DE CONNEXION (« pas d'internet », « lent », « signal », « hors ligne », « coupé ») : appelle check_service AVANT de proposer un déplacement. Si le modem est EN LIGNE → propose d'abord un correctif À DISTANCE (redémarrage) ; s'il est HORS LIGNE avec signal faible/nul → un déplacement (réparation) est justifié. Dis à l'utilisateur ce que tu as constaté (en ligne/hors ligne · Rx dBm) avant de recommander.
|
||||
- PROBLÈME DE CONNEXION (« pas d'internet », « lent », « signal », « hors ligne », « coupé ») : appelle check_service AVANT de proposer un déplacement. Si le modem est EN LIGNE → propose d'abord un correctif À DISTANCE via device_action(reboot) ; s'il est HORS LIGNE avec signal faible/nul → un déplacement (réparation) est justifié. Dis à l'utilisateur ce que tu as constaté (en ligne/hors ligne · Rx dBm) avant de recommander.
|
||||
- CORRECTIF À DISTANCE (Do Stuff) : device_action agit sur l'ONU du client via l'OLT — reboot (redémarrer), speed (changer le forfait), suspend/unsuspend (couper/rétablir l'internet), activate/replace/remove (provisionnement). Fournis serial OU customer_id (on résout l'ONU). Utilise-le pour éviter un déplacement quand c'est réparable à distance. ÉCRITURE RÉSEAU conséquente → toujours PROPOSÉE puis CONFIRMÉE ; n'affirme jamais que c'est fait avant confirmation.
|
||||
- INTERVENTION à créer/assigner : procède par ÉTAPES conversationnelles, ne saute PAS à la création. (1) Cerne la RAISON (si vague ou absente, pose UNE question courte plutôt que deviner). (2) resolve_skill → compétence requise. (3) resource_availability (combien de techs qualifiés dispo + heures libres) puis find_slot (créneaux précis) → DISPONIBILITÉ réelle. (4) SEULEMENT ensuite propose create_job / assign_tech / proposer_rdv_client. Résume ton diagnostic (raison → compétence → ressource dispo) AVANT de proposer, et inclus l'adresse dans create_job si fournie.
|
||||
- Les outils d'ÉCRITURE (create_job, assign_tech, proposer_rdv_client, add_assistant, set_job_status, reschedule_job, create_recurring_shift, follow_doc) NE S'EXÉCUTENT PAS tout de suite : ils sont PROPOSÉS puis CONFIRMÉS par l'utilisateur. Chaque appel te renvoie un APERÇU (staged=true). N'affirme JAMAIS qu'une action est faite ; annonce ce qui SERA fait après confirmation.
|
||||
- Les outils d'ÉCRITURE (create_job, assign_tech, proposer_rdv_client, add_assistant, set_job_status, reschedule_job, create_recurring_shift, follow_doc, device_action) NE S'EXÉCUTENT PAS tout de suite : ils sont PROPOSÉS puis CONFIRMÉS par l'utilisateur. Chaque appel te renvoie un APERÇU (staged=true). N'affirme JAMAIS qu'une action est faite ; annonce ce qui SERA fait après confirmation.
|
||||
- POUR PROPOSER une action d'écriture (même conséquente comme suspend/remove/reboot), tu DOIS APPELER l'outil : l'appel crée l'aperçu + le bouton « Confirmer et exécuter » que l'utilisateur clique. NE demande JAMAIS « voulez-vous que je procède ? » en TEXTE à la place de l'appel, et n'attends pas un « oui » avant d'appeler — l'APPEL EST la proposition, et rien ne s'exécute avant le clic de confirmation. (Poser une question ne vaut que s'il MANQUE une donnée pour construire l'appel : quel client/serial, quel job, quelle date.)
|
||||
- Horaire/quart RÉCURRENT : days parmi mon,tue,wed,thu,fri,sat,sun. « jours de semaine » = mon,tue,wed,thu,fri ; retire les exceptions (« sauf le vendredi » → mon,tue,wed,thu). « fin de semaine » = sat,sun. Convertis « 8-16h » en start 08:00 / end 16:00. Il FAUT un technicien : si aucun n'est nommé, DEMANDE-le.
|
||||
- S'il manque une information essentielle (quel job ? quel tech ? quelle date ?), pose UNE question courte au lieu d'appeler un outil d'écriture.
|
||||
- Réponds en français, bref. Après avoir proposé des actions, résume-les en une phrase et invite à confirmer.`
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user