From 17e85d88a904cc1ef2951b2be68e3b0ed53eea8b Mon Sep 17 00:00:00 2001 From: louispaulb Date: Sun, 19 Jul 2026 21:17:49 -0400 Subject: [PATCH] =?UTF-8?q?feat(dispatch-agent):=20lecteur=20check=5Fservi?= =?UTF-8?q?ce=20=E2=80=94=20diagnostic=20=C3=A0=20distance=20(modem/signal?= =?UTF-8?q?)=20avant=20un=20d=C3=A9placement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complète le diagnostic : nouveau lecteur check_service (customer|query) → état LIVE du service via ticket-collab.serviceStatus (modem/ONU en ligne/hors ligne, Rx dBm, abonnement) + conseil « correctif à distance vs déplacement ». Prompt : pour un problème de CONNEXION (pas d'internet / lent / signal / hors ligne), appeler check_service AVANT de proposer un déplacement — si en ligne → tenter un redémarrage à distance ; si hors ligne + signal faible/nul → déplacement (réparation) justifié ; annoncer le constat (en ligne/hors ligne · Rx) avant de recommander. Évite les camions inutiles. Vérifié : check_service C-LPB4 → « Hors ligne » (tr069) ; outil listé dans /staff-agent/tools. Déployé, hub sain. Co-Authored-By: Claude Fable 5 --- services/targo-hub/lib/agent-tools.json | 1 + services/targo-hub/lib/staff-agent.js | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/services/targo-hub/lib/agent-tools.json b/services/targo-hub/lib/agent-tools.json index 0f800b8..1f5dac4 100644 --- a/services/targo-hub/lib/agent-tools.json +++ b/services/targo-hub/lib/agent-tools.json @@ -19,6 +19,7 @@ { "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": "write", "permission": "create_jobs", "title": "Créer un job", "type": "function", "function": { "name": "create_job", "description": "STAFF/dispatch. Crée un Dispatch Job (intervention). ÉCRITURE — proposée puis confirmée par l'utilisateur avant exécution. auto_assign=true assigne au meilleur tech disponible.", "parameters": { "type": "object", "properties": { "subject": { "type": "string", "description": "Description du travail" }, "customer_id": { "type": "string", "description": "ID client (optionnel)" }, "service_location": { "type": "string", "description": "Nom du lieu de service (optionnel)" }, "address": { "type": "string", "description": "Adresse civique de l'intervention (texte libre, ex. « 2338 rue Ste-Clotilde ») — si aucun lieu de service lié" }, "priority": { "type": "string", "enum": ["low", "medium", "high"], "description": "Priorité (défaut high pour urgence, medium sinon)" }, "job_type": { "type": "string", "enum": ["Installation", "Réparation", "Maintenance", "Retrait", "Dépannage", "Autre"], "description": "Type de travail (défaut Dépannage)" }, "notes": { "type": "string", "description": "Contexte additionnel" }, "auto_assign": { "type": "boolean", "description": "Assigner automatiquement au meilleur tech dispo (défaut true)" } }, "required": ["subject"] } } }, { "audience": "staff", "mode": "write", "permission": "assign_jobs", "title": "Assigner un technicien", "type": "function", "function": { "name": "assign_tech", "description": "STAFF/dispatch. Assigne OU réassigne un Dispatch Job à un technicien (pose aussi l'heure au premier trou libre du quart). ÉCRITURE — confirmée avant exécution. Résous d'abord le tech via list_technicians pour obtenir tech_id.", "parameters": { "type": "object", "properties": { "job": { "type": "string", "description": "Identifiant du Dispatch Job" }, "tech_id": { "type": "string", "description": "id du technicien (TECH-xxxx), via list_technicians" }, "date": { "type": "string", "description": "Date AAAA-MM-JJ (optionnel)" }, "start": { "type": "string", "description": "Heure HH:MM (optionnel)" } }, "required": ["job", "tech_id"] } } }, { "audience": "staff", "mode": "write", "permission": "assign_jobs", "title": "Ajouter un assistant", "type": "function", "function": { "name": "add_assistant", "description": "STAFF/dispatch. Ajoute un technicien en renfort (assistant) sur un job — le tech assigné (lead) reste inchangé. ÉCRITURE — confirmée avant exécution.", "parameters": { "type": "object", "properties": { "job": { "type": "string", "description": "Identifiant du Dispatch Job" }, "tech_id": { "type": "string", "description": "id du technicien assistant (TECH-xxxx)" }, "tech_name": { "type": "string", "description": "Nom du technicien (optionnel, pour l'affichage)" } }, "required": ["job", "tech_id"] } } }, diff --git a/services/targo-hub/lib/staff-agent.js b/services/targo-hub/lib/staff-agent.js index 7073a4f..3874fd9 100644 --- a/services/targo-hub/lib/staff-agent.js +++ b/services/targo-hub/lib/staff-agent.js @@ -111,7 +111,20 @@ async function read_resource_availability ({ skill, after_date, days } = {}) { return { skill: skill || '(toute compétence)', window_days: (o && o.days) || 7, qualified_techs: techs.length, free_hours: freeH, techs: techs.slice(0, 8).map(t => ({ name: t.tech_name, free_h: Math.round((t.free_h || 0) * 10) / 10 })), note: techs.length ? (techs.length + ' tech(s) qualifié(s) « ' + (skill || 'tous') + ' » · ' + freeH + ' h libres sur ' + ((o && o.days) || 7) + ' jours') : 'Aucun tech qualifié disponible sur la période — élargis la fenêtre ou revois la compétence.' } } 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 } +async function read_check_service ({ customer, query } = {}) { + try { + const r = await require('./ticket-collab').serviceStatus({ customer: customer || '', q: query || '' }) + if (!r || r.ok === false) return { error: (r && r.error) || 'statut de service indisponible' } + if (r.found === false) return { found: false, ambiguous: !!r.ambiguous, matches: (r.matches || []).slice(0, 6).map(m => ({ id: m.name, name: m.customer_name || m.name })), note: r.ambiguous ? 'Plusieurs clients correspondent — précise lequel (customer=id).' : 'Client/service introuvable.' } + 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." + 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 } } +} +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 } // ── Jours de la semaine — clés du weekly_schedule (= DOW_KEYS de roster.js) ── const DAY_KEYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] @@ -267,7 +280,8 @@ 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 (list_technicians, get_job, resolve_skill, resource_availability, find_slot) pour RÉSOUDRE 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 (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. - 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. - 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.