feat(v2): add ticket normalization and bounded AI context builder
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
30e532f811
commit
7dccb85efa
@@ -0,0 +1,62 @@
|
|||||||
|
function normalizeTicket(row, extras) {
|
||||||
|
const followups = (extras && extras.followups) || [];
|
||||||
|
const tasks = (extras && extras.tasks) || [];
|
||||||
|
return {
|
||||||
|
id: Number(row.id),
|
||||||
|
titulo: row.titulo,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAiContextText(ticket, maxChars) {
|
||||||
|
const budget = Math.max(200, Number(maxChars) || 4000);
|
||||||
|
const followupsText = ticket.followups
|
||||||
|
.slice(-5)
|
||||||
|
.map((f) => `- [followup ${f.date}] ${truncate(f.texto, 300)}`)
|
||||||
|
.join('\n');
|
||||||
|
const tasksText = ticket.tasks
|
||||||
|
.slice(-5)
|
||||||
|
.map((t) => `- [tarefa ${t.date} status=${t.status}] ${truncate(t.texto, 300)}`)
|
||||||
|
.join('\n');
|
||||||
|
const header = [
|
||||||
|
`Chamado #${ticket.id} - ${ticket.titulo}`,
|
||||||
|
`Status: ${ticket.status_nome} | Categoria: ${ticket.categoria} | Entidade: ${ticket.entidade}`,
|
||||||
|
`Tecnico: ${ticket.tecnico_atribuido}`,
|
||||||
|
`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');
|
||||||
|
const body = [
|
||||||
|
'Ultimos apontamentos:',
|
||||||
|
followupsText || '(nenhum)',
|
||||||
|
'',
|
||||||
|
'Ultimas tarefas:',
|
||||||
|
tasksText || '(nenhuma)',
|
||||||
|
].join('\n');
|
||||||
|
return truncate(`${header}\n\n${body}`, budget);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { normalizeTicket, buildAiContextText, truncate };
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { normalizeTicket, buildAiContextText, truncate } = require('./context');
|
||||||
|
|
||||||
|
function rawRow(overrides) {
|
||||||
|
return Object.assign({
|
||||||
|
id: '38611', titulo: ' Colocar etiqueta sala 03 ', status: '2', status_nome: 'Em atendimento',
|
||||||
|
aberto_em: '2026-08-01 08:00:00', ultima_interacao_em: '2026-08-22 08:00:00',
|
||||||
|
dias_aberto: '23', dias_sem_interacao: '2', sla_vencido: '1', sla_em_risco: '0', horas_ate_sla: null,
|
||||||
|
prioridade: '3', urgencia: '3', impacto: '3', entidade: 'Matriz', categoria: 'Facilities',
|
||||||
|
tecnico_atribuido: 'Sem tecnico atribuido', sem_tecnico: '1', link_glpi: 'https://glpi/x?id=38611',
|
||||||
|
score_criticidade: '145',
|
||||||
|
}, overrides);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('normalizeTicket converte tipos e booleans 0/1', () => {
|
||||||
|
const t = normalizeTicket(rawRow({}), { followups: [], tasks: [] });
|
||||||
|
assert.equal(t.id, 38611);
|
||||||
|
assert.equal(t.titulo, ' Colocar etiqueta sala 03 ');
|
||||||
|
assert.equal(t.sla_vencido, true);
|
||||||
|
assert.equal(t.sla_em_risco, false);
|
||||||
|
assert.equal(t.sem_tecnico, true);
|
||||||
|
assert.equal(t.dias_aberto, 23);
|
||||||
|
assert.deepEqual(t.followups, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('truncate corta e adiciona reticencias', () => {
|
||||||
|
const long = 'a'.repeat(50);
|
||||||
|
assert.equal(truncate(long, 10), `${'a'.repeat(7)}...`);
|
||||||
|
assert.equal(truncate('curto', 10), 'curto');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildAiContextText respeita o orcamento de caracteres', () => {
|
||||||
|
const t = normalizeTicket(rawRow({}), {
|
||||||
|
followups: [{ id: 1, date: '2026-08-20', texto: 'x'.repeat(500) }],
|
||||||
|
tasks: [{ id: 1, date: '2026-08-19', status: 1, texto: 'y'.repeat(500) }],
|
||||||
|
});
|
||||||
|
const text = buildAiContextText(t, 300);
|
||||||
|
assert.ok(text.length <= 300);
|
||||||
|
assert.ok(text.includes('#38611'));
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user