feat(v2): normalize GLPI text before context/hash, add real ticket description
context.normalizeTicket now runs normalizeGlpiText on titulo, descricao (new field, sourced from glpi_tickets.content -- never fetched before), and every followup/task texto. buildAiContextText restructured into delimited sections (TITULO / DESCRICAO ORIGINAL / STATUS-TECNICO-GRUPO / FOLLOW-UPS / TAREFAS) instead of one flat string. hash.buildContextFingerprint now includes titulo/descricao and the normalized followup/task texto (previously only id/date/status), so: (a) the same content encoded differently (HTML vs escaped entities) produces the same hash, and (b) editing a followup's actual content (same id/date) is now detected as a real context change -- previously silently invisible to the hash. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d7650308a9
commit
db7a261a10
+46
-21
@@ -1,9 +1,21 @@
|
||||
const { normalizeGlpiText } = require('./normalize');
|
||||
|
||||
function normalizeTicket(row, extras) {
|
||||
const followups = (extras && extras.followups) || [];
|
||||
const tasks = (extras && extras.tasks) || [];
|
||||
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: row.titulo,
|
||||
titulo: normalizeGlpiText(row.titulo),
|
||||
descricao: normalizeGlpiText(row.descricao),
|
||||
status: Number(row.status),
|
||||
status_nome: row.status_nome,
|
||||
aberto_em: row.aberto_em,
|
||||
@@ -32,31 +44,44 @@ function truncate(text, maxChars) {
|
||||
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) => `- [followup ${f.date}] ${truncate(f.texto, 300)}`)
|
||||
.map((f) => `- [${f.date}] ${truncate(f.texto, 300) || '(sem conteudo)'}`)
|
||||
.join('\n');
|
||||
const tasksText = ticket.tasks
|
||||
.slice(-5)
|
||||
.map((t) => `- [tarefa ${t.date} status=${t.status}] ${truncate(t.texto, 300)}`)
|
||||
.map((t) => `- [${t.date} status=${t.status}] ${truncate(t.texto, 300) || '(sem conteudo)'}`)
|
||||
.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);
|
||||
|
||||
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 };
|
||||
module.exports = { normalizeTicket, buildAiContextText, truncate, truncatePreservingLines };
|
||||
|
||||
Reference in New Issue
Block a user