| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- import { config } from "#config/index.js";
- import { buscarAtendimentosSemelhantes } from "#services/atendimentoRagService.js";
- function parseArgs(argv) {
- const flags = { hyde: null, minScore: 0, topK: 5, label: "", collection: null, model: null, casos: null, slice: null };
- for (const arg of argv) {
- if (arg === "--hyde") flags.hyde = true;
- else if (arg === "--no-hyde") flags.hyde = false;
- else if (arg.startsWith("--min-score=")) flags.minScore = Number(arg.split("=")[1]);
- else if (arg.startsWith("--top-k=")) flags.topK = Number(arg.split("=")[1]);
- else if (arg.startsWith("--label=")) flags.label = arg.split("=").slice(1).join("=");
- else if (arg.startsWith("--collection=")) flags.collection = arg.split("=").slice(1).join("=");
- else if (arg.startsWith("--model=")) flags.model = arg.split("=").slice(1).join("=");
- else if (arg.startsWith("--casos=")) flags.casos = Number(arg.split("=")[1]);
- else if (arg.startsWith("--slice=")) {
- const [ini, fim] = arg.split("=")[1].split("-").map(Number);
- flags.slice = { ini, fim };
- }
- }
- return flags;
- }
- function amostrar(cases, n) {
- if (!n || n >= cases.length) return cases;
- const positivos = cases.filter((c) => c.type === "positive");
- const negativos = cases.filter((c) => c.type === "negative");
- const metade = Math.ceil(n / 2);
- return [...positivos.slice(0, metade), ...negativos.slice(0, n - metade)];
- }
- const POSITIVE_CASES = [
-
- { query: "cliente reclamando que a internet caiu de novo", setor: "SUP" },
- { query: "problema de internet lenta", setor: "SUP" },
- { query: "roteador não conecta depois da queda de energia", setor: "SUP" },
- { query: "sinal de wifi caindo toda hora", setor: "SUP" },
- { query: "velocidade de internet abaixo do contratado", setor: "SUP" },
- { query: "modem piscando luz vermelha sem conexão", setor: "SUP" },
-
- { query: "cliente pedindo segunda via de boleto", setor: "FIN" },
- { query: "cliente questionando cobrança duplicada na fatura", setor: "FIN" },
- { query: "fatura veio com valor diferente do combinado", setor: "FIN" },
- { query: "negociação de dívida em atraso", setor: "FIN" },
- { query: "desconto prometido não apareceu na fatura", setor: "FIN" },
-
- { query: "solicitação de mudança de endereço de instalação", setor: "ATE" },
- { query: "atualização de cadastro do cliente", setor: "ATE" },
- { query: "dúvida sobre os detalhes do plano contratado", setor: "ATE" },
- { query: "reclamação sobre atendimento anterior mal resolvido", setor: "ATE" },
- { query: "cliente elogiando o atendente que resolveu o problema", setor: "ATE" },
-
- { query: "pedido de upgrade de plano de internet", setor: "VEN" },
- { query: "cliente interessado em portabilidade de número", setor: "VEN" },
- { query: "contratação de novo serviço combo com TV", setor: "VEN" },
- { query: "cliente perguntando sobre planos disponíveis na região", setor: "VEN" },
-
- { query: "instalação atrasada, técnico não compareceu", setor: "POS" },
- { query: "acompanhamento do status da instalação agendada", setor: "POS" },
- { query: "solicitação de troca de titularidade do contrato", setor: "POS" },
- { query: "cliente reclamando de equipamento entregue com defeito", setor: "POS" },
- { query: "cliente sem internet à noite, caso urgente", setor: "PLA" },
- { query: "queda de energia afetando o serviço fora do horário comercial", setor: "PLA" },
- { query: "chamado de emergência fora do expediente normal", setor: "PLA" },
-
- { query: "cancelamento de contrato", setor: null },
- { query: "cliente pedindo desbloqueio do serviço suspenso", setor: null },
- { query: "renovação de contrato prestes a vencer", setor: null }
- ];
- const NEGATIVE_UNRELATED = [
- "previsão do tempo para amanhã em Paris",
- "receita de bolo de chocolate",
- "quem ganhou o jogo de futebol ontem",
- "qual a capital da Austrália",
- "como treinar para uma maratona",
- "melhores filmes de ficção científica dos anos 80",
- "resumo da história do Brasil colonial",
- "dicas de jardinagem para iniciantes",
- "como aprender inglês sozinho",
- "cotação do dólar hoje",
- "letra da música mais tocada este ano",
- "curiosidades sobre o sistema solar"
- ];
- const NEGATIVE_BOILERPLATE = [
- "digite 1 para confirmar ou 2 para cancelar",
- "obrigado por aguardar, em instantes um atendente irá te atender",
- "encaminhando para o setor responsável",
- "aguarde enquanto conectamos você a um atendente disponível",
- "sua senha de atendimento é a 25",
- "deseja avaliar nosso atendimento de 1 a 5",
- "este atendimento foi encerrado por inatividade",
- "digite o número da opção desejada no menu"
- ];
- const CASES = [
- ...POSITIVE_CASES.map((c) => ({ query: c.query, type: "positive" })),
- ...NEGATIVE_UNRELATED.map((query) => ({ query, type: "negative" })),
- ...NEGATIVE_BOILERPLATE.map((query) => ({ query, type: "negative" }))
- ];
- async function runCase(testCase, flags) {
- const hits = await buscarAtendimentosSemelhantes({
- query: testCase.query,
- topK: flags.topK,
- minScore: flags.minScore,
- ...(flags.hyde !== null ? { hyde: flags.hyde } : {}),
- ...(flags.collection ? { collectionName: flags.collection } : {}),
- ...(flags.model ? { embeddingsModel: flags.model } : {})
- });
- const top1 = hits[0] ?? null;
- return {
- query: testCase.query,
- type: testCase.type,
- top1_score: top1 ? top1.score.toFixed(4) : "-",
- top1_source: top1?.source ?? "-"
- };
- }
- function separabilidade(positiveScores, negativeScores) {
- if (!positiveScores.length || !negativeScores.length) return NaN;
- let wins = 0;
- let total = 0;
- for (const p of positiveScores) {
- for (const n of negativeScores) {
- total += 1;
- if (p > n) wins += 1;
- else if (p === n) wins += 0.5;
- }
- }
- return (wins / total) * 100;
- }
- async function main() {
- const flags = parseArgs(process.argv.slice(2));
- const hydeEfetivo = flags.hyde !== null ? flags.hyde : config.atendimentosRag.hyde;
- const collectionEfetiva = flags.collection ?? config.atendimentosRag.collection;
- const modelEfetivo = flags.model ?? config.ollama.embeddingsModel;
- const casos = flags.slice ? CASES.slice(flags.slice.ini, flags.slice.fim) : amostrar(CASES, flags.casos);
- console.log(`\n=== evalAtendimentosRetrieval ${flags.label ? `[${flags.label}] ` : ""}===`);
- console.log(
- `modelo=${modelEfetivo} minScore=${flags.minScore} topK=${flags.topK} ` +
- `hyde=${hydeEfetivo} colecao=${collectionEfetiva} casos=${casos.length}/${CASES.length}\n`
- );
- const rows = [];
- for (const testCase of casos) {
- rows.push(await runCase(testCase, flags));
- }
- console.table(rows);
- const positives = rows.filter((r) => r.type === "positive");
- const negatives = rows.filter((r) => r.type === "negative");
- const positiveScores = positives.map((r) => Number(r.top1_score)).filter((n) => !Number.isNaN(n));
- const negativeScores = negatives.map((r) => Number(r.top1_score)).filter((n) => !Number.isNaN(n));
- const avg = (arr) => (arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : NaN);
- const positivoMin = positiveScores.length ? Math.min(...positiveScores) : NaN;
- const negativoMax = negativeScores.length ? Math.max(...negativeScores) : NaN;
- const gap = positivoMin - negativoMax;
- const auc = separabilidade(positiveScores, negativeScores);
- console.log("\n--- resumo ---");
- console.log(`positivos: ${positiveScores.length} casos, score médio top-1 = ${avg(positiveScores).toFixed(4)}`);
- console.log(`positivos: score mínimo top-1 (piso de segurança) = ${positiveScores.length ? positivoMin.toFixed(4) : "-"}`);
- console.log(`negativos: ${negativeScores.length} casos, score médio top-1 = ${avg(negativeScores).toFixed(4)}`);
- console.log(`negativos: score máximo top-1 (teto de ruído) = ${negativeScores.length ? negativoMax.toFixed(4) : "-"}`);
- 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") : ""}`);
- console.log(`separabilidade (% pares positivo>negativo, tipo AUC) = ${Number.isNaN(auc) ? "-" : auc.toFixed(1) + "%"}`);
- console.log("");
- }
- main()
- .then(() => process.exit(0))
- .catch((err) => {
- console.error("[evalAtendimentosRetrieval] erro:", err);
- process.exit(1);
- });
|