|
|
@@ -2,31 +2,106 @@ import { config } from "#config/index.js";
|
|
|
import { buscarAtendimentosSemelhantes } from "#services/atendimentoRagService.js";
|
|
|
|
|
|
function parseArgs(argv) {
|
|
|
- const flags = { hyde: null, minScore: 0, topK: 5, label: "" };
|
|
|
+ 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 = [
|
|
|
- { query: "cliente pedindo segunda via de boleto", type: "positive" },
|
|
|
- { query: "problema de internet lenta", type: "positive" },
|
|
|
- { query: "cancelamento de contrato", type: "positive" },
|
|
|
- { query: "cliente reclamando que a internet caiu de novo", type: "positive" },
|
|
|
- { query: "solicitação de mudança de endereço de instalação", type: "positive" },
|
|
|
- { query: "cliente questionando cobrança duplicada na fatura", type: "positive" },
|
|
|
- { query: "pedido de upgrade de plano de internet", type: "positive" },
|
|
|
- { query: "previsão do tempo para amanhã em Paris", type: "negative" },
|
|
|
- { query: "receita de bolo de chocolate", type: "negative" },
|
|
|
- { query: "quem ganhou o jogo de futebol ontem", type: "negative" },
|
|
|
- { query: "qual a capital da Austrália", type: "negative" },
|
|
|
- { query: "como treinar para uma maratona", type: "negative" },
|
|
|
- { query: "melhores filmes de ficção científica dos anos 80", type: "negative" }
|
|
|
+ ...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) {
|
|
|
@@ -34,7 +109,9 @@ async function runCase(testCase, flags) {
|
|
|
query: testCase.query,
|
|
|
topK: flags.topK,
|
|
|
minScore: flags.minScore,
|
|
|
- ...(flags.hyde !== null ? { hyde: flags.hyde } : {})
|
|
|
+ ...(flags.hyde !== null ? { hyde: flags.hyde } : {}),
|
|
|
+ ...(flags.collection ? { collectionName: flags.collection } : {}),
|
|
|
+ ...(flags.model ? { embeddingsModel: flags.model } : {})
|
|
|
});
|
|
|
|
|
|
const top1 = hits[0] ?? null;
|
|
|
@@ -47,18 +124,37 @@ async function runCase(testCase, flags) {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+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=${config.ollama.embeddingsModel} minScore=${flags.minScore} topK=${flags.topK} ` +
|
|
|
- `hyde=${hydeEfetivo} colecao=${config.atendimentosRag.collection}\n`
|
|
|
+ `modelo=${modelEfetivo} minScore=${flags.minScore} topK=${flags.topK} ` +
|
|
|
+ `hyde=${hydeEfetivo} colecao=${collectionEfetiva} casos=${casos.length}/${CASES.length}\n`
|
|
|
);
|
|
|
|
|
|
const rows = [];
|
|
|
- for (const testCase of CASES) {
|
|
|
+ for (const testCase of casos) {
|
|
|
rows.push(await runCase(testCase, flags));
|
|
|
}
|
|
|
|
|
|
@@ -75,13 +171,15 @@ async function main() {
|
|
|
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: score médio top-1 = ${avg(positiveScores).toFixed(4)}`);
|
|
|
+ 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: score médio top-1 = ${avg(negativeScores).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("");
|
|
|
}
|
|
|
|