evalAtendimentosRetrieval.mjs 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import { config } from "#config/index.js";
  2. import { buscarAtendimentosSemelhantes } from "#services/atendimentoRagService.js";
  3. function parseArgs(argv) {
  4. const flags = { hyde: null, minScore: 0, topK: 5, label: "", collection: null, model: null, casos: null, slice: null };
  5. for (const arg of argv) {
  6. if (arg === "--hyde") flags.hyde = true;
  7. else if (arg === "--no-hyde") flags.hyde = false;
  8. else if (arg.startsWith("--min-score=")) flags.minScore = Number(arg.split("=")[1]);
  9. else if (arg.startsWith("--top-k=")) flags.topK = Number(arg.split("=")[1]);
  10. else if (arg.startsWith("--label=")) flags.label = arg.split("=").slice(1).join("=");
  11. else if (arg.startsWith("--collection=")) flags.collection = arg.split("=").slice(1).join("=");
  12. else if (arg.startsWith("--model=")) flags.model = arg.split("=").slice(1).join("=");
  13. else if (arg.startsWith("--casos=")) flags.casos = Number(arg.split("=")[1]);
  14. else if (arg.startsWith("--slice=")) {
  15. const [ini, fim] = arg.split("=")[1].split("-").map(Number);
  16. flags.slice = { ini, fim };
  17. }
  18. }
  19. return flags;
  20. }
  21. function amostrar(cases, n) {
  22. if (!n || n >= cases.length) return cases;
  23. const positivos = cases.filter((c) => c.type === "positive");
  24. const negativos = cases.filter((c) => c.type === "negative");
  25. const metade = Math.ceil(n / 2);
  26. return [...positivos.slice(0, metade), ...negativos.slice(0, n - metade)];
  27. }
  28. const POSITIVE_CASES = [
  29. { query: "cliente reclamando que a internet caiu de novo", setor: "SUP" },
  30. { query: "problema de internet lenta", setor: "SUP" },
  31. { query: "roteador não conecta depois da queda de energia", setor: "SUP" },
  32. { query: "sinal de wifi caindo toda hora", setor: "SUP" },
  33. { query: "velocidade de internet abaixo do contratado", setor: "SUP" },
  34. { query: "modem piscando luz vermelha sem conexão", setor: "SUP" },
  35. { query: "cliente pedindo segunda via de boleto", setor: "FIN" },
  36. { query: "cliente questionando cobrança duplicada na fatura", setor: "FIN" },
  37. { query: "fatura veio com valor diferente do combinado", setor: "FIN" },
  38. { query: "negociação de dívida em atraso", setor: "FIN" },
  39. { query: "desconto prometido não apareceu na fatura", setor: "FIN" },
  40. { query: "solicitação de mudança de endereço de instalação", setor: "ATE" },
  41. { query: "atualização de cadastro do cliente", setor: "ATE" },
  42. { query: "dúvida sobre os detalhes do plano contratado", setor: "ATE" },
  43. { query: "reclamação sobre atendimento anterior mal resolvido", setor: "ATE" },
  44. { query: "cliente elogiando o atendente que resolveu o problema", setor: "ATE" },
  45. { query: "pedido de upgrade de plano de internet", setor: "VEN" },
  46. { query: "cliente interessado em portabilidade de número", setor: "VEN" },
  47. { query: "contratação de novo serviço combo com TV", setor: "VEN" },
  48. { query: "cliente perguntando sobre planos disponíveis na região", setor: "VEN" },
  49. { query: "instalação atrasada, técnico não compareceu", setor: "POS" },
  50. { query: "acompanhamento do status da instalação agendada", setor: "POS" },
  51. { query: "solicitação de troca de titularidade do contrato", setor: "POS" },
  52. { query: "cliente reclamando de equipamento entregue com defeito", setor: "POS" },
  53. { query: "cliente sem internet à noite, caso urgente", setor: "PLA" },
  54. { query: "queda de energia afetando o serviço fora do horário comercial", setor: "PLA" },
  55. { query: "chamado de emergência fora do expediente normal", setor: "PLA" },
  56. { query: "cancelamento de contrato", setor: null },
  57. { query: "cliente pedindo desbloqueio do serviço suspenso", setor: null },
  58. { query: "renovação de contrato prestes a vencer", setor: null }
  59. ];
  60. const NEGATIVE_UNRELATED = [
  61. "previsão do tempo para amanhã em Paris",
  62. "receita de bolo de chocolate",
  63. "quem ganhou o jogo de futebol ontem",
  64. "qual a capital da Austrália",
  65. "como treinar para uma maratona",
  66. "melhores filmes de ficção científica dos anos 80",
  67. "resumo da história do Brasil colonial",
  68. "dicas de jardinagem para iniciantes",
  69. "como aprender inglês sozinho",
  70. "cotação do dólar hoje",
  71. "letra da música mais tocada este ano",
  72. "curiosidades sobre o sistema solar"
  73. ];
  74. const NEGATIVE_BOILERPLATE = [
  75. "digite 1 para confirmar ou 2 para cancelar",
  76. "obrigado por aguardar, em instantes um atendente irá te atender",
  77. "encaminhando para o setor responsável",
  78. "aguarde enquanto conectamos você a um atendente disponível",
  79. "sua senha de atendimento é a 25",
  80. "deseja avaliar nosso atendimento de 1 a 5",
  81. "este atendimento foi encerrado por inatividade",
  82. "digite o número da opção desejada no menu"
  83. ];
  84. const CASES = [
  85. ...POSITIVE_CASES.map((c) => ({ query: c.query, type: "positive" })),
  86. ...NEGATIVE_UNRELATED.map((query) => ({ query, type: "negative" })),
  87. ...NEGATIVE_BOILERPLATE.map((query) => ({ query, type: "negative" }))
  88. ];
  89. async function runCase(testCase, flags) {
  90. const hits = await buscarAtendimentosSemelhantes({
  91. query: testCase.query,
  92. topK: flags.topK,
  93. minScore: flags.minScore,
  94. ...(flags.hyde !== null ? { hyde: flags.hyde } : {}),
  95. ...(flags.collection ? { collectionName: flags.collection } : {}),
  96. ...(flags.model ? { embeddingsModel: flags.model } : {})
  97. });
  98. const top1 = hits[0] ?? null;
  99. return {
  100. query: testCase.query,
  101. type: testCase.type,
  102. top1_score: top1 ? top1.score.toFixed(4) : "-",
  103. top1_source: top1?.source ?? "-"
  104. };
  105. }
  106. function separabilidade(positiveScores, negativeScores) {
  107. if (!positiveScores.length || !negativeScores.length) return NaN;
  108. let wins = 0;
  109. let total = 0;
  110. for (const p of positiveScores) {
  111. for (const n of negativeScores) {
  112. total += 1;
  113. if (p > n) wins += 1;
  114. else if (p === n) wins += 0.5;
  115. }
  116. }
  117. return (wins / total) * 100;
  118. }
  119. async function main() {
  120. const flags = parseArgs(process.argv.slice(2));
  121. const hydeEfetivo = flags.hyde !== null ? flags.hyde : config.atendimentosRag.hyde;
  122. const collectionEfetiva = flags.collection ?? config.atendimentosRag.collection;
  123. const modelEfetivo = flags.model ?? config.ollama.embeddingsModel;
  124. const casos = flags.slice ? CASES.slice(flags.slice.ini, flags.slice.fim) : amostrar(CASES, flags.casos);
  125. console.log(`\n=== evalAtendimentosRetrieval ${flags.label ? `[${flags.label}] ` : ""}===`);
  126. console.log(
  127. `modelo=${modelEfetivo} minScore=${flags.minScore} topK=${flags.topK} ` +
  128. `hyde=${hydeEfetivo} colecao=${collectionEfetiva} casos=${casos.length}/${CASES.length}\n`
  129. );
  130. const rows = [];
  131. for (const testCase of casos) {
  132. rows.push(await runCase(testCase, flags));
  133. }
  134. console.table(rows);
  135. const positives = rows.filter((r) => r.type === "positive");
  136. const negatives = rows.filter((r) => r.type === "negative");
  137. const positiveScores = positives.map((r) => Number(r.top1_score)).filter((n) => !Number.isNaN(n));
  138. const negativeScores = negatives.map((r) => Number(r.top1_score)).filter((n) => !Number.isNaN(n));
  139. const avg = (arr) => (arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : NaN);
  140. const positivoMin = positiveScores.length ? Math.min(...positiveScores) : NaN;
  141. const negativoMax = negativeScores.length ? Math.max(...negativeScores) : NaN;
  142. const gap = positivoMin - negativoMax;
  143. const auc = separabilidade(positiveScores, negativeScores);
  144. console.log("\n--- resumo ---");
  145. console.log(`positivos: ${positiveScores.length} casos, score médio top-1 = ${avg(positiveScores).toFixed(4)}`);
  146. console.log(`positivos: score mínimo top-1 (piso de segurança) = ${positiveScores.length ? positivoMin.toFixed(4) : "-"}`);
  147. console.log(`negativos: ${negativeScores.length} casos, score médio top-1 = ${avg(negativeScores).toFixed(4)}`);
  148. console.log(`negativos: score máximo top-1 (teto de ruído) = ${negativeScores.length ? negativoMax.toFixed(4) : "-"}`);
  149. console.log(`gap (piso positivos - teto negativos) = ${Number.isNaN(gap) ? "-" : gap.toFixed(4)} ${!Number.isNaN(gap) ? (gap > 0 ? "-> HÁ separação viável" : "-> SEM separação") : ""}`);
  150. console.log(`separabilidade (% pares positivo>negativo, tipo AUC) = ${Number.isNaN(auc) ? "-" : auc.toFixed(1) + "%"}`);
  151. console.log("");
  152. }
  153. main()
  154. .then(() => process.exit(0))
  155. .catch((err) => {
  156. console.error("[evalAtendimentosRetrieval] erro:", err);
  157. process.exit(1);
  158. });