diff --git a/services/targo-hub/lib/agent-tools.json b/services/targo-hub/lib/agent-tools.json index 00c7019..15fb1b7 100644 --- a/services/targo-hub/lib/agent-tools.json +++ b/services/targo-hub/lib/agent-tools.json @@ -19,7 +19,8 @@ { "audience": "staff", "mode": "read", "title": "Compétence requise (diagnostic)", "type": "function", "function": { "name": "resolve_skill", "description": "STAFF/dispatch. DIAGNOSTIC : à partir de la RAISON décrite en langage naturel (ex. « voir signal fibre », « pas d'internet », « installation », « ajout TV ») → détermine la/les COMPÉTENCE(S) terrain requise(s) + la confiance. LECTURE. À appeler AVANT de créer/assigner une intervention, pour cibler la ressource ; enchaîne ensuite find_slot (avec la compétence) pour la disponibilité. Si la raison est vague, pose UNE question avant.", "parameters": { "type": "object", "properties": { "text": { "type": "string", "description": "La raison / le problème décrit par l'utilisateur ou le client" }, "department": { "type": "string", "description": "Département/motif si déjà connu (optionnel)" } }, "required": ["text"] } } }, { "audience": "staff", "mode": "read", "title": "Trouver un créneau", "type": "function", "function": { "name": "find_slot", "description": "STAFF/dispatch. Propose les meilleurs créneaux disponibles (technicien + date + heure) selon la durée et la compétence requise. LECTURE — ne réserve rien ; présente les options à l'utilisateur.", "parameters": { "type": "object", "properties": { "duration_h": { "type": "number", "description": "Durée en heures (défaut 1)" }, "skill": { "type": "string", "description": "Compétence requise (optionnel)" }, "after_date": { "type": "string", "description": "Chercher à partir de cette date AAAA-MM-JJ (défaut aujourd'hui)" } }, "required": [] } } }, { "audience": "staff", "mode": "read", "title": "Ressources disponibles", "type": "function", "function": { "name": "resource_availability", "description": "STAFF/dispatch. DIAGNOSTIC : combien de techniciens QUALIFIÉS pour une compétence sont DISPONIBLES sur une période, et combien d'heures libres. LECTURE. À appeler après resolve_skill pour répondre « a-t-on la ressource ? » avant de proposer ; find_slot donnera ensuite les créneaux précis.", "parameters": { "type": "object", "properties": { "skill": { "type": "string", "description": "Compétence requise (issue de resolve_skill), ex. réparation, installation" }, "after_date": { "type": "string", "description": "Dès cette date AAAA-MM-JJ (défaut aujourd'hui)" }, "days": { "type": "number", "description": "Fenêtre en jours (défaut 7)" } }, "required": [] } } }, - { "audience": "staff", "mode": "read", "title": "État du service (modem/signal)", "type": "function", "function": { "name": "check_service", "description": "STAFF/dispatch. DIAGNOSTIC À DISTANCE : état LIVE du service d'un client — modem/ONU en ligne ou hors ligne, puissance du signal (Rx dBm), abonnement. LECTURE. À utiliser AVANT de proposer un déplacement pour un problème de connexion : si l'équipement 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é. Fournis customer (id C-xxxx) ou query (nom/adresse/téléphone/courriel).", "parameters": { "type": "object", "properties": { "customer": { "type": "string", "description": "ID client (C-xxxx) si connu" }, "query": { "type": "string", "description": "Sinon : nom / adresse / téléphone / courriel du client" } }, "required": [] } } }, + { "audience": "staff", "mode": "read", "title": "État du service (modem/signal)", "type": "function", "function": { "name": "check_service", "description": "STAFF/dispatch. DIAGNOSTIC À DISTANCE : état LIVE du service d'un client — modem/ONU en ligne ou hors ligne, puissance du signal (Rx dBm) si connue, abonnement. RAPIDE (statut passif). LECTURE. À utiliser AVANT de proposer un déplacement pour un problème de connexion : si l'équipement 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é. Si tu as besoin de la FORCE du signal et qu'elle est absente ici, appelle check_signal. Fournis customer (id C-xxxx) ou query (nom/adresse/téléphone/courriel).", "parameters": { "type": "object", "properties": { "customer": { "type": "string", "description": "ID client (C-xxxx) si connu" }, "query": { "type": "string", "description": "Sinon : nom / adresse / téléphone / courriel du client" } }, "required": [] } } }, + { "audience": "staff", "mode": "read", "title": "Signal en direct (comme F)", "type": "function", "function": { "name": "check_signal", "description": "STAFF. MESURE LE SIGNAL EN DIRECT de l'équipement d'un client — parité avec le « Do Stuff get » de F. FIBRE : interroge l'OLT → Rx/Tx dBm, distance, uptime. SANS-FIL (airOS) : signal + signal AP (dBm), CCQ, débit TX/RX, capacité airMAX du CPE. LENT (~10-30 s : interrogation en direct) → préviens « un instant, je mesure le signal… ». À appeler dès qu'on veut la QUALITÉ/FORCE du signal, ou si check_service ne l'a pas. NE conclus JAMAIS « signal non mesurable » sans avoir appelé cet outil. Fournis customer (id C-xxxx) ou serial.", "parameters": { "type": "object", "properties": { "customer": { "type": "string", "description": "ID client C-xxxx (via find_customer)" }, "serial": { "type": "string", "description": "N° de série de l'équipement (optionnel si customer fourni)" } }, "required": [] } } }, { "audience": "staff", "mode": "read", "title": "Trouver un client", "type": "function", "function": { "name": "find_customer", "description": "STAFF. Recherche un client par nom / adresse / téléphone / courriel et renvoie les CANDIDATS probables. LECTURE. À appeler dès qu'un client n'est PAS déjà identifié par son id (C-xxxx) : UN seul candidat → prends son id et continue ; PLUSIEURS → NE DEVINE PAS, demande brièvement « lequel ? » — l'interface affiche la liste cliquable des candidats, ne les réénumère donc pas tous en texte. Enchaîne l'action avec l'id choisi.", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "Nom, adresse, téléphone ou courriel (même partiel)" } }, "required": ["query"] } } }, { "audience": "staff", "mode": "read", "title": "Aperçu client (360)", "type": "function", "function": { "name": "customer_overview", "description": "STAFF. Aperçu 360 d'un client : identité, SOLDE du compte, abonnements actifs, tickets ouverts. LECTURE. Utilise pour « c'est qui ? », « résume le compte », l'état général. Fournis customer (id C-xxxx obtenu via find_customer).", "parameters": { "type": "object", "properties": { "customer": { "type": "string", "description": "ID client C-xxxx (via find_customer)" } }, "required": ["customer"] } } }, { "audience": "staff", "mode": "read", "title": "Solde et factures", "type": "function", "function": { "name": "customer_balance", "description": "STAFF. SOLDE du compte (montant total dû) + factures récentes (montants, dates, statut de paiement). LECTURE. Utilise pour « quel est le solde / le montant dû », « factures impayées ». Fournis customer (id C-xxxx).", "parameters": { "type": "object", "properties": { "customer": { "type": "string", "description": "ID client C-xxxx" } }, "required": ["customer"] } } }, diff --git a/services/targo-hub/lib/staff-agent.js b/services/targo-hub/lib/staff-agent.js index 9d0cda0..f8c974f 100644 --- a/services/targo-hub/lib/staff-agent.js +++ b/services/targo-hub/lib/staff-agent.js @@ -136,12 +136,62 @@ async function read_check_service ({ customer, query } = {}) { } const s = r.summary || {} let advice - if (s.online === true) advice = 'Équipement EN LIGNE' + (s.rxPower != null ? ' (Rx ' + s.rxPower + ' dBm)' : '') + " → tente d'abord un correctif À DISTANCE (redémarrage / reprovision) ; un déplacement n'est probablement PAS nécessaire." - else if (s.online === false) advice = 'Équipement HORS LIGNE' + (s.rxPower != null ? ' (Rx ' + s.rxPower + ' dBm)' : '') + ' → signal faible/nul = DÉPLACEMENT probable (réparation fibre) ; sinon propose un redémarrage avant.' - else advice = "Statut indéterminé — pas de lecture fiable de l'équipement." + // Le signal (Rx/Tx dBm) n'est PAS toujours dans le statut passif (fibre tech-3 non couverte par la sonde locale) → + // NE PAS conclure « non mesurable » : la QUALITÉ du signal se mesure EN DIRECT via check_signal (comme F). + const needSignal = s.rxPower == null ? ' Pour la FORCE du signal (Rx/Tx dBm en direct), appelle check_signal — ne dis pas « non mesurable ».' : '' + if (s.online === true) advice = 'Équipement EN LIGNE' + (s.rxPower != null ? ' (Rx ' + s.rxPower + ' dBm)' : '') + " → tente d'abord un correctif À DISTANCE (redémarrage / reprovision) ; un déplacement n'est probablement PAS nécessaire." + needSignal + else if (s.online === false) advice = 'Équipement HORS LIGNE' + (s.rxPower != null ? ' (Rx ' + s.rxPower + ' dBm)' : '') + ' → signal faible/nul = DÉPLACEMENT probable (réparation fibre) ; sinon propose un redémarrage avant.' + needSignal + else advice = "Statut indéterminé — pas de lecture fiable de l'équipement." + needSignal return { found: true, customer: r.customer && r.customer.name, customer_name: r.customer && r.customer.customer_name, online: s.online, rx_power_dbm: s.rxPower != null ? s.rxPower : null, signal: s.signal || null, status_label: s.label || '', source: s.source || '', subscriptions: (r.subscriptions || []).map(x => (x.plan || '?') + ' · ' + (x.status || '?')), advice } } catch (e) { return { error: e.message } } } +// Qualité du signal FIBRE (Rx optique, dBm). Plage normale ~ -8 à -26 ; plus négatif = plus faible. +function fibreAdvice (rx) { + if (rx == null) return null + if (rx > -8) return 'Rx élevé (' + rx + ' dBm) — atténuation très faible ; OK (vérifier sur-puissance si tout proche).' + if (rx >= -25) return 'Rx BON (' + rx + ' dBm) — un correctif à distance suffit probablement, pas de déplacement.' + if (rx >= -28) return 'Rx FAIBLE (' + rx + ' dBm) — à surveiller ; connecteur/soudure fibre à vérifier.' + return 'Rx CRITIQUE (' + rx + ' dBm) — DÉPLACEMENT probable (réparation/soudure fibre).' +} +// Qualité du signal SANS-FIL (airOS, dBm). -50 excellent … < -78 critique. +function wifiAdvice (s) { + if (s == null) return null + if (s >= -60) return 'Signal BON (' + s + ' dBm).' + if (s >= -70) return 'Signal CORRECT (' + s + ' dBm) — peut faiblir par mauvais temps.' + if (s >= -78) return 'Signal FAIBLE (' + s + ' dBm) — réalignement d’antenne probable.' + return 'Signal CRITIQUE (' + s + ' dBm) — DÉPLACEMENT (réalignement / ligne de vue) probable.' +} +// SIGNAL EN DIRECT (parité « Do Stuff get » de F). FIBRE : dostuff n8n (tech-3, Rx/Tx dBm) → repli poller SNMP (tech-2). +// SANS-FIL : ops_airos (pont F : signal/CCQ/débit/airMAX). Résout l'équipement du client depuis Service Equipment. +async function read_check_signal ({ customer, serial } = {}) { + try { + let eq = null + const F = ['name', 'serial_number', 'mac_address', 'ip_address', 'olt_ip', 'customer', 'status', 'equipment_type', 'brand', 'model'] + if (serial) { const r = await erp.list('Service Equipment', { filters: [['serial_number', '=', String(serial)]], fields: F, limit: 1 }); eq = r && r[0] } + if (!eq && customer) { + const r = await erp.list('Service Equipment', { filters: [['customer', '=', String(customer)]], fields: F, limit: 20 }) + const act = (r || []).filter(e => String(e.status || '').toLowerCase() !== 'retiré') + eq = act.find(e => e.olt_ip && e.serial_number) || act.find(e => e.ip_address && e.serial_number) || act.find(e => e.olt_ip || e.ip_address) || act[0] || (r || [])[0] + } + if (!eq) return { error: 'Aucun équipement réseau pour ce client — signal non récupérable. Vérifie que l’appareil est rattaché (Service Equipment).' } + const sn = eq.serial_number || '', olt = eq.olt_ip || '', ip = eq.ip_address || '', mac = eq.mac_address || '' + const tc = require('./ticket-collab') + // ── FIBRE (olt_ip) : F get = dostuff (OLT en direct). tech-3 = n8n ; tech-2 = repli sonde SNMP locale. ── + if (olt) { + const live = await tc.dostuffStatus({ sn, olt }) + if (live && live.ok) return { found: true, kind: 'fibre', serial: sn, online: live.online, rx_dbm: live.rxPower, tx_dbm: live.txPower, signal: live.signal, distance_m: live.distance, uptime: live.uptime, wan_ip: live.wanIp, firmware: live.firmware, source: 'dostuff (OLT en direct, comme F)', advice: fibreAdvice(live.rxPower) } + try { const onu = require('./olt-snmp').getOnuBySerial(String(sn)); if (onu && onu.rxPower != null) return { found: true, kind: 'fibre', serial: sn, rx_dbm: onu.rxPower, tx_dbm: (onu.txPower != null ? onu.txPower : null), source: 'olt-snmp (sonde locale, tech-2)', advice: fibreAdvice(onu.rxPower) } } catch (e) { /* poller indispo */ } + return { found: true, kind: 'fibre', serial: sn, rx_dbm: null, online: (live && live.online != null) ? live.online : null, note: 'Signal live non remonté (OLT lente ou hors couverture). Réessaie, ou propose un redémarrage.', advice: null } + } + // ── SANS-FIL (airOS) : F get = ops_airos (pont F). Signal / CCQ / débit / capacité airMAX du CPE. ── + if (ip || mac) { + const air = await tc.airosSignal({ sn, ip, mac }) + if (air && air.ok) return { found: true, kind: 'sans-fil', serial: sn, model: air.model, signal_dbm: air.signal, signal_ap_dbm: air.signalAp, ccq_pct: air.ccq, tx_rate: air.txRate, rx_rate: air.rxRate, airmax_down_mbps: air.capDown, airmax_up_mbps: air.capUp, uptime: air.uptime, source: 'ops_airos (CPE en direct, comme F)', advice: wifiAdvice(air.signal) } + return { found: true, kind: 'sans-fil', serial: sn, note: (air && air.error) || 'CPE injoignable', advice: null } + } + return { found: true, serial: sn, note: 'Équipement sans coordonnées réseau (ni OLT ni IP de gestion) — signal non récupérable ; vérifie le rattachement de l’appareil.' } + } catch (e) { return { error: e.message } } +} // Recherche floue d'un client → CANDIDATS (l'UI les rend cliquables dans le fil). Réutilise /collab/customer-search // (même endpoint que l'ancien typeahead) MAIS c'est l'ASSISTANT qui liste les options — plus besoin d'un 2e champ. async function read_find_customer ({ query } = {}) { @@ -196,7 +246,7 @@ async function read_customer_history ({ customer } = {}) { return { customer: cid, count: tickets.length, open_count: openN, tickets, note: tickets.length ? `${tickets.length} ticket(s) au dossier (${openN} ouvert(s)) — des problèmes sont déjà survenus.` : 'Aucun ticket au dossier — aucun problème enregistré pour ce client.' } } catch (e) { return { error: e.message } } } -const READERS = { list_technicians: read_list_technicians, get_job: read_get_job, find_slot: read_find_slot, resolve_skill: read_resolve_skill, resource_availability: read_resource_availability, check_service: read_check_service, find_customer: read_find_customer, customer_overview: read_customer_overview, customer_balance: read_customer_balance, customer_subscriptions: read_customer_subscriptions, customer_equipment: read_customer_equipment, customer_history: read_customer_history } +const READERS = { list_technicians: read_list_technicians, get_job: read_get_job, find_slot: read_find_slot, resolve_skill: read_resolve_skill, resource_availability: read_resource_availability, check_service: read_check_service, check_signal: read_check_signal, find_customer: read_find_customer, customer_overview: read_customer_overview, customer_balance: read_customer_balance, customer_subscriptions: read_customer_subscriptions, customer_equipment: read_customer_equipment, customer_history: read_customer_history } // ── Jours de la semaine — clés du weekly_schedule (= DOW_KEYS de roster.js) ── const DAY_KEYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] @@ -402,10 +452,11 @@ function systemPrompt (email) { Aujourd'hui = ${todayET()}. Tu agis AU NOM de l'utilisateur connecté${email ? ` (${email})` : ''}. Tu transformes une commande en langage naturel en APPELS D'OUTILS. RÈGLES : -- Utilise d'abord les outils de LECTURE (find_customer, customer_overview, customer_balance, customer_subscriptions, customer_equipment, customer_history, list_technicians, get_job, resolve_skill, resource_availability, find_slot, check_service) pour RÉSOUDRE, RENSEIGNER et DIAGNOSTIQUER. Résous toujours un technicien nommé (« Simon ») en tech_id via list_technicians avant d'agir. +- Utilise d'abord les outils de LECTURE (find_customer, customer_overview, customer_balance, customer_subscriptions, customer_equipment, customer_history, list_technicians, get_job, resolve_skill, resource_availability, find_slot, check_service, check_signal) pour RÉSOUDRE, RENSEIGNER et DIAGNOSTIQUER. Résous toujours un technicien nommé (« Simon ») en tech_id via list_technicians avant d'agir. - IDENTIFIER LE CLIENT : si le client n'est pas donné par son id (C-xxxx), appelle find_customer avec ce que tu as (nom, adresse, téléphone, courriel). UN seul candidat → prends son id et continue. PLUSIEURS → NE DEVINE PAS : demande brièvement « lequel ? » ; l'interface affiche la liste cliquable des candidats, donc NE les réénumère PAS tous en texte. Utilise ensuite l'id choisi pour l'action. - QUESTIONS CLIENT (solde, montant dû, factures, abonnements, forfait, équipement, historique, « c'est qui ? », « résume le compte », « un problème est-il déjà arrivé ? ») : résous d'abord le client (find_customer → id C-xxxx), puis appelle le bon lecteur — customer_overview (résumé 360 : identité + SOLDE + abonnements + tickets ouverts), customer_balance (solde + factures), customer_subscriptions, customer_equipment, customer_history (tickets passés = problème déjà survenu ?). LECTURE : réponds directement, en français, sans étape de confirmation. Cite le SOLDE en dollars quand on le demande. -- 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. +- PROBLÈME DE CONNEXION (« pas d'internet », « lent », « 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. +- QUALITÉ / FORCE DU SIGNAL (« quel est son signal ? », « signal fibre/sans-fil », « c'est bon comme signal ? », ou dès que check_service renvoie un signal nul) : appelle **check_signal** — il MESURE EN DIRECT (comme le « Do Stuff get » de F) : fibre = Rx/Tx dBm + distance via l'OLT ; sans-fil (airOS) = signal/CCQ/débit/capacité airMAX du CPE. C'est LENT (~10-30 s) → dis « un instant, je mesure le signal en direct… ». NE conclus JAMAIS « signal non mesurable / pas mesurable à distance » : appelle check_signal d'abord ; s'il ne remonte rien, dis-le précisément (OLT lente / hors couverture / CPE injoignable) et propose un redémarrage. - 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, 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.