import "dotenv/config"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { Atendimento } from "../src/models/Atendimento.model.js"; import { AtendimentoAvaliacao } from "../src/models/AtendimentoAvaliacao.model.js"; import { gerarAvaliacaoViaLLM } from "../src/services/atendimentoAvaliacaoService.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); function argValor(nome, padrao) { const idx = process.argv.indexOf(`--${nome}`); return idx > -1 ? process.argv[idx + 1] : padrao; } const AMOSTRA = Number(argValor("amostra", 20)); const REPETICOES = Number(argValor("repeticoes", 5)); const SETOR = argValor("setor", null); const IDS = argValor("ids", null) ?.split(",") .map((s) => Number(s.trim())) .filter(Number.isFinite) ?? null; const CAMPOS_CATEGORICOS = ["Resolvido", "Sentimento", "StatusCancelamento"]; function media(valores) { return valores.reduce((a, b) => a + b, 0) / valores.length; } function desvioPadrao(valores) { if (valores.length < 2) return 0; const m = media(valores); const variancia = media(valores.map((v) => (v - m) ** 2)); return Math.sqrt(variancia); } async function selecionarAmostra() { if (IDS?.length) return IDS.map((Id) => ({ Id })); let query = AtendimentoAvaliacao.query() .where("Avaliavel", true) .join("atendimentos as a", "a.Id", "atendimento_avaliacoes.AtendimentoId") .select("atendimento_avaliacoes.AtendimentoId as Id") .orderByRaw("RAND()") .limit(AMOSTRA); if (SETOR) query = query.where("a.Setor", SETOR); return query; } async function avaliarRepetidamente(atendimentoId) { const atendimento = await Atendimento.query() .findById(atendimentoId) .withGraphFetched("[cliente, mensagens]") .modifyGraph("mensagens", (q) => q.orderBy("Timestamp", "asc").orderBy("Id", "asc")); const mensagens = atendimento.mensagens ?? []; const execucoes = []; for (let i = 0; i < REPETICOES; i++) { const { dados } = await gerarAvaliacaoViaLLM(atendimento, mensagens); execucoes.push(dados); } return { atendimento, execucoes }; } function metricasAtendimento(codigo, execucoes) { const scores = execucoes.map((e) => e.ScoreAtendente).filter((s) => s !== null); const scoreRange = scores.length ? Math.max(...scores) - Math.min(...scores) : null; const scoreDesvio = scores.length ? Number(desvioPadrao(scores).toFixed(2)) : null; const concordancia = {}; for (const campo of CAMPOS_CATEGORICOS) { const valores = execucoes.map((e) => e[campo]); concordancia[campo] = new Set(valores).size === 1; } return { codigo, scores, scoreRange, scoreDesvio, concordancia }; } function resumoAgregado(metricas) { const comScore = metricas.filter((m) => m.scoreRange !== null); const rangeMedio = comScore.length ? Number(media(comScore.map((m) => m.scoreRange)).toFixed(2)) : null; const desvioMedio = comScore.length ? Number(media(comScore.map((m) => m.scoreDesvio)).toFixed(2)) : null; const distribuicaoRange = { range0: 0, range1: 0, range2mais: 0 }; for (const m of comScore) { if (m.scoreRange === 0) distribuicaoRange.range0 += 1; else if (m.scoreRange === 1) distribuicaoRange.range1 += 1; else distribuicaoRange.range2mais += 1; } const taxaConcordancia = {}; for (const campo of CAMPOS_CATEGORICOS) { const concordantes = metricas.filter((m) => m.concordancia[campo]).length; taxaConcordancia[campo] = Number(((concordantes / metricas.length) * 100).toFixed(1)); } return { totalAtendimentos: metricas.length, scoreRangeMedio: rangeMedio, scoreDesvioMedio: desvioMedio, distribuicaoRange, taxaConcordanciaPercent: taxaConcordancia }; } async function main() { const alvos = await selecionarAmostra(); const total = alvos.length; console.log(`[variancia-avaliacao] amostra: ${total} atendimentos | ${REPETICOES} repetições cada | ${total * REPETICOES} chamadas ao LLM`); const metricas = []; const detalhes = []; let processados = 0; let erros = 0; const t0 = Date.now(); for (const { Id } of alvos) { try { const { atendimento, execucoes } = await avaliarRepetidamente(Id); const m = metricasAtendimento(atendimento.Codigo, execucoes); metricas.push(m); detalhes.push({ atendimentoId: Id, codigo: atendimento.Codigo, execucoes }); } catch (err) { erros += 1; console.warn(`[variancia-avaliacao] erro no atendimento ${Id}: ${err.message}`); } finally { processados += 1; const s = Math.round((Date.now() - t0) / 1000); const ritmo = s / processados; const restamMin = Math.round(((total - processados) * ritmo) / 60); console.log( `[variancia-avaliacao] progresso: ${processados}/${total} | erros: ${erros} | ${Math.round(s / 60)}min decorridos | ~${restamMin}min restantes` ); } } const resumo = resumoAgregado(metricas); console.log("\n=== Resumo ==="); console.log(`Atendimentos processados: ${resumo.totalAtendimentos} (erros: ${erros})`); console.log(`Score — range médio: ${resumo.scoreRangeMedio} | desvio padrão médio: ${resumo.scoreDesvioMedio}`); console.log( `Distribuição de range: estável (0) = ${resumo.distribuicaoRange.range0} | range 1 = ${resumo.distribuicaoRange.range1} | range ≥2 = ${resumo.distribuicaoRange.range2mais}` ); console.log("Concordância entre execuções (campos categóricos):"); for (const [campo, pct] of Object.entries(resumo.taxaConcordanciaPercent)) { console.log(` ${campo}: ${pct}%`); } const backupDir = path.resolve(__dirname, "logs"); fs.mkdirSync(backupDir, { recursive: true }); const backupPath = path.join(backupDir, `variancia-avaliacao-${new Date().toISOString().replace(/[:.]/g, "-")}.json`); fs.writeFileSync(backupPath, JSON.stringify({ config: { amostra: AMOSTRA, repeticoes: REPETICOES, setor: SETOR, ids: IDS }, resumo, metricas, detalhes }, null, 2)); console.log(`\n[variancia-avaliacao] resultado completo salvo em ${backupPath}`); process.exit(0); } main().catch((err) => { console.error("[variancia-avaliacao] falha fatal:", err); process.exit(1); });