Criado .env para todas as filas e copiados as avaliações de 07/26

This commit is contained in:
2026-07-20 08:11:03 -03:00
parent 0092cb2e43
commit aaf68e044f
40 changed files with 2449 additions and 650 deletions

4
.env
View File

@@ -6,8 +6,8 @@ PIPERUN_CLIENT_ID=14
PIPERUN_CLIENT_SECRET=5MzZLKPjAexuANkl7g5dzyzm3sKqO8r159iSoF4x PIPERUN_CLIENT_SECRET=5MzZLKPjAexuANkl7g5dzyzm3sKqO8r159iSoF4x
PIPERUN_BOT_USERNAME=bot.piperun@nova.net.br PIPERUN_BOT_USERNAME=bot.piperun@nova.net.br
PIPERUN_BOT_PASSWORD=s?K<W>KN=twKEjTcTxoMv52=9fF5EwFT PIPERUN_BOT_PASSWORD=s?K<W>KN=twKEjTcTxoMv52=9fF5EwFT
MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE=4 MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE=10
MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE=6 MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE=10
BOT_EMAIL=bot.piperun@nova.net.br BOT_EMAIL=bot.piperun@nova.net.br
BOT_EMAIL_PASSWORD=diQAVg,gK7rZ:5XRsqJ4zFay:fkAaH.4 BOT_EMAIL_PASSWORD=diQAVg,gK7rZ:5XRsqJ4zFay:fkAaH.4
OLLAMA_AI_MODEL_DATA_SANITIZATION=phi4:latest OLLAMA_AI_MODEL_DATA_SANITIZATION=phi4:latest

21
.env.age Normal file
View File

@@ -0,0 +1,21 @@
OLLAMA_URL=2804:11d4:1:1002::14:18
OLLAMA_PORT=11434
OLLAMA_AI_MODEL=gpt-oss:20b
PIPERUN_API_URL=novanet.cxm.pipe.run
PIPERUN_CLIENT_ID=14
PIPERUN_CLIENT_SECRET=5MzZLKPjAexuANkl7g5dzyzm3sKqO8r159iSoF4x
PIPERUN_BOT_USERNAME=bot.piperun@nova.net.br
PIPERUN_BOT_PASSWORD=s?K<W>KN=twKEjTcTxoMv52=9fF5EwFT
MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE=4
MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE=6
BOT_EMAIL=bot.piperun@nova.net.br
BOT_EMAIL_PASSWORD=diQAVg,gK7rZ:5XRsqJ4zFay:fkAaH.4
OLLAMA_AI_MODEL_DATA_SANITIZATION=phi4:latest
FILA_ID=20
PROMPT_X=PROMPT_AGE.txt
FILTER_X=FILTER_AGE.txt
PASTA=evaluations_age
NOME_FILA="Agendamento"
INICIAL_AGENTE="AGE -"
EMAILS="nicolas.borges@nova.net.br, wilson.oliveira@nova.net.br"
CONSIDERAR_TEMPLATE=false

13
.env.com-on Normal file
View File

@@ -0,0 +1,13 @@
OLLAMA_URL=2804:11d4:1:1002::14:18
OLLAMA_PORT=11434
OLLAMA_AI_MODEL=gpt-oss:20b
PIPERUN_API_URL=novanet.cxm.pipe.run
PIPERUN_CLIENT_ID=14
PIPERUN_CLIENT_SECRET=5MzZLKPjAexuANkl7g5dzyzm3sKqO8r159iSoF4x
PIPERUN_BOT_USERNAME=bot.piperun@nova.net.br
PIPERUN_BOT_PASSWORD=s?K<W>KN=twKEjTcTxoMv52=9fF5EwFT
MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE=2
MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE=40
BOT_EMAIL=bot.piperun@nova.net.br
BOT_EMAIL_PASSWORD=diQAVg,gK7rZ:5XRsqJ4zFay:fkAaH.4
OLLAMA_AI_MODEL_DATA_SANITIZATION=phi4:latest

21
.env.com-v Normal file
View File

@@ -0,0 +1,21 @@
OLLAMA_URL=2804:11d4:1:1002::14:18
OLLAMA_PORT=11434
OLLAMA_AI_MODEL=gpt-oss:20b
PIPERUN_API_URL=novanet.cxm.pipe.run
PIPERUN_CLIENT_ID=14
PIPERUN_CLIENT_SECRET=5MzZLKPjAexuANkl7g5dzyzm3sKqO8r159iSoF4x
PIPERUN_BOT_USERNAME=bot.piperun@nova.net.br
PIPERUN_BOT_PASSWORD=s?K<W>KN=twKEjTcTxoMv52=9fF5EwFT
MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE=2
MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE=40
BOT_EMAIL=bot.piperun@nova.net.br
BOT_EMAIL_PASSWORD=diQAVg,gK7rZ:5XRsqJ4zFay:fkAaH.4
OLLAMA_AI_MODEL_DATA_SANITIZATION=phi4:latest
FILA_ID=11
PROMPT_X=PROMPT_COM-V.txt
FILTER_X=FILTER_COM-V.txt
PASTA=evaluations_com-v
NOME_FILA="Comercial - Vendas"
INICIAL_AGENTE="COM -"
EMAILS="nicolas.borges@nova.net.br, wilson.oliveira@nova.net.br, thais.vaz@nova.net.br"
CONSIDERAR_TEMPLATE=false

21
.env.cx-cs Normal file
View File

@@ -0,0 +1,21 @@
OLLAMA_URL=2804:11d4:1:1002::14:18
OLLAMA_PORT=11434
OLLAMA_AI_MODEL=gpt-oss:20b
PIPERUN_API_URL=novanet.cxm.pipe.run
PIPERUN_CLIENT_ID=14
PIPERUN_CLIENT_SECRET=5MzZLKPjAexuANkl7g5dzyzm3sKqO8r159iSoF4x
PIPERUN_BOT_USERNAME=bot.piperun@nova.net.br
PIPERUN_BOT_PASSWORD=s?K<W>KN=twKEjTcTxoMv52=9fF5EwFT
MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE=30
MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE=30
BOT_EMAIL=bot.piperun@nova.net.br
BOT_EMAIL_PASSWORD=diQAVg,gK7rZ:5XRsqJ4zFay:fkAaH.4
OLLAMA_AI_MODEL_DATA_SANITIZATION=phi4:latest
FILA_ID=21
PROMPT_X=PROMPT_CX-CS.txt
FILTER_X=FILTER_CX-CS.txt
PASTA=evaluations_cx-cs
NOME_FILA="CX-CS"
INICIAL_AGENTE="CX-CS -"
EMAILS="nicolas.borges@nova.net.br, wilson.oliveira@nova.net.br"
CONSIDERAR_TEMPLATE=true

21
.env.fin Normal file
View File

@@ -0,0 +1,21 @@
OLLAMA_URL=2804:11d4:1:1002::14:18
OLLAMA_PORT=11434
OLLAMA_AI_MODEL=gpt-oss:20b
PIPERUN_API_URL=novanet.cxm.pipe.run
PIPERUN_CLIENT_ID=14
PIPERUN_CLIENT_SECRET=5MzZLKPjAexuANkl7g5dzyzm3sKqO8r159iSoF4x
PIPERUN_BOT_USERNAME=bot.piperun@nova.net.br
PIPERUN_BOT_PASSWORD=s?K<W>KN=twKEjTcTxoMv52=9fF5EwFT
MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE=20
MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE=27
BOT_EMAIL=bot.piperun@nova.net.br
BOT_EMAIL_PASSWORD=diQAVg,gK7rZ:5XRsqJ4zFay:fkAaH.4
OLLAMA_AI_MODEL_DATA_SANITIZATION=phi4:latest
FILA_ID=16
PROMPT_X=PROMPT_FIN.txt
FILTER_X=FILTER_FIN.txt
PASTA=evaluations_fin
NOME_FILA="Financeiro NVL2"
INICIAL_AGENTE="FIN -"
EMAILS="nicolas.borges@nova.net.br, wilson.oliveira@nova.net.br"
CONSIDERAR_TEMPLATE=false

View File

@@ -17,6 +17,5 @@ FILTER_X=FILTER_NOC-CLI.txt
PASTA=evaluations_noc-cli PASTA=evaluations_noc-cli
NOME_FILA="NOC - Clientes" NOME_FILA="NOC - Clientes"
INICIAL_AGENTE="NOC -" INICIAL_AGENTE="NOC -"
EMAIL_1=nicolas.borges@nova.net.br EMAILS="nicolas.borges@nova.net.br, wilson.oliveira@nova.net.br"
EMAIL_2=wilson.oliveira@nova.net.br CONSIDERAR_TEMPLATE=true

View File

@@ -17,5 +17,5 @@ FILTER_X=FILTER_NOC-CORP.txt
PASTA=evaluations_noc-corp PASTA=evaluations_noc-corp
NOME_FILA="NOC - Corporativo" NOME_FILA="NOC - Corporativo"
INICIAL_AGENTE="NOC -" INICIAL_AGENTE="NOC -"
EMAIL_1=nicolas.borges@nova.net.br EMAILS="nicolas.borges@nova.net.br, wilson.oliveira@nova.net.br"
EMAIL_2=wilson.oliveira@nova.net.br CONSIDERAR_TEMPLATE=true

21
.env.sup Normal file
View File

@@ -0,0 +1,21 @@
OLLAMA_URL=2804:11d4:1:1002::14:18
OLLAMA_PORT=11434
OLLAMA_AI_MODEL=gpt-oss:20b
PIPERUN_API_URL=novanet.cxm.pipe.run
PIPERUN_CLIENT_ID=14
PIPERUN_CLIENT_SECRET=5MzZLKPjAexuANkl7g5dzyzm3sKqO8r159iSoF4x
PIPERUN_BOT_USERNAME=bot.piperun@nova.net.br
PIPERUN_BOT_PASSWORD=s?K<W>KN=twKEjTcTxoMv52=9fF5EwFT
MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE=25
MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE=25
BOT_EMAIL=bot.piperun@nova.net.br
BOT_EMAIL_PASSWORD=diQAVg,gK7rZ:5XRsqJ4zFay:fkAaH.4
OLLAMA_AI_MODEL_DATA_SANITIZATION=phi4:latest
FILA_ID=13
PROMPT_X=PROMPT_SUP.txt
FILTER_X=FILTER_SUP.txt
PASTA=evaluations_sup
NOME_FILA="Suporte"
INICIAL_AGENTE="ATEND -"
EMAILS="nicolas.borges@nova.net.br, wilson.oliveira@nova.net.br, isadora.moura@nova.net.br"
CONSIDERAR_TEMPLATE=false

View File

