leonardo hace 1 mes
padre
commit
816e235920

+ 4 - 0
.env.example

@@ -38,6 +38,10 @@ OLLAMA_URL=http://localhost:11434
 OLLAMA_EMBEDDINGS_MODEL=nomic-embed-text
 OLLAMA_CHAT_MODEL=llama3
 OLLAMA_VISION_MODEL=llava
+# Modelo usado na sugestão de resposta ao atendente (ConversasWhatsapp). Default
+# já é "star-atendente" no código mesmo sem essa variável — só setar se publicar
+# o modelo fine-tuned com outro nome no Ollama.
+# OLLAMA_ATENDENTE_MODEL=star-atendente
 # Timeout (ms) para chamadas não-streaming ao Ollama (chat/embeddings). Sem isso, uma
 # chamada travada (modelo sobrecarregado, processo do Ollama pendurado) nunca resolve
 # nem rejeita — trava indefinidamente locks como o de avaliação em lote de atendimentos.

+ 84 - 0
scripts/backfillCancelamento.js

@@ -0,0 +1,84 @@
+// Reseta atendimentos já avaliados (Avaliavel=true) para pendente, para que a próxima
+// rodada de avaliação também extraia StatusCancelamento/MotivoCancelamento (campos novos).
+// Não faz a reavaliação em si — só o reset. Depois de rodar com --apply, drenar a fila
+// com scripts/avaliarTudo.js (idempotente entre reinícios, ao contrário deste script).
+//
+// Uso: node scripts/backfillCancelamento.js [--dias 90] [--apply] [--limite N]
+//   --dias N   : só atendimentos com Abertura nos últimos N dias (omitir = todo o histórico Avaliavel=true)
+//   --apply    : executa de fato (sem essa flag, é dry-run)
+//   --limite N : limita quantos IDs entram no backfill (teste)
+
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import { AtendimentoAvaliacao } from "../src/models/AtendimentoAvaliacao.model.js";
+import { contarPendentes } from "../src/services/atendimentoAvaliacaoService.js";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const APLICAR = process.argv.includes("--apply");
+
+const diasIdx = process.argv.indexOf("--dias");
+const DIAS = diasIdx > -1 ? Number(process.argv[diasIdx + 1]) : null;
+
+const limiteIdx = process.argv.indexOf("--limite");
+const LIMITE = limiteIdx > -1 ? Number(process.argv[limiteIdx + 1]) : null;
+
+async function buscarCandidatos() {
+  const query = AtendimentoAvaliacao.query()
+    .where("Avaliavel", true)
+    .join("atendimentos as a", "a.Id", "atendimento_avaliacoes.AtendimentoId")
+    .select(
+      "atendimento_avaliacoes.AtendimentoId",
+      "atendimento_avaliacoes.StatusCancelamento",
+      "atendimento_avaliacoes.MotivoCancelamento",
+      "a.Abertura"
+    )
+    .orderBy("a.Abertura", "desc");
+
+  if (DIAS) {
+    const cutoff = new Date(Date.now() - DIAS * 24 * 60 * 60 * 1000);
+    query.where("a.Abertura", ">=", cutoff);
+  }
+
+  return query;
+}
+
+async function main() {
+  const candidatos = await buscarCandidatos();
+  const alvos = LIMITE ? candidatos.slice(0, LIMITE) : candidatos;
+
+  console.log(
+    `[backfill-cancelamento] ${candidatos.length} candidatos` +
+    `${DIAS ? ` (Abertura nos últimos ${DIAS} dias)` : " (todo o histórico Avaliavel=true)"}.`
+  );
+  console.log(`[backfill-cancelamento] ${alvos.length} avaliações serão resetadas para pendente${LIMITE ? ` (--limite ${LIMITE})` : ""}.`);
+
+  if (!APLICAR) {
+    console.log("[backfill-cancelamento] dry-run (nenhuma alteração feita). Use --apply para executar.");
+    process.exit(0);
+  }
+  if (alvos.length === 0) {
+    console.log("[backfill-cancelamento] nada para reavaliar.");
+    process.exit(0);
+  }
+
+  const backupDir = path.resolve(__dirname, "logs");
+  fs.mkdirSync(backupDir, { recursive: true });
+  const backupPath = path.join(backupDir, `backfill-cancelamento-${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
+  fs.writeFileSync(backupPath, JSON.stringify(alvos, null, 2));
+  console.log(`[backfill-cancelamento] backup dos valores antigos salvo em ${backupPath}`);
+
+  const ids = alvos.map((a) => a.AtendimentoId);
+  const pendentesAntes = await contarPendentes();
+  await AtendimentoAvaliacao.query().whereIn("AtendimentoId", ids).patch({ UltimaMensagemId: null });
+  const pendentesDepois = await contarPendentes();
+  console.log(`[backfill-cancelamento] resetados ${ids.length} — fila de pendentes ${pendentesAntes} -> ${pendentesDepois}.`);
+  console.log("[backfill-cancelamento] pronto. Agora rode scripts/avaliarTudo.js para drenar a fila.");
+
+  process.exit(0);
+}
+
+main().catch((e) => {
+  console.error("[backfill-cancelamento] falha:", e.message);
+  process.exit(1);
+});

+ 4 - 0
src/services/atendenteSugestaoService.js

@@ -9,6 +9,10 @@ const SYSTEM_PROMPT = [
     "Seja cordial, objetivo e resolutivo: entenda o problema, faça as perguntas necessarias,",
     "explique os passos com clareza e conduza ate a solução ou o encaminhamento correto.",
     "Responda em português brasileiro.",
+    "NUNCA peça numero de cartão de credito ou debito, senha, código de verificação/SMS,",
+    "ou dados de pagamento - a star internet jamais solicita esse tipo de informação pelo",
+    "WhatsApp. Para confirmar a identidade do cliente use apenas dados como CPF, nome",
+    "completo ou numero do contrato/protocolo.",
     "Gere APENAS o texto da mensagem que seria enviada ao cliente agora, sem comentarios,",
     "explicações ou marcações extras - nada alem da resposta em si."
 ].join(" ");

+ 5 - 1
training/README.md

@@ -60,7 +60,11 @@ ollama: star-atendente                   ← modelo fundido + quantizado q4_K_M
    `star-atendente` quantizado e remove o intermediário fp16 (~16 GB temporários em disco).
 
 6. **Validar e ativar**: compare respostas com o llama3.1 puro em perguntas típicas
-   de suporte. Para o Oráculo usar: `OLLAMA_CHAT_MODEL=star-atendente` no `backend/.env`.
+   de suporte. O backend já usa `star-atendente` por default
+   (`config.atendenteSugestao.model`, na feature de sugestão de resposta ao
+   atendente) — só é preciso setar `OLLAMA_ATENDENTE_MODEL=star-atendente` no
+   `backend/.env` se o modelo for publicado com outro nome. Não confundir com
+   `OLLAMA_CHAT_MODEL`, que é o modelo do chat RAG geral.
 
 ## Quando re-treinar
 

+ 4 - 3
training/publicar_no_ollama.sh

@@ -23,14 +23,14 @@ mlx_lm.fuse \
   --model "$MODEL" \
   --adapter-path "$ADAPTER_DIR" \
   --save-path "$FUSED_DIR" \
-  --de-quantize
+  --dequantize
 
 echo "==> Montando Modelfile (template herdado do llama3.1 instalado no Ollama)"
 TEMPLATE=$(ollama show llama3.1 --template)
 cat > Modelfile <<EOF
 FROM ./$FUSED_DIR
 TEMPLATE """$TEMPLATE"""
-SYSTEM """Você é um atendente da Star Internet, atendendo clientes pelo WhatsApp. Seja cordial, objetivo e resolutivo: entenda o problema, faça as perguntas necessárias, explique os passos com clareza e conduza o atendimento até a solução ou encaminhamento correto. Responda em português brasileiro."""
+SYSTEM """Você é um atendente da Star Internet, atendendo clientes pelo WhatsApp. Seja cordial, objetivo e resolutivo: entenda o problema, faça as perguntas necessárias, explique os passos com clareza e conduza o atendimento até a solução ou encaminhamento correto. Responda em português brasileiro. NUNCA peça número de cartão de crédito ou débito, senha, código de verificação/SMS, ou dados de pagamento - a Star Internet jamais solicita esse tipo de informação pelo WhatsApp. Para confirmar a identidade do cliente use apenas dados como CPF, nome completo ou número do contrato/protocolo."""
 PARAMETER temperature 0.3
 PARAMETER num_ctx 8192
 EOF
@@ -43,4 +43,5 @@ rm -rf "$FUSED_DIR"
 
 echo
 echo "Pronto. Teste com:  ollama run $NOME_OLLAMA 'Boa tarde, minha internet está lenta'"
-echo "Para o backend usar no chat: OLLAMA_CHAT_MODEL=$NOME_OLLAMA no backend/.env"
+echo "O backend já usa esse modelo por default (config.atendenteSugestao.model)."
+echo "Só é preciso setar OLLAMA_ATENDENTE_MODEL=$NOME_OLLAMA no backend/.env se publicar com outro nome."