Ignorar chats que iniciam com template + response_time semana e mensal + novo prompt + novo filter #2

Open
nicolas.borges wants to merge 1 commits from nicolas.borges/piperun-bot:main into main
6 changed files with 930 additions and 61 deletions

View File

@@ -15,4 +15,15 @@ ativar conta
link de ativação
paramount
https://play.watch.tv.br/ative
watch
watch
prosseguimento por ligação
seguimento por ligação
Agendamento [privado] efetuado por
Atendimento transferido para o agente [CC - Nicollas Timm] por [CC -
Atendimento transferido para o agente [CC - Ravel Borges] por [CC -
Atendimento transferido para o agente [CC - Renata Ramson] por [CC -
Atendimento transferido para o agente [CC - Isadora Moura] por [CC -
Atendimento transferido para o agente [CC - Erraoander Quintana] por [CC -
Atendimento transferido para o agente [CC - Estevan Macedo] por [CC -
Atendimento transferido para o agente [CC - Rafael Moura] por [CC -
Atendimento transferido para a fila de espera pelo agente [CC -

View File

@@ -8,68 +8,37 @@ SEGUINDO OS CRITÉRIOS QUE VÃO ESTAR ABAIXO, AVALIE ATENDIMENTOS DE SUPORTE PRE
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..
05 (USO DO PORTUGUÊS) - O AGENTE DEVE UTILIZAR CORRETAMENTE O PORTUGUÊS (PONTUAÇÃO E ACENTUAÇÃO).
NÃO ESTÁ ERRADO SE O AGENTE UTILIZAR 'TU', 'TEU', 'TUA'. SOMOS UMA EMPRESA REGIONAL.
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 (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?".
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 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."
"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 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"
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, EXPLICANDO O MOTIVO DE CADA SOLICITAÇÃO OU VERIFICAÇÃ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Ê.
✅ EXEMPLOS CORRETOS:
"ACESSEI A ANTENA E VI QUE ESTAVA UM POUCO LENTO, REINICIEI A MESMA PARA DESCARTAR UM TRAVAMENTO. PODERIA VERIFICAR SE ESTÁ FUNCIONANDO AGORA?"
"REALIZEI ALGUMAS ALTERAÇÕES NO ROTEADOR PARA PADRONIZAR AS CONFIGURAÇÕES. PODES TESTAR A CONEXÃO NOVAMENTE?"
"PARA DESCARTARMOS A POSSIBILIDADE DA ENTRADA ESTAR QUEIMADA, PODERIA COLOCAR O CABO NA OUTRA ENTRADA AMARELA?"
"PARA EVITAR TRAVAMENTOS E MELHORAR O DESEMPENHO, TERIA COMO REINICIAR OS EQUIPAMENTOS AGORA?"
"IDENTIFIQUEI QUE O PROBLEMA ESTÁ NO EQUIPAMENTO DA FIBRA, PODERIA CONFIRMAR SE O EQUIPAMENTO MENOR SEM ANTENAS ESTÁ COM UMA LUZ VERMELHA ACESA?"
"VERIFICANDO PELO SISTEMA, PARECE SE TRATAR DE UM PROBLEMA NA PARTE DA FIBRA, PODERIA VERIFICAR SE NO EQUIPAMENTO PEQUENO E SEM ANTENAS, POSSUI UMA LUZ VERMELHA PISCANDO?"
"ATÉ O EQUIPAMENTO DA FIBRA ESTÁ TUDO CERTO, O PROBLEMA PODE ESTAR NO ROTEADOR. QUANDO TU TENTAS TE CONECTAR AO WI-FI NO CELULAR, APARECE O NOME DA REDE NORMAL OU ALGO COMO 'TP-LINK'?"
"DE FORMA REMOTA EU IDENTIFIQUEI QUE O PROBLEMA ESTÁ NO APARELHO DA FIBRA. TU PODERIA ME CONFIRMAR SE O EQUIPAMENTO MENOR SEM ANTENAS ESTÁ COM UMA LUZ VERMELHA PISCANDO?"
"DE FORMA REMOTA EU IDENTIFIQUEI QUE O EQUIPAMENTO DA FIBRA NÃO ESTÁ RECEBENDO ENERGIA, PODERIA ME INFORMAR SE ESSE EQUIPAMENTO ESTÁ COM AS LUZES ACESAS?"
"REALIZEI ALGUMAS ALTERAÇÕES NO TEU ROTEADOR EM BUSCA DE DEIXAR AS CONFIGURAÇÕES PADRONIZADAS, TAMBÉM ALTEREI ALGUMAS CONFIGURAÇÕES REFERENTE AO SINAL E FREQUÊNCIA. PODERIA VERIFICAR COMO TUA CONEXÃO SE COMPORTA NO MOMENTO?"
❌ NÃO CONSIDERAR DIDÁTICO:
QUANDO O AGENTE PEDE ALGO SEM DIZER O MOTIVO.
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 SOLICITAÇÃO DE TELEFONE COMO ALGO DIDATICO.
NÃO CONSIDERAR A CONFIRMAÇÃO DE TELEFONE COMO ALGO DIDATICO.
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, 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 OU DE TELEFONE NÃO DEVEM SER CONSIDERADOS NESSE ITEM.
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.
✅ 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"
"TU ESTÁ RECEBENDO A VELOCIDADE CONTRATADA, MAS COMO O PLANO É LIMITADO, QUALQUER USO UM POUCO MAIS PESADO VAI AUMENTAR A LATÊNCIA, CAUSANDO TRAVAMENTOS E LENTIDÃO"
"NÃO ENCONTREI NENHUM PONTO DE ACESSO COM SINAL SUPERIOR PARA TUA ANTENA. SERÁ NECESSÁRIO ABRIR UMA ORDEM DE SERVIÇO PARA REALINHAR A ANTENA"
"DE FORMA REMOTA IDENTIFIQUEI QUE O EQUIPAMENTO DA FIBRA NÃO ESTÁ RECEBENDO ENERGIA. SERÁ NECESSÁRIO ABRIR UMA ORDEM DE SERVIÇO PARA QUE UM TÉCNICO VÁ AO LOCAL"
"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"
"O IDEAL SERIA CONECTAR TUA TV AO ROTEADOR POR UM CABO DE REDE, POIS ASSIM, O SINAL SERÁ TRANSMITIDO DIRETO, SEM SOFRER INTERFERÊNCIA.
-----------------------------------
As mensagens do chat estão estruturadas no formato JSON com os campos:
- **message**: conteúdo da mensagem
- **sent_at**: horário de envio
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.
Em cada categoria, atribua 0 quando o agente não tiver atendido o critétio e 1 caso tenha atendido.
A sua resposta deve ser uma apenas uma tabela CSV e nada mais, utilizando como separador o caracter ';' com as colunas: CATEGORIA;PONTOS;MOTIVO;EVIDENCIA onde cada linha contém a avaliação de um dos critérios acima.
A sua resposta deve ser apenas uma tabela CSV e nada mais, utilizando como separador o caracter ';' com as colunas: CATEGORIA;PONTOS;MOTIVO;EVIDENCIA1;EVIDENCIA2; on>
A evidencia1 deve ser diferente da evidencia2 mas ambas devem justificar a pontuação daquela categoria.
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.
-----------------------------------
A seguir estão as mensagens do atendimento, em JSON, avalie e retorne apenas um CSV.

View File

@@ -32,6 +32,20 @@ struct CsvEvaluation {
ID_TALK: String,
}
// --- ADICAO PARA AGRUPAR O response_time.csv ---
#[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 ADIÇÃO ---
fn main() {
match dotenv::dotenv().ok() {
Some(_) => println!("Environment variables loaded from .env file"),
@@ -369,12 +383,118 @@ fn main() {
None => {}
}
// --- ADICAO DO PROCESSAMENTO MENSAL DO response_time.csv ---
let response_times_data = 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 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 do mês
if !response_times_data.is_empty() {
let response_time_file_path = format!(
"./groupped/{first_day_of_last_month}/response_times_consolidated_{first_day_of_last_month}.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");
// Calcular estatísticas mensais
let total_records = response_times_data.len();
let avg_response_time = response_times_data.iter()
.map(|r| r.TEMPO_DE_RESPOSTA)
.sum::<u32>() as f32 / total_records as f32;
let min_response_time = response_times_data.iter()
.map(|r| r.TEMPO_DE_RESPOSTA)
.min()
.unwrap_or(0);
let max_response_time = response_times_data.iter()
.map(|r| r.TEMPO_DE_RESPOSTA)
.max()
.unwrap_or(0);
println!("Response times consolidated successfully for month {}!", first_day_of_last_month);
println!("Total records: {}", total_records);
println!("Average response time: {:.2} seconds", avg_response_time);
println!("Min response time: {} seconds", min_response_time);
println!("Max response time: {} seconds", max_response_time);
} else {
println!("No response time data found for the month {}.", first_day_of_last_month);
}
// --- FIM DA ADIÇÃO ---
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>";
let recipients = "Wilson da Conceição Oliveira <wilson.oliveira@nova.net.br>, nicolas.borges@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}"),

View File

@@ -31,6 +31,20 @@ struct CsvEvaluation {
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"),
@@ -369,6 +383,93 @@ fn main() {
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}"
@@ -378,7 +479,7 @@ fn main() {
)),
);
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>, nicolas.borges@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!(

View File

@@ -1,5 +1,4 @@
use std::{any::Any, env, fmt::format, iter, time::Duration};
use chrono::{self, Timelike};
use dotenv;
use ipaddress;
@@ -298,12 +297,16 @@ fn main() -> anyhow::Result<()> {
let message = message_object["message"]
.as_str()
.expect("Failed to decode message as string");
let found = filter_keywords.iter().any(|keyword| {
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;
found
});
@@ -526,16 +529,16 @@ fn main() -> anyhow::Result<()> {
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>";
println!("Trying to send email... Recipients {recipients}");
// 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}");
send_mail_util::send_mail_util::send_email(
&format!("Avaliacao atendimentos {formatted_day_before}"),
&BOT_EMAIL,
&BOT_EMAIL_PASSWORD,
recipients,
&output_zip_file_str,
);
// send_mail_util::send_mail_util::send_email(
// &format!("Avaliacao atendimentos {formatted_day_before}"),
// &BOT_EMAIL,
// &BOT_EMAIL_PASSWORD,
// recipients,
// &output_zip_file_str,
// );
return Ok(());
}

665
src/main_old.rs Normal file
View File

@@ -0,0 +1,665 @@
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");
}
// 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_before_at_23_59_59,
);
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 found = message.find(
"Atendimento transferido para a fila [NovaNet -> Atendimento -> Suporte]",
);
found.is_some()
});
match found {
None => {
return None;
}
Some(pos) => {
let pos_found = pos.0;
if pos_found < MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE {
return None;
}
}
};
// Filter Bot finished chats
if json["agent"]["user"]["name"]
.as_str()
.unwrap_or("unknown_user")
== "PipeBot"
{
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 found = filter_keywords.iter().any(|keyword| {
message
.to_uppercase()
.find(&keyword.to_uppercase())
.is_some()
});
found
});
if filter_keywords_found {
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"];
// 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
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 transferido para a fila [NovaNet -> Atendimento -> Suporte]");
let found =
message.find("Atendimento entregue da fila de espera para o agente");
found.is_some()
});
// 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 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()
})
.take(1)
.collect_vec();
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
)
})
.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"
))
.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
),
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
let source_dir_str = format!("./evaluations/{formatted_day_before}");
let output_zip_file_str = format!("./evaluations/{formatted_day_before}.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 = "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}");
send_mail_util::send_mail_util::send_email(
&format!("Avaliacao atendimentos {formatted_day_before}"),
&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_before_at_23_59_59: String,
) -> Vec<serde_json::Value> {
let start_of_talk_code: String = "talk_start".to_string();
let support_queue_id: String = "13".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()),
("end_date", formatted_day_before_at_23_59_59.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()),
("end_date", formatted_day_before_at_23_59_59.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
}