medirVarianciaAvaliacao.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import "dotenv/config";
  2. import fs from "node:fs";
  3. import path from "node:path";
  4. import { fileURLToPath } from "node:url";
  5. import { Atendimento } from "../src/models/Atendimento.model.js";
  6. import { AtendimentoAvaliacao } from "../src/models/AtendimentoAvaliacao.model.js";
  7. import { gerarAvaliacaoViaLLM } from "../src/services/atendimentoAvaliacaoService.js";
  8. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  9. function argValor(nome, padrao) {
  10. const idx = process.argv.indexOf(`--${nome}`);
  11. return idx > -1 ? process.argv[idx + 1] : padrao;
  12. }
  13. const AMOSTRA = Number(argValor("amostra", 20));
  14. const REPETICOES = Number(argValor("repeticoes", 5));
  15. const SETOR = argValor("setor", null);
  16. const IDS = argValor("ids", null)
  17. ?.split(",")
  18. .map((s) => Number(s.trim()))
  19. .filter(Number.isFinite) ?? null;
  20. const CAMPOS_CATEGORICOS = ["Resolvido", "Sentimento", "StatusCancelamento"];
  21. function media(valores) {
  22. return valores.reduce((a, b) => a + b, 0) / valores.length;
  23. }
  24. function desvioPadrao(valores) {
  25. if (valores.length < 2) return 0;
  26. const m = media(valores);
  27. const variancia = media(valores.map((v) => (v - m) ** 2));
  28. return Math.sqrt(variancia);
  29. }
  30. async function selecionarAmostra() {
  31. if (IDS?.length) return IDS.map((Id) => ({ Id }));
  32. let query = AtendimentoAvaliacao.query()
  33. .where("Avaliavel", true)
  34. .join("atendimentos as a", "a.Id", "atendimento_avaliacoes.AtendimentoId")
  35. .select("atendimento_avaliacoes.AtendimentoId as Id")
  36. .orderByRaw("RAND()")
  37. .limit(AMOSTRA);
  38. if (SETOR) query = query.where("a.Setor", SETOR);
  39. return query;
  40. }
  41. async function avaliarRepetidamente(atendimentoId) {
  42. const atendimento = await Atendimento.query()
  43. .findById(atendimentoId)
  44. .withGraphFetched("[cliente, mensagens]")
  45. .modifyGraph("mensagens", (q) => q.orderBy("Timestamp", "asc").orderBy("Id", "asc"));
  46. const mensagens = atendimento.mensagens ?? [];
  47. const execucoes = [];
  48. for (let i = 0; i < REPETICOES; i++) {
  49. const { dados } = await gerarAvaliacaoViaLLM(atendimento, mensagens);
  50. execucoes.push(dados);
  51. }
  52. return { atendimento, execucoes };
  53. }
  54. function metricasAtendimento(codigo, execucoes) {
  55. const scores = execucoes.map((e) => e.ScoreAtendente).filter((s) => s !== null);
  56. const scoreRange = scores.length ? Math.max(...scores) - Math.min(...scores) : null;
  57. const scoreDesvio = scores.length ? Number(desvioPadrao(scores).toFixed(2)) : null;
  58. const concordancia = {};
  59. for (const campo of CAMPOS_CATEGORICOS) {
  60. const valores = execucoes.map((e) => e[campo]);
  61. concordancia[campo] = new Set(valores).size === 1;
  62. }
  63. return {
  64. codigo,
  65. scores,
  66. scoreRange,
  67. scoreDesvio,
  68. concordancia
  69. };
  70. }
  71. function resumoAgregado(metricas) {
  72. const comScore = metricas.filter((m) => m.scoreRange !== null);
  73. const rangeMedio = comScore.length ? Number(media(comScore.map((m) => m.scoreRange)).toFixed(2)) : null;
  74. const desvioMedio = comScore.length ? Number(media(comScore.map((m) => m.scoreDesvio)).toFixed(2)) : null;
  75. const distribuicaoRange = { range0: 0, range1: 0, range2mais: 0 };
  76. for (const m of comScore) {
  77. if (m.scoreRange === 0) distribuicaoRange.range0 += 1;
  78. else if (m.scoreRange === 1) distribuicaoRange.range1 += 1;
  79. else distribuicaoRange.range2mais += 1;
  80. }
  81. const taxaConcordancia = {};
  82. for (const campo of CAMPOS_CATEGORICOS) {
  83. const concordantes = metricas.filter((m) => m.concordancia[campo]).length;
  84. taxaConcordancia[campo] = Number(((concordantes / metricas.length) * 100).toFixed(1));
  85. }
  86. return {
  87. totalAtendimentos: metricas.length,
  88. scoreRangeMedio: rangeMedio,
  89. scoreDesvioMedio: desvioMedio,
  90. distribuicaoRange,
  91. taxaConcordanciaPercent: taxaConcordancia
  92. };
  93. }
  94. async function main() {
  95. const alvos = await selecionarAmostra();
  96. const total = alvos.length;
  97. console.log(`[variancia-avaliacao] amostra: ${total} atendimentos | ${REPETICOES} repetições cada | ${total * REPETICOES} chamadas ao LLM`);
  98. const metricas = [];
  99. const detalhes = [];
  100. let processados = 0;
  101. let erros = 0;
  102. const t0 = Date.now();
  103. for (const { Id } of alvos) {
  104. try {
  105. const { atendimento, execucoes } = await avaliarRepetidamente(Id);
  106. const m = metricasAtendimento(atendimento.Codigo, execucoes);
  107. metricas.push(m);
  108. detalhes.push({ atendimentoId: Id, codigo: atendimento.Codigo, execucoes });
  109. } catch (err) {
  110. erros += 1;
  111. console.warn(`[variancia-avaliacao] erro no atendimento ${Id}: ${err.message}`);
  112. } finally {
  113. processados += 1;
  114. const s = Math.round((Date.now() - t0) / 1000);
  115. const ritmo = s / processados;
  116. const restamMin = Math.round(((total - processados) * ritmo) / 60);
  117. console.log(
  118. `[variancia-avaliacao] progresso: ${processados}/${total} | erros: ${erros} | ${Math.round(s / 60)}min decorridos | ~${restamMin}min restantes`
  119. );
  120. }
  121. }
  122. const resumo = resumoAgregado(metricas);
  123. console.log("\n=== Resumo ===");
  124. console.log(`Atendimentos processados: ${resumo.totalAtendimentos} (erros: ${erros})`);
  125. console.log(`Score — range médio: ${resumo.scoreRangeMedio} | desvio padrão médio: ${resumo.scoreDesvioMedio}`);
  126. console.log(
  127. `Distribuição de range: estável (0) = ${resumo.distribuicaoRange.range0} | range 1 = ${resumo.distribuicaoRange.range1} | range ≥2 = ${resumo.distribuicaoRange.range2mais}`
  128. );
  129. console.log("Concordância entre execuções (campos categóricos):");
  130. for (const [campo, pct] of Object.entries(resumo.taxaConcordanciaPercent)) {
  131. console.log(` ${campo}: ${pct}%`);
  132. }
  133. const backupDir = path.resolve(__dirname, "logs");
  134. fs.mkdirSync(backupDir, { recursive: true });
  135. const backupPath = path.join(backupDir, `variancia-avaliacao-${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
  136. fs.writeFileSync(backupPath, JSON.stringify({ config: { amostra: AMOSTRA, repeticoes: REPETICOES, setor: SETOR, ids: IDS }, resumo, metricas, detalhes }, null, 2));
  137. console.log(`\n[variancia-avaliacao] resultado completo salvo em ${backupPath}`);
  138. process.exit(0);
  139. }
  140. main().catch((err) => {
  141. console.error("[variancia-avaliacao] falha fatal:", err);
  142. process.exit(1);
  143. });