Compare commits

14 Commits

6 changed files with 456 additions and 70 deletions

13
.env Normal file
View File

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

View File

@@ -16,3 +16,14 @@ link de ativação
paramount paramount
https://play.watch.tv.br/ative 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 [ATEND - Nicollas Timm] por [ATEND -
Atendimento transferido para o agente [ATEND - Ravel Borges] por [ATEND -
Atendimento transferido para o agente [ATEND - Renata Ramson] por [ATEND -
Atendimento transferido para o agente [ATEND - Isadora Moura] por [ATEND -
Atendimento transferido para o agente [ATEND - Erraoander Quintana] por [ATEND -
Atendimento transferido para o agente [ATEND - Estevan Macedo] por [ATEND -
Atendimento transferido para o agente [ATEND - Rafael Moura] por [ATEND -
Atendimento transferido para a fila de espera pelo agente [ATEND -

View File

@@ -8,52 +8,25 @@ 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. 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) AVALIE EXCLUSIVAMENTE SE O AGENTE ESTÁ PONTUANDO CORRETAMENTE, ACENTUANDO CORRETAMENTE E INICIANDO AS FRASES COM LETRA MAIÚSCULA.
NÃO ESTÁ ERRADO SE O AGENTE UTILIZAR 'TU', 'TEU', 'TUA'. SOMOS UMA EMPRESA REGIONAL. IGNORE COMPLETAMENTE QUALQUER ERRO NAS MENSAGENS type: "in".
É PERMITIDO O USO DE LINGUAGEM REGIONAL COMO “TU”, “TEU”, “TUA”, POIS ISSO FAZ PARTE DA IDENTIDADE DA EMPRESA.
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. 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: 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.
"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"
09 (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 DURANTE O ATENDIMENTO, EXPLICANDO DE FORMA CLARA CADA AÇÃO REALIZADA OU SOLICITADA AO CLIENTE.
A EXPLICAÇÃO PODE VIR ANTES OU DEPOIS DA SOLICITAÇÃO. O AGENTE É CONSIDERADO DIDÁTICO QUANDO PEDE PARA O CLIENTE REALIZAR ALGUM PROCEDIMENTO E EXPLICA O MOTIVO DA SOLICITAÇÃO.
NÃO É NECESSÁRIO USAR AS MESMAS PALAVRAS DOS EXEMPLOS, BASTA DEIXAR CLARO O PORQUÊ. QUANDO PEDE PARA O CLIENTE FAZER ALGUMA VERIFICAÇÃO E EXPLICA O MOTIVO.
✅ EXEMPLOS CORRETOS: 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.
"ACESSEI A ANTENA E VI QUE ESTAVA UM POUCO LENTO, REINICIEI A MESMA PARA DESCARTAR UM TRAVAMENTO. PODERIA VERIFICAR SE ESTÁ FUNCIONANDO AGORA?" SOLICITAÇÃO OU CONFIRMAÇÃO DE E-MAIL NÃO DEVEM SER CONSIDERADOS NESSE ITEM.
"REALIZEI ALGUMAS ALTERAÇÕES NO ROTEADOR PARA PADRONIZAR AS CONFIGURAÇÕES. PODES TESTAR A CONEXÃO NOVAMENTE?" SOLICITAÇÃO OU CONFIRMAÇÃO DE TELEFONE NÃO DEVEM SER CONSIDERADOS NESSE ITEM.
"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.
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: As mensagens do chat estão estruturadas no formato JSON com os campos:
@@ -66,10 +39,10 @@ O fluxo de mensagens inica-se com o cliente interagindo com o BOT, e então a me
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;EVIDENCIA on>
Portando crie essas colunas para todo agente que for avaliado.
Na saída CSV, na coluna categoria, utilize o nome correspondente ao invés do número 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, 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() { fn main() {
match dotenv::dotenv().ok() { match dotenv::dotenv().ok() {
Some(_) => println!("Environment variables loaded from .env file"), Some(_) => println!("Environment variables loaded from .env file"),
@@ -251,7 +265,7 @@ fn main() {
// filename example is: CC - Erraoander Quintana - 515578 - 20251020515578.csv // filename example is: CC - Erraoander Quintana - 515578 - 20251020515578.csv
// id talk is the last information, so in the example is: 20251020515578 // id talk is the last information, so in the example is: 20251020515578
let regex_filename = let regex_filename =
regex::Regex::new(r"(CC - )((\w+\s*)+) - (\d+) - (\d+).csv").unwrap(); regex::Regex::new(r"(CC - |ATEND - )((\w+\s*)+) - (\d+) - (\d+).csv").unwrap();
let filename = file_path_csv let filename = file_path_csv
.file_name() .file_name()
@@ -369,12 +383,118 @@ fn main() {
None => {} 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( 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}")),
std::path::Path::new(&format!("./groupped/{first_day_of_last_month}.zip")), 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}"); println!("Trying to send mail... {recipients}");
send_mail_util::send_mail_util::send_email( send_mail_util::send_mail_util::send_email(
&format!("Relatório agrupado dos atendimentos do mês {first_day_of_last_month}"), &format!("Relatório agrupado dos atendimentos do mês {first_day_of_last_month}"),

View File

@@ -5,9 +5,12 @@ use polars::prelude::*;
use reqwest; use reqwest;
use std::env; use std::env;
use std::time::Duration; use std::time::Duration;
use std::path::Path;
use csv; use csv;
use std::fs::metadata;
use std::io::Read;
pub mod send_mail_util; pub mod send_mail_util;
pub mod zip_directory_util; pub mod zip_directory_util;
@@ -19,18 +22,32 @@ struct CsvHeader {
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]
struct CsvEvaluation { struct CsvEvaluation {
APRESENTAÇÃO: u8, APRESENTACAO: u8,
CONFIRMAÇÃO_DE_EMAIL: u8, CONFIRMAÇÃO_DE_EMAIL: u8,
CONFIRMAÇÃO_DE_TELEFONE: u8, CONFIRMAÇÃO_DE_TELEFONE: u8,
PROTOCOLO: u8, PROTOCOLO: u8,
USO_DO_PORTUGUÊS: u8, USO_DO_PORTUGUES: u8,
PACIÊNCIA_E_EDUCAÇÃO: u8, PACIENCIA_E_EDUCACAO: u8,
DISPONIBILIDADE: u8, DISPONIBILIDADE: u8,
CONHECIMENTO_TÉCNICO: u8, CONHECIMENTO_TÉCNICO: u8,
DIDATISMO: u8, DIDATISMO: u8,
ID_TALK: String, 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() { fn main() {
match dotenv::dotenv().ok() { match dotenv::dotenv().ok() {
Some(_) => println!("Environment variables loaded from .env file"), Some(_) => println!("Environment variables loaded from .env file"),
@@ -214,14 +231,93 @@ fn main() {
} }
}; };
}) })
.filter_map_ok(|(ai_repsonse, file_path_csv)| { .filter_map_ok(|(ai_response, file_path_csv)| {
let mut reader = csv::ReaderBuilder::new()
.has_headers(true) // ---------- LOG 1: mostra qual arquivo está sendo processado ----------
.delimiter(b';') // eprintln!("🔍 Processando arquivo: {:?}", file_path_csv);
.from_reader(ai_repsonse.as_bytes());
// Mostra o caminho absoluto
let path = file_path_csv.path();
// Caminho absoluto
if let Ok(abs_path) = std::fs::canonicalize(&path) {
eprintln!("📁 Caminho absoluto: {:?}", abs_path);
}
// Metadados
if let Ok(meta) = std::fs::metadata(&path) {
if let Ok(modified) = meta.modified() {
let datetime: chrono::DateTime<chrono::Local> = modified.into();
eprintln!("📅 Modificado (local): {}", datetime.format("%Y-%m-%d %H:%M:%S%.3f"));
//eprintln!("🕒 Modificado: {:?}", modified);
}
eprintln!("📏 Tamanho: {} bytes", meta.len());
}
// Opcional: mostrar as primeiras linhas do CSV
eprintln!("📄 Primeiras 200 caracteres do CSV antes de sanitizar:\n{}", &ai_response[..200.min(ai_response.len())]);
//
// --- SALVAR CSV SANITIZADO PARA INSPEÇÃO ---
//let sanitized_path = file_path_csv.path().with_extension("sanitized.csv");
// if let Err(e) = std::fs::write(&sanitized_path, &ai_response) {
// eprintln!("⚠️ Erro ao salvar CSV sanitizado: {}", e);
//} else {
// eprintln!("💾 CSV sanitizado salvo: {:?}", sanitized_path);
// }
// ---------------------------------------------
// --- SALVAR CSV SANITIZADO EM PASTA SEPARADA ---
// Define o diretório base para os arquivos sanitizados
let sanitized_base = Path::new("./evaluations_sanitized");
// Obtém o caminho relativo do arquivo original em relação a "./evaluations"
// Exemplo: "./evaluations/2026-02-09/arquivo.csv" -> "2026-02-09/arquivo.csv"
if let Ok(relative_path) = file_path_csv.path().strip_prefix("./evaluations") {
let dest_path = sanitized_base.join(relative_path);
// Cria o diretório de destino, se necessário
if let Some(parent) = dest_path.parent() {
std::fs::create_dir_all(parent).expect("Falha ao criar diretório para sanitizados");
}
// Altera a extensão para .sanitized.csv (ou mantém .csv, como preferir)
let dest_path = dest_path.with_extension("sanitized.csv");
// Escreve o arquivo
if let Err(e) = std::fs::write(&dest_path, &ai_response) {
eprintln!("⚠️ Erro ao salvar CSV sanitizado em {:?}: {}", dest_path, e);
} else {
eprintln!("💾 CSV sanitizado salvo em: {:?}", dest_path);
}
} else {
eprintln!("⚠️ Caminho do arquivo não está dentro de ./evaluations: {:?}", file_path_csv.path());
}
let mut reader = csv::ReaderBuilder::new()
.has_headers(true)
.delimiter(b';')
.from_reader(ai_response.as_bytes());
// ---------- LOG 2: tenta desserializar e conta os registros ----------
let deserialized = reader.deserialize::<CsvHeader>().collect::<Vec<_>>();
eprintln!("🧾 Total de linhas lidas (incluindo cabeçalho?): {}", deserialized.len());
for (i, result) in deserialized.iter().enumerate() {
match result {
Ok(record) => {
eprintln!("✅ Linha {}: CATEGORIA={}, PONTOS={:?}", i, record.CATEGORIA, record.PONTOS);
}
Err(e) => {
eprintln!("❌ Linha {} ERRO: {}", i, e);
}
}
}
/*
let mut deserialized_iter = reader.deserialize::<CsvHeader>(); let mut deserialized_iter = reader.deserialize::<CsvHeader>();
let mut columns = deserialized_iter let mut columns = deserialized_iter
.filter_ok(|value| value.PONTOS.is_some()) .filter_ok(|value| value.PONTOS.is_some())
.map_ok(|value| { .map_ok(|value| {
@@ -236,16 +332,38 @@ fn main() {
None None
}) })
.collect_vec(); .collect_vec();
*/
let mut columns = deserialized
.into_iter() // usa os registros já lidos
.filter_ok(|value| {
// Se PONTOS for None, considera como 0 e mantém a linha
true // sempre mantém
})
.map_ok(|value| {
let pontos = value.PONTOS.unwrap_or(0) as u32;
Column::new(value.CATEGORIA.into(), [pontos])
})
/*
.filter_ok(|value| value.PONTOS.is_some())
.map_ok(|value| {
let pontos = value.PONTOS.unwrap() as u32;
Column::new(value.CATEGORIA.into(), [pontos])
})
*/
.filter_map(|value| value.ok())
.collect_vec();
if columns.len() != 9 { if columns.len() != 9 {
return None; return None;
} }
// Parse id talk from file_path // Parse id talk from file_path
// filename example is: CC - Erraoander Quintana - 515578 - 20251020515578.csv // filename example is: FIN - Lais Mota - 515578 - 20251020515578.csv
// id talk is the last information, so in the example is: 20251020515578 // id talk is the last information, so in the example is: 20251020515578
let regex_filename = let regex_filename =
regex::Regex::new(r"(CC - )((\w+\s*)+) - (\d+) - (\d+).csv").unwrap(); //regex::Regex::new(r"(FIN - )((\s*\w+\s*)+) - (\d+) - (\d+).csv").unwrap();
regex::Regex::new(r"ATEND - (.+?) - (\d+) - (\d+)\.csv").unwrap();
let filename = file_path_csv let filename = file_path_csv
.file_name() .file_name()
@@ -256,10 +374,10 @@ fn main() {
.expect("Failed to do regex capture"); .expect("Failed to do regex capture");
let user_name = found_regex_groups_in_filename let user_name = found_regex_groups_in_filename
.get(2) .get(1)
.expect("Failed to get the id from regex maches"); .expect("Failed to get the id from regex maches");
let talk_id = found_regex_groups_in_filename let talk_id = found_regex_groups_in_filename
.get(5) .get(3)
.expect("Failed to get the id from regex maches"); .expect("Failed to get the id from regex maches");
let excelence_percentual = columns let excelence_percentual = columns
@@ -369,6 +487,93 @@ fn main() {
None => {} 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( zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file(
std::path::Path::new(&format!( std::path::Path::new(&format!(
"./groupped/{first_day_of_last_week} - {last_day_of_last_week}" "./groupped/{first_day_of_last_week} - {last_day_of_last_week}"
@@ -378,11 +583,11 @@ 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.moura@nova.net.br";
println!("Trying to send mail... {recipients}"); println!("Trying to send mail... {recipients}");
send_mail_util::send_mail_util::send_email( send_mail_util::send_mail_util::send_email(
&format!( &format!(
"Relatório agrupado dos atendimentos semana {first_day_of_last_week} - {last_day_of_last_week}" "Relatório agrupado dos atendimentos da fila do Suporte - semana {first_day_of_last_week} - {last_day_of_last_week}"
), ),
&BOT_EMAIL, &BOT_EMAIL,
&BOT_EMAIL_PASSWORD, &BOT_EMAIL_PASSWORD,

View File

@@ -1,5 +1,4 @@
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;
@@ -184,6 +183,53 @@ fn main() -> anyhow::Result<()> {
.expect("Failed to response_time.csv"); .expect("Failed to response_time.csv");
} }
/*
// --- NOVO: processar argumento de linha de comando para data específica ---
use std::env;
use chrono::NaiveDate;
let args: Vec<String> = env::args().collect();
let target_day = if args.len() > 1 {
// Se um argumento foi passado, interpretar como YYYY-MM-DD
let naive_date = NaiveDate::parse_from_str(&args[1], "%Y-%m-%d")
.expect("Formato de data inválido. Use YYYY-MM-DD");
// Converter para DateTime com hora 00:00:00 do fuso local
naive_date.and_hms_opt(0, 0, 0).unwrap()
} else {
// Comportamento padrão: dia anterior ao atual
(chrono::Local::now() - chrono::Duration::days(1)).naive_local()
};
println!("Processando o dia: {}", target_day.format("%Y-%m-%d"));
// Definir início e fim do dia (para consultas na API)
let day_start = target_day; // já 00:00
let day_end = target_day
.with_hour(23).unwrap()
.with_minute(59).unwrap()
.with_second(59).unwrap();
let formatted_day = target_day.format("%Y-%m-%d").to_string();
let formatted_day_start = day_start.format("%Y-%m-%d %H:%M").to_string();
let formatted_day_end = day_end.format("%Y-%m-%d %H:%M").to_string();
println!("Início do dia: {}", formatted_day_start);
println!("Fim do dia: {}", formatted_day_end);
// Criar pasta com o nome do dia processado
if !std::fs::exists(format!("./evaluations/{formatted_day}")).unwrap() {
std::fs::create_dir(format!("./evaluations/{formatted_day}"))
.expect("Failed to create directory");
}
// Criar arquivo response_time.csv se não existir
if !std::fs::exists(format!("./evaluations/{formatted_day}/response_time.csv")).unwrap() {
let _ = std::fs::File::create_new(format!(
"./evaluations/{formatted_day}/response_time.csv"
))
.expect("Failed to create response_time.csv");
}
*/
// Read system prompt // Read system prompt
let prompt = std::fs::read_to_string("PROMPT.txt").unwrap(); let prompt = std::fs::read_to_string("PROMPT.txt").unwrap();
let filter_file_contents = std::fs::read_to_string("FILTER.txt").unwrap_or(String::new()); let filter_file_contents = std::fs::read_to_string("FILTER.txt").unwrap_or(String::new());
@@ -197,7 +243,9 @@ fn main() -> anyhow::Result<()> {
&client, &client,
&access_token, &access_token,
formatted_day_before_at_midnight, formatted_day_before_at_midnight,
//formatted_day_start,
formatted_day_before_at_23_59_59, formatted_day_before_at_23_59_59,
//formatted_day_end,
); );
println!("Number of consolidated talks: {}", talks_array.len()); println!("Number of consolidated talks: {}", talks_array.len());
@@ -298,12 +346,16 @@ fn main() -> anyhow::Result<()> {
let message = message_object["message"] let message = message_object["message"]
.as_str() .as_str()
.expect("Failed to decode message as string"); .expect("Failed to decode message as string");
let found = filter_keywords.iter().any(|keyword| { let found1 = filter_keywords.iter().any(|keyword| {
message message
.to_uppercase() .to_uppercase()
.find(&keyword.to_uppercase()) .find(&keyword.to_uppercase())
.is_some() .is_some()
}); });
let found2 = message_object["is_template"]
.as_bool()
.unwrap_or(true);
let found = found1 || found2;
found found
}); });
@@ -421,6 +473,7 @@ fn main() -> anyhow::Result<()> {
.write(true) .write(true)
.open(format!( .open(format!(
"./evaluations/{formatted_day_before}/response_time.csv" "./evaluations/{formatted_day_before}/response_time.csv"
//"./evaluations/{formatted_day}/response_time.csv"
)) ))
.expect("Failed to open response time file for write"); .expect("Failed to open response time file for write");
response_time_file response_time_file
@@ -499,6 +552,7 @@ fn main() -> anyhow::Result<()> {
format!( format!(
"./evaluations/{}/{} - {} - {}.csv", "./evaluations/{}/{} - {} - {}.csv",
formatted_day_before, user_name, talk_id, tracking_number formatted_day_before, user_name, talk_id, tracking_number
//formatted_day, user_name, talk_id, tracking_number
), ),
csv_response, csv_response,
) )
@@ -507,6 +561,7 @@ fn main() -> anyhow::Result<()> {
format!( format!(
"./evaluations/{}/{} - {} - {} - prompt.txt", "./evaluations/{}/{} - {} - {} - prompt.txt",
formatted_day_before, user_name, talk_id, tracking_number formatted_day_before, user_name, talk_id, tracking_number
//formatted_day, user_name, talk_id, tracking_number
), ),
format!("{prompt} \n{talk}"), format!("{prompt} \n{talk}"),
) )
@@ -520,17 +575,20 @@ fn main() -> anyhow::Result<()> {
// Compress folder into zip // Compress folder into zip
let source_dir_str = format!("./evaluations/{formatted_day_before}"); let source_dir_str = format!("./evaluations/{formatted_day_before}");
//let source_dir_str = format!("./evaluations/{formatted_day}");
let output_zip_file_str = format!("./evaluations/{formatted_day_before}.zip"); let output_zip_file_str = format!("./evaluations/{formatted_day_before}.zip");
//let output_zip_file_str = format!("./evaluations/{formatted_day}.zip");
let source_dir = std::path::Path::new(source_dir_str.as_str()); let source_dir = std::path::Path::new(source_dir_str.as_str());
let output_zip_file = std::path::Path::new(output_zip_file_str.as_str()); let output_zip_file = std::path::Path::new(output_zip_file_str.as_str());
zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file(source_dir, output_zip_file); zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file(source_dir, output_zip_file);
// Send folder to email // 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 = "nicolas.borges@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_mail_util::send_mail_util::send_email( send_mail_util::send_mail_util::send_email(
&format!("Avaliacao atendimentos {formatted_day_before}"), &format!("Avaliacao atendimentos {formatted_day_before}"),
//&format!("Avaliacao atendimentos {formatted_day}"),
&BOT_EMAIL, &BOT_EMAIL,
&BOT_EMAIL_PASSWORD, &BOT_EMAIL_PASSWORD,
recipients, recipients,
@@ -545,7 +603,9 @@ fn get_piperun_chats_on_date(
client: &reqwest::blocking::Client, client: &reqwest::blocking::Client,
access_token: &String, access_token: &String,
formatted_day_before_at_midnight: String, formatted_day_before_at_midnight: String,
//formatted_day_start: String,
formatted_day_before_at_23_59_59: String, formatted_day_before_at_23_59_59: String,
//formatted_day_end: String,
) -> Vec<serde_json::Value> { ) -> Vec<serde_json::Value> {
let start_of_talk_code: String = "talk_start".to_string(); let start_of_talk_code: String = "talk_start".to_string();
let support_queue_id: String = "13".to_string(); let support_queue_id: String = "13".to_string();
@@ -565,7 +625,9 @@ fn get_piperun_chats_on_date(
("perPage", per_page.clone()), ("perPage", per_page.clone()),
("report_type", report_type.clone()), ("report_type", report_type.clone()),
("start_date", formatted_day_before_at_midnight.clone()), ("start_date", formatted_day_before_at_midnight.clone()),
//("start_date", formatted_day_start.clone()),
("end_date", formatted_day_before_at_23_59_59.clone()), ("end_date", formatted_day_before_at_23_59_59.clone()),
//("end_date", formatted_day_end.clone()),
("date_range_type", start_of_talk_code.clone()), ("date_range_type", start_of_talk_code.clone()),
("queue_id[]", support_queue_id.clone()), ("queue_id[]", support_queue_id.clone()),
]); ]);
@@ -621,7 +683,9 @@ fn get_piperun_chats_on_date(
("perPage", per_page.clone()), ("perPage", per_page.clone()),
("report_type", report_type.clone()), ("report_type", report_type.clone()),
("start_date", formatted_day_before_at_midnight.clone()), ("start_date", formatted_day_before_at_midnight.clone()),
//("start_date", formatted_day_start.clone()),
("end_date", formatted_day_before_at_23_59_59.clone()), ("end_date", formatted_day_before_at_23_59_59.clone()),
//("end_date", formatted_day_end.clone()),
("date_range_type", start_of_talk_code.clone()), ("date_range_type", start_of_talk_code.clone()),
("queue_id[]", support_queue_id.clone()), ("queue_id[]", support_queue_id.clone()),
]); ]);