@@ -0,0 +1,13 @@
Atendimento transferido para o agente [NOC - Cristiano Rodrigues] por [NOC -
Atendimento transferido para o agente [NOC - Marcelo Marcowich] por [NOC -
Atendimento transferido para o agente [NOC - Rafael Moura] por [NOC -
Agendamento [público] efetuado por
Atendimento transferido para a fila de espera pelo agente [NOC -
Este atendimento está sendo encerrado devido a inexistência de diálogo
Este atendimento está sendo encerrado devido à inexistência de diálogo
Esse atendimento está sendo encerrado devido a inexistência de diálogo
Esse atendimento está sendo encerrado devido à inexistência de diálogo
telefonia
CONECTA MAIS CANGUÇU LTDA
CONECTA MAIS

6
filters/FILTER_AGE.txt Normal file
View File

@@ -0,0 +1,6 @@
Vinicius Pereira
retirada dos equipamentos
retirada de equipamentos
recolhimento dos equipamentos
recolhimento de equipamentos
Como não houve interação em nosso chat, estamos encerrando este atendimento no momento.

11
filters/FILTER_COM-ON.txt Normal file
View File

@@ -0,0 +1,11 @@
Atendimento transferido para o agente [COM - Amanda Machado] por [COM -
Atendimento transferido para o agente [COM - Franthiesca Maia] por [COM -
Atendimento transferido para o agente [COM - Isadora Ferraz] por [COM -
Atendimento transferido para o agente [COM - Mateus Zarnott] por [COM -
Atendimento transferido para o agente [COM - Sheila Soares] por [COM -
Atendimento transferido para o agente [COM - Vanessa Silva] por [COM -
GER COM - Thais Vaz
não conseguimos te atender
não conseguimos atender
https://novanet.native-infinity.com.br/
Atendimento transferido para a fila de espera pelo agente [COM -

11
filters/FILTER_COM-V.txt Normal file
View File

@@ -0,0 +1,11 @@
Atendimento transferido para o agente [COM - Amanda Machado] por [COM -
Atendimento transferido para o agente [COM - Franthiesca Maia] por [COM -
Atendimento transferido para o agente [COM - Isadora Ferraz] por [COM -
Atendimento transferido para o agente [COM - Mateus Zarnott] por [COM -
Atendimento transferido para o agente [COM - Sheila Soares] por [COM -
Atendimento transferido para o agente [COM - Vanessa Silva] por [COM -
GER COM - Thais Vaz
não conseguimos te atender
não conseguimos atender
https://novanet.native-infinity.com.br/
Atendimento transferido para a fila de espera pelo agente [COM -

9
filters/FILTER_CX-CS.txt Normal file
View File

@@ -0,0 +1,9 @@
Atendimento transferido para o agente [NOC - Cristiano Rodrigues] por [NOC -
Atendimento transferido para o agente [NOC - Marcelo Marcowich] por [NOC -
Atendimento transferido para o agente [NOC - Rafael Moura] por [NOC -
Agendamento [público] efetuado por
Atendimento transferido para a fila de espera pelo agente [NOC -
Este atendimento está sendo encerrado devido a inexistência de diálogo
Este atendimento está sendo encerrado devido à inexistência de diálogo
Esse atendimento está sendo encerrado devido a inexistência de diálogo
Esse atendimento está sendo encerrado devido à inexistência de diálogo

12
filters/FILTER_FIN.txt Normal file
View File

@@ -0,0 +1,12 @@
Esse atendimento está sendo encerrado devido a inexistência de diálogo.
CONECTA MAIS CANGUÇU LTDA
Como o expediente deles já encerrou hoje, tu vai receber um retorno
Atendimento transferido para o agente [FIN - Vanessa Schulz] por [FIN
Atendimento transferido para o agente [FIN - Cristiano Domingues] por [FIN
Atendimento transferido para o agente [FIN - Laís Mota] por [FIN
Atendimento transferido para o agente [FIN - Tiffany Nunes] por [FIN
loja
filial
Nota Fiscal
nota fiscal
Nota fiscal

View File

@@ -0,0 +1,10 @@
Atendimento transferido para o agente [NOC - Cristiano Rodrigues] por [NOC -
Atendimento transferido para o agente [NOC - Marcelo Marcowich] por [NOC -
Atendimento transferido para o agente [NOC - Rafael Moura] por [NOC -
Agendamento [público] efetuado por
Atendimento transferido para a fila de espera pelo agente [NOC -
Este atendimento está sendo encerrado devido a inexistência de diálogo
Este atendimento está sendo encerrado devido à inexistência de diálogo
Esse atendimento está sendo encerrado devido a inexistência de diálogo
Esse atendimento está sendo encerrado devido à inexistência de diálogo
telefonia

View File

@@ -0,0 +1,9 @@
Atendimento transferido para o agente [NOC - Cristiano Rodrigues] por [NOC -
Atendimento transferido para o agente [NOC - Marcelo Marcowich] por [NOC -
Atendimento transferido para o agente [NOC - Rafael Moura] por [NOC -
Agendamento [público] efetuado por
Atendimento transferido para a fila de espera pelo agente [NOC -
Este atendimento está sendo encerrado devido a inexistência de diálogo
Este atendimento está sendo encerrado devido à inexistência de diálogo
Esse atendimento está sendo encerrado devido a inexistência de diálogo
Esse atendimento está sendo encerrado devido à inexistência de diálogo

35
filters/FILTER_SUP.txt Normal file
View File

@@ -0,0 +1,35 @@
https://novanet.portal.7az.com.br/login
Esse atendimento está sendo encerrado devido a inexistência de diálogo
Esse atendimento está sendo encerrado devido à inexistência de diálogo
ATENÇÃO CLIENTE COM NOTIFICAÇÃO DE PARADA ATIVA
setor de redes
trocar minha senha
trocar a senha
troca de senha
troca senha
alterar senha
altera senha
Transbordo automático para a fila [NovaNet -> Atendimento -> Suporte], pois não havia agentes disponíveis na fila [NovaNet -> Atendimento -> Comercial]
comprovante de pagamento
restrição de velocidade
*Basta desligar e ligar teus equipamentos da tomada para a conexão normalizar
ativar conta
link de ativação
paramount
https://play.watch.tv.br/ative
watch
prosseguimento por ligação
seguimento por ligação
Atendimento transferido para o agente [ATEND - Erick Otto] por [ATEND -
Atendimento transferido para o agente [ATEND - Ravel Borges] por [ATEND -
Atendimento transferido para o agente [ATEND - Renata Ramson] por [ATEND -
Atendimento transferido para o agente [ATEND - Isadora Moura] por [ATEND -
Atendimento transferido para o agente [ATEND - Erraoander Quintana] por [ATEND -
Atendimento transferido para o agente [ATEND - Estevan Macedo] por [ATEND -
Atendimento transferido para o agente [ATEND - Luan Pagel] por [ATEND -
Atendimento transferido para a fila de espera pelo agente [ATEND -
Este atendimento está sendo encerrado devido a inexistência de diálogo
Este atendimento está sendo encerrado devido à inexistência de diálogo
Agendamento [público] efetuado por
Se quiser, posso te enviar a lista completa dos materiais que podem ser utilizados
Isadora Moura

56
prompts/PROMPT_AGE.txt Normal file
View File

@@ -0,0 +1,56 @@
SEGUINDO OS CRITERIOS ABAIXO, AVALIE A QUALIDADE DO ATENDIMENTO PRESTADO PELO AGENTE DO SETOR DE AGENDAMENTO.
01 (APRESENTAÇÃO) - O AGENTE DEVE SE APRESENTAR DURANTE O ATENDIMENTO.
02 (PROTOCOLO) - O AGENTE DEVE CITAR, INFORMAR OU RELEMBRAR O PROTOCOLO DE ATENDIMENTO DURANTE A CONVERSA.
SEGUE ABAIXO ALGUNS EXEMPLOS ONDE O AGENTE INFORMA OU RELEMBRA O PROTOCOLO DE ATENDIMENTO:
"Protocolo: 2603.4406"
"Protocolo de registro: 2605.196"
"Gerado protocolo: 2402.4141"
"Protocolo para registro de atendimento: 2603.3310"
"Aqui está o protocolo do teu atendimento: 2308.3216"
"Prosseguindo no atendimento de protocolo 2604.201"
"Seguindo no atendimento de protocolo 2702.2"
03 (USO DO PORTUGUÊS) — O AGENTE DEVE APENAS ACENTUAR CORRETAMENTE E INICIAR AS FRASES COM LETRA MAIÚSCULA.
É PERMITIDO INICIAR FRASES COM LETRA MINÚSCULA APÓS UMA VÍRGULA.
É PERMITIDO INICIAR FRASES COM ASTERISTICO.
É PERMITIDO O USO DE LINGUAGEM REGIONAL COMO "TU", "TEU", "TUA" E "COSTUMAS".
É PERMITIDO O USO DE SIGLAS COMO "QR", "LINK" E "PIX".
É PERMITIDO O USO DE ASPAS.
LINKS COMO "https://novanet.portal.7az.com.br/login" PODEM INICIAR COM LETRA MINÚSCULA.
04 (AGENDAMENTO) - O AGENTE DEVE INFORMAR OU TENTAR COMBINAR A REALIZAÇÃO DA VISITA.
CONSIDERAR COMO CRITÉRIO ATENDINDO TAMBÉM, SE O AGENTE INFORMAR QUE UMA EQUIPE ESTÁ NO LOCAL.
CONSIDERAR COMO CRITÉRIO ATENDINDO TAMBÉM, SE O AGENTE INFORMAR QUE UM TÉCNICO ESTÁ NO LOCAL.
CONSIDERAR COMO CRITÉRIO ATENDINDO TAMBÉM, SE O AGENTE INFORMAR QUE UMA EQUIPE ESTÁ EM DESLOCAMENTO ATÉ O LOCAL.
CONSIDERAR COMO CRITÉRIO ATENDINDO TAMBÉM, SE O AGENTE INFORMAR QUE UM TÉCNICO ESTÁ EM DESLOCAMENTO ATÉ O LOCAL.
CONSIDERAR COMO CRITÉRIO ATENDINDO TAMBÉM, SE O AGENTE INFORMAR QUE ESTAMOS INDO EM "TAL" TURNO.
05 (PACIÊNCIA E EDUCAÇÃO) - O AGENTE DEVE SER PACIENTE E EDUCADO, UTILIZANDO O USO DE AGRADECIMENTOS, SAUDAÇÕES, PEDIDOS DE DESCULPAS E LINGUAGEM RESPEITOSA QUANDO NECESSÁRIO.
COMO POR EXEMPLO: "COMO POSSO TE AJUDAR?", "AGUARDE MEU RETORNO, POR GENTILEZA", "POSSO TE AJUDAR EM ALGO MAIS?", "OBRIGADO PELAS INFORMAÇÕES", "VOU VERIFICAR, AGUARDE UM MOMENTO POR GENTILEZA", "DESCULPE, MAS NÃO TE COMPREENDI", "PODERIA EXPLICAR DE NOVO?".
06 (DISPONIBILIDADE) - O AGENTE DEVE SE COLOCAR À DISPOSIÇÃO DO CLIENTE, DEIXANDO CLARO QUE A EMPRESA ESTÁ SEMPRE DISPONÍVEL PARA AJUDAR.
COMO POR EXEMPLO "CASO TENHA ALGUMA DÚVIDA, NOS CONTATE. A EQUIPE DA NOVANET ESTÁ SEMPRE AQUI PARA TE AJUDAR."
-----------------------------------
As mensagens do chat estão estruturadas no formato JSON com os campos:
- **message**: conteúdo da mensagem
- **sent_at**: horário de envio
- **type**: tipo da mensagem ('IN', 'OUT' ou 'SYSTEM'). As mensagens do agente são sempre do tipo 'OUT'.
- **user_name**: nome do usuário que enviou a mensagem. Não considere respostas do PipeBot como do agente
O fluxo de mensagens inicia-se com o cliente interagindo com o BOT, e então a mensagem é transferida para o atendente.
Em cada categoria, atribua 0 quando o agente não tiver atendido o critétio e 1 caso tenha atendido. Não atribua nenhum outro valor além destes dois .
Devem ser avaliados somente os agentes que o nome de usuario inicie com "AGE -" .
A sua resposta deve ser apenas uma tabela CSV e nada mais, utilizando como separador o caracter ';' com as colunas: CATEGORIA;PONTOS;MOTIVO;EVIDENCIA; on>
Portanto crie essas colunas para todo agente que for avaliado.
Na saída CSV, na coluna categoria, utilize o nome correspondente ao invés do número
A seguir estão as mensagens do atendimento, em JSON, avalie e retorne apenas um CSV.

67
prompts/PROMPT_COM-ON.txt Normal file
View File

@@ -0,0 +1,67 @@
SEGUINDO OS CRITÉRIOS QUE VÃO ESTAR ABAIXO, AVALIE ATENDIMENTOS PRESTADOS VIA CHAT PELO SETOR COMERCIAL.
01 (APRESENTAÇÃO) - O AGENTE DEVE SE APRESENTAR DURANTE O ATENDIMENTO.
02 (CONFIRMAÇÃO DE NOME) - O AGENTE DEVE MENCIONAR O NOME DO CLIENTE EM ALGUM MOMENTO DA CONVERSA.
03 (CONFIRMAÇÃO DE VIABILIDADE) - O AGENTE DEVE INFORMAR SE CONSEGUIMOS ATENDER O CLIENTE.
04 (TENTATIVA DE LIGAÇÃO) - O AGENTE DEVE PERGUNTAR PARA O CLIENTE SE O PROSSEGUIMENTO DO ATENDIMENTO PODE SER VIA LIGAÇÃO.
05 (SONDAGEM) - O AGENTE DEVE FAZER PERGUNTAS PARA ENTENDER A NECESSIDADE DO CLIENTE.
06 (INDICAÇÃO DO PLANO) - O AGENTE DEVE INDICAR UM PLANO PARA O CLIENTE.
07 (INFORMAÇÃO DE VALOR) - O AGENTE DEVE INFORMAR OS VALORES DO PLANO QUE SERÁ CONTRATADO.
08 (INFORMAÇÃO DE PAGAMENTO) - O AGENTE DEVE INFORMAR AS POSSIVEIS FORMAS DE PAGAMENTO.
09 (SOLICITAÇÃO DE FOTO) - O AGENTE DEVE PEDIR UMA FOTO DO TITULAR JUNTO A UM DOCUMENTO DE IDENTIFICAÇÃO.
10 (PROTOCOLO) - O AGENTE DEVE CITAR, INFORMAR OU RELEMBRAR O PROTOCOLO DE ATENDIMENTO DURANTE A CONVERSA.
SEGUE ABAIXO ALGUNS EXEMPLOS ONDE O AGENTE INFORMA OU RELEMBRA O PROTOCOLO DE ATENDIMENTO:
"Protocolo: 2603.4406"
"Protocolo de registro: 2605.196"
"Gerado protocolo: 2402.4141"
"Protocolo para registro de atendimento: 2603.3310"
"Aqui está o protocolo do teu atendimento: 2308.3216"
"Prosseguindo no atendimento de protocolo 2604.201"
"Seguindo no atendimento de protocolo 2702.2"
11 (USO DO PORTUGUÊS) — O AGENTE DEVE APENAS ACENTUAR CORRETAMENTE E INICIAR AS FRASES COM LETRA MAIÚSCULA.
É PERMITIDO INICIAR FRASES COM LETRA MINÚSCULA APÓS UMA VÍRGULA.
É PERMITIDO INICIAR FRASES COM ASTERISTICO.
É PERMITIDO O USO DE LINGUAGEM REGIONAL COMO "TU", "TEU", "TUA" E "COSTUMAS".
É PERMITIDO O USO DE SIGLAS COMO "QR", "LINK" E "PIX".
É PERMITIDO O USO DE ASPAS.
LINKS COMO "https://novanet.portal.7az.com.br/login" PODEM INICIAR COM LETRA MINÚSCULA.
12 - (PACIÊNCIA E EDUCAÇÃO) - O AGENTE DEVE SER PACIENTE E EDUCADO, UTILIZANDO O USO DE AGRADECIMENTOS, SAUDAÇÕES, PEDIDOS DE DESCULPAS E LINGUAGEM RESPEITOSA QUANDO NECESSÁRIO.
COMO POR EXEMPLO: "COMO POSSO TE AJUDAR?", "POSSO TE AJUDAR EM ALGO MAIS?", "OBRIGADO PELAS INFORMAÇÕES", "VOU VERIFICAR, AGUARDE UM MOMENTO POR GENTILEZA", "DESCULPE, MAS NÃO TE COMPREENDI", "PODERIA EXPLICAR DE NOVO?".
13 - (PEDIDO DE INDICAÇÃO) - O AGENTE DEVE INFORMAR SOBRE NOSSA PROMOÇÃO DE INDIQUE E GANHE.
14 - (DISPONIBILIDADE) - O AGENTE DEVE SE COLOCAR À DISPOSIÇÃO DO CLIENTE, DEIXANDO CLARO QUE A EMPRESA ESTÁ SEMPRE DISPONÍVEL PARA AJUDAR.
COMO POR EXEMPLO "CASO TENHA ALGUMA DÚVIDA, NOS CONTATE. A EQUIPE DA NOVANET ESTÁ SEMPRE AQUI PARA TE AJUDAR."
-----------------------------------
As mensagens do chat estão estruturadas no formato JSON com os campos:
- **message**: conteúdo da mensagem
- **sent_at**: horário de envio
- **type**: tipo da mensagem ('IN', 'OUT' ou 'SYSTEM'). As mensagens do agente são sempre do tipo 'OUT'.
- **user_name**: nome do usuário que enviou a mensagem. Não considere respostas do PipeBot como do agente
O fluxo de mensagens inicia-se com o cliente interagindo com o BOT, e então a mensagem é transferida para o atendente.
Em cada categoria, atribua 0 quando o agente não tiver atendido o critétio e 1 caso tenha atendido. Não atribua nenhum outro valor além destes dois .
Devem ser avaliados somente os agentes que o nome de usuario inicie com "COM -" .
A sua resposta deve ser apenas uma tabela CSV e nada mais, utilizando como separador o caracter ';' com as colunas: CATEGORIA;PONTOS;MOTIVO;EVIDENCIA; on>
Portanto crie essas colunas para todo agente que for avaliado.
Na saída CSV, na coluna categoria, utilize o nome correspondente ao invés do número
A seguir estão as mensagens do atendimento, em JSON, avalie e retorne apenas um CSV.

67
prompts/PROMPT_COM-V.txt Normal file
View File

@@ -0,0 +1,67 @@
SEGUINDO OS CRITÉRIOS QUE VÃO ESTAR ABAIXO, AVALIE ATENDIMENTOS PRESTADOS VIA CHAT PELO SETOR COMERCIAL.
01 (APRESENTAÇÃO) - O AGENTE DEVE SE APRESENTAR DURANTE O ATENDIMENTO.
02 (CONFIRMAÇÃO DE NOME) - O AGENTE DEVE MENCIONAR O NOME DO CLIENTE EM ALGUM MOMENTO DA CONVERSA.
03 (CONFIRMAÇÃO DE VIABILIDADE) - O AGENTE DEVE INFORMAR SE CONSEGUIMOS ATENDER O CLIENTE.
04 (TENTATIVA DE LIGAÇÃO) - O AGENTE DEVE PERGUNTAR PARA O CLIENTE SE O PROSSEGUIMENTO DO ATENDIMENTO PODE SER VIA LIGAÇÃO.
05 (SONDAGEM) - O AGENTE DEVE FAZER PERGUNTAS PARA ENTENDER A NECESSIDADE DO CLIENTE.
06 (INDICAÇÃO DO PLANO) - O AGENTE DEVE INDICAR UM PLANO PARA O CLIENTE.
07 (INFORMAÇÃO DE VALOR) - O AGENTE DEVE INFORMAR OS VALORES DO PLANO QUE SERÁ CONTRATADO.
08 (INFORMAÇÃO DE PAGAMENTO) - O AGENTE DEVE INFORMAR AS POSSIVEIS FORMAS DE PAGAMENTO.
09 (SOLICITAÇÃO DE FOTO) - O AGENTE DEVE PEDIR UMA FOTO DO TITULAR JUNTO A UM DOCUMENTO DE IDENTIFICAÇÃO.
10 (PROTOCOLO) - O AGENTE DEVE CITAR, INFORMAR OU RELEMBRAR O PROTOCOLO DE ATENDIMENTO DURANTE A CONVERSA.
SEGUE ABAIXO ALGUNS EXEMPLOS ONDE O AGENTE INFORMA OU RELEMBRA O PROTOCOLO DE ATENDIMENTO:
"Protocolo: 2603.4406"
"Protocolo de registro: 2605.196"
"Gerado protocolo: 2402.4141"
"Protocolo para registro de atendimento: 2603.3310"
"Aqui está o protocolo do teu atendimento: 2308.3216"
"Prosseguindo no atendimento de protocolo 2604.201"
"Seguindo no atendimento de protocolo 2702.2"
11 (USO DO PORTUGUÊS) — O AGENTE DEVE APENAS ACENTUAR CORRETAMENTE E INICIAR AS FRASES COM LETRA MAIÚSCULA.
É PERMITIDO INICIAR FRASES COM LETRA MINÚSCULA APÓS UMA VÍRGULA.
É PERMITIDO INICIAR FRASES COM ASTERISTICO.
É PERMITIDO O USO DE LINGUAGEM REGIONAL COMO "TU", "TEU", "TUA" E "COSTUMAS".
É PERMITIDO O USO DE SIGLAS COMO "QR", "LINK" E "PIX".
É PERMITIDO O USO DE ASPAS.
LINKS COMO "https://novanet.portal.7az.com.br/login" PODEM INICIAR COM LETRA MINÚSCULA.
12 - (PACIÊNCIA E EDUCAÇÃO) - O AGENTE DEVE SER PACIENTE E EDUCADO, UTILIZANDO O USO DE AGRADECIMENTOS, SAUDAÇÕES, PEDIDOS DE DESCULPAS E LINGUAGEM RESPEITOSA QUANDO NECESSÁRIO.
COMO POR EXEMPLO: "COMO POSSO TE AJUDAR?", "POSSO TE AJUDAR EM ALGO MAIS?", "OBRIGADO PELAS INFORMAÇÕES", "VOU VERIFICAR, AGUARDE UM MOMENTO POR GENTILEZA", "DESCULPE, MAS NÃO TE COMPREENDI", "PODERIA EXPLICAR DE NOVO?".
13 - (PEDIDO DE INDICAÇÃO) - O AGENTE DEVE INFORMAR SOBRE NOSSA PROMOÇÃO DE INDIQUE E GANHE.
14 - (DISPONIBILIDADE) - O AGENTE DEVE SE COLOCAR À DISPOSIÇÃO DO CLIENTE, DEIXANDO CLARO QUE A EMPRESA ESTÁ SEMPRE DISPONÍVEL PARA AJUDAR.
COMO POR EXEMPLO "CASO TENHA ALGUMA DÚVIDA, NOS CONTATE. A EQUIPE DA NOVANET ESTÁ SEMPRE AQUI PARA TE AJUDAR."
-----------------------------------
As mensagens do chat estão estruturadas no formato JSON com os campos:
- **message**: conteúdo da mensagem
- **sent_at**: horário de envio
- **type**: tipo da mensagem ('IN', 'OUT' ou 'SYSTEM'). As mensagens do agente são sempre do tipo 'OUT'.
- **user_name**: nome do usuário que enviou a mensagem. Não considere respostas do PipeBot como do agente
O fluxo de mensagens inicia-se com o cliente interagindo com o BOT, e então a mensagem é transferida para o atendente.
Em cada categoria, atribua 0 quando o agente não tiver atendido o critétio e 1 caso tenha atendido. Não atribua nenhum outro valor além destes dois .
Devem ser avaliados somente os agentes que o nome de usuario inicie com "COM -" .
A sua resposta deve ser apenas uma tabela CSV e nada mais, utilizando como separador o caracter ';' com as colunas: CATEGORIA;PONTOS;MOTIVO;EVIDENCIA; on>
Portanto crie essas colunas para todo agente que for avaliado.
Na saída CSV, na coluna categoria, utilize o nome correspondente ao invés do número
A seguir estão as mensagens do atendimento, em JSON, avalie e retorne apenas um CSV.

57
prompts/PROMPT_CX-CS.txt Normal file
View File

@@ -0,0 +1,57 @@
SEGUINDO OS CRITERIOS ABAIXO, AVALIE A QUALIDADE DO ATENDIMENTO PRESTADO PELO AGENTE DO SETOR DE QUALIDADE.
01 (APRESENTAÇÃO) - O AGENTE DEVE SE APRESENTAR DURANTE O ATENDIMENTO.
02 (CONFIRMAÇÃO DE E-MAIL) - O AGENTE DEVE CITAR, SOLICITAR OU CONFIRMAR UM E-MAIL DURANTE A CONVERSA.
03 (CONFIRMAÇÃO DE TELEFONE) - O AGENTE DEVE CITAR, SOLICITAR OU CONFIRMAR UM TELEFONE DURANTE A CONVERSA.
04 (SUGESTÃO DE NOVO PLANO) - O AGENTE DEVE RELEMBRAR QUAL É O PLANO ATUAL DO CLIENTE E PASSAR INFORMAÇÕES SOBRE O NOVO PLANO.
05 (INFORMAÇÃO DE VALOR) - O AGENTE DEVE INFORMAR OS VALORES ENVOLVIDOS NA ATUALIZAÇÃO PARA O NOVO PLANO.
06 (PROTOCOLO) - O AGENTE DEVE CITAR, INFORMAR OU RELEMBRAR O PROTOCOLO DE ATENDIMENTO DURANTE A CONVERSA.
SEGUE ABAIXO ALGUNS EXEMPLOS ONDE O AGENTE INFORMA OU RELEMBRA O PROTOCOLO DE ATENDIMENTO:
"Protocolo: 2603.4406"
"Protocolo de registro: 2605.196"
"Gerado protocolo: 2402.4141"
"Protocolo para registro de atendimento: 2603.3310"
"Aqui está o protocolo do teu atendimento: 2308.3216"
"Prosseguindo no atendimento de protocolo 2604.201"
"Seguindo no atendimento de protocolo 2702.2"
07 (USO DO PORTUGUÊS) — O AGENTE DEVE APENAS ACENTUAR CORRETAMENTE E INICIAR AS FRASES COM LETRA MAIÚSCULA.
É PERMITIDO INICIAR FRASES COM LETRA MINÚSCULA APÓS UMA VÍRGULA.
É PERMITIDO INICIAR FRASES COM ASTERISTICO.
É PERMITIDO O USO DE LINGUAGEM REGIONAL COMO "TU", "TEU", "TUA" E "COSTUMAS".
É PERMITIDO O USO DE SIGLAS COMO "QR", "LINK" E "PIX".
É PERMITIDO O USO DE ASPAS.
LINKS COMO "https://novanet.portal.7az.com.br/login" PODEM INICIAR COM LETRA MINÚSCULA.
08 (PACIÊNCIA E EDUCAÇÃO) - O AGENTE DEVE SER PACIENTE E EDUCADO, UTILIZANDO O USO DE AGRADECIMENTOS, SAUDAÇÕES, PEDIDOS DE DESCULPAS E LINGUAGEM RESPEITOSA QUANDO NECESSÁRIO.
COMO POR EXEMPLO: "COMO POSSO TE AJUDAR?", "AGUARDE MEU RETORNO, POR GENTILEZA", "POSSO TE AJUDAR EM ALGO MAIS?", "OBRIGADO PELAS INFORMAÇÕES", "VOU VERIFICAR, AGUARDE UM MOMENTO POR GENTILEZA", "DESCULPE, MAS NÃO TE COMPREENDI", "PODERIA EXPLICAR DE NOVO?".
09 (DISPONIBILIDADE) - O AGENTE DEVE SE COLOCAR À DISPOSIÇÃO DO CLIENTE, DEIXANDO CLARO QUE A EMPRESA ESTÁ SEMPRE DISPONÍVEL PARA AJUDAR.
COMO POR EXEMPLO "CASO TENHA ALGUMA DÚVIDA, NOS CONTATE. A EQUIPE DA NOVANET ESTÁ SEMPRE AQUI PARA TE AJUDAR."
-----------------------------------
As mensagens do chat estão estruturadas no formato JSON com os campos:
- **message**: conteúdo da mensagem
- **sent_at**: horário de envio
- **type**: tipo da mensagem ('IN', 'OUT' ou 'SYSTEM'). As mensagens do agente são sempre do tipo 'OUT'.
- **user_name**: nome do usuário que enviou a mensagem. Não considere respostas do PipeBot como do agente
O fluxo de mensagens inicia-se com o cliente interagindo com o BOT, e então a mensagem é transferida para o atendente.
Em cada categoria, atribua 0 quando o agente não tiver atendido o critétio e 1 caso tenha atendido. Não atribua nenhum outro valor além destes dois .
Devem ser avaliados somente os agentes que o nome de usuario inicie com "CX-CS -" .
A sua resposta deve ser apenas uma tabela CSV e nada mais, utilizando como separador o caracter ';' com as colunas: CATEGORIA;PONTOS;MOTIVO;EVIDENCIA; on>
Portanto crie essas colunas para todo agente que for avaliado.
Na saída CSV, na coluna categoria, utilize o nome correspondente ao invés do número
A seguir estão as mensagens do atendimento, em JSON, avalie e retorne apenas um CSV.

View File

@@ -1,4 +1,4 @@
SEGUINDO OS CRITÉRIOS QUE VÃO ESTAR ABAIXO, AVALIE ATENDIMENTOS PRESTADOS VIA CHAT PELO SETOR FINANCEIRO. SEGUINDO OS CRITERIOS ABAIXO, AVALIE A QUALIDADE DO ATENDIMENTO PRESTADO PELO AGENTE DO SETOR FINANCEIRO.
01 (APRESENTAÇÃO) - O AGENTE DEVE SE APRESENTAR DURANTE O ATENDIMENTO. 01 (APRESENTAÇÃO) - O AGENTE DEVE SE APRESENTAR DURANTE O ATENDIMENTO.
@@ -7,14 +7,25 @@ SEGUINDO OS CRITÉRIOS QUE VÃO ESTAR ABAIXO, AVALIE ATENDIMENTOS PRESTADOS VIA
03 (CONFIRMAÇÃO DE TELEFONE) - O AGENTE DEVE CITAR, SOLICITAR OU CONFIRMAR O TELEFONE DO CLIENTE DURANTE A CONVERSA. 03 (CONFIRMAÇÃO DE TELEFONE) - O AGENTE DEVE CITAR, SOLICITAR OU CONFIRMAR O TELEFONE DO CLIENTE DURANTE A CONVERSA.
04 (PROTOCOLO) - O AGENTE DEVE CITAR, INFORMAR OU RELEMBRAR O PROTOCOLO DE ATENDIMENTO DURANTE A CONVERSA. 04 (PROTOCOLO) - O AGENTE DEVE CITAR, INFORMAR OU RELEMBRAR O PROTOCOLO DE ATENDIMENTO DURANTE A CONVERSA.
SEGUE ABAIXO ALGUNS EXEMPLOS ONDE O AGENTE INFORMA OU RELEMBRA O PROTOCOLO DE ATENDIMENTO:
"Protocolo: 2603.4406"
"Protocolo de registro: 2605.196"
"Gerado protocolo: 2402.4141"
"Protocolo para registro de atendimento: 2603.3310"
"Aqui está o protocolo do teu atendimento: 2308.3216"
"Prosseguindo no atendimento de protocolo 2604.201"
"Seguindo no atendimento de protocolo 2702.2"
05 (USO DO PORTUGUÊS) O AGENTE DEVE APENAS PONTUAR CORRETAMENTE, ACENTUAR CORRETAMENTE E INICIAR AS FRASES COM LETRA MAIÚSCULA. 05 (USO DO PORTUGUÊS) O AGENTE DEVE APENAS ACENTUAR CORRETAMENTE E INICIAR AS FRASES COM LETRA MAIÚSCULA.
AVALIE APENAS AS MENSAGENS ENVIADAS PELO AGENTE (type: "in",). É PERMITIDO INICIAR FRASES COM LETRA MINÚSCULA APÓS UMA VÍRGULA.
IGNORE COMPLETAMENTE QUALQUER MENSAGEM ENVIADA PELO CLIENTE (type: "out",) É PERMITIDO INICIAR FRASES COM ASTERISTICO.
É PERMITIDO O USO DE LINGUAGEM REGIONAL COMO TU, TEU, TUA”, POIS ISSO FAZ PARTE DA IDENTIDADE DA EMPRESA. É PERMITIDO O USO DE LINGUAGEM REGIONAL COMO "TU", "TEU", "TUA" E "COSTUMAS".
É PERMITIDO O USO DE SIGLAS COMO "QR", "LINK" E "PIX".
É PERMITIDO O USO DE ASPAS.
LINKS COMO "https://novanet.portal.7az.com.br/login" PODEM INICIAR COM LETRA MINÚSCULA.
06 (PACIÊNCIA E EDUCAÇÃO) - O AGENTE DEVE SER PACIENTE E EDUCADO, UTILIZANDO O USO DE AGRADECIMENTOS, SAUDAÇÕES, PEDIDOS DE DESCULPAS E LINGUAGEM RESPEITOSA QUANDO NECESSÁRIO. 06 (PACIÊNCIA E EDUCAÇÃO) - O AGENTE DEVE SER PACIENTE E EDUCADO, UTILIZANDO O USO DE AGRADECIMENTOS, SAUDAÇÕES, PEDIDOS DE DESCULPAS E LINGUAGEM RESPEITOSA QUANDO NECESSÁRIO.
COMO POR EXEMPLO: "COMO POSSO TE AJUDAR?", "POSSO TE AJUDAR EM ALGO MAIS?", "OBRIGADO PELAS INFORMAÇÕES", "VOU VERIFICAR, AGUARDE UM MOMENTO POR GENTILEZA", "DESCULPE, MAS NÃO TE COMPREENDI", "PODERIA EXPLICAR DE NOVO?". COMO POR EXEMPLO: "COMO POSSO TE AJUDAR?", "AGUARDE MEU RETORNO, POR GENTILEZA", "POSSO TE AJUDAR EM ALGO MAIS?", "OBRIGADO PELAS INFORMAÇÕES", "VOU VERIFICAR, AGUARDE UM MOMENTO POR GENTILEZA", "DESCULPE, MAS NÃO TE COMPREENDI", "PODERIA EXPLICAR DE NOVO?".
07 (DISPONIBILIDADE) - O AGENTE DEVE SE COLOCAR À DISPOSIÇÃO DO CLIENTE, DEIXANDO CLARO QUE A EMPRESA ESTÁ SEMPRE DISPONÍVEL PARA AJUDAR. 07 (DISPONIBILIDADE) - O AGENTE DEVE SE COLOCAR À DISPOSIÇÃO DO CLIENTE, DEIXANDO CLARO QUE A EMPRESA ESTÁ SEMPRE DISPONÍVEL PARA AJUDAR.
COMO POR EXEMPLO "CASO TENHA ALGUMA DÚVIDA, NOS CONTATE. A EQUIPE DA NOVANET ESTÁ SEMPRE AQUI PARA TE AJUDAR." COMO POR EXEMPLO "CASO TENHA ALGUMA DÚVIDA, NOS CONTATE. A EQUIPE DA NOVANET ESTÁ SEMPRE AQUI PARA TE AJUDAR."
@@ -34,7 +45,7 @@ O fluxo de mensagens inicia-se com o cliente interagindo com o BOT, e então a m
Em cada categoria, atribua 0 quando o agente não tiver atendido o critétio e 1 caso tenha atendido. Não atribua nenhum outro valor além destes dois . Em cada categoria, atribua 0 quando o agente não tiver atendido o critétio e 1 caso tenha atendido. Não atribua nenhum outro valor além destes dois .
Devem ser avaliados somente os agentes que o nome de usuario inicie com "NOC -" . Devem ser avaliados somente os agentes que o nome de usuario inicie com "FIN -" .
A sua resposta deve ser apenas uma tabela CSV e nada mais, utilizando como separador o caracter ';' com as colunas: CATEGORIA;PONTOS;MOTIVO;EVIDENCIA; on> A sua resposta deve ser apenas uma tabela CSV e nada mais, utilizando como separador o caracter ';' com as colunas: CATEGORIA;PONTOS;MOTIVO;EVIDENCIA; on>

View File

@@ -0,0 +1,67 @@
SEGUINDO OS CRITERIOS ABAIXO, AVALIE A QUALIDADE DO ATENDIMENTO PRESTADO PELO AGENTE DE SUPORTE.
01 (APRESENTAÇÃO) - O AGENTE DEVE SE APRESENTAR DURANTE O ATENDIMENTO.
02 (CONFIRMAÇÃO DE E-MAIL) - O AGENTE DEVE CITAR, SOLICITAR OU CONFIRMAR O E-MAIL DO CLIENTE DURANTE A CONVERSA.
03 (CONFIRMAÇÃO DE TELEFONE) - O AGENTE DEVE CITAR, SOLICITAR OU CONFIRMAR O TELEFONE DO CLIENTE DURANTE A CONVERSA.
04 (PROTOCOLO) - O AGENTE DEVE CITAR, INFORMAR OU RELEMBRAR O PROTOCOLO DE ATENDIMENTO DURANTE A CONVERSA.
SEGUE ABAIXO ALGUNS EXEMPLOS ONDE O AGENTE INFORMA OU RELEMBRA O PROTOCOLO DE ATENDIMENTO:
"Protocolo: 2603.4406"
"Protocolo de registro: 2605.196"
"Gerado protocolo: 2402.4141"
"Protocolo para registro de atendimento: 2603.3310"
"Aqui está o protocolo do teu atendimento: 2308.3216"
"Prosseguindo no atendimento de protocolo 2604.201"
"Seguindo no atendimento de protocolo 2702.2"
05 (USO DO PORTUGUÊS) — O AGENTE DEVE APENAS ACENTUAR CORRETAMENTE E INICIAR AS FRASES COM LETRA MAIÚSCULA.
É PERMITIDO INICIAR FRASES COM LETRA MINÚSCULA APÓS UMA VÍRGULA.
É PERMITIDO O USO DE LINGUAGEM REGIONAL COMO "TU", "TEU", "TUA" E "COSTUMAS".
É PERMITIDO O USO DE SIGLAS COMO "QR", "LINK" E "PIX".
É PERMITIDO O USO DE ASPAS.
LINKS COMO "https://novanet.portal.7az.com.br/login" PODEM INICIAR COM LETRA MINÚSCULA.
06 (PACIÊNCIA E EDUCAÇÃO) - O AGENTE DEVE SER PACIENTE E EDUCADO, UTILIZANDO O USO DE AGRADECIMENTOS, SAUDAÇÕES, PEDIDOS DE DESCULPAS E LINGUAGEM RESPEITOSA QUANDO NECESSÁRIO.
COMO POR EXEMPLO: "COMO POSSO TE AJUDAR?", "AGUARDE MEU RETORNO, POR GENTILEZA", "POSSO TE AJUDAR EM ALGO MAIS?", "OBRIGADO PELAS INFORMAÇÕES", "VOU VERIFICAR, AGUARDE UM MOMENTO POR GENTILEZA", "DESCULPE, MAS NÃO TE COMPREENDI", "PODERIA EXPLICAR DE NOVO?".
07 (DISPONIBILIDADE) - O AGENTE DEVE SE COLOCAR À DISPOSIÇÃO DO CLIENTE, DEIXANDO CLARO QUE A EMPRESA ESTÁ SEMPRE DISPONÍVEL PARA AJUDAR.
COMO POR EXEMPLO "CASO TENHA ALGUMA DÚVIDA, NOS CONTATE. A EQUIPE DA NOVANET ESTÁ SEMPRE AQUI PARA TE AJUDAR."
08 (ESCLARECIMENTO) - O AGENTE DEVE MANTER O CLIENTE INFORMADO SOBRE O DECORRER DOS ACONTECIMENTOS DURANTE O ATENDIMENTO
ESTE CRITÉRIO DEVE SER CONSIDERADO ATENDIDO QUANDO O AGENTE REALIZAR PELO MENOS UMA DAS AÇÕES ABAIXO:
INFORMAR SE IDENTIFICOU OU NÃO ALGUM PROBLEMA NA CONEXÃO OU NOS EQUIPAMENTOS DO CLIENTE.
EXEMPLOS DE EQUIPAMENTOS QUE PODEM SER MENCIONADOS: "ROTEADOR", "ONU", "SWITCH", "RB", "ANTENA".
INFORMAR SE IDENTIFICOU ALGUM BLOQUEIO OU NÃO.
EXPLICAR DE FORMA CLARA CADA AÇÃO REALIZADA OU SOLICITADA AO CLIENTE.
INFORMAR COMO O PROBLEMA FOI OU SERÁ RESOLVIDO.
FAZER ALGUMA RECOMENDAÇÃO TÉCNICA PARA O CLIENTE, EXPLICANDO O MOTIVO DESSA RECOMENDAÇÃO.
INFORMAR QUE ABRIU UMA ORDEM DE SERVIÇO OU GEROU UMA NOVA DEMANDA, EXPLICANDO O MOTIVO.
NÃO DEVEM SER CONSIDERADOS COMO ESCLARECIMENTO:
SOLICITAÇÃO OU CONFIRMAÇÃO DE E-MAIL NÃO DEVEM SER CONSIDERADOS NESSE ITEM.
SOLICITAÇÃO OU CONFIRMAÇÃO DE TELEFONE NÃO DEVEM SER CONSIDERADOS NESSE ITEM.
-----------------------------------
As mensagens do chat estão estruturadas no formato JSON com os campos:
- **message**: conteúdo da mensagem
- **sent_at**: horário de envio
- **type**: tipo da mensagem ('IN', 'OUT' ou 'SYSTEM'). As mensagens do agente são sempre do tipo 'OUT'.
- **user_name**: nome do usuário que enviou a mensagem. Não considere respostas do PipeBot como do agente
O fluxo de mensagens inicia-se com o cliente interagindo com o BOT, e então a mensagem é transferida para o atendente.
Em cada categoria, atribua 0 quando o agente não tiver atendido o critétio e 1 caso tenha atendido. Não atribua nenhum outro valor além destes dois .
Devem ser avaliados somente os agentes que o nome de usuario inicie com "NOC -" .
A sua resposta deve ser apenas uma tabela CSV e nada mais, utilizando como separador o caracter ';' com as colunas: CATEGORIA;PONTOS;MOTIVO;EVIDENCIA; on>
Portanto crie essas colunas para todo agente que for avaliado.
Na saída CSV, na coluna categoria, utilize o nome correspondente ao invés do número
A seguir estão as mensagens do atendimento, em JSON, avalie e retorne apenas um CSV.

View File

@@ -0,0 +1,62 @@
SEGUINDO OS CRITERIOS ABAIXO, AVALIE A QUALIDADE DO ATENDIMENTO PRESTADO PELO AGENTE DE SUPORTE.
01 (APRESENTAÇÃO) - O AGENTE DEVE SE APRESENTAR DURANTE O ATENDIMENTO.
02 (PROTOCOLO) - O AGENTE DEVE CITAR, INFORMAR OU RELEMBRAR O PROTOCOLO DE ATENDIMENTO DURANTE A CONVERSA.
SEGUE ABAIXO ALGUNS EXEMPLOS ONDE O AGENTE INFORMA OU RELEMBRA O PROTOCOLO DE ATENDIMENTO:
"Protocolo: 2603.4406"
"Protocolo de registro: 2605.196"
"Gerado protocolo: 2402.4141"
"Protocolo para registro de atendimento: 2603.3310"
"Aqui está o protocolo do teu atendimento: 2308.3216"
"Prosseguindo no atendimento de protocolo 2604.201"
"Seguindo no atendimento de protocolo 2702.2"
03 (USO DO PORTUGUÊS) — O AGENTE DEVE APENAS ACENTUAR CORRETAMENTE E INICIAR AS FRASES COM LETRA MAIÚSCULA.
É PERMITIDO INICIAR FRASES COM LETRA MINÚSCULA APÓS UMA VÍRGULA.
É PERMITIDO INICIAR FRASES COM ASTERISTICO.
É PERMITIDO O USO DE LINGUAGEM REGIONAL COMO "TU", "TEU", "TUA" E "COSTUMAS".
É PERMITIDO O USO DE SIGLAS COMO "QR", "LINK" E "PIX".
É PERMITIDO O USO DE ASPAS.
LINKS COMO "https://novanet.portal.7az.com.br/login" PODEM INICIAR COM LETRA MINÚSCULA.
04 (PACIÊNCIA E EDUCAÇÃO) - O AGENTE DEVE SER PACIENTE E EDUCADO, UTILIZANDO O USO DE AGRADECIMENTOS, SAUDAÇÕES, PEDIDOS DE DESCULPAS E LINGUAGEM RESPEITOSA QUANDO NECESSÁRIO.
COMO POR EXEMPLO: "COMO POSSO TE AJUDAR?", "AGUARDE MEU RETORNO, POR GENTILEZA", "POSSO TE AJUDAR EM ALGO MAIS?", "OBRIGADO PELAS INFORMAÇÕES", "VOU VERIFICAR, AGUARDE UM MOMENTO POR GENTILEZA", "DESCULPE, MAS NÃO TE COMPREENDI", "PODERIA EXPLICAR DE NOVO?".
05 (DISPONIBILIDADE) - O AGENTE DEVE SE COLOCAR À DISPOSIÇÃO DO CLIENTE, DEIXANDO CLARO QUE A EMPRESA ESTÁ SEMPRE DISPONÍVEL PARA AJUDAR.
COMO POR EXEMPLO "CASO TENHA ALGUMA DÚVIDA, NOS CONTATE. A EQUIPE DA NOVANET ESTÁ SEMPRE AQUI PARA TE AJUDAR."
06 (ESCLARECIMENTO) - O AGENTE DEVE MANTER O CLIENTE INFORMADO SOBRE O DECORRER DOS ACONTECIMENTOS DURANTE O ATENDIMENTO
ESTE CRITÉRIO DEVE SER CONSIDERADO ATENDIDO QUANDO O AGENTE REALIZAR PELO MENOS UMA DAS AÇÕES ABAIXO:
INFORMAR SE IDENTIFICOU OU NÃO ALGUM PROBLEMA NA CONEXÃO OU NOS EQUIPAMENTOS DO CLIENTE.
EXEMPLOS DE EQUIPAMENTOS QUE PODEM SER MENCIONADOS: "ROTEADOR", "ONU", "SWITCH", "RB", "ANTENA".
EXPLICAR DE FORMA CLARA CADA AÇÃO REALIZADA OU SOLICITADA AO CLIENTE.
INFORMAR COMO O PROBLEMA FOI OU SERÁ RESOLVIDO.
FAZER ALGUMA RECOMENDAÇÃO TÉCNICA PARA O CLIENTE, EXPLICANDO O MOTIVO DESSA RECOMENDAÇÃO.
NÃO DEVEM SER CONSIDERADOS COMO ESCLARECIMENTO:
SOLICITAÇÃO OU CONFIRMAÇÃO DE E-MAIL NÃO DEVEM SER CONSIDERADOS NESSE ITEM.
SOLICITAÇÃO OU CONFIRMAÇÃO DE TELEFONE NÃO DEVEM SER CONSIDERADOS NESSE ITEM.
-----------------------------------
As mensagens do chat estão estruturadas no formato JSON com os campos:
- **message**: conteúdo da mensagem
- **sent_at**: horário de envio
- **type**: tipo da mensagem ('IN', 'OUT' ou 'SYSTEM'). As mensagens do agente são sempre do tipo 'OUT'.
- **user_name**: nome do usuário que enviou a mensagem. Não considere respostas do PipeBot como do agente
O fluxo de mensagens inicia-se com o cliente interagindo com o BOT, e então a mensagem é transferida para o atendente.
Em cada categoria, atribua 0 quando o agente não tiver atendido o critétio e 1 caso tenha atendido. Não atribua nenhum outro valor além destes dois .
Devem ser avaliados somente os agentes que o nome de usuario inicie com "NOC -" .
A sua resposta deve ser apenas uma tabela CSV e nada mais, utilizando como separador o caracter ';' com as colunas: CATEGORIA;PONTOS;MOTIVO;EVIDENCIA; on>
Portanto crie essas colunas para todo agente que for avaliado.
Na saída CSV, na coluna categoria, utilize o nome correspondente ao invés do número
A seguir estão as mensagens do atendimento, em JSON, avalie e retorne apenas um CSV.

61
prompts/PROMPT_SUP.txt Normal file
View File

@@ -0,0 +1,61 @@
SEGUINDO OS CRITERIOS ABAIXO, AVALIE A QUALIDADE DO ATENDIMENTO PRESTADO PELO AGENTE DE SUPORTE.
01 (APRESENTAÇÃO) - O AGENTE DEVE SE APRESENTAR DURANTE O ATENDIMENTO.
02 (CONFIRMAÇÃO DE E-MAIL) - O AGENTE DEVE CITAR, SOLICITAR OU CONFIRMAR O E-MAIL DO CLIENTE DURANTE A CONVERSA.
03 (CONFIRMAÇÃO DE TELEFONE) - O AGENTE DEVE CITAR, SOLICITAR OU CONFIRMAR O TELEFONE DO CLIENTE DURANTE A CONVERSA.
04 (PROTOCOLO) - O AGENTE DEVE CITAR, INFORMAR OU RELEMBRAR O PROTOCOLO DE ATENDIMENTO DURANTE A CONVERSA.
SEGUE ABAIXO ALGUNS EXEMPLOS ONDE O AGENTE INFORMA OU RELEMBRA O PROTOCOLO DE ATENDIMENTO:
"Protocolo: 2603.4406"
"Protocolo de registro: 2605.196"
"Gerado protocolo: 2402.4141"
"Protocolo para registro de atendimento: 2603.3310"
"Aqui está o protocolo do teu atendimento: 2308.3216"
"Prosseguindo no atendimento de protocolo 2604.201"
"Seguindo no atendimento de protocolo 2702.2"
É PERMITIDO INICIAR FRASES COM LETRA MINÚSCULA APÓS UMA VÍRGULA.
É PERMITIDO INICIAR FRASES COM ASTERISTICO.
É PERMITIDO O USO DE LINGUAGEM REGIONAL COMO "TU", "TEU", "TUA" E "COSTUMAS".
É PERMITIDO O USO DE SIGLAS COMO "QR", "LINK" E "PIX".
É PERMITIDO O USO DE ASPAS.
LINKS COMO "https://novanet.portal.7az.com.br/login" PODEM INICIAR COM LETRA MINÚSCULA.
06 (PACIÊNCIA E EDUCAÇÃO) - O AGENTE DEVE SER PACIENTE E EDUCADO, UTILIZANDO O USO DE AGRADECIMENTOS, SAUDAÇÕES, PEDIDOS DE DESCULPAS E LINGUAGEM RESPEITOSA QUANDO NECESSÁRIO.
COMO POR EXEMPLO: "COMO POSSO TE AJUDAR?", "AGUARDE MEU RETORNO, POR GENTILEZA", "POSSO TE AJUDAR EM ALGO MAIS?", "OBRIGADO PELAS INFORMAÇÕES", "VOU VERIFICAR, AGUARDE UM MOMENTO POR GENTILEZA", "DESCULPE, MAS NÃO TE COMPREENDI", "PODERIA EXPLICAR DE NOVO?".
07 (DISPONIBILIDADE) - O AGENTE DEVE SE COLOCAR À DISPOSIÇÃO DO CLIENTE, DEIXANDO CLARO QUE A EMPRESA ESTÁ SEMPRE DISPONÍVEL PARA AJUDAR.
COMO POR EXEMPLO "CASO TENHA ALGUMA DÚVIDA, NOS CONTATE. A EQUIPE DA NOVANET ESTÁ SEMPRE AQUI PARA TE AJUDAR."
08 (CONHECIMENTO TÉCNICO) - O AGENTE DEVE INFORMAR DE FORMA CLARA SE IDENTIFICOU OU NÃO ALGUM PROBLEMA NA CONEXÃO OU NOS EQUIPAMENTOS DO CLIENTE.
09 (DIDATISMO) O AGENTE DEVE SER DIDÁTICO DURANTE O ATENDIMENTO, EXPLICANDO DE FORMA CLARA CADA AÇÃO REALIZADA OU SOLICITADA AO CLIENTE.
O AGENTE É CONSIDERADO DIDÁTICO QUANDO PEDE PARA O CLIENTE REALIZAR ALGUM PROCEDIMENTO E EXPLICA O MOTIVO DA SOLICITAÇÃO.
QUANDO PEDE PARA O CLIENTE FAZER ALGUMA VERIFICAÇÃO E EXPLICA O MOTIVO.
QUANDO ESCLARECE A SITUAÇÃO INFORMANDO ONDE ESTAVA O PROBLEMA E COMO ELE FOI OU SERÁ RESOLVIDO, OU QUANDO FAZ ALGUMA RECOMENDAÇÃO TÉCNICA PARA O CLIENTE, EXPLICANDO O MOTIVO DESSA RECOMENDAÇÃO.
SOLICITAÇÃO OU CONFIRMAÇÃO DE E-MAIL NÃO DEVEM SER CONSIDERADOS NESSE ITEM.
SOLICITAÇÃO OU CONFIRMAÇÃO DE TELEFONE NÃO DEVEM SER CONSIDERADOS NESSE ITEM.
-----------------------------------
As mensagens do chat estão estruturadas no formato JSON com os campos:
- **message**: conteúdo da mensagem
- **sent_at**: horário de envio
- **type**: tipo da mensagem ('IN', 'OUT' ou 'SYSTEM'). As mensagens do agente são sempre do tipo 'OUT'.
- **user_name**: nome do usuário que enviou a mensagem. Não considere respostas do PipeBot como do agente
O fluxo de mensagens inica-se com o cliente interagindo com o BOT, e então a mensagem é transferida para o atendente.
Em cada categoria, atribua 0 quando o agente não tiver atendido o critétio e 1 caso tenha atendido.
Devem ser avaliados somente os agentes que o nome de usuario inicie com "ATEND -" .
A sua resposta deve ser apenas uma tabela CSV e nada mais, utilizando como separador o caracter ';' com as colunas: CATEGORIA;PONTOS;MOTIVO;EVIDENCIA on>
Portando crie essas colunas para todo agente que for avaliado.
Na saída CSV, na coluna categoria, utilize o nome correspondente ao invés do número
A seguir estão as mensagens do atendimento, em JSON, avalie e retorne apenas um CSV.

68
src/funcoes/config.rs Normal file
View File

@@ -0,0 +1,68 @@
use std::env;
use anyhow::Result;
pub struct AppConfig {
pub ollama_url: String,
pub ollama_port: u16,
pub ollama_model: String,
pub piperun_api_url: String,
pub piperun_client_id: i32,
pub piperun_client_secret: String,
pub piperun_username: String,
pub piperun_password: String,
pub bot_email: String,
pub bot_email_password: String,
pub min_messages: usize,
pub min_agent_messages: usize,
}
impl AppConfig {
pub fn from_env() -> Result<Self> {
dotenv::dotenv().ok();
Ok(AppConfig {
ollama_url: env::var("OLLAMA_URL").unwrap_or_else(|_| "localhost".to_string()),
ollama_port: env::var("OLLAMA_PORT")
.unwrap_or_else(|_| "11432".to_string())
.parse::<u16>()
.unwrap_or(11432),
ollama_model: env::var("OLLAMA_AI_MODEL")
.expect("OLLAMA_AI_MODEL has not been set!"),
piperun_api_url: env::var("PIPERUN_API_URL")
.expect("PIPERUN_API_URL has not been set!"),
piperun_client_id: env::var("PIPERUN_CLIENT_ID")
.expect("PIPERUN_CLIENT_ID has not been set!")
.parse::<i32>()
.unwrap_or(0),
piperun_client_secret: env::var("PIPERUN_CLIENT_SECRET")
.expect("PIPERUN_CLIENT_SECRET has not been set!"),
piperun_username: env::var("PIPERUN_BOT_USERNAME")
.expect("PIPERUN_BOT_USERNAME has not been set!"),
piperun_password: env::var("PIPERUN_BOT_PASSWORD")
.expect("PIPERUN_BOT_PASSWORD has not been set!"),
bot_email: env::var("BOT_EMAIL")
.expect("BOT_EMAIL has not been set!"),
bot_email_password: env::var("BOT_EMAIL_PASSWORD")
.expect("BOT_EMAIL_PASSWORD has not been set!"),
min_messages: env::var("MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE")
.expect("MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE has not been set!")
.parse::<usize>()
.unwrap_or(10),
min_agent_messages: env::var("MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE")
.expect("MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE has not been set!")
.parse::<usize>()
.unwrap_or(12),
})
}
pub fn print_config(&self) {
println!("OLLAMA_URL: {}", self.ollama_url);
println!("OLLAMA_PORT: {}", self.ollama_port);
println!("OLLAMA_AI_MODEL: {}", self.ollama_model);
println!("PIPERUN_API_URL: {}", self.piperun_api_url);
println!("PIPERUN_CLIENT_ID: {}", self.piperun_client_id);
println!("PIPERUN_CLIENT_SECRET: {}", self.piperun_client_secret);
println!("PIPERUN_BOT_USERNAME: {}", self.piperun_username);
println!("PIPERUN_BOT_PASSWORD: {}", self.piperun_password);
}
}

79
src/funcoes/date_utils.rs Normal file
View File

@@ -0,0 +1,79 @@
use anyhow::{anyhow, Result};
use chrono::{Duration, Local, NaiveDate, NaiveDateTime, Timelike};
pub struct DateRange {
pub date_str: String, // "YYYY-MM-DD"
pub start_datetime: String, // "YYYY-MM-DD HH:MM"
pub end_datetime: String, // "YYYY-MM-DD HH:MM"
}
/// Retorna o intervalo do dia anterior ao atual (meia-noite até 23:59:59)
pub fn get_previous_date_range() -> Result<DateRange> {
let now = Local::now();
let yesterday = now
.checked_sub_signed(Duration::days(1))
.expect("Failed to subtract one day");
let start = yesterday
.with_hour(0)
.and_then(|d| d.with_minute(0))
.and_then(|d| d.with_second(0))
.expect("Failed to set time to 00:00:00");
let end = yesterday
.with_hour(23)
.and_then(|d| d.with_minute(59))
.and_then(|d| d.with_second(59))
.expect("Failed to set time to 23:59:59");
let date_str = yesterday.format("%Y-%m-%d").to_string();
let start_str = start.format("%Y-%m-%d %H:%M").to_string();
let end_str = end.format("%Y-%m-%d %H:%M").to_string();
Ok(DateRange {
date_str,
start_datetime: start_str,
end_datetime: end_str,
})
}
/// Constrói o DateRange a partir de uma data (meia-noite)
fn build_date_range(date: NaiveDateTime) -> Result<DateRange> {
let start = date;
let end = date
.with_hour(23)
.and_then(|d| d.with_minute(59))
.and_then(|d| d.with_second(59))
.ok_or_else(|| anyhow!("Falha ao definir horário de fim do dia"))?;
let date_str = date.format("%Y-%m-%d").to_string();
let start_str = start.format("%Y-%m-%d %H:%M").to_string();
let end_str = end.format("%Y-%m-%d %H:%M").to_string();
Ok(DateRange {
date_str,
start_datetime: start_str,
end_datetime: end_str,
})
}
pub fn parse_date_arg(arg: &str) -> Result<DateRange> {
let naive = NaiveDate::parse_from_str(arg, "%Y-%m-%d")
.map_err(|_| anyhow!("Formato inválido"))?;
let date = naive.and_hms_opt(0, 0, 0).unwrap();
build_date_range(date)
}
/// Cria o diretório de saída e o arquivo response_time.csv vazio (se não existir)
pub fn setup_output_directory(dir_path: &str) -> Result<()> {
if !std::fs::exists(dir_path)? {
std::fs::create_dir_all(dir_path)?;
}
let csv_path = format!("{}/response_time.csv", dir_path);
if !std::fs::exists(&csv_path)? {
let _ = std::fs::File::create_new(&csv_path)
.expect("Failed to create response_time.csv");
}
Ok(())
}

173
src/funcoes/main.rs Normal file
View File

@@ -0,0 +1,173 @@
mod funcoes;
use funcoes::config::AppConfig;
use funcoes::date_utils;
use funcoes::ollama;
use funcoes::piperun;
use funcoes::send_mail_util;
use funcoes::zip_directory_util;
use anyhow::Result;
use reqwest::blocking::Client;
fn main() -> Result<()> {
// 1. Carregar configuração
let config = AppConfig::from_env()?;
config.print_config();
// 2. Cliente HTTP
let client = Client::new();
// 3. Autenticar na Piperun
let access_token = piperun::authenticate(&client, &config)?;
println!("Autenticado com sucesso!");
// 4. Coletar argumentos
let args: Vec<String> = std::env::args().collect();
let mut idx = 1; // índice atual
// Data (opcional)
let date_range = if args.len() > 1 {
match date_utils::parse_date_arg(&args[1]) {
Ok(dr) => {
idx = 2;
dr
}
Err(_) => {
eprintln!("Aviso: argumento de data inválido, usando dia anterior.");
date_utils::get_previous_date_range()?
}
}
} else {
date_utils::get_previous_date_range()?
};
println!("Processando dia: {}", date_range.date_str);
// Parâmetros obrigatórios: QUEUE_ID PROMPT_FILE FILTER_FILE MIN_AGENT_MSGS FOLDER_NAME EMAILS
let expected_min = if idx == 2 { 7 } else { 6 };
if args.len() < idx + expected_min {
eprintln!("Uso: cargo run -- [DATA] QUEUE_ID PROMPT_FILE FILTER_FILE MIN_AGENT_MSGS FOLDER_NAME EMAILS");
eprintln!(" DATA (opcional) - YYYY-MM-DD");
eprintln!(" Exemplo: cargo run -- 2026-07-12 19 prompts/PROMPT_NOC-CLI.txt filters/FILTER_NOC-CLI.txt 10 evaluations_noc-cli nicolas@nova.net,wilson@nova.net");
std::process::exit(1);
}
let queue_id = &args[idx];
let prompt_file = &args[idx+1];
let filter_file = &args[idx+2];
let min_agent_msgs: usize = args[idx+3].parse().expect("MIN_AGENT_MSGS deve ser um número");
let folder_name = &args[idx+4];
let emails = &args[idx+5];
println!("Queue ID: {}", queue_id);
println!("Prompt file: {}", prompt_file);
println!("Filter file: {}", filter_file);
println!("Min agent messages: {}", min_agent_msgs);
println!("Folder name: {}", folder_name);
println!("E-mails: {}", emails);
// 5. Preparar diretório de saída
let output_dir = format!("./evaluations/{}/{}", folder_name, date_range.date_str);
date_utils::setup_output_directory(&output_dir)?;
// 6. Carregar prompt e filtros
let prompt = std::fs::read_to_string(prompt_file)?;
let filter_content = std::fs::read_to_string(filter_file).unwrap_or_default();
let filter_keywords: Vec<String> = filter_content
.split('\n')
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();
// 7. Buscar talks
let talks = piperun::fetch_talks(
&client,
&access_token,
&config,
&date_range.start_datetime,
&date_range.end_datetime,
queue_id,
)?;
println!("Total de talks encontrados: {}", talks.len());
let talk_ids = piperun::extract_talk_ids(&talks);
// 8. Processar chats
let (valid_chats, response_time_csv) = piperun::process_all_chats(
talk_ids,
&client,
&access_token,
&config,
&filter_keywords,
min_agent_msgs,
)?;
println!("Chats válidos após filtros: {}", valid_chats.len());
// 9. Salvar CSV de tempos
let header = "NOME;ID_TALK;TEMPO DE RESPOSTA;TRANFERENCIA PELO BOT;PRIMEIRA RESPOSTA DO AGENTE";
let response_time_path = format!("{}/response_time.csv", output_dir);
std::fs::write(&response_time_path, format!("{}\n{}", header, response_time_csv))?;
// 10. Avaliar com Ollama
for chat in &valid_chats {
match ollama::evaluate_chat(&client, &config, chat, &prompt) {
Ok(csv) => {
// Reconstroi o talk_text
let histories = chat["talk_histories"]
.as_array()
.or_else(|| chat["talk_histories"]["data"].as_array())
.expect("histories array");
let talk_text = histories
.iter()
.rev()
.map(|msg| {
format!(
"message: {}, sent_at: {}, type: {}, user_name: {}",
msg["message"],
msg["sent_at"],
msg["type"],
msg["user"]["name"]
)
})
.collect::<Vec<_>>()
.join("\n");
if let Err(e) = ollama::save_evaluation_result(
chat,
&csv,
&prompt,
&talk_text,
&output_dir,
) {
eprintln!("Erro ao salvar resultado: {}", e);
}
}
Err(e) => {
eprintln!("Erro ao avaliar chat com Ollama: {}", e);
}
}
}
// 11. Compactar e enviar e-mail
let zip_file = format!("{}.zip", output_dir);
zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file(
std::path::Path::new(&output_dir),
std::path::Path::new(&zip_file),
);
println!("Enviando e-mail para {}", emails);
send_mail_util::send_mail_util::send_email(
&format!(
"Avaliação fila {} do dia {}",
queue_id,
date_range.date_str
),
&config.bot_email,
&config.bot_email_password,
emails,
&zip_file,
);
println!("Processamento finalizado para a fila {}", queue_id);
Ok(())
}

6
src/funcoes/mod.rs Normal file
View File

@@ -0,0 +1,6 @@
pub mod config;
pub mod date_utils;
pub mod ollama;
pub mod piperun;
pub mod send_mail_util;
pub mod zip_directory_util;

98
src/funcoes/ollama.rs Normal file
View File

@@ -0,0 +1,98 @@
use anyhow::{anyhow, Result};
use reqwest::blocking::Client;
use serde_json::Value;
use std::time::Duration;
use crate::funcoes::config::AppConfig;
pub fn evaluate_chat(
client: &Client,
config: &AppConfig,
chat_json: &Value,
prompt: &str,
) -> Result<String> {
let histories = chat_json["talk_histories"]
.as_array()
.or_else(|| chat_json["talk_histories"]["data"].as_array())
.ok_or_else(|| anyhow!("Missing talk_histories array"))?;
let talk_text = histories
.iter()
.rev()
.map(|msg| {
format!(
"message: {}, sent_at: {}, type: {}, user_name: {}",
msg["message"],
msg["sent_at"],
msg["type"],
msg["user"]["name"]
)
})
.collect::<Vec<_>>()
.join("\n");
let full_prompt = format!("{}\n{}", prompt, talk_text);
let sanitized_ip = if config.ollama_url.contains(':') && !config.ollama_url.starts_with('[') {
format!("[{}]", config.ollama_url)
} else {
config.ollama_url.clone()
};
let url = format!("http://{}:{}/api/generate", sanitized_ip, config.ollama_port);
let body = serde_json::json!({
"model": config.ollama_model,
"prompt": full_prompt,
"stream": false,
});
let resp = client
.post(&url)
.body(body.to_string())
.timeout(Duration::from_secs(3600))
.send()?;
if !resp.status().is_success() {
return Err(anyhow!(
"Ollama request failed: status {}",
resp.status()
));
}
let json: Value = resp.json()?;
let ai_response = json["response"]
.as_str()
.ok_or_else(|| anyhow!("Missing 'response' field in Ollama reply"))?
.to_string();
let csv = ai_response
.replace("```csv\n", "")
.replace("```", "")
.trim()
.to_string();
Ok(csv)
}
pub fn save_evaluation_result(
chat_json: &Value,
csv_response: &str,
prompt: &str,
talk_text: &str,
output_dir: &str,
) -> Result<()> {
let user_name = chat_json["agent"]["user"]["name"]
.as_str()
.unwrap_or("unknown_user");
let talk_id = chat_json["id"].as_u64().unwrap_or(0);
let tracking = chat_json["tracking_number"].as_str().unwrap_or("");
let base_name = format!("{} - {} - {}", user_name, talk_id, tracking);
let csv_path = format!("{}/{}.csv", output_dir, base_name);
std::fs::write(&csv_path, csv_response)?;
let prompt_path = format!("{}/{} - prompt.txt", output_dir, base_name);
std::fs::write(&prompt_path, format!("{}\n{}", prompt, talk_text))?;
Ok(())
}

329
src/funcoes/piperun.rs Normal file
View File

@@ -0,0 +1,329 @@
use anyhow::{anyhow, Result};
use reqwest::blocking::Client;
use serde_json::Value;
use crate::funcoes::config::AppConfig;
// ------------------------------
// Funções públicas
// ------------------------------
pub fn authenticate(client: &Client, config: &AppConfig) -> Result<String> {
let url = format!("https://{}/oauth/token", config.piperun_api_url);
let body = serde_json::json!({
"grant_type": "password",
"client_id": config.piperun_client_id,
"client_secret": config.piperun_client_secret,
"username": config.piperun_username,
"password": config.piperun_password,
});
let resp = client
.post(&url)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.body(body.to_string())
.send()?;
let status = resp.status();
if !status.is_success() {
let error_body: Value = resp.json()?;
return Err(anyhow!(
"Authentication failed: status {}; body: {:?}",
status,
error_body
));
}
let json: Value = resp.json()?;
let token = json["access_token"]
.as_str()
.ok_or_else(|| anyhow!("Access token not found in response"))?
.to_string();
Ok(token)
}
pub fn fetch_talks(
client: &Client,
access_token: &str,
config: &AppConfig,
start_date: &str,
end_date: &str,
queue_id: &str,
) -> Result<Vec<Value>> {
let per_page = "15".to_string();
let report_type = "consolidated".to_string();
let mut all_talks = Vec::new();
let mut current_page = 1;
let mut last_page = 1;
loop {
let url = format!("https://{}/api/v2/reports/talks", config.piperun_api_url);
let resp = client
.get(&url)
.bearer_auth(access_token)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.query(&[
("page", current_page.to_string()),
("perPage", per_page.clone()),
("report_type", report_type.clone()),
("start_date", start_date.to_string()),
("end_date", end_date.to_string()),
("date_range_type", "talk_start".to_string()),
("queue_id[]", queue_id.to_string()),
])
.send()?;
let status = resp.status();
if !status.is_success() {
let error_body: Value = resp.json()?;
return Err(anyhow!(
"Failed to fetch talks: status {}; body: {:?}",
status,
error_body
));
}
let json: Value = resp.json()?;
let data = json["data"]
.as_array()
.ok_or_else(|| anyhow!("Missing 'data' array in response"))?
.clone();
all_talks.extend(data);
last_page = json["last_page"]
.as_i64()
.ok_or_else(|| anyhow!("Missing 'last_page'"))? as i32;
let current = json["current_page"]
.as_i64()
.ok_or_else(|| anyhow!("Missing 'current_page'"))? as i32;
if current >= last_page {
break;
}
current_page = current + 1;
}
Ok(all_talks)
}
pub fn extract_talk_ids(talks: &[Value]) -> Vec<String> {
talks
.iter()
.filter_map(|talk| talk["id"].as_i64().map(|id| id.to_string()))
.collect()
}
pub fn process_all_chats(
talk_ids: Vec<String>,
client: &Client,
access_token: &str,
config: &AppConfig,
filter_keywords: &[String],
min_agent_msgs: usize,
) -> Result<(Vec<Value>, String)> {
let mut valid_chats = Vec::new();
let mut response_lines = Vec::new();
for talk_id in talk_ids {
let history_json = match fetch_talk_history(client, access_token, config, &talk_id) {
Ok(j) => j,
Err(e) => {
eprintln!("Erro ao buscar histórico do talk {}: {}", talk_id, e);
continue;
}
};
let filtered = match filter_chat(&history_json, filter_keywords, config.min_messages, min_agent_msgs) {
Some(v) => v,
None => continue,
};
if let Some(line) = compute_response_time(&filtered) {
response_lines.push(line);
}
valid_chats.push(filtered);
}
let response_csv = response_lines.join("\n");
Ok((valid_chats, response_csv))
}
// ------------------------------
// Funções auxiliares privadas
// ------------------------------
fn fetch_talk_history(
client: &Client,
access_token: &str,
config: &AppConfig,
talk_id: &str,
) -> Result<Value> {
let url = format!("https://{}/api/talk_histories", config.piperun_api_url);
let resp = client
.get(&url)
.bearer_auth(access_token)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.query(&[
("talk_id", talk_id.to_string()),
("type", "a".to_string()),
("only_view", "1".to_string()),
])
.send()?;
let status = resp.status();
if !status.is_success() {
return Err(anyhow!(
"Failed to fetch history for talk {}: status {}",
talk_id,
status
));
}
let json: Value = resp.json()?;
Ok(json)
}
fn filter_chat(
chat_json: &Value,
filter_keywords: &[String],
min_messages: usize,
min_agent_msgs: usize,
) -> Option<Value> {
let histories = extract_histories_array(chat_json)?;
if histories.len() < min_messages {
println!("Descartado - Chat com poucas mensagens.");
return None;
}
let found = histories.iter().enumerate().find(|(_, msg)| {
let message = msg["message"].as_str().unwrap_or("");
let is_template = msg["is_template"].as_bool().unwrap_or(false);
let is_transfer = message.contains(
"Atendimento transferido para a fila [NovaNet -> Atendimento -> NOC",
);
is_transfer || is_template
});
let (pos, _) = match found {
Some(p) => p,
None => {
println!("Descartado - Não encontrou transferência ou template.");
return None;
}
};
if pos < min_agent_msgs {
println!("Descartado - Poucas mensagens com agente após transferência.");
return None;
}
if chat_json["finished_at"].is_null() {
println!("Descartado - Chat não finalizado.");
return None;
}
if chat_json["agent"]["user"]["name"]
.as_str()
.unwrap_or("")
== "PipeBot"
{
println!("Descartado - Chat finalizado pelo PipeBot.");
return None;
}
let has_keyword = histories.iter().any(|msg| {
let text = msg["message"].as_str().unwrap_or("");
filter_keywords
.iter()
.any(|kw| text.to_uppercase().contains(&kw.to_uppercase()))
});
if has_keyword {
println!("Descartado - Contém palavra do filtro.");
return None;
}
Some(chat_json.clone())
}
fn compute_response_time(chat_json: &Value) -> Option<String> {
let histories = extract_histories_array(chat_json)?;
let trigger = histories.iter().enumerate().rev().find(|(_, msg)| {
let is_template = msg["is_template"].as_bool().unwrap_or(false);
let user = msg["user"]["name"].as_str().unwrap_or("");
let message = msg["message"].as_str().unwrap_or("");
let is_transfer = user == "PipeBot"
&& message.contains("Atendimento entregue da fila de espera para o agente [NOC");
is_template || is_transfer
});
let (trigger_pos, trigger_msg) = trigger?;
let is_transfer_case = {
let user = trigger_msg["user"]["name"].as_str().unwrap_or("");
let message = trigger_msg["message"].as_str().unwrap_or("");
user == "PipeBot"
&& message.contains("Atendimento entregue da fila de espera para o agente [NOC")
};
let (ref_pos, ref_msg) = if is_transfer_case {
(trigger_pos, trigger_msg)
} else {
if trigger_pos == 0 {
return None;
}
(trigger_pos - 1, &histories[trigger_pos - 1])
};
let agent_msg = (0..ref_pos).rev().find_map(|i| {
let m = &histories[i];
if m["type"].as_str() == Some("out")
&& m["user"]["name"]
.as_str()
.map_or(false, |name| name.starts_with("NOC -"))
{
Some(m)
} else {
None
}
})?;
let format = "%Y-%m-%d %H:%M:%S";
let t_ref = chrono::NaiveDateTime::parse_from_str(
ref_msg["sent_at"].as_str()?,
format,
)
.ok()?;
let t_agent = chrono::NaiveDateTime::parse_from_str(
agent_msg["sent_at"].as_str()?,
format,
)
.ok()?;
let response_time = (t_agent - t_ref).num_seconds() as f32;
let name = agent_msg["user"]["name"].as_str().unwrap_or("").to_owned();
let id = chat_json["tracking_number"].as_str().unwrap_or("").to_owned();
let ref_date = ref_msg["sent_at"].as_str().unwrap_or("").to_owned();
let agent_date = agent_msg["sent_at"].as_str().unwrap_or("").to_owned();
Some(format!(
"{};{};{};{};{}",
name, id, response_time, ref_date, agent_date
))
}
fn extract_histories_array(chat_json: &Value) -> Option<&[Value]> {
chat_json["talk_histories"]
.as_array()
.or_else(|| chat_json["talk_histories"]["data"].as_array())
.map(|v| &**v) // <-- conversão de &Vec<Value> para &[Value]
}

View File

@@ -0,0 +1,46 @@
pub mod send_mail_util {
use lettre::{
Message, SmtpTransport, Transport,
message::{self, Attachment, Mailboxes, MultiPart, SinglePart, header::ContentType},
};
pub fn send_email(
subject_of_email: &str,
bot_email: &str,
bot_email_password: &str,
to: &str,
zip_file_name: &str,
) {
let filebody = std::fs::read(zip_file_name).unwrap();
let content_type = ContentType::parse("application/zip").unwrap();
let attachment = Attachment::new(zip_file_name.to_string()).body(filebody, content_type);
let mailboxes: Mailboxes = to.parse().unwrap();
let to_header: message::header::To = mailboxes.into();
let email = Message::builder()
.from(format!("PipeRUN bot <{bot_email}>").parse().unwrap())
.reply_to(format!("PipeRUN bot <{bot_email}>").parse().unwrap())
.mailbox(to_header)
.subject(format!("{subject_of_email}"))
.multipart(
MultiPart::mixed()
.singlepart(
SinglePart::builder()
.header(ContentType::TEXT_PLAIN)
.body(String::from("Avaliacao dos atendimentos")),
)
.singlepart(attachment),
)
.unwrap();
// Create the SMTPS transport
let sender = SmtpTransport::from_url(&format!(
"smtps://{bot_email}:{bot_email_password}@mail.nova.net.br"
))
.unwrap()
.build();
// Send the email via remote relay
sender.send(&email).unwrap();
}
}

View File

@@ -0,0 +1,69 @@
pub mod zip_directory_util {
use std::io::prelude::*;
use zip::write::SimpleFileOptions;
use std::fs::File;
use std::path::Path;
use walkdir::{DirEntry, WalkDir};
fn zip_dir<T>(
it: &mut dyn Iterator<Item = DirEntry>,
prefix: &Path,
writer: T,
method: zip::CompressionMethod,
) where
T: Write + Seek,
{
let mut zip = zip::ZipWriter::new(writer);
let options = SimpleFileOptions::default()
.compression_method(method)
.unix_permissions(0o755);
let prefix = Path::new(prefix);
let mut buffer = Vec::new();
for entry in it {
let path = entry.path();
let name = path.strip_prefix(prefix).unwrap();
let path_as_string = name
.to_str()
.map(str::to_owned)
.expect("Failed to parse path");
// Write file or directory explicitly
// Some unzip tools unzip files with directory paths correctly, some do not!
if path.is_file() {
println!("adding file {path:?} as {name:?} ...");
zip.start_file(path_as_string, options)
.expect("Failed to add file");
let mut f = File::open(path).unwrap();
f.read_to_end(&mut buffer).expect("Failed to read file");
zip.write_all(&buffer).expect("Failed to write file");
buffer.clear();
} else if !name.as_os_str().is_empty() {
// Only if not root! Avoids path spec / warning
// and mapname conversion failed error on unzip
println!("adding dir {path_as_string:?} as {name:?} ...");
zip.add_directory(path_as_string, options)
.expect("Failed to add directory");
}
}
zip.finish().expect("Failed to ZIP");
}
pub fn zip_source_dir_to_dst_file(src_dir: &Path, dst_file: &Path) {
if !Path::new(src_dir).is_dir() {
panic!("src_dir must be a directory");
}
let method = zip::CompressionMethod::Stored;
let path = Path::new(dst_file);
let file = File::create(path).unwrap();
let walkdir = WalkDir::new(src_dir);
let it = walkdir.into_iter();
zip_dir(&mut it.filter_map(|e| e.ok()), src_dir, file, method);
}
}

View File

@@ -1,597 +0,0 @@
use std::fmt::Debug;
use itertools::Itertools;
use polars::prelude::*;
use reqwest;
use std::env;
use std::time::Duration;
use std::path::Path;
use csv;
use std::fs::metadata;
use std::io::Read;
pub mod send_mail_util;
pub mod zip_directory_util;
#[derive(Debug, serde::Deserialize)]
struct CsvHeader {
CATEGORIA: String,
PONTOS: Option<u8>,
}
#[derive(Debug, serde::Deserialize)]
struct CsvEvaluation {
APRESENTACAO: u8,
CONFIRMAÇÃO_DE_EMAIL: u8,
CONFIRMAÇÃO_DE_TELEFONE: u8,
PROTOCOLO: u8,
USO_DO_PORTUGUES: u8,
PACIENCIA_E_EDUCACAO: u8,
DISPONIBILIDADE: u8,
// CONHECIMENTO_TÉCNICO: u8,
ESCLARECIMENTO: u8,
ID_TALK: String,
}
//inclusão de estrutura para agrupar o response_time.cvs
#[derive(Debug, serde::Deserialize)]
struct ResponseTimeRecord {
NOME: String,
ID_TALK: String,
#[serde(rename = "TEMPO DE RESPOSTA")]
TEMPO_DE_RESPOSTA: u32,
#[serde(rename = "TRANFERENCIA PELO BOT")]
TRANFERENCIA_PELO_BOT: String,
#[serde(rename = "PRIMEIRA RESPOSTA DO AGENTE")]
PRIMEIRA_RESPOSTA_DO_AGENTE: String,
}
//fim da inclusão
fn main() {
match dotenv::dotenv().ok() {
Some(_) => println!("Environment variables loaded from .env file"),
None => eprintln!("Failed to load .env file, using defaults"),
}
// Read environment variables
let OLLAMA_URL = env::var("OLLAMA_URL").unwrap_or("localhost".to_string());
let OLLAMA_PORT = env::var("OLLAMA_PORT")
.unwrap_or("11432".to_string())
.parse::<u16>()
.unwrap_or(11432);
let OLLAMA_AI_MODEL_DATA_SANITIZATION = env::var("OLLAMA_AI_MODEL_DATA_SANITIZATION")
.expect("Missing environment variable OLLAMA_AI_MODEL_DATA_SANITIZATION");
let BOT_EMAIL = env::var("BOT_EMAIL").expect("BOT_EMAIL has not been set!");
let BOT_EMAIL_PASSWORD =
env::var("BOT_EMAIL_PASSWORD").expect("BOT_EMAIL_PASSWORD has not been set!");
let ip_address = ipaddress::IPAddress::parse(OLLAMA_URL.to_string());
let OLLAMA_SANITIZED_IP = match ip_address {
Ok(ip) => {
if ip.is_ipv4() {
OLLAMA_URL.clone()
} else {
format!("[{}]", OLLAMA_URL.clone())
}
}
Err(e) => OLLAMA_URL.clone(),
};
// Get the current day in the format YYYY-MM-DD
let current_date = chrono::Local::now();
let formatted_date = current_date.format("%Y-%m-%d").to_string();
let current_date = chrono::Local::now();
let first_day_of_current_week = current_date
.date_naive()
.week(chrono::Weekday::Sun)
.first_day();
let current_date_minus_one_week = first_day_of_current_week
.checked_sub_days(chrono::Days::new(1))
.expect("Failed to subtract one day");
let first_day_of_last_week = current_date_minus_one_week
.week(chrono::Weekday::Sun)
.first_day();
let last_day_of_last_week = current_date_minus_one_week
.week(chrono::Weekday::Sun)
.last_day();
let previous_week_folder_names = std::fs::read_dir(std::path::Path::new("./evaluations"))
.expect("Failed to read directory ./evaluations")
.filter_map_ok(|entry| {
if entry.metadata().unwrap().is_dir() {
Some(entry.file_name())
} else {
None
}
})
.filter_map_ok(|entry_string_name| {
let regex_match_date =
regex::Regex::new(r"(\d{4}-\d{2}-\d{2})").expect("Failed to build regex");
let filename = entry_string_name.to_str().unwrap();
let matches_find = regex_match_date.find(filename);
match matches_find {
Some(found) => {
let date = chrono::NaiveDate::parse_from_str(found.as_str(), "%Y-%m-%d");
return Some((date.unwrap().week(chrono::Weekday::Sun), entry_string_name));
}
None => {
return None;
}
};
})
.filter_map_ok(|(week, directory_string)| {
let first_day_of_week_in_folder_name = week.first_day();
if first_day_of_last_week == first_day_of_week_in_folder_name {
return Some(directory_string);
}
return None;
})
.filter_map(|value| {
if value.is_ok() {
return Some(value.unwrap());
} else {
return None;
}
})
.sorted()
.collect_vec();
println!("{:?}", previous_week_folder_names);
let prompt_data_sanitization = std::fs::read_to_string("./PROMPT_DATA_SANITIZATION.txt")
.expect("Failed to read PROMPT_DATA_SANITIZATION.txt");
let client = reqwest::blocking::Client::new();
let groupped_values = previous_week_folder_names
.iter()
.map(|folder_name| {
let folder_base_path = std::path::Path::new("./evaluations");
let folder_date_path = folder_base_path.join(folder_name);
std::fs::read_dir(folder_date_path)
})
.filter_map_ok(|files_inside_folder_on_date| {
let groupped_by_user_on_day = files_inside_folder_on_date
.filter_ok(|entry| {
let entry_file_name_as_str = entry
.file_name()
.into_string()
.expect("Failed to get filename as a String");
entry_file_name_as_str.ends_with(".csv")
&& !entry_file_name_as_str.contains("response_time.csv")
})
.filter_map(|value| {
if value.is_ok() {
return Some(value.unwrap());
}
None
})
.map(|file_name_csv| {
println!("{:?}", file_name_csv.path());
let file_contents = std::fs::read_to_string(file_name_csv.path())
.expect("Failed to read CSV file");
let ollama_api_request = client
.post(format!(
"http://{OLLAMA_SANITIZED_IP}:{OLLAMA_PORT}/api/generate"
))
.body(
serde_json::json!({
"model": OLLAMA_AI_MODEL_DATA_SANITIZATION,
"prompt": format!("{prompt_data_sanitization} \n{file_contents}"),
"temperature": 0.0, // Get predictable and reproducible output
"stream": false,
})
.to_string(),
);
let result = ollama_api_request.timeout(Duration::from_secs(3600)).send();
match result {
Ok(response) => {
println!("Response: {:?}", response);
let response_json = response
.json::<serde_json::Value>()
.expect("Failed to deserialize response to JSON");
let ai_response = response_json["response"]
.as_str()
.expect("Failed to get AI response as string");
let ai_response = ai_response.to_string();
let ai_response = if let Some(resp) = ai_response
.strip_prefix(" ")
.unwrap_or(&ai_response)
.strip_prefix("```csv\n")
{
resp.to_string()
} else {
ai_response
};
let ai_response = if let Some(resp) = ai_response
.strip_suffix(" ")
.unwrap_or(&ai_response)
.strip_suffix("```")
{
resp.to_string()
} else {
ai_response
};
return Ok((ai_response, file_name_csv));
}
Err(error) => {
println!("Error {error}");
return Err(error);
}
};
})
.filter_map_ok(|(ai_response, file_path_csv)| {
// ---------- LOG 1: mostra qual arquivo está sendo processado ----------
// eprintln!("🔍 Processando arquivo: {:?}", file_path_csv);
// Mostra o caminho absoluto
let path = file_path_csv.path();
// Caminho absoluto
if let Ok(abs_path) = std::fs::canonicalize(&path) {
eprintln!("📁 Caminho absoluto: {:?}", abs_path);
}
// Metadados
if let Ok(meta) = std::fs::metadata(&path) {
if let Ok(modified) = meta.modified() {
let datetime: chrono::DateTime<chrono::Local> = modified.into();
eprintln!("📅 Modificado (local): {}", datetime.format("%Y-%m-%d %H:%M:%S%.3f"));
//eprintln!("🕒 Modificado: {:?}", modified);
}
eprintln!("📏 Tamanho: {} bytes", meta.len());
}
// Opcional: mostrar as primeiras linhas do CSV
eprintln!("📄 Primeiras 200 caracteres do CSV antes de sanitizar:\n{}", &ai_response[..200.min(ai_response.len())]);
//
// --- SALVAR CSV SANITIZADO PARA INSPEÇÃO ---
//let sanitized_path = file_path_csv.path().with_extension("sanitized.csv");
// if let Err(e) = std::fs::write(&sanitized_path, &ai_response) {
// eprintln!("⚠️ Erro ao salvar CSV sanitizado: {}", e);
//} else {
// eprintln!("💾 CSV sanitizado salvo: {:?}", sanitized_path);
// }
// ---------------------------------------------
// --- SALVAR CSV SANITIZADO EM PASTA SEPARADA ---
// Define o diretório base para os arquivos sanitizados
let sanitized_base = Path::new("./evaluations_sanitized");
// Obtém o caminho relativo do arquivo original em relação a "./evaluations"
// Exemplo: "./evaluations/2026-02-09/arquivo.csv" -> "2026-02-09/arquivo.csv"
if let Ok(relative_path) = file_path_csv.path().strip_prefix("./evaluations") {
let dest_path = sanitized_base.join(relative_path);
// Cria o diretório de destino, se necessário
if let Some(parent) = dest_path.parent() {
std::fs::create_dir_all(parent).expect("Falha ao criar diretório para sanitizados");
}
// Altera a extensão para .sanitized.csv (ou mantém .csv, como preferir)
let dest_path = dest_path.with_extension("sanitized.csv");
// Escreve o arquivo
if let Err(e) = std::fs::write(&dest_path, &ai_response) {
eprintln!("⚠️ Erro ao salvar CSV sanitizado em {:?}: {}", dest_path, e);
} else {
eprintln!("💾 CSV sanitizado salvo em: {:?}", dest_path);
}
} else {
eprintln!("⚠️ Caminho do arquivo não está dentro de ./evaluations: {:?}", file_path_csv.path());
}
let mut reader = csv::ReaderBuilder::new()
.has_headers(true)
.delimiter(b';')
.from_reader(ai_response.as_bytes());
// ---------- LOG 2: tenta desserializar e conta os registros ----------
let deserialized = reader.deserialize::<CsvHeader>().collect::<Vec<_>>();
eprintln!("🧾 Total de linhas lidas (incluindo cabeçalho?): {}", deserialized.len());
for (i, result) in deserialized.iter().enumerate() {
match result {
Ok(record) => {
eprintln!("✅ Linha {}: CATEGORIA={}, PONTOS={:?}", i, record.CATEGORIA, record.PONTOS);
}
Err(e) => {
eprintln!("❌ Linha {} ERRO: {}", i, e);
}
}
}
/*
let mut deserialized_iter = reader.deserialize::<CsvHeader>();
let mut columns = deserialized_iter
.filter_ok(|value| value.PONTOS.is_some())
.map_ok(|value| {
let col =
Column::new(value.CATEGORIA.into(), [value.PONTOS.unwrap() as u32]);
col
})
.filter_map(|value| {
if value.is_ok() {
return Some(value.unwrap());
}
None
})
.collect_vec();
*/
let mut columns = deserialized
.into_iter() // usa os registros já lidos
.filter_ok(|value| {
// Se PONTOS for None, considera como 0 e mantém a linha
true // sempre mantém
})
.map_ok(|value| {
let pontos = value.PONTOS.unwrap_or(0) as u32;
Column::new(value.CATEGORIA.into(), [pontos])
})
/*
.filter_ok(|value| value.PONTOS.is_some())
.map_ok(|value| {
let pontos = value.PONTOS.unwrap() as u32;
Column::new(value.CATEGORIA.into(), [pontos])
})
*/
.filter_map(|value| value.ok())
.collect_vec();
if columns.len() != 8 {
return None;
}
// Parse id talk from file_path
// filename example is: FIN - Lais Mota - 515578 - 20251020515578.csv
// id talk is the last information, so in the example is: 20251020515578
let regex_filename =
//regex::Regex::new(r"(FIN - )((\s*\w+\s*)+) - (\d+) - (\d+).csv").unwrap();
regex::Regex::new(r"FIN - (.+?) - (\d+) - (\d+)\.csv").unwrap();
let filename = file_path_csv
.file_name()
.into_string()
.expect("Failed to convert file name as Rust &str");
let found_regex_groups_in_filename = regex_filename
.captures(filename.as_str())
.expect("Failed to do regex capture");
let user_name = found_regex_groups_in_filename
.get(1)
.expect("Failed to get the id from regex maches");
let talk_id = found_regex_groups_in_filename
.get(3)
.expect("Failed to get the id from regex maches");
let excelence_percentual = columns
.iter()
.map(|col| col.as_materialized_series().u32().unwrap().sum().unwrap())
.sum::<u32>() as f32
/ columns.iter().len() as f32
* 100.0;
columns.push(Column::new(
"PERCENTUAL DE EXELENCIA".into(),
[format!("{excelence_percentual:.2}")],
));
columns.push(Column::new("ID_TALK".into(), [talk_id.clone().as_str()]));
let df = polars::frame::DataFrame::new(columns)
.expect("Failed to concatenate into a dataframe");
// return a tuple with the dataframe and the user name, so it can be correctly merged after
return Some((user_name.as_str().to_owned(), df));
})
.filter_map(|res| {
if res.is_ok() {
return Some(res.unwrap());
}
return None;
})
.into_group_map()
.into_iter()
.map(|(name, eval_dataframe_vec)| {
let groupped_df = eval_dataframe_vec
.iter()
.cloned()
.reduce(|acc, e| acc.vstack(&e).unwrap())
.expect("Failed to concatenate dataframes");
(name, groupped_df)
})
.into_group_map();
dbg!(&groupped_by_user_on_day);
return Some(groupped_by_user_on_day);
})
.filter_map(|res| {
if res.is_ok() {
return Some(res.unwrap());
}
return None;
})
.reduce(|mut acc, mut e| {
e.iter_mut().for_each(|(key, val)| {
if acc.contains_key(key) {
acc.get_mut(key)
.expect("Failed to obtain key that should already be present")
.append(val);
} else {
acc.insert(key.to_owned(), val.to_owned());
}
});
acc
})
.and_then(|groupped_hashmap_df| {
let result = groupped_hashmap_df
.iter()
.map(|(key, val)| {
let dfs = val
.iter()
.cloned()
.reduce(|acc, e| acc.vstack(&e).unwrap())
.expect("Failed to concatenate dataframes");
(key.clone(), dfs)
})
.collect_vec();
return Some(result);
});
// Setup groupped folder
if !std::fs::exists(format!("./groupped/")).unwrap() {
std::fs::create_dir(format!("./groupped")).expect("Failed to create directory")
}
// Setup previous week folder
if !std::fs::exists(format!(
"./groupped/{first_day_of_last_week} - {last_day_of_last_week}"
))
.unwrap()
{
std::fs::create_dir(format!(
"./groupped/{first_day_of_last_week} - {last_day_of_last_week}"
))
.expect("Failed to create directory")
}
match groupped_values {
Some(mut val) => {
val.iter_mut().for_each(|(agent, groupped_evaluations)| {
let mut save_file_csv = std::fs::File::create(format!(
"./groupped/{first_day_of_last_week} - {last_day_of_last_week}/{agent}.csv"
))
.expect("Could not create csv file for saving");
CsvWriter::new(&mut save_file_csv)
.include_header(true)
.with_separator(b';')
.finish(groupped_evaluations)
.expect("Failed to save Groupped DataFrame to CSV File");
});
}
None => {}
}
//inclusão nova para agrupar o response_time.csv
// Processar response_time.csv separadamente
let response_times_data = previous_week_folder_names
.iter()
.map(|folder_name| {
let folder_base_path = std::path::Path::new("./evaluations");
let folder_date_path = folder_base_path.join(folder_name);
std::fs::read_dir(folder_date_path)
})
.filter_map_ok(|files_inside_folder_on_date| {
let response_time_files = files_inside_folder_on_date
.filter_ok(|entry| {
let entry_file_name_as_str = entry
.file_name()
.into_string()
.expect("Failed to get filename as a String");
entry_file_name_as_str.ends_with("response_time.csv")
})
.filter_map(|value| {
if value.is_ok() {
return Some(value.unwrap());
}
None
})
.map(|file_path| {
println!("Processing response time file: {:?}", file_path.path());
let mut rdr = csv::ReaderBuilder::new()
.delimiter(b';')
.has_headers(true)
.from_reader(std::fs::File::open(file_path.path()).unwrap());
let records: Vec<ResponseTimeRecord> = rdr
.deserialize()
.filter_map(Result::ok)
.collect();
records
})
.flat_map(|records| records)
.collect_vec();
Some(response_time_files)
})
.filter_map(|res| {
if res.is_ok() {
return Some(res.unwrap());
}
return None;
})
.flat_map(|records| records)
.collect_vec();
// Salvar response times consolidados
if !response_times_data.is_empty() {
let response_time_file_path = format!(
"./groupped/{first_day_of_last_week} - {last_day_of_last_week}/response_times_consolidated.csv"
);
let mut wtr = csv::WriterBuilder::new()
.delimiter(b';')
.from_path(response_time_file_path)
.expect("Failed to create response times CSV");
// Escrever cabeçalho
wtr.write_record(&["NOME", "ID_TALK", "TEMPO DE RESPOSTA", "TRANFERENCIA PELO BOT", "PRIMEIRA RESPOSTA DO AGENTE"])
.expect("Failed to write header");
for record in response_times_data {
wtr.write_record(&[
&record.NOME,
&record.ID_TALK,
&record.TEMPO_DE_RESPOSTA.to_string(),
&record.TRANFERENCIA_PELO_BOT,
&record.PRIMEIRA_RESPOSTA_DO_AGENTE,
]).expect("Failed to write record");
}
wtr.flush().expect("Failed to flush writer");
println!("Response times consolidated successfully!");
} else {
println!("No response time data found for the period.");
}
// --- FIM DA ADIÇÃO ---
//fim da inclusão
zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file(
std::path::Path::new(&format!(
"./groupped/{first_day_of_last_week} - {last_day_of_last_week}"
)),
std::path::Path::new(&format!(
"./groupped/{first_day_of_last_week} - {last_day_of_last_week}.zip"
)),
);
let recipients = "Wilson da Conceição Oliveira <wilson.oliveira@nova.net.br>, nicolas.borges@nova.net.br";
println!("Trying to send mail... {recipients}");
send_mail_util::send_mail_util::send_email(
&format!(
"Relatório agrupado dos atendimentos da fila do Financeiro N2 - semana {first_day_of_last_week} - {last_day_of_last_week}"
),
&BOT_EMAIL,
&BOT_EMAIL_PASSWORD,
recipients,
&format!("./groupped/{first_day_of_last_week} - {last_day_of_last_week}.zip"),
);
}

View File

@@ -70,9 +70,11 @@ fn main() -> anyhow::Result<()> {
let PASTA = env::var("PASTA").expect("PASTA has not been set!"); let PASTA = env::var("PASTA").expect("PASTA has not been set!");
let NOME_FILA = env::var("NOME_FILA").expect("NOME_FILA has not been set!"); let NOME_FILA = env::var("NOME_FILA").expect("NOME_FILA has not been set!");
let INICIAL_AGENTE = env::var("INICIAL_AGENTE").expect("INICIAL_AGENTE has not been set!"); let INICIAL_AGENTE = env::var("INICIAL_AGENTE").expect("INICIAL_AGENTE has not been set!");
let EMAIL_1 = env::var("EMAIL_1").expect("EMAIL_1 has not been set!"); let EMAILS = env::var("EMAILS").expect("EMAILS has not been set!");
let EMAIL_2 = env::var("EMAIL_2").expect("EMAIL_2 has not been set!"); let CONSIDERAR_TEMPLATE = env::var("CONSIDERAR_TEMPLATE")
// let EMAIL_3 = env::var("EMAIL_3").expect("EMAIL_3 has not been set!"); .expect("CONSIDERAR_TEMPLATE has not been set!")
.parse::<bool>()
.unwrap_or(false); // Se não conseguir parsear, assume false
// Print the configuration // Print the configuration
println!("OLLAMA_URL: {}", OLLAMA_URL); println!("OLLAMA_URL: {}", OLLAMA_URL);
@@ -89,6 +91,8 @@ fn main() -> anyhow::Result<()> {
println!("PASTA: {}", PASTA); println!("PASTA: {}", PASTA);
println!("PROMPT_X: {}", PROMPT_X); println!("PROMPT_X: {}", PROMPT_X);
println!("FILTER_X: {}", FILTER_X); println!("FILTER_X: {}", FILTER_X);
println!("E-MAILS: {}", EMAILS);
println!("CONSIDERAR TEMPLATE: {}", CONSIDERAR_TEMPLATE);
// Get the current day in the format YYYY-MM-DD // Get the current day in the format YYYY-MM-DD
let current_date = chrono::Local::now(); let current_date = chrono::Local::now();
@@ -356,9 +360,10 @@ fn main() -> anyhow::Result<()> {
let template_id = message_object["is_template"] let template_id = message_object["is_template"]
.as_bool() .as_bool()
.unwrap_or(true); .unwrap_or(true);
let considera_template = template_id && CONSIDERAR_TEMPLATE;
let mt_fila = format!("Atendimento transferido para a fila [NovaNet -> Atendimento -> {}]", NOME_FILA); let mt_fila = format!("Atendimento transferido para a fila [NovaNet -> Atendimento -> {}]", NOME_FILA);
let found = message.find(&mt_fila ); let found = message.find(&mt_fila);
found.is_some() || template_id found.is_some() || considera_template
}); });
match found { match found {
@@ -432,10 +437,11 @@ fn main() -> anyhow::Result<()> {
// 1. Encontrar o primeiro gatilho na ordem cronológica (percorrendo do índice mais alto para o mais baixo) // 1. Encontrar o primeiro gatilho na ordem cronológica (percorrendo do índice mais alto para o mais baixo)
let trigger = histories.iter().enumerate().rev().find(|(_, msg)| { let trigger = histories.iter().enumerate().rev().find(|(_, msg)| {
let is_template = msg["is_template"].as_bool().unwrap_or(false); let is_template = msg["is_template"].as_bool().unwrap_or(false);
let considerar_template2 = is_template && CONSIDERAR_TEMPLATE;
let user = msg["user"]["name"].as_str().unwrap_or(""); let user = msg["user"]["name"].as_str().unwrap_or("");
let message = msg["message"].as_str().unwrap_or(""); let message = msg["message"].as_str().unwrap_or("");
let is_transfer = user == "PipeBot" && message.contains(&mt_agente); let is_transfer = user == "PipeBot" && message.contains(&mt_agente);
is_template || is_transfer considerar_template2 || is_transfer
}); });
let (trigger_pos, trigger_msg) = match trigger { let (trigger_pos, trigger_msg) = match trigger {
@@ -630,8 +636,7 @@ fn main() -> anyhow::Result<()> {
zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file(source_dir, output_zip_file); zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file(source_dir, output_zip_file);
// Send folder to email // Send folder to email
let recipients = format!("{}, {}", EMAIL_1, EMAIL_2); let recipients = format!("{}", EMAILS);
//let recipients = "nicolas@nova.net.br, wilson@nova.net.br";
println!("Trying to send email... Recipients {}", recipients); println!("Trying to send email... Recipients {}", recipients);
send_mail_util::send_mail_util::send_email( send_mail_util::send_mail_util::send_email(

View File

@@ -125,6 +125,8 @@ fn main() -> anyhow::Result<()> {
} }
}; };
// Get the current day in the format YYYY-MM-DD // Get the current day in the format YYYY-MM-DD
let current_date = chrono::Local::now(); let current_date = chrono::Local::now();
let formatted_date = current_date.format("%Y-%m-%d").to_string(); let formatted_date = current_date.format("%Y-%m-%d").to_string();
@@ -184,6 +186,55 @@ fn main() -> anyhow::Result<()> {
.expect("Failed to response_time.csv"); .expect("Failed to response_time.csv");
} }
/*
// --- NOVO: processar argumento de linha de comando para data específica ---
use std::env;
use chrono::NaiveDate;
let args: Vec<String> = env::args().collect();
let target_day = if args.len() > 1 {
// Se um argumento foi passado, interpretar como YYYY-MM-DD
let naive_date = NaiveDate::parse_from_str(&args[1], "%Y-%m-%d")
.expect("Formato de data inválido. Use YYYY-MM-DD");
// Converter para DateTime com hora 00:00:00 do fuso local
naive_date.and_hms_opt(0, 0, 0).unwrap()
} else {
// Comportamento padrão: dia anterior ao atual
(chrono::Local::now() - chrono::Duration::days(1)).naive_local()
};
println!("Processando o dia: {}", target_day.format("%Y-%m-%d"));
// Definir início e fim do dia (para consultas na API)
let day_start = target_day; // já 00:00
let day_end = target_day
.with_hour(23).unwrap()
.with_minute(59).unwrap()
.with_second(59).unwrap();
let formatted_day = target_day.format("%Y-%m-%d").to_string();
let formatted_day_start = day_start.format("%Y-%m-%d %H:%M").to_string();
let formatted_day_end = day_end.format("%Y-%m-%d %H:%M").to_string();
println!("Início do dia: {}", formatted_day_start);
println!("Fim do dia: {}", formatted_day_end);
// Criar pasta com o nome do dia processado
if !std::fs::exists(format!("./evaluations/{formatted_day}")).unwrap() {
std::fs::create_dir(format!("./evaluations/{formatted_day}"))
.expect("Failed to create directory");
}
// Criar arquivo response_time.csv se não existir
if !std::fs::exists(format!("./evaluations/{formatted_day}/response_time.csv")).unwrap() {
let _ = std::fs::File::create_new(format!(
"./evaluations/{formatted_day}/response_time.csv"
))
.expect("Failed to create response_time.csv");
}
*/
// Read system prompt // Read system prompt
let prompt = std::fs::read_to_string("PROMPT.txt").unwrap(); let prompt = std::fs::read_to_string("PROMPT.txt").unwrap();
let filter_file_contents = std::fs::read_to_string("FILTER.txt").unwrap_or(String::new()); let filter_file_contents = std::fs::read_to_string("FILTER.txt").unwrap_or(String::new());
@@ -197,7 +248,9 @@ fn main() -> anyhow::Result<()> {
&client, &client,
&access_token, &access_token,
formatted_day_before_at_midnight, formatted_day_before_at_midnight,
//formatted_day_start,
formatted_day_before_at_23_59_59, formatted_day_before_at_23_59_59,
//formatted_day_end,
); );
println!("Number of consolidated talks: {}", talks_array.len()); println!("Number of consolidated talks: {}", talks_array.len());
@@ -262,10 +315,13 @@ fn main() -> anyhow::Result<()> {
let message = message_object["message"] let message = message_object["message"]
.as_str() .as_str()
.expect("Failed to decode message as string"); .expect("Failed to decode message as string");
let found = message.find( let found_m = message.find(
"Atendimento transferido para a fila [NovaNet -> Atendimento -> Financeiro NVL2]", "Atendimento transferido para a fila [NovaNet -> Atendimento -> NOC - Clientes]",
); );
found.is_some() let found_t = message_object["data"]["is_template"]
.as_bool()
.expect("Failed to decode boolean is_template");
found_m.is_some() || found_t
}); });
match found { match found {
@@ -285,9 +341,16 @@ fn main() -> anyhow::Result<()> {
.as_str() .as_str()
.unwrap_or("unknown_user") .unwrap_or("unknown_user")
== "PipeBot" == "PipeBot"
{ {
return None; return None;
} }
// Filtra os chats que nao foram encerrados
if json["finished_at"].to_string()
== "null"
{
return None;
}
// Apply keyword based filtering // Apply keyword based filtering
let filter_keywords_found = talk_histories let filter_keywords_found = talk_histories
@@ -325,10 +388,6 @@ fn main() -> anyhow::Result<()> {
let json = messages.unwrap(); let json = messages.unwrap();
let talk_histories = &json["talk_histories"]; let talk_histories = &json["talk_histories"];
// dbg!(&talk_histories);
// talk_histories.as_array().unwrap().into_iter().enumerate().for_each(|(pos, message_obj)|{println!("{}: {}", pos, message_obj["message"])});
// find the bot transfer message // find the bot transfer message
let bot_transfer_message = talk_histories let bot_transfer_message = talk_histories
.as_array() .as_array()
@@ -345,14 +404,19 @@ fn main() -> anyhow::Result<()> {
let message = message_object["message"] let message = message_object["message"]
.as_str() .as_str()
.expect("Failed to decode message as string"); .expect("Failed to decode message as string");
//let found = message.find("Atendimento transferido para a fila [NovaNet -> Atendimento -> Financeiro NVL2]"); let found = message.find("Atendimento entregue da fila de espera para o agente [NOC - ");
let found = message.find("Atendimento entregue da fila de espera para o agente [FIN - "); let found = message.find("Olá, faço parte do setor de Redes aqui da NovaNet.");
found.is_some() found.is_some()
}); });
// Find first agent message sent after the last bot message // Find first agent message sent after the last bot message
let (pos, transfer_message) = // let (pos, transfer_message) =
bot_transfer_message.expect("Failed to get the transfer bot message position"); // bot_transfer_message.expect("Failed to get the transfer bot message position");
let (pos, transfer_message) = match bot_transfer_message {
Some((pos, msg)) => (pos, msg),
None => return "".to_string(), // Retorna string vazia em vez de panic
};
let msg = talk_histories let msg = talk_histories
.as_array() .as_array()
@@ -363,14 +427,15 @@ fn main() -> anyhow::Result<()> {
.filter(|message| { .filter(|message| {
message["type"] == "out".to_string() message["type"] == "out".to_string()
// && message["user"]["name"] != "PipeBot".to_string() // && message["user"]["name"] != "PipeBot".to_string()
&& message["user"]["name"].as_str().map_or(false, |name| name.starts_with("FIN -")) && message["user"]["name"].as_str().map_or(false, |name| name.starts_with("NOC -"))
}) })
.take(1) .take(1)
.collect_vec(); .collect_vec();
// Se não encontrar mensagem do agente, retorna string vazia
if msg.is_empty() {
return "".to_string();
}
//if msg[0] {
// return None;
//}
let agent_first_message = msg[0]; let agent_first_message = msg[0];
// Calculate time difference between bot message and agent message // Calculate time difference between bot message and agent message
@@ -418,6 +483,7 @@ fn main() -> anyhow::Result<()> {
name, id, response_time, bot_transfer_date, user_response_date name, id, response_time, bot_transfer_date, user_response_date
) )
}) })
.filter(|s| !s.is_empty()) // Filtra strings vazias
.reduce(|acc, e| format!("{}\n{}", acc, e)) .reduce(|acc, e| format!("{}\n{}", acc, e))
.unwrap_or("".to_string()); .unwrap_or("".to_string());
@@ -427,7 +493,8 @@ fn main() -> anyhow::Result<()> {
let mut response_time_file = std::fs::OpenOptions::new() let mut response_time_file = std::fs::OpenOptions::new()
.write(true) .write(true)
.open(format!( .open(format!(
"./evaluations/{formatted_day_before}/response_time.csv" "./evaluations/{formatted_day_before}/response_time.csv"
//"./evaluations/{formatted_day}/response_time.csv"
)) ))
.expect("Failed to open response time file for write"); .expect("Failed to open response time file for write");
response_time_file response_time_file
@@ -506,6 +573,7 @@ fn main() -> anyhow::Result<()> {
format!( format!(
"./evaluations/{}/{} - {} - {}.csv", "./evaluations/{}/{} - {} - {}.csv",
formatted_day_before, user_name, talk_id, tracking_number formatted_day_before, user_name, talk_id, tracking_number
//formatted_day, user_name, talk_id, tracking_number
), ),
csv_response, csv_response,
) )
@@ -514,6 +582,7 @@ fn main() -> anyhow::Result<()> {
format!( format!(
"./evaluations/{}/{} - {} - {} - prompt.txt", "./evaluations/{}/{} - {} - {} - prompt.txt",
formatted_day_before, user_name, talk_id, tracking_number formatted_day_before, user_name, talk_id, tracking_number
//formatted_day, user_name, talk_id, tracking_number
), ),
format!("{prompt} \n{talk}"), format!("{prompt} \n{talk}"),
) )
@@ -527,24 +596,28 @@ fn main() -> anyhow::Result<()> {
// Compress folder into zip // Compress folder into zip
let source_dir_str = format!("./evaluations/{formatted_day_before}"); let source_dir_str = format!("./evaluations/{formatted_day_before}");
//let source_dir_str = format!("./evaluations/{formatted_day}");
let output_zip_file_str = format!("./evaluations/{formatted_day_before}.zip"); let output_zip_file_str = format!("./evaluations/{formatted_day_before}.zip");
//let output_zip_file_str = format!("./evaluations/{formatted_day}.zip");
let source_dir = std::path::Path::new(source_dir_str.as_str()); let source_dir = std::path::Path::new(source_dir_str.as_str());
let output_zip_file = std::path::Path::new(output_zip_file_str.as_str()); let output_zip_file = std::path::Path::new(output_zip_file_str.as_str());
zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file(source_dir, output_zip_file); zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file(source_dir, output_zip_file);
// Send folder to email
//let recipients = "Wilson da Conceição Oliveira <wilson.oliveira@nova.net.br>, Isadora G. Moura de Moura <isadora.moura@nova.net.br>";
let recipients = "Wilson da Conceição Oliveira <wilson.oliveira@nova.net.br>";
println!("Trying to send email... Recipients {recipients}");
send_mail_util::send_mail_util::send_email( // Send folder to email
&format!("Avaliacao atendimentos {formatted_day_before}"), let recipients = "Wilson da Conceição Oliveira <wilson.oliveira@nova.net.br>, Nicolas Borges da Silva <nicolas.borges@nova.net.br>";
&BOT_EMAIL, println!("Trying to send email... Recipients {recipients}");
&BOT_EMAIL_PASSWORD,
recipients, send_mail_util::send_mail_util::send_email(
&output_zip_file_str, &format!("Avaliacao atendimentos da fila NOC - Clientes do dia {formatted_day_before}"),
//&format!("Avaliacao atendimentos da fila Financeiro NVL2 do dia {formatted_day}"),
&BOT_EMAIL,
&BOT_EMAIL_PASSWORD,
recipients,
&output_zip_file_str,
); );
return Ok(()); return Ok(());
} }
@@ -553,10 +626,12 @@ fn get_piperun_chats_on_date(
client: &reqwest::blocking::Client, client: &reqwest::blocking::Client,
access_token: &String, access_token: &String,
formatted_day_before_at_midnight: String, formatted_day_before_at_midnight: String,
//formatted_day_start: String,
formatted_day_before_at_23_59_59: String, formatted_day_before_at_23_59_59: String,
//formatted_day_end: String,
) -> Vec<serde_json::Value> { ) -> Vec<serde_json::Value> {
let start_of_talk_code: String = "talk_start".to_string(); let start_of_talk_code: String = "talk_start".to_string();
let support_queue_id: String = "16".to_string(); let support_queue_id: String = "19".to_string();
// API V2 // API V2
let report_type = "consolidated".to_string(); let report_type = "consolidated".to_string();
@@ -573,7 +648,9 @@ fn get_piperun_chats_on_date(
("perPage", per_page.clone()), ("perPage", per_page.clone()),
("report_type", report_type.clone()), ("report_type", report_type.clone()),
("start_date", formatted_day_before_at_midnight.clone()), ("start_date", formatted_day_before_at_midnight.clone()),
("end_date", formatted_day_before_at_23_59_59.clone()), //("start_date", formatted_day_start.clone()),
("end_date", formatted_day_before_at_23_59_59.clone()),
//("end_date", formatted_day_end.clone()),
("date_range_type", start_of_talk_code.clone()), ("date_range_type", start_of_talk_code.clone()),
("queue_id[]", support_queue_id.clone()), ("queue_id[]", support_queue_id.clone()),
]); ]);
@@ -629,7 +706,9 @@ fn get_piperun_chats_on_date(
("perPage", per_page.clone()), ("perPage", per_page.clone()),
("report_type", report_type.clone()), ("report_type", report_type.clone()),
("start_date", formatted_day_before_at_midnight.clone()), ("start_date", formatted_day_before_at_midnight.clone()),
//("start_date", formatted_day_start.clone()),
("end_date", formatted_day_before_at_23_59_59.clone()), ("end_date", formatted_day_before_at_23_59_59.clone()),
//("end_date", formatted_day_end.clone()),
("date_range_type", start_of_talk_code.clone()), ("date_range_type", start_of_talk_code.clone()),
("queue_id[]", support_queue_id.clone()), ("queue_id[]", support_queue_id.clone()),
]); ]);

763
src/main.rs.save.2 Normal file
View File

@@ -0,0 +1,763 @@
use std::{any::Any, env, fmt::format, iter, time::Duration};
use chrono::{self, Timelike};
use dotenv;
use ipaddress;
use itertools::{self, Itertools};
use lettre::{self, message};
use reqwest;
use serde_json::{self, json};
use std::io::prelude::*;
pub mod send_mail_util;
pub mod zip_directory_util;
fn main() -> anyhow::Result<()> {
match dotenv::dotenv().ok() {
Some(_) => println!("Environment variables loaded from .env file"),
None => eprintln!("Failed to load .env file, using defaults"),
}
// Read environment variables
let OLLAMA_URL = env::var("OLLAMA_URL").unwrap_or("localhost".to_string());
let OLLAMA_PORT = env::var("OLLAMA_PORT")
.unwrap_or("11432".to_string())
.parse::<u16>()
.unwrap_or(11432);
let PIPERUN_API_URL = env::var("PIPERUN_API_URL").expect("PIPERUN_API_URL has not been set!");
let PIPERUN_CLIENT_ID = env::var("PIPERUN_CLIENT_ID")
.expect("PIPERUN_CLIENT_ID has not been set!")
.parse::<i32>()
.unwrap_or(0);
let PIPERUN_CLIENT_SECRET =
env::var("PIPERUN_CLIENT_SECRET").expect("PIPERUN_CLIENT_SECRET has not been set!");
let PIPERUN_BOT_USERNAME =
env::var("PIPERUN_BOT_USERNAME").expect("PIPERUN_BOT_USERNAME has not been set!");
let PIPERUN_BOT_PASSWORD =
env::var("PIPERUN_BOT_PASSWORD").expect("PIPERUN_BOT_PASSWORD has not been set!");
let OLLAMA_AI_MODEL = env::var("OLLAMA_AI_MODEL").expect("OLLAMA_AI_MODEL has not been set!");
let MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE = env::var("MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE")
.expect("MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE has not been set!")
.parse::<usize>()
.unwrap_or(10);
let MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE =
env::var("MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE")
.expect("MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE has not been set!")
.parse::<usize>()
.unwrap_or(12);
let BOT_EMAIL = env::var("BOT_EMAIL").expect("BOT_EMAIL has not been set!");
let BOT_EMAIL_PASSWORD =
env::var("BOT_EMAIL_PASSWORD").expect("BOT_EMAIL_PASSWORD has not been set!");
// Print the configuration
println!("OLLAMA_URL: {}", OLLAMA_URL);
println!("OLLAMA_PORT: {}", OLLAMA_PORT);
println!("OLLAMA_AI_MODEL: {}", OLLAMA_AI_MODEL);
println!("PIPERUN_API_URL: {}", PIPERUN_API_URL);
println!("PIPERUN_CLIENT_ID: {}", PIPERUN_CLIENT_ID);
println!("PIPERUN_CLIENT_SECRET: {}", PIPERUN_CLIENT_SECRET);
println!("PIPERUN_BOT_USERNAME: {}", PIPERUN_BOT_USERNAME);
println!("PIPERUN_BOT_PASSWORD: {}", PIPERUN_BOT_PASSWORD);
let ip_address = ipaddress::IPAddress::parse(OLLAMA_URL.to_string());
let OLLAMA_SANITIZED_IP = match ip_address {
Ok(ip) => {
if ip.is_ipv4() {
OLLAMA_URL.clone()
} else {
format!("[{}]", OLLAMA_URL.clone())
}
}
Err(e) => OLLAMA_URL.clone(),
};
// Send the authentication request
let client = reqwest::blocking::Client::new();
let auth_request = client
.post(format!("https://{}/oauth/token", PIPERUN_API_URL))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.body(
serde_json::json!({
"grant_type": "password",
"client_id": PIPERUN_CLIENT_ID,
"client_secret": PIPERUN_CLIENT_SECRET,
"username": PIPERUN_BOT_USERNAME,
"password": PIPERUN_BOT_PASSWORD,
})
.to_string(),
);
println!("Sending authentication request to Piperun API...");
println!("{:?}", auth_request);
let response = auth_request.send();
let access_token = match response {
Ok(resp) => {
if resp.status().is_success() {
let json: serde_json::Value = resp.json()?;
// println!("Authentication successful: {:?}", json);
// Extract the access token
if let Some(access_token) = json.get("access_token") {
println!("Access Token: {}", access_token);
access_token
.as_str()
.expect("Failed to get token")
.to_string()
} else {
eprintln!("Access token not found in response");
panic!("Failed to retrieve access token from Piperun API");
}
} else {
eprintln!("Authentication failed: {}", resp.status());
let json: serde_json::Value = resp.json()?;
eprintln!("Response body: {:?}", json);
panic!("Failed to authenticate with Piperun API");
}
}
Err(e) => {
eprintln!("Error sending authentication request: {}", e);
panic!("Failed to send authentication request to Piperun API");
}
};
// Get the current day in the format YYYY-MM-DD
let current_date = chrono::Local::now();
let formatted_date = current_date.format("%Y-%m-%d").to_string();
println!("Current date: {}", formatted_date);
// Get the day before the current date
let day_before = current_date
.checked_sub_signed(chrono::Duration::days(1))
.expect("Failed to get the day before");
let formatted_day_before = day_before.format("%Y-%m-%d").to_string();
println!("Day before: {}", formatted_day_before);
let day_before_at_midnight = day_before
.with_hour(0)
.unwrap()
.with_minute(0)
.unwrap()
.with_second(0)
.unwrap();
let formatted_day_before_at_midnight =
day_before_at_midnight.format("%Y-%m-%d %H:%M").to_string();
let day_before_at_23_59_59 = day_before
.with_hour(23)
.unwrap()
.with_minute(59)
.unwrap()
.with_second(59)
.unwrap();
let formatted_day_before_at_23_59_59 =
day_before_at_23_59_59.format("%Y-%m-%d %H:%M").to_string();
println!(
"Day before at midnight: {}, Day before at 23:59:59: {}",
formatted_day_before_at_midnight, formatted_day_before_at_23_59_59
);
let formatted_day_before = day_before_at_midnight.format("%Y-%m-%d").to_string();
// Create a folder named with the day_before
if !std::fs::exists(format!("./evaluations/{formatted_day_before}")).unwrap() {
std::fs::create_dir(format!("./evaluations/{formatted_day_before}"))
.expect("Failed to create directory")
}
// Create the response time folder
if !std::fs::exists(format!(
"./evaluations/{formatted_day_before}/response_time.csv"
))
.unwrap()
{
let mut response_time_file = std::fs::File::create_new(format!(
"./evaluations/{formatted_day_before}/response_time.csv"
))
.expect("Failed to response_time.csv");
}
/*
// --- NOVO: processar argumento de linha de comando para data específica ---
use std::env;
use chrono::NaiveDate;
let args: Vec<String> = env::args().collect();
let target_day = if args.len() > 1 {
// Se um argumento foi passado, interpretar como YYYY-MM-DD
let naive_date = NaiveDate::parse_from_str(&args[1], "%Y-%m-%d")
.expect("Formato de data inválido. Use YYYY-MM-DD");
// Converter para DateTime com hora 00:00:00 do fuso local
naive_date.and_hms_opt(0, 0, 0).unwrap()
} else {
// Comportamento padrão: dia anterior ao atual
(chrono::Local::now() - chrono::Duration::days(1)).naive_local()
};
println!("Processando o dia: {}", target_day.format("%Y-%m-%d"));
// Definir início e fim do dia (para consultas na API)
let day_start = target_day; // já 00:00
let day_end = target_day
.with_hour(23).unwrap()
.with_minute(59).unwrap()
.with_second(59).unwrap();
let formatted_day = target_day.format("%Y-%m-%d").to_string();
let formatted_day_start = day_start.format("%Y-%m-%d %H:%M").to_string();
let formatted_day_end = day_end.format("%Y-%m-%d %H:%M").to_string();
println!("Início do dia: {}", formatted_day_start);
println!("Fim do dia: {}", formatted_day_end);
// Criar pasta com o nome do dia processado
if !std::fs::exists(format!("./evaluations/{formatted_day}")).unwrap() {
std::fs::create_dir(format!("./evaluations/{formatted_day}"))
.expect("Failed to create directory");
}
// Criar arquivo response_time.csv se não existir
if !std::fs::exists(format!("./evaluations/{formatted_day}/response_time.csv")).unwrap() {
let _ = std::fs::File::create_new(format!(
"./evaluations/{formatted_day}/response_time.csv"
))
.expect("Failed to create response_time.csv");
}
*/
// Read system prompt
let prompt = std::fs::read_to_string("PROMPT.txt").unwrap();
let filter_file_contents = std::fs::read_to_string("FILTER.txt").unwrap_or(String::new());
let filter_keywords = filter_file_contents
.split("\n")
.filter(|keyword| !keyword.is_empty())
.collect::<Vec<&str>>();
let talks_array = get_piperun_chats_on_date(
&PIPERUN_API_URL,
&client,
&access_token,
formatted_day_before_at_midnight,
//formatted_day_start,
formatted_day_before_at_23_59_59,
//formatted_day_end,
);
println!("Number of consolidated talks: {}", talks_array.len());
let talk_ids = talks_array
.iter()
.cloned()
.map(|value| {
serde_json::from_value::<serde_json::Value>(value).expect("Failed to parse the JSON")
["id"]
.clone()
.to_string()
})
.collect::<Vec<String>>();
println!("IDS {:?}", talk_ids);
// Gather messages and apply filtering
let filtered_chats = talk_ids
.iter()
.cloned()
.map(|talk_id| {
let talk_id_get_request = client
.get(format!("https://{}/api/talk_histories", PIPERUN_API_URL))
.bearer_auth(&access_token)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.query(&[
("talk_id", talk_id),
("type", "a".to_string()),
("only_view", "1".to_string()),
]);
let talk_id_get_result = talk_id_get_request.send();
return talk_id_get_result;
})
.filter_map_ok(|result| {
let json = result
.json::<serde_json::Value>()
.expect("Failed to deserialize response to JSON")
.to_owned();
let talk_histories = &json["talk_histories"];
let data = &talk_histories["data"];
// Filter chats that have very few messages
let talk_lenght = talk_histories
.as_array()
.expect("Wrong message type received from talk histories")
.len();
if talk_lenght < MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE {
return None;
}
// Filter chats that have less that specified ammount of talks with support agent form the last queue transfer
let found = talk_histories
.as_array()
.expect("Wrong message type received from talk histories.")
.into_iter()
.enumerate()
.find(|(pos, message_object)| {
let message = message_object["message"]
.as_str()
.expect("Failed to decode message as string.");
let template_id = message_object["is_template"]
.as_bool()
.unwrap_or(true);
// println!("Conteudo do template_id da mensagem {}: {:?}", pos, template_id);
let found = message.find(
"Atendimento transferido para a fila [NovaNet -> Atendimento -> NOC - Clientes]",
);
found.is_some() || template_id
});
match found {
None => {
println!("Descartado - Não encontrou a transferencia ou o template no chat. 1");
return None;
}
Some(pos) => {
let pos_found = pos.0;
if pos_found < MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE {
println!("Descartado - Chat não tem o número minimo de mensagens com agente.");
return None;
}
}
};
// Filter Bot finished chats
if json["agent"]["user"]["name"]
.as_str()
.unwrap_or("unknown_user")
== "PipeBot"
{
println!("Descartado - Chat finalizado pelo pipebot.");
return None;
}
// Filtra os chats que nao foram encerrados
if json["finished_at"].to_string()
== "null"
{
println!("Descartado - Chat não finalizado.");
return None;
}
// Apply keyword based filtering
let filter_keywords_found = talk_histories
.as_array()
.expect("Wrong message type received from talk histories")
.into_iter()
.any(|message_object| {
let message = message_object["message"]
.as_str()
.expect("Failed to decode message as string");
let found1 = filter_keywords.iter().any(|keyword| {
message
.to_uppercase()
.find(&keyword.to_uppercase())
.is_some()
});
/*
let found2 = message_object["is_template"]
.as_bool()
.unwrap_or(true);
let found = found1 || found2;
*/
found1
});
if filter_keywords_found {
println!("Descartado - Encontrou uma frase do filtro no chat.");
return None;
}
return Some(json);
});
// Calculate the response time in seconds
let response_time = filtered_chats
.clone()
.map(|messages| {
let json = messages.unwrap();
let talk_histories = &json["talk_histories"];
// find the bot transfer message
let bot_transfer_message = talk_histories
.as_array()
.expect("Wrong message type received from talk histories")
.into_iter()
.enumerate()
.filter(|(pos, message_object)| {
let user_name = message_object["user"]["name"]
.as_str()
.expect("Failed to decode message as string");
user_name == "PipeBot".to_string()
})
.find(|(pos, message_object)| {
let message = message_object["message"]
.as_str()
.expect("Failed to decode message as string");
let found = message.find("Atendimento entregue da fila de espera para o agente [NOC");
let is_template = message_object["is_template"]
.as_bool()
.unwrap_or(true);
found.is_some() || is_template
});
// Find first agent message sent after the last bot message
// let (pos, transfer_message) =
// bot_transfer_message.expect("Failed to get the transfer bot message position");
let (pos, transfer_message) = match bot_transfer_message {
Some((pos, msg)) => (pos, msg),
None => return "".to_string(), // Retorna string vazia em vez de panic
};
let msg = talk_histories
.as_array()
.expect("Wrong message type received from talk histories")
.into_iter()
.take(pos)
.rev()
.filter(|message| {
message["type"] == "out".to_string()
// && message["user"]["name"] != "PipeBot".to_string()
&& message["user"]["name"].as_str().map_or(false, |name| name.starts_with("NOC -"))
})
.take(1)
.collect_vec();
// Se não encontrar mensagem do agente, retorna string vazia
if msg.is_empty() {
return "".to_string();
}
let agent_first_message = msg[0];
// Calculate time difference between bot message and agent message
let date_user_message_sent = agent_first_message["sent_at"].as_str().unwrap();
let format = "%Y-%m-%d %H:%M:%S";
let date_user_message_sent_parsed =
match chrono::NaiveDateTime::parse_from_str(date_user_message_sent, format) {
Ok(dt) => dt,
Err(e) => {
println!("Error parsing DateTime: {}", e);
panic!("Failed parsing date")
}
};
let date_transfer_message_sent_parsed = match chrono::NaiveDateTime::parse_from_str(
transfer_message["sent_at"].as_str().unwrap(),
format,
) {
Ok(dt) => dt,
Err(e) => {
println!("Error parsing DateTime: {}", e);
panic!("Failed parsing date")
}
};
let response_time = (date_user_message_sent_parsed - date_transfer_message_sent_parsed)
.as_seconds_f32();
let name = agent_first_message["user"]["name"]
.as_str()
.unwrap()
.to_owned();
let id = json["tracking_number"].as_str().unwrap_or("").to_owned();
let bot_transfer_date = date_transfer_message_sent_parsed.to_owned();
let user_response_date = date_user_message_sent.to_owned();
println!(
"response_time: {}s",
(date_user_message_sent_parsed - date_transfer_message_sent_parsed)
.as_seconds_f32()
);
format!(
"{};{};{};{};{}",
name, id, response_time, bot_transfer_date, user_response_date
)
})
.filter(|s| !s.is_empty()) // Filtra strings vazias
.reduce(|acc, e| format!("{}\n{}", acc, e))
.unwrap_or("".to_string());
// return Ok(());
// Open file and write to it
let header = "NOME;ID_TALK;TEMPO DE RESPOSTA;TRANFERENCIA PELO BOT;PRIMEIRA RESPOSTA DO AGENTE";
let mut response_time_file = std::fs::OpenOptions::new()
.write(true)
.open(format!(
"./evaluations/{formatted_day_before}/response_time.csv"
//"./evaluations/{formatted_day}/response_time.csv"
))
.expect("Failed to open response time file for write");
response_time_file
.write_all(format!("{header}\n{response_time}").as_bytes())
.expect("Failed to write header to file");
filtered_chats.clone().skip(0).for_each(|result| {
let json = result.unwrap();
let talk_histories = &json["talk_histories"];
let data = &talk_histories["data"];
let talk = talk_histories
.as_array()
.expect("Wrong message type received from talk histories")
.iter()
.rev()
.map(|message_object| {
let new_json_filtered = format!(
"{{
message: {},
sent_at: {},
type: {},
user_name: {}
}}",
message_object["message"],
message_object["sent_at"],
message_object["type"],
message_object["user"]["name"]
);
// println!("{}", new_json_filtered);
new_json_filtered
})
.reduce(|acc, e| format!("{acc}\n{e}"))
.expect("Error extracting talk");
println!("{prompt}\n {talk}");
let ollama_api_request = client
.post(format!(
"http://{OLLAMA_SANITIZED_IP}:{OLLAMA_PORT}/api/generate"
))
.body(
serde_json::json!({
"model": OLLAMA_AI_MODEL,
"prompt": format!("{prompt} \n{talk}"),
// "options": serde_json::json!({"temperature": 0.1}),
"stream": false,
})
.to_string(),
);
let result = ollama_api_request.timeout(Duration::from_secs(3600)).send();
match result {
Ok(response) => {
println!("Response: {:?}", response);
let response_json = response
.json::<serde_json::Value>()
.expect("Failed to deserialize response to JSON");
println!("{}", response_json);
let ai_response = response_json["response"]
.as_str()
.expect("Failed to get AI response as string");
println!("AI Response: {}", ai_response);
let csv_response = ai_response.replace("```csv\n", "").replace("```", "");
// Save the CSV response to a file
let user_name = &json["agent"]["user"]["name"]
.as_str()
.unwrap_or("unknown_user");
let talk_id = &json["id"].as_u64().unwrap_or(0);
let tracking_number = &json["tracking_number"].as_str().unwrap_or("");
std::fs::write(
format!(
"./evaluations/{}/{} - {} - {}.csv",
formatted_day_before, user_name, talk_id, tracking_number
//formatted_day, user_name, talk_id, tracking_number
),
csv_response,
)
.expect("Unable to write file");
std::fs::write(
format!(
"./evaluations/{}/{} - {} - {} - prompt.txt",
formatted_day_before, user_name, talk_id, tracking_number
//formatted_day, user_name, talk_id, tracking_number
),
format!("{prompt} \n{talk}"),
)
.expect("Unable to write file");
}
Err(error) => {
println!("Error {error}");
}
};
});
// Compress folder into zip
let source_dir_str = format!("./evaluations/{formatted_day_before}");
//let source_dir_str = format!("./evaluations/{formatted_day}");
let output_zip_file_str = format!("./evaluations/{formatted_day_before}.zip");
//let output_zip_file_str = format!("./evaluations/{formatted_day}.zip");
let source_dir = std::path::Path::new(source_dir_str.as_str());
let output_zip_file = std::path::Path::new(output_zip_file_str.as_str());
zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file(source_dir, output_zip_file);
// Send folder to email
let recipients = "Nicolas Borges da Silva <nicolas.borges@nova.net.br>, wilson@nova.net.br";
println!("Trying to send email... Recipients {recipients}");
send_mail_util::send_mail_util::send_email(
&format!("Avaliacao atendimentos da fila NOC - Clientes do dia {formatted_day_before}"),
//&format!("Avaliacao atendimentos da fila Financeiro NVL2 do dia {formatted_day}"),
&BOT_EMAIL,
&BOT_EMAIL_PASSWORD,
recipients,
&output_zip_file_str,
);
return Ok(());
}
fn get_piperun_chats_on_date(
PIPERUN_API_URL: &String,
client: &reqwest::blocking::Client,
access_token: &String,
formatted_day_before_at_midnight: String,
//formatted_day_start: String,
formatted_day_before_at_23_59_59: String,
//formatted_day_end: String,
) -> Vec<serde_json::Value> {
let start_of_talk_code: String = "talk_start".to_string();
let support_queue_id: String = "19".to_string();
// API V2
let report_type = "consolidated".to_string();
let page = "1".to_string();
let per_page = "15".to_string();
let talks_request = client
.get(format!("https://{}/api/v2/reports/talks", PIPERUN_API_URL))
.bearer_auth(access_token)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.query(&[
("page", page.clone()),
("perPage", per_page.clone()),
("report_type", report_type.clone()),
("start_date", formatted_day_before_at_midnight.clone()),
//("start_date", formatted_day_start.clone()),
("end_date", formatted_day_before_at_23_59_59.clone()),
//("end_date", formatted_day_end.clone()),
("date_range_type", start_of_talk_code.clone()),
("queue_id[]", support_queue_id.clone()),
]);
println!("Sending request for consolidated talks... {talks_request:?}");
let talks_response = talks_request.send();
let json_response = match talks_response {
Ok(resp) => {
if resp.status().is_success() {
let json: serde_json::Value = resp.json().unwrap();
json
} else {
eprintln!("Failed to get consolidated talks: {}", resp.status());
let json: serde_json::Value = resp.json().unwrap();
eprintln!("Response body: {:?}", json);
panic!("Failed to retrieve consolidated talks from Piperun API");
}
}
Err(e) => {
eprintln!("Error: {e}");
panic!("Failed to send the request for talks to PipeRUN API");
}
};
let mut aggregated_talks = json_response["data"]
.as_array()
.expect("Failed to parse messages as array")
.to_owned();
let current_page = json_response["current_page"]
.as_i64()
.expect("Failed to obtain current page number");
let last_page = json_response["last_page"]
.as_i64()
.expect("Failed to obtain current page number");
if current_page == last_page {
return aggregated_talks;
}
let mut all_other_messages = (current_page..last_page)
.into_iter()
.map(|page| {
let page_to_request = page + 1;
let talks_request = client
.get(format!("https://{}/api/v2/reports/talks", PIPERUN_API_URL))
.bearer_auth(access_token)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.query(&[
("page", page_to_request.to_string()),
("perPage", per_page.clone()),
("report_type", report_type.clone()),
("start_date", formatted_day_before_at_midnight.clone()),
//("start_date", formatted_day_start.clone()),
("end_date", formatted_day_before_at_23_59_59.clone()),
//("end_date", formatted_day_end.clone()),
("date_range_type", start_of_talk_code.clone()),
("queue_id[]", support_queue_id.clone()),
]);
println!("Sending request for consolidated talks... {talks_request:?}");
let talks_response = talks_request.send();
let json_response = match talks_response {
Ok(resp) => {
if resp.status().is_success() {
let json: serde_json::Value = resp.json().unwrap();
json
} else {
eprintln!("Failed to get consolidated talks: {}", resp.status());
let json: serde_json::Value = resp.json().unwrap();
eprintln!("Response body: {:?}", json);
panic!("Failed to retrieve consolidated talks from Piperun API");
}
}
Err(e) => {
eprintln!("Error: {e}");
panic!("Failed to send the request for talks to PipeRUN API");
}
};
let aggregated_talks = json_response["data"]
.as_array()
.expect("Failed to parse messages as array")
.to_owned();
return aggregated_talks;
})
.reduce(|mut this, mut acc| {
acc.append(&mut this);
acc
})
.expect("Failed to concatenate all vectors of messages");
aggregated_talks.append(&mut all_other_messages);
aggregated_talks
}