// ── Triage IA d'un courriel entrant (Phase 3 — Inbox→Ticket→Task) ── // classifyEmail({from,subject,body}) : client connu ? type ? faut-il suggérer un ticket ? // SANS dépendance à la boîte mail (testable seul + réutilisable par le poller une fois la boîte branchée). const cfg = require('./config') const { log } = require('./helpers') const erp = require('./erp') function extractEmail (from) { const m = /<([^>]+)>/.exec(from || '') const raw = (m ? m[1] : String(from || '')).trim().toLowerCase() return /.+@.+\..+/.test(raw) ? raw : '' } // Match client DÉTERMINISTE par courriel (email_id, puis email_billing qui peut contenir plusieurs adresses). async function matchCustomer (email) { if (!email) return null try { let r = await erp.list('Customer', { filters: [['email_id', '=', email]], fields: ['name', 'customer_name'], limit: 1 }) if (r && r[0]) return r[0] r = await erp.list('Customer', { filters: [['email_billing', 'like', '%' + email + '%']], fields: ['name', 'customer_name'], limit: 1 }) if (r && r[0]) return r[0] } catch (e) { log('triage matchCustomer error:', e.message) } return null } // Triage courriel = PII (contenu client) + gros volume → routé par tâche 'triage' (bascule possible vers le modèle LOCAL). // reasoningEffort:'none' : sans ça, gemini-2.5-flash « pense » et ses jetons mangent maxTokens → sur un long courriel, le JSON est tronqué, JSON.parse échoue et on retombe SILENCIEUSEMENT sur les heuristiques regex (triage IA inopérant). Le triage n'a pas besoin de raisonnement. const aiChat = async (messages) => require('./ai').chat({ task: 'triage', maxTokens: 300, temperature: 0, reasoningEffort: 'none', messages }) const { TYPES, CAT } = require('./categories') // taxonomie = SOURCE UNIQUE (voir lib/categories.js) // Heuristiques de repli (regex → type) si l'IA est indisponible/illisible. Propre au triage (pas dans la taxonomie partagée). const HEUR = [ { t: 'facturation', re: /factur|paiement|invoice|solde|rembours|prélèv|carte de cr/i }, { t: 'vente', re: /nouveau client|abonn|forfait|devis|soumission|s'abonner|disponib|couvertur|d[ée]m[ée]nage|prix/i }, { t: 'installation', re: /installation|rendez-?vous|technicien|raccord|branch/i }, { t: 'support', re: /lent|panne|coup[ée]|ne fonctionne|probl[èe]me|pixel|wifi|internet|connexion|signal|bug|hors service/i }, ] function heuristicType (text) { for (const h of HEUR) if (h.re.test(text)) return h.t; return '' } async function classifyEmail ({ from, subject, body } = {}) { const email = extractEmail(from) const customer = await matchCustomer(email) const text = `${subject || ''}\n${String(body || '').slice(0, 1500)}` let type = '', suggestTicket = true, suggestedTitle = String(subject || '').slice(0, 120), serviceAffected = '', reason = '' if (cfg.AI_API_KEY) { try { const sys = 'Tu tries les courriels entrants d\'un fournisseur Internet/télécom (TARGO / Gigafibre). Réponds UNIQUEMENT en JSON, sans texte autour : {"type":"support|facturation|vente|installation|autre","service_affected":"internet|tv|telephone|aucun","suggest_ticket":true|false,"title":"titre court actionnable en français","reason":"une phrase"}. Mets suggest_ticket=false si c\'est un pourriel, une pub, une réponse automatique, une notification no-reply, ou tout courriel SANS action requise.' const out = await aiChat([{ role: 'system', content: sys }, { role: 'user', content: text }]) const j = JSON.parse((out.match(/\{[\s\S]*\}/) || ['{}'])[0]) if (TYPES.includes(j.type)) type = j.type if (typeof j.suggest_ticket === 'boolean') suggestTicket = j.suggest_ticket if (j.title) suggestedTitle = String(j.title).slice(0, 120) serviceAffected = j.service_affected || '' reason = j.reason || '' } catch (e) { log('classifyEmail AI error:', e.message) } } if (!type) type = heuristicType(text) || 'autre' // BRUIT : no-reply / pourriel / notifications automatiques / infolettres → pas de ticket, pas de notif, masqué par défaut. const noise = /no-?reply|noreply|do-?not-?reply|mailer-daemon|postmaster|unsubscribe|d[ée]sabonn|notification@|newsletter|infolettre|bounce|via google|automated|calendar-notification|@docs\.google|@drive\.google/i.test(`${from} ${subject}`) if (noise) suggestTicket = false return { email, is_customer: !!customer, customer: customer ? customer.name : null, customer_name: customer ? (customer.customer_name || customer.name) : null, type, category: CAT[type] || 'Autre', service_affected: serviceAffected, suggest_ticket: suggestTicket, suggested_title: suggestedTitle, reason, noise, } } module.exports = { classifyEmail, matchCustomer, extractEmail }