medirVarianciaAvaliacao.js 5.8 KB

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