const { normalizeGlpiText } = require('./normalize'); function normalizeTicket(row, extras) { const followups = ((extras && extras.followups) || []).map((f) => ({ id: f.id, date: f.date, texto: normalizeGlpiText(f.texto), })); const tasks = ((extras && extras.tasks) || []).map((t) => ({ id: t.id, date: t.date, status: t.status, texto: normalizeGlpiText(t.texto), })); return { id: Number(row.id), titulo: normalizeGlpiText(row.titulo), descricao: normalizeGlpiText(row.descricao), status: Number(row.status), status_nome: row.status_nome, aberto_em: row.aberto_em, ultima_interacao_em: row.ultima_interacao_em, dias_aberto: Number(row.dias_aberto), dias_sem_interacao: Number(row.dias_sem_interacao), sla_vencido: Number(row.sla_vencido) === 1, sla_em_risco: Number(row.sla_em_risco) === 1, horas_ate_sla: row.horas_ate_sla == null ? null : Number(row.horas_ate_sla), prioridade: Number(row.prioridade), urgencia: Number(row.urgencia), impacto: Number(row.impacto), entidade: row.entidade, categoria: row.categoria, tecnico_atribuido: row.tecnico_atribuido, sem_tecnico: Number(row.sem_tecnico) === 1, link_glpi: row.link_glpi, score_criticidade: Number(row.score_criticidade), followups, tasks, }; } function truncate(text, maxChars) { const clean = String(text || '').replace(/\s+/g, ' ').trim(); return clean.length > maxChars ? `${clean.slice(0, Math.max(0, maxChars - 3))}...` : clean; } // Corta pelo tamanho total sem achatar quebras de linha -- usado só no corte final do // contexto inteiro (o texto de cada seção já foi normalizado/truncado individualmente). function truncatePreservingLines(text, maxChars) { const str = String(text || ''); return str.length > maxChars ? `${str.slice(0, Math.max(0, maxChars - 3))}...` : str; } // Contexto com secoes delimitadas (TITULO / DESCRICAO / STATUS-TECNICO-GRUPO / FOLLOW-UPS / // TAREFAS), nao uma unica string sem estrutura semantica. O texto de cada campo ja chega // normalizado (normalizeTicket ja rodou normalizeGlpiText em titulo/descricao/texto). function buildAiContextText(ticket, maxChars) { const budget = Math.max(200, Number(maxChars) || 4000); const followupsText = ticket.followups .slice(-5) .map((f) => `- [${f.date}] ${truncate(f.texto, 300) || '(sem conteudo)'}`) .join('\n'); const tasksText = ticket.tasks .slice(-5) .map((t) => `- [${t.date} status=${t.status}] ${truncate(t.texto, 300) || '(sem conteudo)'}`) .join('\n'); const secoes = [ `TITULO\n${truncate(ticket.titulo, 200) || '(sem titulo)'}`, `DESCRICAO ORIGINAL\n${truncate(ticket.descricao, 800) || '(sem descricao)'}`, [ `STATUS: ${ticket.status_nome}`, `TECNICO: ${ticket.tecnico_atribuido}`, `GRUPO/ENTIDADE: ${ticket.entidade} | CATEGORIA: ${ticket.categoria}`, `PRIORIDADE: ${ticket.prioridade} URGENCIA: ${ticket.urgencia} IMPACTO: ${ticket.impacto}`, `SLA vencido: ${ticket.sla_vencido} | Dias sem interacao: ${ticket.dias_sem_interacao} | Dias aberto: ${ticket.dias_aberto}`, ].join('\n'), `FOLLOW-UPS\n${followupsText || '(nenhum)'}`, `TAREFAS\n${tasksText || '(nenhuma)'}`, ]; const contextoCompleto = `Chamado #${ticket.id}\n\n${secoes.join('\n\n')}`; return truncatePreservingLines(contextoCompleto, budget); } module.exports = { normalizeTicket, buildAiContextText, truncate, truncatePreservingLines };