import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { AtendimentoGoldenLabel } from "../src/models/AtendimentoGoldenLabel.model.js"; import { media, desvioPadrao, correlacaoPearson } from "./utils/estatistica.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const setorIdx = process.argv.indexOf("--setor"); const SETOR = setorIdx > -1 ? process.argv[setorIdx + 1] : null; const CAMPOS_CATEGORICOS = ["Resolvido", "Sentimento", "StatusCancelamento", "HorarioVisitaInformado", "VisitaAgendada"]; // usa o snapshot da IA congelado em atendimento_golden_labels (Ia*) no momento em que // cada item foi rotulado — não junta com atendimento_avaliacoes ao vivo de propósito, // pra comparação nunca ser afetada por reavaliações posteriores do mesmo atendimento async function buscarPares() { let query = AtendimentoGoldenLabel.query() .where("atendimento_golden_labels.Status", "rotulado") .whereNotNull("IaSnapshotEm") .join("atendimentos as a", "a.Id", "atendimento_golden_labels.AtendimentoId") .select("atendimento_golden_labels.*", "a.Codigo as Codigo", "a.Setor as Setor"); if (SETOR) query = query.where("a.Setor", SETOR); return query; } // Jaccard entre dois conjuntos de strings (comparação textual simples, não crítica) function jaccard(a, b) { const setA = new Set((a ?? []).map((s) => s.toLowerCase().trim())); const setB = new Set((b ?? []).map((s) => s.toLowerCase().trim())); if (setA.size === 0 && setB.size === 0) return 1; const intersecao = [...setA].filter((x) => setB.has(x)).length; const uniao = new Set([...setA, ...setB]).size; return uniao === 0 ? 1 : intersecao / uniao; } function metricasCategoricas(pares) { const resultado = {}; for (const campo of CAMPOS_CATEGORICOS) { const campoIa = `Ia${campo}`; const comparaveis = pares.filter((p) => p[campo] !== null && p[campo] !== undefined); const matriz = {}; let acertos = 0; for (const p of comparaveis) { const humano = String(p[campo]); const ia = String(p[campoIa]); matriz[humano] = matriz[humano] ?? {}; matriz[humano][ia] = (matriz[humano][ia] ?? 0) + 1; if (humano === ia) acertos += 1; } resultado[campo] = { amostra: comparaveis.length, exactMatchPercent: comparaveis.length ? Number(((acertos / comparaveis.length) * 100).toFixed(1)) : null, matrizConfusao: matriz }; } return resultado; } function metricasScore(pares) { const comparaveis = pares.filter((p) => p.ScoreAtendente !== null && p.IaScoreAtendente !== null); if (comparaveis.length === 0) { return { amostra: 0, mae: null, correlacao: null, distribuicaoDiferenca: null }; } const diferencas = comparaveis.map((p) => Math.abs(p.ScoreAtendente - p.IaScoreAtendente)); const mae = Number(media(diferencas).toFixed(2)); const correlacao = correlacaoPearson( comparaveis.map((p) => p.ScoreAtendente), comparaveis.map((p) => p.IaScoreAtendente) ); const distribuicaoDiferenca = { diferenca0: 0, diferenca1: 0, diferenca2: 0, diferenca3mais: 0 }; for (const d of diferencas) { if (d === 0) distribuicaoDiferenca.diferenca0 += 1; else if (d === 1) distribuicaoDiferenca.diferenca1 += 1; else if (d === 2) distribuicaoDiferenca.diferenca2 += 1; else distribuicaoDiferenca.diferenca3mais += 1; } return { amostra: comparaveis.length, mae, desvioPadraoDiferenca: Number(desvioPadrao(diferencas).toFixed(2)), correlacao: correlacao !== null ? Number(correlacao.toFixed(2)) : null, distribuicaoDiferenca }; } function metricasListas(pares) { const comAmbos = pares.filter((p) => (p.Atendentes?.length || p.IaAtendentes?.length)); const jaccardMedioAtendentes = comAmbos.length ? Number(media(comAmbos.map((p) => jaccard(p.Atendentes, p.IaAtendentes))).toFixed(2)) : null; const comAmbosSugestoes = pares.filter((p) => (p.Sugestoes?.length || p.IaSugestoes?.length)); const jaccardMedioSugestoes = comAmbosSugestoes.length ? Number(media(comAmbosSugestoes.map((p) => jaccard(p.Sugestoes, p.IaSugestoes))).toFixed(2)) : null; return { atendentes: { amostra: comAmbos.length, jaccardMedio: jaccardMedioAtendentes }, sugestoes: { amostra: comAmbosSugestoes.length, jaccardMedio: jaccardMedioSugestoes } }; } async function main() { const pares = await buscarPares(); console.log(`[comparar-golden-set] ${pares.length} atendimentos rotulados${SETOR ? ` no setor ${SETOR}` : ""} comparados contra a avaliação da IA.`); if (pares.length === 0) { console.log("[comparar-golden-set] nada para comparar ainda — rotule alguns atendimentos no golden set primeiro."); process.exit(0); } const categoricas = metricasCategoricas(pares); const score = metricasScore(pares); const listas = metricasListas(pares); console.log("\n=== Score do atendente ==="); console.log(`Amostra: ${score.amostra} | MAE: ${score.mae} | desvio da diferença: ${score.desvioPadraoDiferenca} | correlação: ${score.correlacao}`); if (score.distribuicaoDiferenca) { console.log( `Distribuição da diferença |humano - IA|: 0 = ${score.distribuicaoDiferenca.diferenca0} | 1 = ${score.distribuicaoDiferenca.diferenca1} | 2 = ${score.distribuicaoDiferenca.diferenca2} | ≥3 = ${score.distribuicaoDiferenca.diferenca3mais}` ); } console.log("\n=== Campos categóricos (exact-match humano vs. IA) ==="); for (const [campo, m] of Object.entries(categoricas)) { console.log(` ${campo}: ${m.exactMatchPercent}% (amostra: ${m.amostra})`); } console.log("\n=== Listas (similaridade de Jaccard, informativo) ==="); console.log(` Atendentes: ${listas.atendentes.jaccardMedio} (amostra: ${listas.atendentes.amostra})`); console.log(` Sugestões: ${listas.sugestoes.jaccardMedio} (amostra: ${listas.sugestoes.amostra})`); const backupDir = path.resolve(__dirname, "logs"); fs.mkdirSync(backupDir, { recursive: true }); const backupPath = path.join(backupDir, `comparar-golden-set-${new Date().toISOString().replace(/[:.]/g, "-")}.json`); fs.writeFileSync( backupPath, JSON.stringify( { config: { setor: SETOR }, resumo: { score, categoricas, listas }, detalhes: pares.map((p) => ({ atendimentoId: p.AtendimentoId, codigo: p.Codigo, setor: p.Setor, humano: { ScoreAtendente: p.ScoreAtendente, Resolvido: p.Resolvido, Sentimento: p.Sentimento, StatusCancelamento: p.StatusCancelamento, HorarioVisitaInformado: p.HorarioVisitaInformado, VisitaAgendada: p.VisitaAgendada, Atendentes: p.Atendentes, Sugestoes: p.Sugestoes }, ia: { ScoreAtendente: p.IaScoreAtendente, Resolvido: p.IaResolvido, Sentimento: p.IaSentimento, StatusCancelamento: p.IaStatusCancelamento, HorarioVisitaInformado: p.IaHorarioVisitaInformado, VisitaAgendada: p.IaVisitaAgendada, Atendentes: p.IaAtendentes, Sugestoes: p.IaSugestoes } })) }, null, 2 ) ); console.log(`\n[comparar-golden-set] resultado completo salvo em ${backupPath}`); process.exit(0); } main().catch((err) => { console.error("[comparar-golden-set] falha fatal:", err); process.exit(1); });