Compare commits
34 Commits
138068c2e5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d48f2b86aa | |||
| 2975dad9cd | |||
| ca227ee45f | |||
| 1ba0510a3e | |||
| 4d82b51796 | |||
| 49ddb9a238 | |||
| 7d2ae90eb3 | |||
| 6037643a28 | |||
| de9dd7a49a | |||
| 47901d4a9f | |||
| 90b30b0d51 | |||
| 14bf25e83a | |||
| 5477f91bb9 | |||
| 5bb0ad8b1a | |||
| ca406b4601 | |||
| 197642727c | |||
| 99a4c81a58 | |||
| 178219c4df | |||
| 7fb6021e64 | |||
| 1edb92af50 | |||
| 6b12637d0a | |||
| 829436b570 | |||
| 28c9d810e9 | |||
| cd44d58f69 | |||
| 9fdaf35e28 | |||
| ff2cee12ae | |||
| 3af0eab693 | |||
| 94218b4477 | |||
| a968b80e0f | |||
| 227ca2af5b | |||
| 4db874bcf6 | |||
| 8d5bec6f06 | |||
| b523e9b7e1 | |||
| 05bf884aa1 |
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/target
|
||||||
|
src/.env
|
||||||
|
.env
|
||||||
|
log/
|
||||||
|
.zip
|
||||||
|
evaluations
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,7 +3,9 @@ src/.env
|
|||||||
.env
|
.env
|
||||||
log/
|
log/
|
||||||
.zip
|
.zip
|
||||||
|
.vscode
|
||||||
evaluations
|
evaluations
|
||||||
|
groupped
|
||||||
# Added by cargo
|
# Added by cargo
|
||||||
#
|
#
|
||||||
# already existing elements were commented out
|
# already existing elements were commented out
|
||||||
|
|||||||
18
.vscode/launch.json
vendored
18
.vscode/launch.json
vendored
@@ -41,6 +41,24 @@
|
|||||||
},
|
},
|
||||||
"args": [],
|
"args": [],
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug executable 'groupper_report'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"--bin=groupped_repport",
|
||||||
|
"--package=piperun-bot"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "groupped_repport",
|
||||||
|
"kind": "bin"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"args": ["cargo", "run", "--bin=groupped_repport"],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
1795
Cargo.lock
generated
1795
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
20
Cargo.toml
@@ -3,6 +3,18 @@ name = "piperun-bot"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "groupped-repport-weekly"
|
||||||
|
path = "src/groupped_repport_weekly.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "groupped-repport-monthly"
|
||||||
|
path = "src/groupped_repport_monthly.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "piperun-bot"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
http = {version = "1.3.1"}
|
http = {version = "1.3.1"}
|
||||||
dotenv = {version = "0.15.0"}
|
dotenv = {version = "0.15.0"}
|
||||||
@@ -11,7 +23,11 @@ reqwest = { version = "0.12.23", features = ["json", "cookies", "blocking"] }
|
|||||||
chrono = { version = "0.4.42" }
|
chrono = { version = "0.4.42" }
|
||||||
itertools = {version = "0.14.0"}
|
itertools = {version = "0.14.0"}
|
||||||
ipaddress = {version = "0.1.3"}
|
ipaddress = {version = "0.1.3"}
|
||||||
zip = { version = "5.1.1"}
|
zip = { version = "6.0.0"}
|
||||||
walkdir = { version = "2.5.0"}
|
walkdir = { version = "2.5.0"}
|
||||||
lettre = {version = "0.11.18", features = ["builder"]}
|
lettre = {version = "0.11.19", features = ["builder"]}
|
||||||
anyhow = { version = "1.0.100"}
|
anyhow = { version = "1.0.100"}
|
||||||
|
polars = { version = "0.52.0"}
|
||||||
|
serde = { version = "1.0.228" }
|
||||||
|
csv = {version = "1.4.0"}
|
||||||
|
regex = { version = "1.12.2" }
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
FROM docker.io/rust:1.90 AS build
|
FROM docker.io/rust:1.90 AS build
|
||||||
COPY . /app
|
COPY . /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
RUN git checkout main
|
||||||
RUN cargo build --release
|
RUN cargo build --release
|
||||||
|
|
||||||
# FROM docker.io/alpine:3.22.1 AS production
|
# FROM docker.io/alpine:3.22.1 AS production
|
||||||
|
|||||||
@@ -11,3 +11,8 @@ Transbordo automático para a fila [NovaNet -> Atendimento -> Suporte], pois nã
|
|||||||
comprovante de pagamento
|
comprovante de pagamento
|
||||||
restrição de velocidade
|
restrição de velocidade
|
||||||
*Basta desligar e ligar teus equipamentos da tomada para a conexão normalizar
|
*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
|
||||||
24
PROMPT.txt
24
PROMPT.txt
@@ -4,23 +4,25 @@ SEGUINDO OS CRITÉRIOS QUE VÃO ESTAR ABAIXO, AVALIE ATENDIMENTOS DE SUPORTE PRE
|
|||||||
|
|
||||||
02 (CONFIRMAÇÃO DE E-MAIL) - O AGENTE DEVE SOLICITAR OU CONFIRMAR O E-MAIL DO CLIENTE DURANTE A CONVERSA.
|
02 (CONFIRMAÇÃO DE E-MAIL) - O AGENTE DEVE SOLICITAR OU CONFIRMAR O E-MAIL DO CLIENTE DURANTE A CONVERSA.
|
||||||
|
|
||||||
03 (PROTOCOLO) - O AGENTE DEVE INFORMAR O PROTOCOLO DE ATENDIMENTO DURANTE A CONVERSA.
|
03 (CONFIRMAÇÃO DE TELEFONE) - O AGENTE DEVE SOLICITAR OU CONFIRMAR O TELEFONE DO TITULAR DURANTE A CONVERSA.
|
||||||
|
|
||||||
04 (USO DO PORTUGUÊS) - O AGENTE DEVE UTILIZAR CORRETAMENTE O PORTUGUÊS..
|
04 (PROTOCOLO) - O AGENTE DEVE INFORMAR O PROTOCOLO DE ATENDIMENTO DURANTE A CONVERSA.
|
||||||
|
|
||||||
|
05 (USO DO PORTUGUÊS) - O AGENTE DEVE UTILIZAR CORRETAMENTE O PORTUGUÊS..
|
||||||
NÃO ESTÁ ERRADO SE O AGENTE UTILIZAR 'TU', 'TEU', 'TUA'. SOMOS UMA EMPRESA REGIONAL.
|
NÃO ESTÁ ERRADO SE O AGENTE UTILIZAR 'TU', 'TEU', 'TUA'. SOMOS UMA EMPRESA REGIONAL.
|
||||||
|
|
||||||
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, COMO POR EXEMPLO "COMO POSSO TE AJUDAR?", "PERFEITO", "POR GENTILEZA", "OBRIGADO PELAS INFORMAÇÕES", "VOU VERIFICAR, AGUARDE UM MOMENTO POR GENTILEZA", "DESCULPE, MAS NÃO TE COMPREENDI", "PODERIA EXPLICAR DE NOVO?".
|
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, COMO POR EXEMPLO "COMO POSSO TE AJUDAR?", "PERFEITO", "POR GENTILEZA", "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.
|
07 (DISPONIBILIDADE) - O AGENTE DEVE SE COLOCAR À DISPOSIÇÃO DO CLIENTE, DEIXANDO CLARO QUE A EMPRESA ESTÁ SEMPRE DISPONÍVEL PARA AJUDAR.
|
||||||
|
|
||||||
07 (CONHECIMENTO TÉCNICO) - O AGENTE DEVE INFORMAR DE FORMA CLARA SE IDENTIFICOU ALGUM PROBLEMA NA CONEXÃO OU SE ESTÁ TUDO NORMAL. COMO POR EXEMPLO:
|
08 (CONHECIMENTO TÉCNICO) - O AGENTE DEVE INFORMAR DE FORMA CLARA SE IDENTIFICOU ALGUM PROBLEMA NA CONEXÃO OU SE ESTÁ TUDO NORMAL. COMO POR EXEMPLO:
|
||||||
"VERIFICANDO ATRAVÉS DOS EQUIPAMENTOS QUE ESTÃO NO LOCAL, NÃO ENCONTREI NENHUM PROBLEMA QUE PUDESSE OCASIONAR O MAU FUNCIONAMENTO DA TUA CONEXÃO."
|
"VERIFICANDO ATRAVÉS DOS EQUIPAMENTOS QUE ESTÃO NO LOCAL, NÃO ENCONTREI NENHUM PROBLEMA QUE PUDESSE OCASIONAR O MAU FUNCIONAMENTO DA TUA CONEXÃO."
|
||||||
"ATÉ A TUA CONEXÃO ESTA TUDO CERTO, OS EQUIPAMENTOS ESTÃO PADRONIZADOS E O VELOCIDADE ESTÁ SENDO ENTREGUE."
|
"ATÉ A TUA CONEXÃO ESTA TUDO CERTO, OS EQUIPAMENTOS ESTÃO PADRONIZADOS E O VELOCIDADE ESTÁ SENDO ENTREGUE."
|
||||||
"DE FORMA REMOTA, IDENTIFIQUEI QUE O EQUIPAMENTO DE FIBRA, O MENOR SEM ANTENAS, NÃO ESTÁ RECEBENDO ENERGIA."
|
"DE FORMA REMOTA, IDENTIFIQUEI QUE O EQUIPAMENTO DE FIBRA, O MENOR SEM ANTENAS, NÃO ESTÁ RECEBENDO ENERGIA."
|
||||||
"DE FORMA REMOTA EU IDENTIFIQUEI QUE O EQUIPAMENTO DA FIBRA NÃO ESTÁ RECEBENDO ENERGIA"
|
"DE FORMA REMOTA EU IDENTIFIQUEI QUE O EQUIPAMENTO DA FIBRA NÃO ESTÁ RECEBENDO ENERGIA"
|
||||||
"ATÉ TUA ANTENA ESTA TUDO CERTO, O SINAL QUE CHEGA ATÉ ELA É BOM, ESTÁ DENTRO DO PADRÃO"
|
"ATÉ TUA ANTENA ESTA TUDO CERTO, O SINAL QUE CHEGA ATÉ ELA É BOM, ESTÁ DENTRO DO PADRÃO"
|
||||||
|
|
||||||
08 (DIDATISMO) – O AGENTE DEVE SER DIDÁTICO, EXPLICANDO O MOTIVO DE CADA SOLICITAÇÃO OU VERIFICAÇÃO.
|
09 (DIDATISMO) – O AGENTE DEVE SER DIDÁTICO, EXPLICANDO O MOTIVO DE CADA SOLICITAÇÃO OU VERIFICAÇÃO.
|
||||||
A EXPLICAÇÃO PODE VIR ANTES OU DEPOIS DA SOLICITAÇÃO.
|
A EXPLICAÇÃO PODE VIR ANTES OU DEPOIS DA SOLICITAÇÃO.
|
||||||
NÃO É NECESSÁRIO USAR AS MESMAS PALAVRAS DOS EXEMPLOS, BASTA DEIXAR CLARO O PORQUÊ.
|
NÃO É NECESSÁRIO USAR AS MESMAS PALAVRAS DOS EXEMPLOS, BASTA DEIXAR CLARO O PORQUÊ.
|
||||||
✅ EXEMPLOS CORRETOS:
|
✅ EXEMPLOS CORRETOS:
|
||||||
@@ -37,9 +39,12 @@ NÃO É NECESSÁRIO USAR AS MESMAS PALAVRAS DOS EXEMPLOS, BASTA DEIXAR CLARO O P
|
|||||||
❌ NÃO CONSIDERAR DIDÁTICO:
|
❌ NÃO CONSIDERAR DIDÁTICO:
|
||||||
QUANDO O AGENTE PEDE ALGO SEM DIZER O MOTIVO.
|
QUANDO O AGENTE PEDE ALGO SEM DIZER O MOTIVO.
|
||||||
FRASES VAGAS COMO: "RECOLOQUE OS CABOS", "DESLIGA E LIGA DE NOVO", "PODE VERIFICAR SE VOLTOU?"
|
FRASES VAGAS COMO: "RECOLOQUE OS CABOS", "DESLIGA E LIGA DE NOVO", "PODE VERIFICAR SE VOLTOU?"
|
||||||
|
NÃO CONSIDERAR A SOLICITAÇÃO DE E-MAIL COMO ALGO DIDATICO.
|
||||||
NÃO CONSIDERAR A CONFIRMAÇÃO DE E-MAIL COMO ALGO DIDATICO.
|
NÃO CONSIDERAR A CONFIRMAÇÃO DE E-MAIL COMO ALGO DIDATICO.
|
||||||
|
NÃO CONSIDERAR A SOLICITAÇÃO DE TELEFONE COMO ALGO DIDATICO.
|
||||||
|
NÃO CONSIDERAR A CONFIRMAÇÃO DE TELEFONE COMO ALGO DIDATICO.
|
||||||
|
|
||||||
09 (ECLARECIMENTO) - DURANTE A CONVERSA, O AGENTE DEVE FECHAR UM DIAGNOSTICO.
|
10 (ECLARECIMENTO) - DURANTE A CONVERSA, O AGENTE DEVE FECHAR UM DIAGNOSTICO.
|
||||||
NÃO É NECESSÁRIO USAR AS MESMAS PALAVRAS DOS EXEMPLOS, BASTA DEIXAR O CLIENTE CIENTE DA CONCLUSÃO DO ATENDIMENTO.
|
NÃO É NECESSÁRIO USAR AS MESMAS PALAVRAS DOS EXEMPLOS, BASTA DEIXAR O CLIENTE CIENTE DA CONCLUSÃO DO ATENDIMENTO.
|
||||||
✅ EXEMPLOS CORRETOS:
|
✅ EXEMPLOS CORRETOS:
|
||||||
"COMO O NOME DA TUA REDE NÃO APARECE, ISSO INDICA QUE O ROTEADOR FOI RESETADO E VOLTOU ÀS CONFIGURAÇÕES DE FÁBRICA. SERÁ NECESSÁRIO ABRIR UMA ORDEM DE SERVIÇO PARA RECONFIGURAR O EQUIPAMENTO"
|
"COMO O NOME DA TUA REDE NÃO APARECE, ISSO INDICA QUE O ROTEADOR FOI RESETADO E VOLTOU ÀS CONFIGURAÇÕES DE FÁBRICA. SERÁ NECESSÁRIO ABRIR UMA ORDEM DE SERVIÇO PARA RECONFIGURAR O EQUIPAMENTO"
|
||||||
@@ -49,11 +54,6 @@ NÃO É NECESSÁRIO USAR AS MESMAS PALAVRAS DOS EXEMPLOS, BASTA DEIXAR O CLIENTE
|
|||||||
"SE A CONEXÃO ESTÁ SENDO USADA POR MUITAS PESSOAS AO MESMO TEMPO E ISSO FAZER COM QUE O CONSUMO SUPERE A VELOCIDADE CONTRATADA, A CONEXÃO PODERÁ APRESENTAR LENTIDÃO"
|
"SE A CONEXÃO ESTÁ SENDO USADA POR MUITAS PESSOAS AO MESMO TEMPO E ISSO FAZER COM QUE O CONSUMO SUPERE A VELOCIDADE CONTRATADA, A CONEXÃO PODERÁ APRESENTAR LENTIDÃO"
|
||||||
"EM ESPECÍFICO, QUANDO SE TRATA DE DISPOSITIVOS BOX, RECEPTORES DE CANAIS E APLICATIVOS IPTV, INFELIZMENTE NÃO CONSEGUIMOS GARANTIR ESTABILIDADE PARA ESSES TIPOS DE SERVIÇOS, JÁ QUE ELES NÃO DEPENDEM APENAS DE UMA BOA QUALIDADE DE INTERNET PARA FUNCIONAR CORRETAMENTE. VOU TE INDICAR A VERIFICAR A RESPEITO COM A PESSOA QUE TE FORNECEU O SERVIÇO, SE POSSÍVEL"
|
"EM ESPECÍFICO, QUANDO SE TRATA DE DISPOSITIVOS BOX, RECEPTORES DE CANAIS E APLICATIVOS IPTV, INFELIZMENTE NÃO CONSEGUIMOS GARANTIR ESTABILIDADE PARA ESSES TIPOS DE SERVIÇOS, JÁ QUE ELES NÃO DEPENDEM APENAS DE UMA BOA QUALIDADE DE INTERNET PARA FUNCIONAR CORRETAMENTE. VOU TE INDICAR A VERIFICAR A RESPEITO COM A PESSOA QUE TE FORNECEU O SERVIÇO, SE POSSÍVEL"
|
||||||
"O IDEAL SERIA CONECTAR TUA TV AO ROTEADOR POR UM CABO DE REDE, POIS ASSIM, O SINAL SERÁ TRANSMITIDO DIRETO, SEM SOFRER INTERFERÊNCIA.
|
"O IDEAL SERIA CONECTAR TUA TV AO ROTEADOR POR UM CABO DE REDE, POIS ASSIM, O SINAL SERÁ TRANSMITIDO DIRETO, SEM SOFRER INTERFERÊNCIA.
|
||||||
|
|
||||||
10 (TEMPO DE ESPERA) – SEMPRE QUE O CLIENTE ENVIAR UMA MENSAGEM, O AGENTE DEVE DAR ALGUM RETORNO EM ATÉ 5 MINUTOS.
|
|
||||||
O RETORNO PODE SER UMA RESPOSTA, UMA SOLICITAÇÃO OU UM AVISO DE QUE ESTÁ VERIFICANDO.
|
|
||||||
CASO O AGENTE DEMORE MAIS DE 5 MINUTOS PARA RESPONDER EM 3 OU MAIS MOMENTOS DIFERENTES, ELE PERDE ESTE CRITÉRIO.
|
|
||||||
É PERMITIDO ULTRAPASSAR OS 5 MINUTOS ATÉ 2 VEZES DURANTE A CONVERSA.
|
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
As mensagens do chat estão estruturadas no formato JSON com os campos:
|
As mensagens do chat estão estruturadas no formato JSON com os campos:
|
||||||
|
|||||||
111
PROMPT_DATA_SANITIZATION.txt
Normal file
111
PROMPT_DATA_SANITIZATION.txt
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
Abaixo está a avaliação de um atendimento que foi realizado. Eu preciso que a formatação fique consistente e padronizada.
|
||||||
|
Padronize o arquivo CSV da seguinte forma, deixando apenas as colunas listadas.
|
||||||
|
Título: CATEGORIA;PONTOS
|
||||||
|
A sua resposta deve ser apenas o CSV com a formatação corrigida, nada mais deve ser incluído na sua resposta, nem mesmo notas sobre a resposta.
|
||||||
|
Se não for possível padronizar o arquivo de entrada de acordo com as instruções fornecidas a resposta deve ser o CSV com o campo de pontuação vazio.
|
||||||
|
As categorias são: APRESENTAÇÃO, CONFIRMAÇÃO DE E-MAIL, CONFIRMAÇÃO DE TELEFONE, PROTOCOLO, USO DO PORTUGUÊS, PACIÊNCIA E EDUCAÇÃO, DISPONIBILIDADE, CONHECIMENTO TÉCNICO, DIDATISMO
|
||||||
|
A coluna pontos deve ter apenas os valores 0, 1 ou vazio, se no arquivo de entrada não houver a avaliação da categoria, a columa de pontos deve ser vazia.
|
||||||
|
|
||||||
|
Aqui estão alguns exemplos de formatação de como deve ser a sua resposta:
|
||||||
|
Exemplo 01:
|
||||||
|
Dado o seguinte arquivo de entrada:
|
||||||
|
APRESENTAÇÃO;1;O agente se apresentou ao cliente.;Boa noite, me chamo Ander.;Certo, um bom final de semana! 😊
|
||||||
|
CONFIRMAÇÃO DE E-MAIL;1;O agente pediu confirmação do e-mail.;Para manter o cadastro atualizado, poderia me confirmar se o e-mail continua sendo janainads.sls@gmail.com e se o telefone do titular do cadastro permanece (53) 98446-2208?;Obrigado pela confirmação.
|
||||||
|
CONFIRMAÇÃO DE TELEFONE;1;O agente pediu confirmação do telefone.;Para manter o cadastro atualizado, poderia me confirmar se o e-mail continua sendo janainads.sls@gmail.com e se o telefone do titular do cadastro permanece (53) 98446-2208?;Obrigado pela confirmação.
|
||||||
|
PROTOCOLO;1;O agente informou o protocolo.;Aqui está o protocolo do teu atendimento: 2510.3624;Boa noite, me chamo Ander.
|
||||||
|
USO DO PORTUGUÊS;1;O agente utilizou português correto.;Aqui está o protocolo do teu atendimento: 2510.3624;Para manter o cadastro atualizado, poderia me confirmar se o e-mail continua sendo janainads.sls@gmail.com e se o telefone do titular do cadastro permanece (53) 98446-2208?
|
||||||
|
PACIÊNCIA E EDUCAÇÃO;1;O agente foi paciente e educado.;Obrigado pela confirmação.;Certo, um bom final de semana! 😊
|
||||||
|
DISPONIBILIDADE;1;O agente demonstrou disponibilidade.;Caso tenha alguma dúvida, nos contate. A equipe da NovaNet está sempre aqui para te ajudar.😊;Certo, um bom final de semana! 😊
|
||||||
|
CONHECIMENTO TÉCNICO;1;O agente identificou que não havia problemas nos equipamentos.;Verifiquei aqui os equipamentos e não identifiquei nenhum problema com eles.;A potência recebida pelo equipamento está normal e as configurações do roteador estão todas corretas.
|
||||||
|
DIDATISMO;1;O agente foi didático ao orientar o cliente.;Certo, tu poderia se conectar na rede do primeiro roteador e verificar se ocorre algum problema?;Entendi, pode ser algum problema neste segundo roteador, tu pode estar reiniciando ele na tomada para caso seja algum travamento.
|
||||||
|
100%
|
||||||
|
|
||||||
|
A resposta sua deve ser:
|
||||||
|
```csv
|
||||||
|
CATEGORIA;PONTOS
|
||||||
|
APRESENTAÇÃO;1
|
||||||
|
CONFIRMAÇÃO DE E-MAIL;1
|
||||||
|
CONFIRMAÇÃO DE TELEFONE;1
|
||||||
|
PROTOCOLO;1
|
||||||
|
USO DO PORTUGUÊS;1
|
||||||
|
PACIÊNCIA E EDUCAÇÃO;1
|
||||||
|
DISPONIBILIDADE;1
|
||||||
|
CONHECIMENTO TÉCNICO;1
|
||||||
|
DIDATISMO;1
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemplo 02:
|
||||||
|
Dado o seguinte arquivo de entrada:
|
||||||
|
01,1,Apresentação,"Boa tarde, me chamo Ander. (12:10:05)"
|
||||||
|
02,1,Confirmou e‑mail,"Para manter o cadastro atualizado, poderia me confirmar se o e‑mail continua sendo mmaicomvoss@gmail.com? (13:01:40)"
|
||||||
|
03,1,Confirmou telefone,"para manter o cadastro atualizado, poderia me confirmar se o telefone continua (53) 98414‑3027? (13:01:40)"
|
||||||
|
04,1,Informa protocolo,"Aqui está o protocolo do teu atendimento: 2510.2749 (12:10:06)"
|
||||||
|
05,1,Uso correto do português,"Todas as mensagens foram escritas em português formal e correto, inclusive com 'tu' permitido. (12:10:05‑13:03:08)"
|
||||||
|
06,1,Uso de linguagem paciente e educada,"Aguarde meu retorno, por gentileza; me informe, por gentileza; obrigado pela confirmação. (12:10:13‑13:03:07)"
|
||||||
|
07,1,Disponibilidade expressa,"Caso tenha alguma dúvida, nos contate. A equipe da NovaNet está sempre aqui para te ajudar. (13:03:08)"
|
||||||
|
08,1,Conhecimento técnico,"Identificou mau contato no cabo, instruiu reconexões, avaliou ordem de serviço, cotou peças. (12:25:44‑12:57:10)"
|
||||||
|
09,1,Didático,"Desconecte o cabo LAN/WAN passo a passo e me informe quando terminar. (12:25:56‑12:26:06)"
|
||||||
|
10,1,Eclaração diagnóstica,"Fez diagnóstico de mau contato no cabo e ofereceu ordem de serviço sem custo. (12:55:17‑13:01:15)"
|
||||||
|
11,2,Tempo de espera excedido, 2 ocorrências,"Intervalos superiores a 5 min: 12:55:17‑13:01:15 (5 min 58 s) e 12:27:51‑12:54:11 (26 min 20 s)"
|
||||||
|
|
||||||
|
A resposta sua deve ser:
|
||||||
|
```csv
|
||||||
|
CATEGORIA;PONTOS
|
||||||
|
APRESENTAÇÃO;1
|
||||||
|
CONFIRMAÇÃO DE E-MAIL;1
|
||||||
|
CONFIRMAÇÃO DE TELEFONE;1
|
||||||
|
PROTOCOLO;1
|
||||||
|
USO DO PORTUGUÊS;1
|
||||||
|
PACIÊNCIA E EDUCAÇÃO;1
|
||||||
|
DISPONIBILIDADE;1
|
||||||
|
CONHECIMENTO TÉCNICO;1
|
||||||
|
DIDATISMO;1
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemplo 03:
|
||||||
|
Dado o seguinte arquivo de entrada:
|
||||||
|
Identificação e abertura do atendimento,1,
|
||||||
|
Confirmação de dados do cliente,1,
|
||||||
|
Verificação do histórico e do plano do cliente,1,
|
||||||
|
Análise e diagnóstico da falha na conectividade,1,
|
||||||
|
Verificação e teste de equipamentos,1,
|
||||||
|
Sugestão de solução de conectividade,1,
|
||||||
|
Escala de serviço,1,
|
||||||
|
Encerramento de atendimento,1,
|
||||||
|
Follow-up,1,
|
||||||
|
Comunicação com o cliente,1,
|
||||||
|
Tempo de Resposta,1
|
||||||
|
|
||||||
|
A sua resposta deve ser vazia neste caso, pois a entrada não fornece a pontuação adequada para os critérios.
|
||||||
|
Ou seja o retorno deve ser o seguinte
|
||||||
|
```csv
|
||||||
|
CATEGORIA;PONTOS
|
||||||
|
APRESENTAÇÃO;
|
||||||
|
CONFIRMAÇÃO DE E-MAIL;
|
||||||
|
CONFIRMAÇÃO DE TELEFONE;
|
||||||
|
PROTOCOLO;
|
||||||
|
USO DO PORTUGUÊS;
|
||||||
|
PACIÊNCIA E EDUCAÇÃO;
|
||||||
|
DISPONIBILIDADE;
|
||||||
|
CONHECIMENTO TÉCNICO;
|
||||||
|
DIDATISMO;
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Aqui um exemplo de formatação de como não deve ser sua resposta
|
||||||
|
Erro 01: Não utilizar o formato estritamente como fornecido nas instruções e copiar da entrada que está sendo avaliada
|
||||||
|
```csv
|
||||||
|
CATEGORIA;PONTOS
|
||||||
|
APRESENTAÇÃO;1
|
||||||
|
Confirmação de e-mail;1
|
||||||
|
CONFIRMAÇÃO DE TELEFONE;1
|
||||||
|
PROTOCOLO;1
|
||||||
|
USO DO PORTUGUÊS;1
|
||||||
|
PACIÊNCIA E EDUCAÇÃO;1
|
||||||
|
DISPONIBILIDADE;1
|
||||||
|
CONHECIMENTO TÉCNICO;1
|
||||||
|
DIDATISMO;1
|
||||||
|
```
|
||||||
|
|
||||||
|
Abaixo está a avaliação que deve ser processada
|
||||||
|
--------------------------------
|
||||||
386
src/groupped_repport_monthly.rs
Normal file
386
src/groupped_repport_monthly.rs
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use chrono::{Datelike, NaiveDate};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use polars::prelude::*;
|
||||||
|
use reqwest;
|
||||||
|
use std::env;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use csv;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
APRESENTAÇÃO: u8,
|
||||||
|
CONFIRMAÇÃO_DE_EMAIL: u8,
|
||||||
|
CONFIRMAÇÃO_DE_TELEFONE: u8,
|
||||||
|
PROTOCOLO: u8,
|
||||||
|
USO_DO_PORTUGUÊS: u8,
|
||||||
|
PACIÊNCIA_E_EDUCAÇÃO: u8,
|
||||||
|
DISPONIBILIDADE: u8,
|
||||||
|
CONHECIMENTO_TÉCNICO: u8,
|
||||||
|
DIDATISMO: u8,
|
||||||
|
ID_TALK: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
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_month = NaiveDate::fro
|
||||||
|
|
||||||
|
let current_day_of_last_month = current_date
|
||||||
|
.checked_sub_months(chrono::Months::new(1))
|
||||||
|
.expect("Failed to subtract one month");
|
||||||
|
|
||||||
|
let first_day_of_last_month = NaiveDate::from_ymd_opt(
|
||||||
|
current_day_of_last_month.year(),
|
||||||
|
current_day_of_last_month.month(),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
.expect("Failed to obtain date");
|
||||||
|
let last_day_of_last_month = NaiveDate::from_ymd_opt(
|
||||||
|
current_day_of_last_month.year(),
|
||||||
|
current_day_of_last_month.month(),
|
||||||
|
current_day_of_last_month.num_days_in_month() as u32,
|
||||||
|
)
|
||||||
|
.expect("Failed to obtain date");
|
||||||
|
|
||||||
|
let previous_month_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(), entry_string_name));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter_map_ok(|(folder_name_date, directory_string)| {
|
||||||
|
if folder_name_date.year() == first_day_of_last_month.year()
|
||||||
|
&& folder_name_date.month() == first_day_of_last_month.month()
|
||||||
|
{
|
||||||
|
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_month_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_month_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_repsonse, file_path_csv)| {
|
||||||
|
let mut reader = csv::ReaderBuilder::new()
|
||||||
|
.has_headers(true)
|
||||||
|
.delimiter(b';')
|
||||||
|
.from_reader(ai_repsonse.as_bytes());
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
if columns.len() != 9 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse id talk from file_path
|
||||||
|
// filename example is: CC - Erraoander Quintana - 515578 - 20251020515578.csv
|
||||||
|
// id talk is the last information, so in the example is: 20251020515578
|
||||||
|
let regex_filename =
|
||||||
|
regex::Regex::new(r"(CC - )((\w+\s*)+) - (\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(2)
|
||||||
|
.expect("Failed to get the id from regex maches");
|
||||||
|
let talk_id = found_regex_groups_in_filename
|
||||||
|
.get(5)
|
||||||
|
.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_month}")).unwrap() {
|
||||||
|
std::fs::create_dir(format!("./groupped/{first_day_of_last_month}"))
|
||||||
|
.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_month}/{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 => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file(
|
||||||
|
std::path::Path::new(&format!("./groupped/{first_day_of_last_month}")),
|
||||||
|
std::path::Path::new(&format!("./groupped/{first_day_of_last_month}.zip")),
|
||||||
|
);
|
||||||
|
|
||||||
|
let recipients = "Wilson da Conceição Oliveira <wilson.oliveira@nova.net.br>, Isadora G. Moura de Moura <isadora.moura@nova.net.br>";
|
||||||
|
println!("Trying to send mail... {recipients}");
|
||||||
|
send_mail_util::send_mail_util::send_email(
|
||||||
|
&format!("Relatório agrupado dos atendimentos do mês {first_day_of_last_month}"),
|
||||||
|
&BOT_EMAIL,
|
||||||
|
&BOT_EMAIL_PASSWORD,
|
||||||
|
recipients,
|
||||||
|
&format!("./groupped/{first_day_of_last_month}.zip"),
|
||||||
|
);
|
||||||
|
}
|
||||||
392
src/groupped_repport_weekly.rs
Normal file
392
src/groupped_repport_weekly.rs
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use itertools::Itertools;
|
||||||
|
use polars::prelude::*;
|
||||||
|
use reqwest;
|
||||||
|
use std::env;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use csv;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
APRESENTAÇÃO: u8,
|
||||||
|
CONFIRMAÇÃO_DE_EMAIL: u8,
|
||||||
|
CONFIRMAÇÃO_DE_TELEFONE: u8,
|
||||||
|
PROTOCOLO: u8,
|
||||||
|
USO_DO_PORTUGUÊS: u8,
|
||||||
|
PACIÊNCIA_E_EDUCAÇÃO: u8,
|
||||||
|
DISPONIBILIDADE: u8,
|
||||||
|
CONHECIMENTO_TÉCNICO: u8,
|
||||||
|
DIDATISMO: u8,
|
||||||
|
ID_TALK: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
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_repsonse, file_path_csv)| {
|
||||||
|
let mut reader = csv::ReaderBuilder::new()
|
||||||
|
.has_headers(true)
|
||||||
|
.delimiter(b';')
|
||||||
|
.from_reader(ai_repsonse.as_bytes());
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
if columns.len() != 9 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse id talk from file_path
|
||||||
|
// filename example is: CC - Erraoander Quintana - 515578 - 20251020515578.csv
|
||||||
|
// id talk is the last information, so in the example is: 20251020515578
|
||||||
|
let regex_filename =
|
||||||
|
regex::Regex::new(r"(CC - )((\w+\s*)+) - (\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(2)
|
||||||
|
.expect("Failed to get the id from regex maches");
|
||||||
|
let talk_id = found_regex_groups_in_filename
|
||||||
|
.get(5)
|
||||||
|
.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 => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>, Isadora G. Moura de Moura <isadora.moura@nova.net.br>";
|
||||||
|
println!("Trying to send mail... {recipients}");
|
||||||
|
send_mail_util::send_mail_util::send_email(
|
||||||
|
&format!(
|
||||||
|
"Relatório agrupado dos atendimentos 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"),
|
||||||
|
);
|
||||||
|
}
|
||||||
414
src/main.rs
414
src/main.rs
@@ -1,15 +1,17 @@
|
|||||||
use std::ffi::OsStr;
|
|
||||||
use std::{any::Any, env, fmt::format, iter, time::Duration};
|
use std::{any::Any, env, fmt::format, iter, time::Duration};
|
||||||
|
|
||||||
use chrono::{self, Timelike};
|
use chrono::{self, Timelike};
|
||||||
use dotenv;
|
use dotenv;
|
||||||
use ipaddress;
|
use ipaddress;
|
||||||
use itertools::{self, Itertools};
|
use itertools::{self, Itertools};
|
||||||
use lettre::message::Mailboxes;
|
|
||||||
use lettre::{self, message};
|
use lettre::{self, message};
|
||||||
use reqwest;
|
use reqwest;
|
||||||
use serde_json::{self, json};
|
use serde_json::{self, json};
|
||||||
use zip;
|
|
||||||
|
use std::io::prelude::*;
|
||||||
|
|
||||||
|
pub mod send_mail_util;
|
||||||
|
pub mod zip_directory_util;
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
match dotenv::dotenv().ok() {
|
match dotenv::dotenv().ok() {
|
||||||
@@ -166,20 +168,37 @@ fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// Create a folder named with the day_before
|
// Create a folder named with the day_before
|
||||||
if !std::fs::exists(format!("./evaluations/{formatted_day_before}")).unwrap() {
|
if !std::fs::exists(format!("./evaluations/{formatted_day_before}")).unwrap() {
|
||||||
std::fs::create_dir(format!("./evaluations/{formatted_day_before}")).expect("Failed to create directory")
|
std::fs::create_dir(format!("./evaluations/{formatted_day_before}"))
|
||||||
|
.expect("Failed to create directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the response time folder
|
// Create the response time folder
|
||||||
if !std::fs::exists(format!("./evaluations/{formatted_day_before}/response_time.csv")).unwrap() {
|
if !std::fs::exists(format!(
|
||||||
let mut response_time_file = std::fs::File::create_new(format!("./evaluations/{formatted_day_before}/response_time.csv")).expect("Failed to response_time.csv");
|
"./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");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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());
|
||||||
let filter_keywords = filter_file_contents.split("\n").collect::<Vec<&str>>();
|
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_before_at_23_59_59);
|
let talks_array = get_piperun_chats_on_date(
|
||||||
|
&PIPERUN_API_URL,
|
||||||
|
&client,
|
||||||
|
&access_token,
|
||||||
|
formatted_day_before_at_midnight,
|
||||||
|
formatted_day_before_at_23_59_59,
|
||||||
|
);
|
||||||
|
|
||||||
println!("Number of consolidated talks: {}", talks_array.len());
|
println!("Number of consolidated talks: {}", talks_array.len());
|
||||||
|
|
||||||
@@ -215,41 +234,82 @@ fn main() -> anyhow::Result<()> {
|
|||||||
let talk_id_get_result = talk_id_get_request.send();
|
let talk_id_get_result = talk_id_get_request.send();
|
||||||
|
|
||||||
return talk_id_get_result;
|
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();
|
.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 talk_histories = &json["talk_histories"];
|
||||||
let data = &talk_histories["data"];
|
let data = &talk_histories["data"];
|
||||||
|
|
||||||
// Filter chats that have very few messages
|
// Filter chats that have very few messages
|
||||||
let talk_lenght = talk_histories.as_array().expect("Wrong message type received from talk histories").len();
|
let talk_lenght = talk_histories
|
||||||
if talk_lenght < MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE {return None;}
|
.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
|
// 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 found = talk_histories
|
||||||
let message = message_object["message"].as_str().expect("Failed to decode message as string");
|
.as_array()
|
||||||
let found = message.find("Atendimento transferido para a fila [NovaNet -> Atendimento -> Suporte]");
|
.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 found = message.find(
|
||||||
|
"Atendimento transferido para a fila [NovaNet -> Atendimento -> Suporte]",
|
||||||
|
);
|
||||||
found.is_some()
|
found.is_some()
|
||||||
});
|
});
|
||||||
|
|
||||||
match found {
|
match found {
|
||||||
None => {return None;},
|
None => {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
Some(pos) => {
|
Some(pos) => {
|
||||||
let pos_found = pos.0;
|
let pos_found = pos.0;
|
||||||
if pos_found < MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE {return None;}
|
if pos_found < MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter Bot finished chats
|
// Filter Bot finished chats
|
||||||
if json["agent"]["user"]["name"].as_str().unwrap_or("unknown_user") == "PipeBot" {return None;}
|
if json["agent"]["user"]["name"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("unknown_user")
|
||||||
|
== "PipeBot"
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
// Apply keyword based filtering
|
// 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 filter_keywords_found = talk_histories
|
||||||
let message = message_object["message"].as_str().expect("Failed to decode message as string");
|
.as_array()
|
||||||
let found = filter_keywords.iter().any(|keyword|{message.to_uppercase().find(&keyword.to_uppercase()).is_some()});
|
.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 found = filter_keywords.iter().any(|keyword| {
|
||||||
|
message
|
||||||
|
.to_uppercase()
|
||||||
|
.find(&keyword.to_uppercase())
|
||||||
|
.is_some()
|
||||||
|
});
|
||||||
found
|
found
|
||||||
});
|
});
|
||||||
|
|
||||||
if filter_keywords_found {return None;}
|
if filter_keywords_found {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
return Some(json);
|
return Some(json);
|
||||||
});
|
});
|
||||||
@@ -267,28 +327,42 @@ fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// find the bot transfer message
|
// find the bot transfer message
|
||||||
let bot_transfer_message = talk_histories
|
let bot_transfer_message = talk_histories
|
||||||
.as_array().expect("Wrong message type received from talk histories").into_iter()
|
.as_array()
|
||||||
|
.expect("Wrong message type received from talk histories")
|
||||||
|
.into_iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|(pos, message_object)| {
|
.filter(|(pos, message_object)| {
|
||||||
let user_name = message_object["user"]["name"].as_str().expect("Failed to decode message as string");
|
let user_name = message_object["user"]["name"]
|
||||||
|
.as_str()
|
||||||
|
.expect("Failed to decode message as string");
|
||||||
user_name == "PipeBot".to_string()
|
user_name == "PipeBot".to_string()
|
||||||
}).find(|(pos, message_object)|{
|
})
|
||||||
let message = message_object["message"].as_str().expect("Failed to decode message as string");
|
.find(|(pos, message_object)| {
|
||||||
|
let message = message_object["message"]
|
||||||
|
.as_str()
|
||||||
|
.expect("Failed to decode message as string");
|
||||||
// let found = message.find("Atendimento transferido para a fila [NovaNet -> Atendimento -> Suporte]");
|
// let found = message.find("Atendimento transferido para a fila [NovaNet -> Atendimento -> Suporte]");
|
||||||
let found = message.find("Atendimento entregue da fila de espera para o agente");
|
let found =
|
||||||
|
message.find("Atendimento entregue da fila de espera para o agente");
|
||||||
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) = bot_transfer_message.expect("Failed to get the transfer bot message position");
|
let (pos, transfer_message) =
|
||||||
|
bot_transfer_message.expect("Failed to get the transfer bot message position");
|
||||||
|
|
||||||
let msg = talk_histories.as_array().expect("Wrong message type received from talk histories").into_iter()
|
let msg = talk_histories
|
||||||
|
.as_array()
|
||||||
|
.expect("Wrong message type received from talk histories")
|
||||||
|
.into_iter()
|
||||||
.take(pos)
|
.take(pos)
|
||||||
.rev()
|
.rev()
|
||||||
.filter(|message| {
|
.filter(|message| {
|
||||||
message["type"] == "out".to_string() && message["user"]["name"] != "PipeBot".to_string()
|
message["type"] == "out".to_string()
|
||||||
|
&& message["user"]["name"] != "PipeBot".to_string()
|
||||||
})
|
})
|
||||||
.take(1).collect_vec();
|
.take(1)
|
||||||
|
.collect_vec();
|
||||||
|
|
||||||
let agent_first_message = msg[0];
|
let agent_first_message = msg[0];
|
||||||
|
|
||||||
@@ -297,45 +371,73 @@ fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let format = "%Y-%m-%d %H:%M:%S";
|
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) {
|
let date_user_message_sent_parsed =
|
||||||
|
match chrono::NaiveDateTime::parse_from_str(date_user_message_sent, format) {
|
||||||
Ok(dt) => dt,
|
Ok(dt) => dt,
|
||||||
Err(e) => {println!("Error parsing DateTime: {}", e); panic!("Failed parsing date")},
|
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) {
|
let date_transfer_message_sent_parsed = match chrono::NaiveDateTime::parse_from_str(
|
||||||
|
transfer_message["sent_at"].as_str().unwrap(),
|
||||||
|
format,
|
||||||
|
) {
|
||||||
Ok(dt) => dt,
|
Ok(dt) => dt,
|
||||||
Err(e) => {println!("Error parsing DateTime: {}", e); panic!("Failed parsing date")},
|
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 response_time = (date_user_message_sent_parsed - date_transfer_message_sent_parsed)
|
||||||
let name = agent_first_message["user"]["name"].as_str().unwrap().to_owned();
|
.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 id = json["tracking_number"].as_str().unwrap_or("").to_owned();
|
||||||
let bot_transfer_date = date_transfer_message_sent_parsed.to_owned();
|
let bot_transfer_date = date_transfer_message_sent_parsed.to_owned();
|
||||||
let user_response_date = date_user_message_sent.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());
|
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)
|
format!(
|
||||||
}).reduce(|acc, e|{format!("{}\n{}",acc,e)})
|
"{};{};{};{};{}",
|
||||||
|
name, id, response_time, bot_transfer_date, user_response_date
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.reduce(|acc, e| format!("{}\n{}", acc, e))
|
||||||
.unwrap_or("".to_string());
|
.unwrap_or("".to_string());
|
||||||
|
|
||||||
// return Ok(());
|
// return Ok(());
|
||||||
// Open file and write to it
|
// Open file and write to it
|
||||||
let header = "NOME;ID_TALK;TEMPO DE RESPOSTA;TRANFERENCIA PELO BOT;PRIMEIRA RESPOSTA DO AGENTE";
|
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")).expect("Failed to open response time file for write");
|
let mut response_time_file = std::fs::OpenOptions::new()
|
||||||
response_time_file.write_all(format!("{header}\n{response_time}").as_bytes()).expect("Failed to write header to file");
|
.write(true)
|
||||||
|
.open(format!(
|
||||||
|
"./evaluations/{formatted_day_before}/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
|
filtered_chats.clone().skip(0).for_each(|result| {
|
||||||
.clone()
|
|
||||||
.skip(0)
|
|
||||||
.take(10)
|
|
||||||
.for_each(|result| {
|
|
||||||
let json = result.unwrap();
|
let json = result.unwrap();
|
||||||
let talk_histories = &json["talk_histories"];
|
let talk_histories = &json["talk_histories"];
|
||||||
let data = &talk_histories["data"];
|
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 talk = talk_histories
|
||||||
{
|
.as_array()
|
||||||
|
.expect("Wrong message type received from talk histories")
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.map(|message_object| {
|
||||||
let new_json_filtered = format!(
|
let new_json_filtered = format!(
|
||||||
"{{
|
"{{
|
||||||
message: {},
|
message: {},
|
||||||
@@ -350,25 +452,34 @@ fn main() -> anyhow::Result<()> {
|
|||||||
);
|
);
|
||||||
// println!("{}", new_json_filtered);
|
// println!("{}", new_json_filtered);
|
||||||
new_json_filtered
|
new_json_filtered
|
||||||
}).reduce(|acc, e| {format!("{acc}\n{e}")}).expect("Error extracting talk");
|
})
|
||||||
|
.reduce(|acc, e| format!("{acc}\n{e}"))
|
||||||
|
.expect("Error extracting talk");
|
||||||
|
|
||||||
println!("{prompt}\n {talk}");
|
println!("{prompt}\n {talk}");
|
||||||
|
|
||||||
let ollama_api_request = client.post(format!("http://{OLLAMA_SANITIZED_IP}:{OLLAMA_PORT}/api/generate"))
|
let ollama_api_request = client
|
||||||
|
.post(format!(
|
||||||
|
"http://{OLLAMA_SANITIZED_IP}:{OLLAMA_PORT}/api/generate"
|
||||||
|
))
|
||||||
.body(
|
.body(
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"model": OLLAMA_AI_MODEL,
|
"model": OLLAMA_AI_MODEL,
|
||||||
"prompt": format!("{prompt} \n{talk}"),
|
"prompt": format!("{prompt} \n{talk}"),
|
||||||
// "options": serde_json::json!({"temperature": 0.1}),
|
// "options": serde_json::json!({"temperature": 0.1}),
|
||||||
"stream": false,
|
"stream": false,
|
||||||
}).to_string()
|
})
|
||||||
|
.to_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = ollama_api_request.timeout(Duration::from_secs(3600)).send();
|
let result = ollama_api_request.timeout(Duration::from_secs(3600)).send();
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(response) => {println!("Response: {:?}", response);
|
Ok(response) => {
|
||||||
let response_json = response.json::<serde_json::Value>().expect("Failed to deserialize response to JSON");
|
println!("Response: {:?}", response);
|
||||||
|
let response_json = response
|
||||||
|
.json::<serde_json::Value>()
|
||||||
|
.expect("Failed to deserialize response to JSON");
|
||||||
println!("{}", response_json);
|
println!("{}", response_json);
|
||||||
let ai_response = response_json["response"]
|
let ai_response = response_json["response"]
|
||||||
.as_str()
|
.as_str()
|
||||||
@@ -379,38 +490,63 @@ fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// Save the CSV response to a file
|
// Save the CSV response to a file
|
||||||
|
|
||||||
let user_name = &json["agent"]["user"]["name"].as_str().unwrap_or("unknown_user");
|
let user_name = &json["agent"]["user"]["name"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("unknown_user");
|
||||||
let talk_id = &json["id"].as_u64().unwrap_or(0);
|
let talk_id = &json["id"].as_u64().unwrap_or(0);
|
||||||
let tracking_number = &json["tracking_number"].as_str().unwrap_or("");
|
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), csv_response).expect("Unable to write file");
|
std::fs::write(
|
||||||
std::fs::write(format!("./evaluations/{}/{} - {} - {} - prompt.txt", formatted_day_before, user_name, talk_id, tracking_number), format!("{prompt} \n{talk}")).expect("Unable to write file");
|
format!(
|
||||||
},
|
"./evaluations/{}/{} - {} - {}.csv",
|
||||||
Err(error) => {println!("Error {error}");}
|
formatted_day_before, 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
|
||||||
|
),
|
||||||
|
format!("{prompt} \n{talk}"),
|
||||||
|
)
|
||||||
|
.expect("Unable to write file");
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
println!("Error {error}");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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 output_zip_file_str = format!("./evaluations/{formatted_day_before}.zip");
|
let output_zip_file_str = format!("./evaluations/{formatted_day_before}.zip");
|
||||||
let source_dir = Path::new(source_dir_str.as_str());
|
let source_dir = std::path::Path::new(source_dir_str.as_str());
|
||||||
let output_zip_file = Path::new(output_zip_file_str.as_str());
|
let output_zip_file = std::path::Path::new(output_zip_file_str.as_str());
|
||||||
doit(source_dir, output_zip_file, zip::CompressionMethod::Stored);
|
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 = "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>, Isadora G. Moura de Moura <isadora.moura@nova.net.br>";
|
||||||
println!("Trying to send email... Recipients {recipients}");
|
println!("Trying to send email... Recipients {recipients}");
|
||||||
|
|
||||||
send_email(
|
send_mail_util::send_mail_util::send_email(
|
||||||
&formatted_day_before,
|
&format!("Avaliacao atendimentos {formatted_day_before}"),
|
||||||
&BOT_EMAIL,
|
&BOT_EMAIL,
|
||||||
&BOT_EMAIL_PASSWORD,
|
&BOT_EMAIL_PASSWORD,
|
||||||
recipients,
|
recipients,
|
||||||
&output_zip_file_str,
|
&output_zip_file_str,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Ok(());
|
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_before_at_23_59_59: String) -> Vec<serde_json::Value> {
|
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_before_at_23_59_59: String,
|
||||||
|
) -> 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 = "13".to_string();
|
let support_queue_id: String = "13".to_string();
|
||||||
|
|
||||||
@@ -455,12 +591,24 @@ fn get_piperun_chats_on_date(PIPERUN_API_URL: &String, client: &reqwest::blockin
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut aggregated_talks = json_response["data"].as_array().expect("Failed to parse messages as array").to_owned();
|
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 current_page = json_response["current_page"]
|
||||||
let last_page = json_response["last_page"].as_i64().expect("Failed to obtain current page number");
|
.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");
|
||||||
|
|
||||||
let mut all_other_messages = (current_page..last_page).into_iter()
|
if current_page == last_page {
|
||||||
|
return aggregated_talks;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut all_other_messages = (current_page..last_page)
|
||||||
|
.into_iter()
|
||||||
.map(|page| {
|
.map(|page| {
|
||||||
let page_to_request = page + 1;
|
let page_to_request = page + 1;
|
||||||
let talks_request = client
|
let talks_request = client
|
||||||
@@ -499,129 +647,19 @@ fn get_piperun_chats_on_date(PIPERUN_API_URL: &String, client: &reqwest::blockin
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let aggregated_talks = json_response["data"].as_array().expect("Failed to parse messages as array").to_owned();
|
let aggregated_talks = json_response["data"]
|
||||||
|
.as_array()
|
||||||
|
.expect("Failed to parse messages as array")
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
return aggregated_talks;
|
return aggregated_talks;
|
||||||
})
|
})
|
||||||
.reduce(|mut this, mut acc| {acc.append(&mut this); acc})
|
.reduce(|mut this, mut acc| {
|
||||||
|
acc.append(&mut this);
|
||||||
|
acc
|
||||||
|
})
|
||||||
.expect("Failed to concatenate all vectors of messages");
|
.expect("Failed to concatenate all vectors of messages");
|
||||||
|
|
||||||
|
|
||||||
aggregated_talks.append(&mut all_other_messages);
|
aggregated_talks.append(&mut all_other_messages);
|
||||||
aggregated_talks
|
aggregated_talks
|
||||||
}
|
}
|
||||||
|
|
||||||
use std::io::prelude::*;
|
|
||||||
use zip::{result::ZipError, write::SimpleFileOptions};
|
|
||||||
|
|
||||||
use std::fs::File;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn doit(src_dir: &Path, dst_file: &Path, method: zip::CompressionMethod) {
|
|
||||||
if !Path::new(src_dir).is_dir() {
|
|
||||||
panic!("src_dir must be a directory");
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
use lettre::{
|
|
||||||
Message, SmtpTransport, Transport,
|
|
||||||
message::Attachment,
|
|
||||||
message::MultiPart,
|
|
||||||
message::SinglePart,
|
|
||||||
message::header::ContentType,
|
|
||||||
transport::smtp::authentication::{Credentials, Mechanism},
|
|
||||||
};
|
|
||||||
|
|
||||||
fn send_email(
|
|
||||||
day_before: &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!("Avaliacao atendimentos {day_before}"))
|
|
||||||
.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();
|
|
||||||
}
|
|
||||||
|
|||||||
46
src/send_mail_util.rs
Normal file
46
src/send_mail_util.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/zip_directory_util.rs
Normal file
69
src/zip_directory_util.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user