|
@@ -1,6 +1,7 @@
|
|
|
import { config } from "../config/index.js";
|
|
import { config } from "../config/index.js";
|
|
|
import { chatCompletion } from "./ollamaClient.js";
|
|
import { chatCompletion } from "./ollamaClient.js";
|
|
|
import { formatMensagem, formatDateTime } from "../utils/atendimentoFormat.js";
|
|
import { formatMensagem, formatDateTime } from "../utils/atendimentoFormat.js";
|
|
|
|
|
+import { estaForaExpediente } from "../config/expediente.js";
|
|
|
import { Atendimento } from "../models/Atendimento.model.js";
|
|
import { Atendimento } from "../models/Atendimento.model.js";
|
|
|
import { AtendimentoAvaliacao } from "../models/AtendimentoAvaliacao.model.js";
|
|
import { AtendimentoAvaliacao } from "../models/AtendimentoAvaliacao.model.js";
|
|
|
import { AtendimentoMensagem } from "../models/AtendimentoMensagem.model.js";
|
|
import { AtendimentoMensagem } from "../models/AtendimentoMensagem.model.js";
|
|
@@ -11,6 +12,10 @@ const AVALIACAO_SYSTEM_PROMPT = [
|
|
|
"Você é um auditor de qualidade de atendimento ao cliente de um provedor de internet (Star).",
|
|
"Você é um auditor de qualidade de atendimento ao cliente de um provedor de internet (Star).",
|
|
|
"Avalie a conversa abaixo entre cliente e atendente(s), extraída do sistema de atendimento (ifbot).",
|
|
"Avalie a conversa abaixo entre cliente e atendente(s), extraída do sistema de atendimento (ifbot).",
|
|
|
"Mensagens rotuladas como 'Sistema' são automáticas e não contam como atuação do atendente.",
|
|
"Mensagens rotuladas como 'Sistema' são automáticas e não contam como atuação do atendente.",
|
|
|
|
|
+ 'Mensagens do cliente marcadas com "[FORA DO EXPEDIENTE]" foram enviadas fora do horário de',
|
|
|
|
|
+ "atendimento do setor responsável; não penalize o atendente pela demora em responder especificamente",
|
|
|
|
|
+ "essa mensagem — esperar até a reabertura do expediente é esperado e não é falha do atendente.",
|
|
|
|
|
+ "Continue avaliando normalmente a qualidade e o conteúdo da resposta dada.",
|
|
|
"Baseie-se APENAS no que está na conversa; não invente fatos, nomes ou desfechos.",
|
|
"Baseie-se APENAS no que está na conversa; não invente fatos, nomes ou desfechos.",
|
|
|
"",
|
|
"",
|
|
|
"Primeiro escreva a justificativa_score (1 a 2 frases analisando a atuação do atendente) e só então",
|
|
"Primeiro escreva a justificativa_score (1 a 2 frases analisando a atuação do atendente) e só então",
|
|
@@ -25,12 +30,12 @@ const AVALIACAO_SYSTEM_PROMPT = [
|
|
|
"Exemplo 1 — resolvido e cliente satisfeito, mas processo com falha (score = piso 6):",
|
|
"Exemplo 1 — resolvido e cliente satisfeito, mas processo com falha (score = piso 6):",
|
|
|
'Cliente: "Minha internet caiu de novo, terceira vez essa semana."',
|
|
'Cliente: "Minha internet caiu de novo, terceira vez essa semana."',
|
|
|
"Sistema: atendimento transferido para o setor técnico.",
|
|
"Sistema: atendimento transferido para o setor técnico.",
|
|
|
- "(Atendente Marcos não responde por mais de 20 minutos.)",
|
|
|
|
|
- 'Cliente: "alguém aí? já fazem 20 minutos"',
|
|
|
|
|
|
|
+ "(Atendente Marcos não responde por mais de 60 minutos.)",
|
|
|
|
|
+ 'Cliente: "alguém aí? já fazem 60 minutos"',
|
|
|
'Atendente Marcos: "Desculpe a demora! Vou reiniciar sua conexão remotamente."',
|
|
'Atendente Marcos: "Desculpe a demora! Vou reiniciar sua conexão remotamente."',
|
|
|
'Cliente: "Voltou, obrigado!"',
|
|
'Cliente: "Voltou, obrigado!"',
|
|
|
'→ justificativa_score: "Problema resolvido e cliente satisfeito ao final, mas o atendente demorou mais',
|
|
'→ justificativa_score: "Problema resolvido e cliente satisfeito ao final, mas o atendente demorou mais',
|
|
|
- ' de 20 minutos para responder, prejudicando a experiência."',
|
|
|
|
|
|
|
+ ' de 60 minutos para responder, prejudicando a experiência."',
|
|
|
"→ score_atendente: 6 — mesmo com falha grave de processo, resolvido=sim e sentimento=positivo nunca",
|
|
"→ score_atendente: 6 — mesmo com falha grave de processo, resolvido=sim e sentimento=positivo nunca",
|
|
|
" ficam abaixo de 6; a falha já está registrada na justificativa e nas sugestões de melhoria.",
|
|
" ficam abaixo de 6; a falha já está registrada na justificativa e nas sugestões de melhoria.",
|
|
|
"",
|
|
"",
|
|
@@ -73,8 +78,7 @@ const AVALIACAO_SYSTEM_PROMPT = [
|
|
|
const AVALIACAO_JSON_SCHEMA = {
|
|
const AVALIACAO_JSON_SCHEMA = {
|
|
|
type: "object",
|
|
type: "object",
|
|
|
properties: {
|
|
properties: {
|
|
|
- // a ordem importa: a gramática do structured output segue a ordem das propriedades,
|
|
|
|
|
- // então o modelo justifica antes de pontuar
|
|
|
|
|
|
|
+
|
|
|
justificativa_score: { type: "string" },
|
|
justificativa_score: { type: "string" },
|
|
|
score_atendente: { type: ["integer", "null"], minimum: 1, maximum: 10 },
|
|
score_atendente: { type: ["integer", "null"], minimum: 1, maximum: 10 },
|
|
|
resolvido: { type: "string", enum: ["sim", "nao", "parcial", "indefinido"] },
|
|
resolvido: { type: "string", enum: ["sim", "nao", "parcial", "indefinido"] },
|
|
@@ -100,7 +104,7 @@ const AVALIACAO_JSON_SCHEMA = {
|
|
|
]
|
|
]
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-// texto consolidado da conversa para o juiz (cabeçalho + mensagens rotuladas)
|
|
|
|
|
|
|
+
|
|
|
function buildConversaTexto({ atendimento, cliente, mensagens }) {
|
|
function buildConversaTexto({ atendimento, cliente, mensagens }) {
|
|
|
const ordenadas = [...mensagens].sort((a, b) => {
|
|
const ordenadas = [...mensagens].sort((a, b) => {
|
|
|
const ta = a.Timestamp ? new Date(a.Timestamp).getTime() : 0;
|
|
const ta = a.Timestamp ? new Date(a.Timestamp).getTime() : 0;
|
|
@@ -116,13 +120,19 @@ function buildConversaTexto({ atendimento, cliente, mensagens }) {
|
|
|
atendimento.Abertura ? `Aberto em: ${formatDateTime(atendimento.Abertura)}` : null
|
|
atendimento.Abertura ? `Aberto em: ${formatDateTime(atendimento.Abertura)}` : null
|
|
|
].filter(Boolean);
|
|
].filter(Boolean);
|
|
|
|
|
|
|
|
- const linhas = ordenadas.map((m) => formatMensagem(m, cliente?.Nome)).filter(Boolean);
|
|
|
|
|
|
|
+ const linhas = ordenadas
|
|
|
|
|
+ .map((m) =>
|
|
|
|
|
+ formatMensagem(m, cliente?.Nome, {
|
|
|
|
|
+ foraExpediente: m.Resposta === 0 && estaForaExpediente(atendimento.Setor, m.Timestamp)
|
|
|
|
|
+ })
|
|
|
|
|
+ )
|
|
|
|
|
+ .filter(Boolean);
|
|
|
return [...cabecalho, "", ...linhas].join("\n").trim();
|
|
return [...cabecalho, "", ...linhas].join("\n").trim();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const TRUNCATION_MARKER = "\n[... trecho intermediário da conversa omitido ...]\n";
|
|
const TRUNCATION_MARKER = "\n[... trecho intermediário da conversa omitido ...]\n";
|
|
|
|
|
|
|
|
-// mantém início (cabeçalho + contexto do problema) e fim (desfecho/sentimento) quando a conversa é longa demais
|
|
|
|
|
|
|
+
|
|
|
function truncateConversa(text, maxChars = config.avaliacao.maxChars) {
|
|
function truncateConversa(text, maxChars = config.avaliacao.maxChars) {
|
|
|
if (text.length <= maxChars) return text;
|
|
if (text.length <= maxChars) return text;
|
|
|
const head = Math.floor(maxChars * 0.45);
|
|
const head = Math.floor(maxChars * 0.45);
|
|
@@ -141,9 +151,7 @@ function normalizeScore(value) {
|
|
|
return Math.min(10, Math.max(1, Math.round(n)));
|
|
return Math.min(10, Math.max(1, Math.round(n)));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// impõe no código a regra de piso do prompt: o modelo juiz (llama3.1 8B) às vezes calcula a
|
|
|
|
|
-// severidade certa na justificativa mas esquece de aplicar a regra condicional ao decidir o
|
|
|
|
|
-// score final — observado em ~6% das avaliações reais.
|
|
|
|
|
|
|
+
|
|
|
const SCORE_MINIMO_RESOLVIDO_POSITIVO = 6;
|
|
const SCORE_MINIMO_RESOLVIDO_POSITIVO = 6;
|
|
|
|
|
|
|
|
export function aplicarPisoScore(dados) {
|
|
export function aplicarPisoScore(dados) {
|