|
@@ -7,7 +7,7 @@ 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";
|
|
|
import { NotFoundError, UnprocessableEntityError } from "../shared/errors/index.js";
|
|
import { NotFoundError, UnprocessableEntityError } from "../shared/errors/index.js";
|
|
|
-import { nomeSetor } from "../config/setores.js";
|
|
|
|
|
|
|
+import { nomeSetor, setorAvaliavel, SETORES_EXCLUIDOS_AVALIACAO } from "../config/setores.js";
|
|
|
|
|
|
|
|
const LIMITE_RELATORIO_PADRAO = 150;
|
|
const LIMITE_RELATORIO_PADRAO = 150;
|
|
|
|
|
|
|
@@ -75,9 +75,23 @@ const AVALIACAO_SYSTEM_PROMPT = [
|
|
|
"9-10 = exemplar; 7-8 = bom, resolveu com pequenas falhas; 5-6 = mediano; 3-4 = fraco (respostas",
|
|
"9-10 = exemplar; 7-8 = bom, resolveu com pequenas falhas; 5-6 = mediano; 3-4 = fraco (respostas",
|
|
|
"confusas, informação incorreta, cliente insistindo sem retorno adequado); 1-2 = muito ruim (atendente",
|
|
"confusas, informação incorreta, cliente insistindo sem retorno adequado); 1-2 = muito ruim (atendente",
|
|
|
"negligente: não respondeu, demorou de forma extrema, deu informação incorreta, transferiu o",
|
|
"negligente: não respondeu, demorou de forma extrema, deu informação incorreta, transferiu o",
|
|
|
- "atendimento sem acompanhar/sem solução, tratou mal, ou induziu a erro).",
|
|
|
|
|
|
|
+ "atendimento SEM FAZER NADA antes — sem verificar/diagnosticar o que cabia ao seu setor e sem avisar",
|
|
|
|
|
+ "o cliente do encaminhamento —, tratou mal, ou induziu a erro).",
|
|
|
|
|
+ 'IMPORTANTE sobre transferências: transferir para outro setor NÃO é, por si só, negligência —',
|
|
|
|
|
+ "é o fluxo normal quando o problema foge do escopo do setor atual. Só penalize a transferência",
|
|
|
|
|
+ "(faixa 1-4) quando ela for feita às cegas: sem nenhuma verificação/diagnóstico dentro do que cabia",
|
|
|
|
|
+ "ao atendente resolver, e/ou sem avisar o cliente. Se o atendente fez a checagem cabível ao seu",
|
|
|
|
|
+ "setor (ex.: financeiro confirma que os pagamentos estão em dia antes de encaminhar um problema",
|
|
|
|
|
+ "técnico ao Suporte) e avisou o cliente antes de transferir, isso é atuação correta e não deve",
|
|
|
|
|
+ "derrubar o score para 1-4 só porque o problema em si não foi resolvido ali — avalie pela qualidade",
|
|
|
|
|
+ "da interação até o momento da transferência (faixa 5-8, a depender de agilidade e clareza), não",
|
|
|
|
|
+ "pelo desfecho técnico que ficou a cargo do outro setor.",
|
|
|
"Um atendimento resolvido com cliente satisfeito NUNCA recebe score abaixo de 6.",
|
|
"Um atendimento resolvido com cliente satisfeito NUNCA recebe score abaixo de 6.",
|
|
|
"Se nenhum atendente humano participou, use score_atendente = null.",
|
|
"Se nenhum atendente humano participou, use score_atendente = null.",
|
|
|
|
|
+ "Pela mesma lógica de escrever a justificativa antes do score: escreva o resumo ANTES de decidir",
|
|
|
|
|
+ "resolvido e sentimento_cliente_final. Montar a narrativa completa primeiro (o que cada atendente",
|
|
|
|
|
+ "fez, se houve verificação antes de um encaminhamento, etc.) deixa essas categorias mais precisas —",
|
|
|
|
|
+ "elas devem ser consequência do que você acabou de narrar no resumo, não o contrário.",
|
|
|
"",
|
|
"",
|
|
|
"Exemplos de calibração (conversas fictícias e resumidas — sirvam de referência para o score, não copie o texto):",
|
|
"Exemplos de calibração (conversas fictícias e resumidas — sirvam de referência para o score, não copie o texto):",
|
|
|
"",
|
|
"",
|
|
@@ -158,12 +172,118 @@ const AVALIACAO_SYSTEM_PROMPT = [
|
|
|
" como gatilho: sempre confira QUEM está cancelando o quê.",
|
|
" como gatilho: sempre confira QUEM está cancelando o quê.",
|
|
|
'→ motivo_cancelamento: null',
|
|
'→ motivo_cancelamento: null',
|
|
|
"",
|
|
"",
|
|
|
|
|
+ "Exemplo 8 — cliente entra em contato sem detalhar o problema original e diz que já está resolvido",
|
|
|
|
|
+ "(resolvido = \"sim\", sentimento positivo, score alto — mesmo sem diagnóstico visível na conversa):",
|
|
|
|
|
+ 'Cliente: "Oi, bom dia"',
|
|
|
|
|
+ 'Atendente Renata: "Bom dia! Em que posso ajudar?"',
|
|
|
|
|
+ 'Cliente: "Ah, já está tudo certo agora, era só um susto, obrigada"',
|
|
|
|
|
+ 'Atendente Renata: "Que bom! Qualquer coisa estou à disposição."',
|
|
|
|
|
+ '→ resolvido: "sim" — o próprio cliente confirma explicitamente que o problema já está resolvido;',
|
|
|
|
|
+ " isso vale como resolução mesmo que a causa original nunca tenha sido detalhada na conversa nem",
|
|
|
|
|
+ " o atendente tenha feito uma ação técnica visível. Não marque \"nao\"/\"indefinido\" só porque falta",
|
|
|
|
|
+ ' contexto sobre qual era o problema — a confirmação do cliente é o que importa aqui.',
|
|
|
|
|
+ '→ sentimento_cliente_final: "positivo"',
|
|
|
|
|
+ '→ score_atendente: 8-9 — atendimento breve mas cordial e resolvido; o piso 6 para resolvido=sim +',
|
|
|
|
|
+ " sentimento=positivo se aplica normalmente.",
|
|
|
|
|
+ "",
|
|
|
"Campos:",
|
|
"Campos:",
|
|
|
- '- resolvido: "sim" se o problema do cliente foi claramente resolvido na conversa; "parcial" se houve',
|
|
|
|
|
- ' encaminhamento/paliativo; "nao" se terminou sem solução; "indefinido" se não dá para concluir.',
|
|
|
|
|
|
|
+ '- resolvido: "sim" se o problema do cliente foi claramente resolvido na conversa — inclui o caso em',
|
|
|
|
|
+ " que o PRÓPRIO CLIENTE confirma que já está tudo certo/resolvido, mesmo sem ter detalhado qual era",
|
|
|
|
|
+ ' o problema original ou sem uma ação técnica visível do atendente (ver Exemplo 8); "parcial" se houve',
|
|
|
|
|
+ " encaminhamento/paliativo com alguma mitigação real (ex.: solução temporária enquanto o definitivo",
|
|
|
|
|
+ " não chega); \"nao\" se terminou sem solução — isso inclui um encaminhamento/transferência correto e",
|
|
|
|
|
+ " diligente cujo problema em si não foi resolvido nesta conversa (a diligência do atendente já é",
|
|
|
|
|
+ ' reconhecida no score e no resumo, não precisa inflar o "resolvido" por causa dela); "indefinido" se',
|
|
|
|
|
+ " não dá para concluir.",
|
|
|
'- sentimento_cliente_final: sentimento do cliente AO FINAL da conversa ("positivo", "neutro", "negativo"',
|
|
'- sentimento_cliente_final: sentimento do cliente AO FINAL da conversa ("positivo", "neutro", "negativo"',
|
|
|
' ou "indefinido" quando o cliente some/não se manifesta no final).',
|
|
' ou "indefinido" quando o cliente some/não se manifesta no final).',
|
|
|
- "- resumo: 2 a 4 frases objetivas: quem procurou, qual o problema, o que foi feito, como terminou.",
|
|
|
|
|
|
|
+ "- resumo: 2 a 4 frases objetivas e específicas — nunca genéricas. Regras: (1) se um problema",
|
|
|
|
|
+ " mencionado é causado por outro (ex.: internet bloqueada POR CAUSA de boleto em atraso), explicite",
|
|
|
|
|
+ " essa relação de causa em vez de listar os dois como problemas soltos e desconectados; (2) diga o",
|
|
|
|
|
+ ' que o cliente pediu/solicitou concretamente (ex.: "agendar o pagamento para liberar a internet"),',
|
|
|
|
|
+ " não só o problema em si; (3) diga a ação concreta que resolveu ou encaminhou o caso — evite frases",
|
|
|
|
|
+ ' vagas como "resolvido após várias transferências" sem dizer o que de fato foi feito (ex.: o que o',
|
|
|
|
|
+ " técnico/financeiro efetivamente fez); (4) descreva como terminou.",
|
|
|
|
|
+ ' Exemplo ruim: "Cliente Maria procurou atendimento por problemas com internet bloqueada e boleto em',
|
|
|
|
|
+ ' atraso. O problema foi resolvido após várias transferências de atendimento."',
|
|
|
|
|
+ ' Exemplo bom: "Cliente Maria procurou atendimento devido a internet bloqueada por boleto em atraso,',
|
|
|
|
|
+ ' solicitando agendar o pagamento para liberar a internet. O atendente confirmou o agendamento do',
|
|
|
|
|
+ ' pagamento junto ao financeiro e a internet foi liberada."',
|
|
|
|
|
+ " (5) quando uma transferência de setor aparecer, diga POR QUE ela aconteceu se der pra inferir da",
|
|
|
|
|
+ ' conversa (ex.: "transferiu para o financeiro, que é quem faz o desbloqueio") — uma transferência',
|
|
|
|
|
+ " para o setor correto/responsável é um encaminhamento normal do processo, não uma falha; não a",
|
|
|
|
|
+ ' descreva como "sem solução encontrada" só porque o desfecho final não apareceu na conversa. Se o',
|
|
|
|
|
+ " atendente fez alguma verificação/diagnóstico ANTES de transferir (ex.: conferiu que os pagamentos",
|
|
|
|
|
+ " estavam em dia, descartando causa financeira) e/ou avisou o cliente antes de encaminhar, é",
|
|
|
|
|
+ " OBRIGATÓRIO citar essa verificação no resumo — nunca omita esse detalhe quando ele estiver na",
|
|
|
|
|
+ " conversa; ele é o que diferencia uma transferência diligente de uma transferência às cegas.",
|
|
|
|
|
+ ' Exemplo ruim: "Cliente Letícia procurou atendimento devido à falta de sinal de internet, mas o',
|
|
|
|
|
+ ' problema não foi resolvido. O atendente Bruna transferiu para o setor Suporte sem solução."',
|
|
|
|
|
+ ' Exemplo bom: "Cliente Letícia procurou atendimento devido à falta de sinal de internet. A atendente',
|
|
|
|
|
+ ' Bruna verificou que os pagamentos estavam todos em dia e encaminhou a questão para o Suporte',
|
|
|
|
|
+ ' verificar, avisando a cliente antes de transferir."',
|
|
|
|
|
+ " (6) se",
|
|
|
|
|
+ " o motivo de não ter concluído foi o CLIENTE não responder a um contato/retorno do atendente, diga",
|
|
|
|
|
+ " isso explicitamente (é a causa real do desfecho em aberto) em vez de atribuir a pendência de forma",
|
|
|
|
|
+ " vaga ao processo/atendimento. (7) use os nomes dos atendentes quando a conversa tiver mais de um",
|
|
|
|
|
+ " e a ação de cada um for relevante para entender o desfecho (quem transferiu, quem tentou contato).",
|
|
|
|
|
+ ' Exemplo ruim: "Gaby procurou atendimento por problemas de internet, mas o atendimento foi',
|
|
|
|
|
+ ' transferido para o setor financeiro sem solução encontrada."',
|
|
|
|
|
+ ' Exemplo bom: "Gaby procurou atendimento devido à internet bloqueada por ter esquecido de pagar a',
|
|
|
|
|
+ ' fatura; ela já havia realizado o pagamento e queria a liberação. A atendente Carla transferiu para',
|
|
|
|
|
+ ' o setor financeiro, responsável pelo desbloqueio; a atendente Helen tentou contato em seguida, mas',
|
|
|
|
|
+ ' a cliente não respondeu."',
|
|
|
|
|
+ " (8) não use uma frase-modelo genérica para o pedido do cliente ou para a causa da liberação só",
|
|
|
|
|
+ " porque ela apareceu em outro atendimento parecido — descreva o que REALMENTE aconteceu nesta",
|
|
|
|
|
+ ' conversa (ex.: o cliente pode ter pedido a fatura para pagar por conta própria via PIX, em vez de',
|
|
|
|
|
+ ' pedir para "agendar o pagamento"). Nunca escreva uma causa circular/tautológica, onde o motivo',
|
|
|
|
|
+ ' citado é só outro nome para o próprio resultado (ex.: "a internet foi liberada após confirmação da',
|
|
|
|
|
+ ' liberação" não diz nada) — identifique o evento real que disparou a ação (ex.: "após o cliente',
|
|
|
|
|
+ ' enviar/confirmar o comprovante de pagamento").',
|
|
|
|
|
+ ' Exemplo ruim: "Cliente procurou atendimento devido a fatura em aberto e solicitou agendar o',
|
|
|
|
|
+ ' pagamento via PIX. O atendente forneceu a fatura e instruções para pagamento, e a internet foi',
|
|
|
|
|
+ ' liberada após confirmação da liberação." (a causa final é circular e "agendar o pagamento" não é',
|
|
|
|
|
+ " o que o cliente pediu)",
|
|
|
|
|
+ ' Exemplo bom: "Cliente procurou atendimento devido a fatura em aberto e solicitou a fatura para',
|
|
|
|
|
+ ' realizar o pagamento via PIX. O atendente forneceu a fatura; após o cliente enviar o comprovante',
|
|
|
|
|
+ ' de pagamento, a internet foi liberada."',
|
|
|
|
|
+ ' (9) mesma lógica da regra sobre mensagens "[FORA DO EXPEDIENTE]" definida no início deste prompt:',
|
|
|
|
|
+ " se o atendimento ficou sem solução porque a resposta do cliente chegou fora do horário de",
|
|
|
|
|
+ " atendimento do setor (ex.: fim de semana) e por isso não houve mais retorno do atendente dentro da",
|
|
|
|
|
+ ' conversa, diga isso explicitamente no resumo — não descreva como "atendente não conseguiu resolver"',
|
|
|
|
|
+ " nem como falha do atendente; o motivo real é o horário, não a atuação de quem atendeu.",
|
|
|
|
|
+ ' Exemplo ruim: "Cliente solicitou o código para colocar sua fatura em débito em conta, mas atendente',
|
|
|
|
|
+ ' não conseguiu resolver a solicitação. Atendimento foi encerrado sem solução."',
|
|
|
|
|
+ ' Exemplo bom: "Cliente solicitou o código para colocar sua fatura em débito em conta. A cliente',
|
|
|
|
|
+ ' respondeu fora do horário comercial do setor (fim de semana), então o atendimento ficou sem',
|
|
|
|
|
+ ' retorno dentro da conversa até a reabertura do expediente."',
|
|
|
|
|
+ " (10) quando o atendente citar um dado concreto da conversa — um prazo, uma data-limite, um valor,",
|
|
|
|
|
+ ' uma condição específica — cite esse dado concreto no resumo em vez de resumir com uma expressão',
|
|
|
|
|
+ ' vaga como "ofereceu uma solução alternativa"/"deu instruções". Se o atendente apenas EXPLICOU um',
|
|
|
|
|
+ " prazo ou regra já existente (ex.: até quando o pagamento pode ser feito), isso é uma explicação,",
|
|
|
|
|
+ " não a criação de uma solução alternativa — não infira uma ação que não ocorreu.",
|
|
|
|
|
+ ' Exemplo ruim: "O atendente Bruna explicou que não era possível agendar com dois vencimentos em',
|
|
|
|
|
+ ' aberto, mas ofereceu uma solução alternativa. Após entendimento do problema e apresentação da',
|
|
|
|
|
+ ' solução, o cliente aceitou e a conexão foi liberada." (não cita a data e inventa uma "solução',
|
|
|
|
|
+ ' alternativa" que não houve)',
|
|
|
|
|
+ ' Exemplo bom: "A atendente Bruna explicou que não era possível agendar com dois vencimentos em',
|
|
|
|
|
+ ' aberto, mas informou que o prazo máximo para pagamento seria até [data citada na conversa]. O',
|
|
|
|
|
+ ' cliente aceitou e a conexão foi liberada."',
|
|
|
|
|
+ " (11) não invente uma ação do atendente que não está na conversa — se o motivo de não haver resposta",
|
|
|
|
|
+ ' imediata foi simplesmente estar fora do horário de atendimento (ex.: mensagem "[FORA DO EXPEDIENTE]"',
|
|
|
|
|
+ ' ou ausência de resposta humana), NÃO escreva que "o atendente informou que estava fora do horário"',
|
|
|
|
|
+ ' nem que "o atendente aguardou resposta do cliente" a menos que isso literalmente apareça na',
|
|
|
|
|
+ " conversa — descreva o fato como ele é (ficou fora do expediente até a reabertura), sem atribuir uma",
|
|
|
|
|
+ " ação a quem não agiu. Se o PRÓPRIO CLIENTE relatar que o problema se resolveu sozinho (ex.: o sinal",
|
|
|
|
|
+ ' voltou depois de um tempo, sem intervenção do atendente), registre isso explicitamente como',
|
|
|
|
|
+ ' resolução espontânea — conta como resolvido="sim", já que o cliente confirma que o problema não',
|
|
|
|
|
+ " existe mais, mesmo sem ação técnica do atendente.",
|
|
|
|
|
+ ' Exemplo ruim: "Cliente procurou atendimento por problemas com internet sem sinal, solicitando',
|
|
|
|
|
+ ' ajuda. O atendente informou que estava fora do horário de atendimento e aguardou resposta do',
|
|
|
|
|
+ ' cliente até a reabertura do expediente." (inventa uma ação do atendente que não ocorreu)',
|
|
|
|
|
+ ' Exemplo bom: "Cliente entrou em contato reclamando de falta de sinal de internet, mas já era fora',
|
|
|
|
|
+ ' do horário de atendimento do setor. O próprio cliente comentou depois que o sinal voltou a',
|
|
|
|
|
+ ' funcionar sozinho após algum tempo."',
|
|
|
"- sugestoes_melhoria: 0 a 5 itens curtos e acionáveis para o atendente/equipe melhorar; lista vazia se exemplar.",
|
|
"- sugestoes_melhoria: 0 a 5 itens curtos e acionáveis para o atendente/equipe melhorar; lista vazia se exemplar.",
|
|
|
"- atendentes: nomes dos atendentes humanos que participaram (como aparecem na conversa).",
|
|
"- atendentes: nomes dos atendentes humanos que participaram (como aparecem na conversa).",
|
|
|
"- visita_agendada: true se ficou combinada visita técnica presencial na conversa.",
|
|
"- visita_agendada: true se ficou combinada visita técnica presencial na conversa.",
|
|
@@ -200,9 +320,9 @@ const AVALIACAO_JSON_SCHEMA = {
|
|
|
|
|
|
|
|
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 },
|
|
|
|
|
+ resumo: { type: "string" },
|
|
|
resolvido: { type: "string", enum: ["sim", "nao", "parcial", "indefinido"] },
|
|
resolvido: { type: "string", enum: ["sim", "nao", "parcial", "indefinido"] },
|
|
|
sentimento_cliente_final: { type: "string", enum: ["positivo", "neutro", "negativo", "indefinido"] },
|
|
sentimento_cliente_final: { type: "string", enum: ["positivo", "neutro", "negativo", "indefinido"] },
|
|
|
- resumo: { type: "string" },
|
|
|
|
|
sugestoes_melhoria: { type: "array", items: { type: "string" }, maxItems: 5 },
|
|
sugestoes_melhoria: { type: "array", items: { type: "string" }, maxItems: 5 },
|
|
|
atendentes: { type: "array", items: { type: "string" } },
|
|
atendentes: { type: "array", items: { type: "string" } },
|
|
|
visita_agendada: { type: "boolean" },
|
|
visita_agendada: { type: "boolean" },
|
|
@@ -438,6 +558,30 @@ export async function avaliarAtendimento(atendimentoId) {
|
|
|
const ultimaMensagemId = calcularUltimaMensagemId(mensagens);
|
|
const ultimaMensagemId = calcularUltimaMensagemId(mensagens);
|
|
|
const { cliente: msgsCliente, atendente: msgsAtendente } = contarInteracoes(mensagens);
|
|
const { cliente: msgsCliente, atendente: msgsAtendente } = contarInteracoes(mensagens);
|
|
|
|
|
|
|
|
|
|
+ // chat interno entre técnicos, não atendimento a cliente — não faz sentido pro juiz
|
|
|
|
|
+ if (!setorAvaliavel(atendimento.Setor)) {
|
|
|
|
|
+ const avaliacao = await upsertAvaliacao(atendimento.Id, {
|
|
|
|
|
+ Avaliavel: false,
|
|
|
|
|
+ ScoreAtendente: null,
|
|
|
|
|
+ JustificativaScore: null,
|
|
|
|
|
+ Resolvido: "indefinido",
|
|
|
|
|
+ Sentimento: "indefinido",
|
|
|
|
|
+ Resumo: null,
|
|
|
|
|
+ Sugestoes: [],
|
|
|
|
|
+ Atendentes: [],
|
|
|
|
|
+ VisitaAgendada: null,
|
|
|
|
|
+ HorarioVisitaInformado: null,
|
|
|
|
|
+ HorarioVisita: null,
|
|
|
|
|
+ StatusCancelamento: "nao",
|
|
|
|
|
+ MotivoCancelamento: null,
|
|
|
|
|
+ Modelo: null,
|
|
|
|
|
+ ContextoIxcStatus: "nao_verificado",
|
|
|
|
|
+ UltimaMensagemId: ultimaMensagemId,
|
|
|
|
|
+ TotalMensagens: mensagens.length
|
|
|
|
|
+ });
|
|
|
|
|
+ return { avaliado: false, motivo: "setor_interno", avaliacao };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (msgsCliente === 0 || msgsAtendente === 0) {
|
|
if (msgsCliente === 0 || msgsAtendente === 0) {
|
|
|
const avaliacao = await upsertAvaliacao(atendimento.Id, {
|
|
const avaliacao = await upsertAvaliacao(atendimento.Id, {
|
|
|
Avaliavel: false,
|
|
Avaliavel: false,
|
|
@@ -504,7 +648,8 @@ function pendentesQuery({ setor } = {}) {
|
|
|
q.whereNull("av.AtendimentoId")
|
|
q.whereNull("av.AtendimentoId")
|
|
|
.orWhereNull("av.UltimaMensagemId")
|
|
.orWhereNull("av.UltimaMensagemId")
|
|
|
.orWhereRaw("av.UltimaMensagemId < m.MaxMsgId");
|
|
.orWhereRaw("av.UltimaMensagemId < m.MaxMsgId");
|
|
|
- });
|
|
|
|
|
|
|
+ })
|
|
|
|
|
+ .whereNotIn("atendimentos.Setor", SETORES_EXCLUIDOS_AVALIACAO);
|
|
|
|
|
|
|
|
if (setor) query.where("atendimentos.Setor", setor);
|
|
if (setor) query.where("atendimentos.Setor", setor);
|
|
|
return query;
|
|
return query;
|