diff --git a/services/targo-hub/lib/agent-tools.json b/services/targo-hub/lib/agent-tools.json index d25c8cb..ce13bef 100644 --- a/services/targo-hub/lib/agent-tools.json +++ b/services/targo-hub/lib/agent-tools.json @@ -16,8 +16,9 @@ { "audience": "staff", "mode": "read", "title": "Lister les techniciens", "type": "function", "function": { "name": "list_technicians", "description": "STAFF/dispatch. Liste les techniciens (id, nom, statut, compétences). Sert à RÉSOUDRE un technicien par son nom (« Simon » → TECH-4693) avant d'assigner/planifier. Filtre optionnel par nom partiel et/ou compétence.", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "Nom partiel du technicien (optionnel)" }, "skill": { "type": "string", "description": "Compétence requise (optionnel), ex. fibre, monteur, sans-fil" } }, "required": [] } } }, { "audience": "staff", "mode": "read", "title": "Détails du job", "type": "function", "function": { "name": "get_job", "description": "STAFF/dispatch. Détails d'un Dispatch Job (sujet, statut, technicien assigné, date/heure, client, lieu, durée). Sert à vérifier un job avant de le modifier/réassigner/reporter.", "parameters": { "type": "object", "properties": { "job": { "type": "string", "description": "Identifiant du Dispatch Job, ex. DJ-2026-0001 ou LEG-253958" } }, "required": ["job"] } } }, + { "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": "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)" }, "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": "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"] } } }, { "audience": "staff", "mode": "write", "permission": "assign_jobs", "title": "Changer le statut", "type": "function", "function": { "name": "set_job_status", "description": "STAFF/dispatch. Change le statut d'un Dispatch Job. ÉCRITURE — confirmée avant exécution. Cancelled = annuler l'intervention (conséquent).", "parameters": { "type": "object", "properties": { "job": { "type": "string", "description": "Identifiant du Dispatch Job" }, "status": { "type": "string", "enum": ["open", "On Hold", "Cancelled", "assigned"], "description": "Nouveau statut" } }, "required": ["job", "status"] } } }, diff --git a/services/targo-hub/lib/dispatch.js b/services/targo-hub/lib/dispatch.js index ca56491..a484d40 100644 --- a/services/targo-hub/lib/dispatch.js +++ b/services/targo-hub/lib/dispatch.js @@ -1041,13 +1041,13 @@ async function handle (req, res, method, path) { json(res, 404, { error: 'Dispatch endpoint not found' }) } -async function agentCreateDispatchJob ({ customer_id, service_location, subject, priority, job_type, notes, auto_assign }) { - let address = '', latitude = null, longitude = null +async function agentCreateDispatchJob ({ customer_id, service_location, address: addressIn, subject, priority, job_type, notes, auto_assign }) { + let address = addressIn || '', latitude = null, longitude = null if (service_location) { const locRes = await erpFetch(`/api/resource/Service Location/${encodeURIComponent(service_location)}`) if (locRes.status === 200) { const loc = locRes.data.data - address = [loc.address_line, loc.city, loc.postal_code].filter(Boolean).join(', ') + address = [loc.address_line, loc.city, loc.postal_code].filter(Boolean).join(', ') || address latitude = loc.latitude || null longitude = loc.longitude || null } diff --git a/services/targo-hub/lib/staff-agent.js b/services/targo-hub/lib/staff-agent.js index 9f54946..df1459b 100644 --- a/services/targo-hub/lib/staff-agent.js +++ b/services/targo-hub/lib/staff-agent.js @@ -96,7 +96,14 @@ async function read_find_slot ({ duration_h, skill, after_date } = {}) { return { count: (slots || []).length, slots: (slots || []).slice(0, 5) } } catch (e) { return { error: e.message } } } -const READERS = { list_technicians: read_list_technicians, get_job: read_get_job, find_slot: read_find_slot } +async function read_resolve_skill ({ text, department } = {}) { + try { + const r = await require('./skill-resolver').resolveSkills({ text: text || '', department: department || '', useAI: true }) + const skills = r.skills || [] + return { skills, primary: r.primary || '', confidence: r.confidence, reason: r.reason || '', note: skills.length ? ('Compétence(s) requise(s) : ' + skills.join(', ') + '. Enchaîne find_slot avec skill=' + (r.primary || skills[0]) + ' pour la disponibilité.') : 'Aucune compétence terrain — traitable à distance (tous les agents).' } + } 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 } // ── Jours de la semaine — clés du weekly_schedule (= DOW_KEYS de roster.js) ── const DAY_KEYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] @@ -131,12 +138,12 @@ const WRITES = { create_job: { permission: 'create_jobs', async build (p) { - const params = { subject: String(p.subject || '').trim(), customer_id: p.customer_id || '', service_location: p.service_location || '', priority: p.priority || '', job_type: p.job_type || '', notes: p.notes || '', auto_assign: p.auto_assign !== false } - const preview = `« ${params.subject || 'Intervention'} »${params.job_type ? ' · ' + params.job_type : ''}${params.priority ? ' · priorité ' + params.priority : ''}${params.customer_id ? ' · client ' + params.customer_id : ''} · ${params.auto_assign ? 'assignation auto' : 'non assigné'}` + const params = { subject: String(p.subject || '').trim(), customer_id: p.customer_id || '', service_location: p.service_location || '', address: p.address || '', priority: p.priority || '', job_type: p.job_type || '', notes: p.notes || '', auto_assign: p.auto_assign !== false } + const preview = `« ${params.subject || 'Intervention'} »${params.job_type ? ' · ' + params.job_type : ''}${params.priority ? ' · priorité ' + params.priority : ''}${params.address ? ' · ' + params.address : ''}${params.customer_id ? ' · client ' + params.customer_id : ''} · ${params.auto_assign ? 'assignation auto' : 'non assigné'}` return { title: META.create_job.title, preview, params } }, async exec (p) { - const r = await dispatch().agentCreateDispatchJob({ customer_id: p.customer_id || '', service_location: p.service_location || '', subject: p.subject, priority: p.priority || undefined, job_type: p.job_type || undefined, notes: p.notes || '', auto_assign: p.auto_assign }) + const r = await dispatch().agentCreateDispatchJob({ customer_id: p.customer_id || '', service_location: p.service_location || '', address: p.address || '', subject: p.subject, priority: p.priority || undefined, job_type: p.job_type || undefined, notes: p.notes || '', auto_assign: p.auto_assign }) return { ok: !!(r && r.success), message: (r && r.message) || 'job créé', job_id: r && r.job_id, assigned_tech: r && r.assigned_tech } }, }, @@ -252,7 +259,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, find_slot) pour RÉSOUDRE les entités. 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, find_slot, resolve_skill) pour RÉSOUDRE et DIAGNOSTIQUER. Résous toujours un technicien nommé (« Simon ») en tech_id via list_technicians avant d'agir. +- 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) find_slot avec cette compétence → DISPONIBILITÉ réelle (techs qualifiés + créneaux). (4) SEULEMENT ensuite propose create_job / assign_tech / proposer_rdv_client. Résume ton diagnostic (raison → compétence → dispo) AVANT de proposer l'action, et inclus l'adresse dans create_job si elle est 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. - S'il manque une information essentielle (quel job ? quel tech ? quelle date ?), pose UNE question courte au lieu d'appeler un outil d'écriture.