diff --git a/src/funcoes/config.rs b/src/funcoes/config.rs deleted file mode 100644 index 447073d..0000000 --- a/src/funcoes/config.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::env; -use anyhow::Result; - -pub struct AppConfig { - pub ollama_url: String, - pub ollama_port: u16, - pub ollama_model: String, - pub piperun_api_url: String, - pub piperun_client_id: i32, - pub piperun_client_secret: String, - pub piperun_username: String, - pub piperun_password: String, - pub bot_email: String, - pub bot_email_password: String, - pub min_messages: usize, - pub min_agent_messages: usize, -} - -impl AppConfig { - pub fn from_env() -> Result { - dotenv::dotenv().ok(); - - Ok(AppConfig { - ollama_url: env::var("OLLAMA_URL").unwrap_or_else(|_| "localhost".to_string()), - ollama_port: env::var("OLLAMA_PORT") - .unwrap_or_else(|_| "11432".to_string()) - .parse::() - .unwrap_or(11432), - ollama_model: env::var("OLLAMA_AI_MODEL") - .expect("OLLAMA_AI_MODEL has not been set!"), - piperun_api_url: env::var("PIPERUN_API_URL") - .expect("PIPERUN_API_URL has not been set!"), - piperun_client_id: env::var("PIPERUN_CLIENT_ID") - .expect("PIPERUN_CLIENT_ID has not been set!") - .parse::() - .unwrap_or(0), - piperun_client_secret: env::var("PIPERUN_CLIENT_SECRET") - .expect("PIPERUN_CLIENT_SECRET has not been set!"), - piperun_username: env::var("PIPERUN_BOT_USERNAME") - .expect("PIPERUN_BOT_USERNAME has not been set!"), - piperun_password: env::var("PIPERUN_BOT_PASSWORD") - .expect("PIPERUN_BOT_PASSWORD has not been set!"), - bot_email: env::var("BOT_EMAIL") - .expect("BOT_EMAIL has not been set!"), - bot_email_password: env::var("BOT_EMAIL_PASSWORD") - .expect("BOT_EMAIL_PASSWORD has not been set!"), - min_messages: env::var("MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE") - .expect("MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE has not been set!") - .parse::() - .unwrap_or(10), - min_agent_messages: env::var("MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE") - .expect("MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE has not been set!") - .parse::() - .unwrap_or(12), - }) - } - - pub fn print_config(&self) { - println!("OLLAMA_URL: {}", self.ollama_url); - println!("OLLAMA_PORT: {}", self.ollama_port); - println!("OLLAMA_AI_MODEL: {}", self.ollama_model); - println!("PIPERUN_API_URL: {}", self.piperun_api_url); - println!("PIPERUN_CLIENT_ID: {}", self.piperun_client_id); - println!("PIPERUN_CLIENT_SECRET: {}", self.piperun_client_secret); - println!("PIPERUN_BOT_USERNAME: {}", self.piperun_username); - println!("PIPERUN_BOT_PASSWORD: {}", self.piperun_password); - } -} diff --git a/src/funcoes/date_utils.rs b/src/funcoes/date_utils.rs deleted file mode 100644 index c955c2d..0000000 --- a/src/funcoes/date_utils.rs +++ /dev/null @@ -1,79 +0,0 @@ -use anyhow::{anyhow, Result}; -use chrono::{Duration, Local, NaiveDate, NaiveDateTime, Timelike}; - -pub struct DateRange { - pub date_str: String, // "YYYY-MM-DD" - pub start_datetime: String, // "YYYY-MM-DD HH:MM" - pub end_datetime: String, // "YYYY-MM-DD HH:MM" -} - -/// Retorna o intervalo do dia anterior ao atual (meia-noite até 23:59:59) -pub fn get_previous_date_range() -> Result { - let now = Local::now(); - let yesterday = now - .checked_sub_signed(Duration::days(1)) - .expect("Failed to subtract one day"); - - let start = yesterday - .with_hour(0) - .and_then(|d| d.with_minute(0)) - .and_then(|d| d.with_second(0)) - .expect("Failed to set time to 00:00:00"); - - let end = yesterday - .with_hour(23) - .and_then(|d| d.with_minute(59)) - .and_then(|d| d.with_second(59)) - .expect("Failed to set time to 23:59:59"); - - let date_str = yesterday.format("%Y-%m-%d").to_string(); - let start_str = start.format("%Y-%m-%d %H:%M").to_string(); - let end_str = end.format("%Y-%m-%d %H:%M").to_string(); - - Ok(DateRange { - date_str, - start_datetime: start_str, - end_datetime: end_str, - }) -} - -/// Constrói o DateRange a partir de uma data (meia-noite) -fn build_date_range(date: NaiveDateTime) -> Result { - let start = date; - let end = date - .with_hour(23) - .and_then(|d| d.with_minute(59)) - .and_then(|d| d.with_second(59)) - .ok_or_else(|| anyhow!("Falha ao definir horário de fim do dia"))?; - - let date_str = date.format("%Y-%m-%d").to_string(); - let start_str = start.format("%Y-%m-%d %H:%M").to_string(); - let end_str = end.format("%Y-%m-%d %H:%M").to_string(); - - Ok(DateRange { - date_str, - start_datetime: start_str, - end_datetime: end_str, - }) -} - -pub fn parse_date_arg(arg: &str) -> Result { - let naive = NaiveDate::parse_from_str(arg, "%Y-%m-%d") - .map_err(|_| anyhow!("Formato inválido"))?; - let date = naive.and_hms_opt(0, 0, 0).unwrap(); - build_date_range(date) -} - -/// Cria o diretório de saída e o arquivo response_time.csv vazio (se não existir) -pub fn setup_output_directory(dir_path: &str) -> Result<()> { - if !std::fs::exists(dir_path)? { - std::fs::create_dir_all(dir_path)?; - } - - let csv_path = format!("{}/response_time.csv", dir_path); - if !std::fs::exists(&csv_path)? { - let _ = std::fs::File::create_new(&csv_path) - .expect("Failed to create response_time.csv"); - } - Ok(()) -} diff --git a/src/funcoes/main.rs b/src/funcoes/main.rs deleted file mode 100644 index a962ec0..0000000 --- a/src/funcoes/main.rs +++ /dev/null @@ -1,173 +0,0 @@ -mod funcoes; - -use funcoes::config::AppConfig; -use funcoes::date_utils; -use funcoes::ollama; -use funcoes::piperun; -use funcoes::send_mail_util; -use funcoes::zip_directory_util; - -use anyhow::Result; -use reqwest::blocking::Client; - -fn main() -> Result<()> { - // 1. Carregar configuração - let config = AppConfig::from_env()?; - config.print_config(); - - // 2. Cliente HTTP - let client = Client::new(); - - // 3. Autenticar na Piperun - let access_token = piperun::authenticate(&client, &config)?; - println!("Autenticado com sucesso!"); - - // 4. Coletar argumentos - let args: Vec = std::env::args().collect(); - let mut idx = 1; // índice atual - - // Data (opcional) - let date_range = if args.len() > 1 { - match date_utils::parse_date_arg(&args[1]) { - Ok(dr) => { - idx = 2; - dr - } - Err(_) => { - eprintln!("Aviso: argumento de data inválido, usando dia anterior."); - date_utils::get_previous_date_range()? - } - } - } else { - date_utils::get_previous_date_range()? - }; - println!("Processando dia: {}", date_range.date_str); - - // Parâmetros obrigatórios: QUEUE_ID PROMPT_FILE FILTER_FILE MIN_AGENT_MSGS FOLDER_NAME EMAILS - let expected_min = if idx == 2 { 7 } else { 6 }; - if args.len() < idx + expected_min { - eprintln!("Uso: cargo run -- [DATA] QUEUE_ID PROMPT_FILE FILTER_FILE MIN_AGENT_MSGS FOLDER_NAME EMAILS"); - eprintln!(" DATA (opcional) - YYYY-MM-DD"); - eprintln!(" Exemplo: cargo run -- 2026-07-12 19 prompts/PROMPT_NOC-CLI.txt filters/FILTER_NOC-CLI.txt 10 evaluations_noc-cli nicolas@nova.net,wilson@nova.net"); - std::process::exit(1); - } - - let queue_id = &args[idx]; - let prompt_file = &args[idx+1]; - let filter_file = &args[idx+2]; - let min_agent_msgs: usize = args[idx+3].parse().expect("MIN_AGENT_MSGS deve ser um número"); - let folder_name = &args[idx+4]; - let emails = &args[idx+5]; - - println!("Queue ID: {}", queue_id); - println!("Prompt file: {}", prompt_file); - println!("Filter file: {}", filter_file); - println!("Min agent messages: {}", min_agent_msgs); - println!("Folder name: {}", folder_name); - println!("E-mails: {}", emails); - - // 5. Preparar diretório de saída - let output_dir = format!("./evaluations/{}/{}", folder_name, date_range.date_str); - date_utils::setup_output_directory(&output_dir)?; - - // 6. Carregar prompt e filtros - let prompt = std::fs::read_to_string(prompt_file)?; - let filter_content = std::fs::read_to_string(filter_file).unwrap_or_default(); - let filter_keywords: Vec = filter_content - .split('\n') - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()) - .collect(); - - // 7. Buscar talks - let talks = piperun::fetch_talks( - &client, - &access_token, - &config, - &date_range.start_datetime, - &date_range.end_datetime, - queue_id, - )?; - println!("Total de talks encontrados: {}", talks.len()); - - let talk_ids = piperun::extract_talk_ids(&talks); - - // 8. Processar chats - let (valid_chats, response_time_csv) = piperun::process_all_chats( - talk_ids, - &client, - &access_token, - &config, - &filter_keywords, - min_agent_msgs, - )?; - println!("Chats válidos após filtros: {}", valid_chats.len()); - - // 9. Salvar CSV de tempos - let header = "NOME;ID_TALK;TEMPO DE RESPOSTA;TRANFERENCIA PELO BOT;PRIMEIRA RESPOSTA DO AGENTE"; - let response_time_path = format!("{}/response_time.csv", output_dir); - std::fs::write(&response_time_path, format!("{}\n{}", header, response_time_csv))?; - - // 10. Avaliar com Ollama - for chat in &valid_chats { - match ollama::evaluate_chat(&client, &config, chat, &prompt) { - Ok(csv) => { - // Reconstroi o talk_text - let histories = chat["talk_histories"] - .as_array() - .or_else(|| chat["talk_histories"]["data"].as_array()) - .expect("histories array"); - let talk_text = histories - .iter() - .rev() - .map(|msg| { - format!( - "message: {}, sent_at: {}, type: {}, user_name: {}", - msg["message"], - msg["sent_at"], - msg["type"], - msg["user"]["name"] - ) - }) - .collect::>() - .join("\n"); - - if let Err(e) = ollama::save_evaluation_result( - chat, - &csv, - &prompt, - &talk_text, - &output_dir, - ) { - eprintln!("Erro ao salvar resultado: {}", e); - } - } - Err(e) => { - eprintln!("Erro ao avaliar chat com Ollama: {}", e); - } - } - } - - // 11. Compactar e enviar e-mail - let zip_file = format!("{}.zip", output_dir); - zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file( - std::path::Path::new(&output_dir), - std::path::Path::new(&zip_file), - ); - - println!("Enviando e-mail para {}", emails); - send_mail_util::send_mail_util::send_email( - &format!( - "Avaliação fila {} do dia {}", - queue_id, - date_range.date_str - ), - &config.bot_email, - &config.bot_email_password, - emails, - &zip_file, - ); - - println!("Processamento finalizado para a fila {}", queue_id); - Ok(()) -} diff --git a/src/funcoes/mod.rs b/src/funcoes/mod.rs deleted file mode 100644 index c8199bf..0000000 --- a/src/funcoes/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod config; -pub mod date_utils; -pub mod ollama; -pub mod piperun; -pub mod send_mail_util; -pub mod zip_directory_util; diff --git a/src/funcoes/ollama.rs b/src/funcoes/ollama.rs deleted file mode 100644 index 1c4234d..0000000 --- a/src/funcoes/ollama.rs +++ /dev/null @@ -1,98 +0,0 @@ -use anyhow::{anyhow, Result}; -use reqwest::blocking::Client; -use serde_json::Value; -use std::time::Duration; -use crate::funcoes::config::AppConfig; - -pub fn evaluate_chat( - client: &Client, - config: &AppConfig, - chat_json: &Value, - prompt: &str, -) -> Result { - let histories = chat_json["talk_histories"] - .as_array() - .or_else(|| chat_json["talk_histories"]["data"].as_array()) - .ok_or_else(|| anyhow!("Missing talk_histories array"))?; - - let talk_text = histories - .iter() - .rev() - .map(|msg| { - format!( - "message: {}, sent_at: {}, type: {}, user_name: {}", - msg["message"], - msg["sent_at"], - msg["type"], - msg["user"]["name"] - ) - }) - .collect::>() - .join("\n"); - - let full_prompt = format!("{}\n{}", prompt, talk_text); - - let sanitized_ip = if config.ollama_url.contains(':') && !config.ollama_url.starts_with('[') { - format!("[{}]", config.ollama_url) - } else { - config.ollama_url.clone() - }; - - let url = format!("http://{}:{}/api/generate", sanitized_ip, config.ollama_port); - - let body = serde_json::json!({ - "model": config.ollama_model, - "prompt": full_prompt, - "stream": false, - }); - - let resp = client - .post(&url) - .body(body.to_string()) - .timeout(Duration::from_secs(3600)) - .send()?; - - if !resp.status().is_success() { - return Err(anyhow!( - "Ollama request failed: status {}", - resp.status() - )); - } - - let json: Value = resp.json()?; - let ai_response = json["response"] - .as_str() - .ok_or_else(|| anyhow!("Missing 'response' field in Ollama reply"))? - .to_string(); - - let csv = ai_response - .replace("```csv\n", "") - .replace("```", "") - .trim() - .to_string(); - - Ok(csv) -} - -pub fn save_evaluation_result( - chat_json: &Value, - csv_response: &str, - prompt: &str, - talk_text: &str, - output_dir: &str, -) -> Result<()> { - let user_name = chat_json["agent"]["user"]["name"] - .as_str() - .unwrap_or("unknown_user"); - let talk_id = chat_json["id"].as_u64().unwrap_or(0); - let tracking = chat_json["tracking_number"].as_str().unwrap_or(""); - - let base_name = format!("{} - {} - {}", user_name, talk_id, tracking); - let csv_path = format!("{}/{}.csv", output_dir, base_name); - std::fs::write(&csv_path, csv_response)?; - - let prompt_path = format!("{}/{} - prompt.txt", output_dir, base_name); - std::fs::write(&prompt_path, format!("{}\n{}", prompt, talk_text))?; - - Ok(()) -} diff --git a/src/funcoes/piperun.rs b/src/funcoes/piperun.rs deleted file mode 100644 index a76d53b..0000000 --- a/src/funcoes/piperun.rs +++ /dev/null @@ -1,329 +0,0 @@ -use anyhow::{anyhow, Result}; -use reqwest::blocking::Client; -use serde_json::Value; -use crate::funcoes::config::AppConfig; - -// ------------------------------ -// Funções públicas -// ------------------------------ - -pub fn authenticate(client: &Client, config: &AppConfig) -> Result { - let url = format!("https://{}/oauth/token", config.piperun_api_url); - let body = serde_json::json!({ - "grant_type": "password", - "client_id": config.piperun_client_id, - "client_secret": config.piperun_client_secret, - "username": config.piperun_username, - "password": config.piperun_password, - }); - - let resp = client - .post(&url) - .header("Content-Type", "application/json") - .header("Accept", "application/json") - .body(body.to_string()) - .send()?; - - let status = resp.status(); - if !status.is_success() { - let error_body: Value = resp.json()?; - return Err(anyhow!( - "Authentication failed: status {}; body: {:?}", - status, - error_body - )); - } - - let json: Value = resp.json()?; - let token = json["access_token"] - .as_str() - .ok_or_else(|| anyhow!("Access token not found in response"))? - .to_string(); - - Ok(token) -} - -pub fn fetch_talks( - client: &Client, - access_token: &str, - config: &AppConfig, - start_date: &str, - end_date: &str, - queue_id: &str, -) -> Result> { - let per_page = "15".to_string(); - let report_type = "consolidated".to_string(); - - let mut all_talks = Vec::new(); - let mut current_page = 1; - let mut last_page = 1; - - loop { - let url = format!("https://{}/api/v2/reports/talks", config.piperun_api_url); - let resp = client - .get(&url) - .bearer_auth(access_token) - .header("Content-Type", "application/json") - .header("Accept", "application/json") - .query(&[ - ("page", current_page.to_string()), - ("perPage", per_page.clone()), - ("report_type", report_type.clone()), - ("start_date", start_date.to_string()), - ("end_date", end_date.to_string()), - ("date_range_type", "talk_start".to_string()), - ("queue_id[]", queue_id.to_string()), - ]) - .send()?; - - let status = resp.status(); - if !status.is_success() { - let error_body: Value = resp.json()?; - return Err(anyhow!( - "Failed to fetch talks: status {}; body: {:?}", - status, - error_body - )); - } - - let json: Value = resp.json()?; - let data = json["data"] - .as_array() - .ok_or_else(|| anyhow!("Missing 'data' array in response"))? - .clone(); - - all_talks.extend(data); - - last_page = json["last_page"] - .as_i64() - .ok_or_else(|| anyhow!("Missing 'last_page'"))? as i32; - let current = json["current_page"] - .as_i64() - .ok_or_else(|| anyhow!("Missing 'current_page'"))? as i32; - - if current >= last_page { - break; - } - current_page = current + 1; - } - - Ok(all_talks) -} - -pub fn extract_talk_ids(talks: &[Value]) -> Vec { - talks - .iter() - .filter_map(|talk| talk["id"].as_i64().map(|id| id.to_string())) - .collect() -} - -pub fn process_all_chats( - talk_ids: Vec, - client: &Client, - access_token: &str, - config: &AppConfig, - filter_keywords: &[String], - min_agent_msgs: usize, -) -> Result<(Vec, String)> { - let mut valid_chats = Vec::new(); - let mut response_lines = Vec::new(); - - for talk_id in talk_ids { - let history_json = match fetch_talk_history(client, access_token, config, &talk_id) { - Ok(j) => j, - Err(e) => { - eprintln!("Erro ao buscar histórico do talk {}: {}", talk_id, e); - continue; - } - }; - - let filtered = match filter_chat(&history_json, filter_keywords, config.min_messages, min_agent_msgs) { - Some(v) => v, - None => continue, - }; - - if let Some(line) = compute_response_time(&filtered) { - response_lines.push(line); - } - - valid_chats.push(filtered); - } - - let response_csv = response_lines.join("\n"); - Ok((valid_chats, response_csv)) -} - -// ------------------------------ -// Funções auxiliares privadas -// ------------------------------ - -fn fetch_talk_history( - client: &Client, - access_token: &str, - config: &AppConfig, - talk_id: &str, -) -> Result { - let url = format!("https://{}/api/talk_histories", config.piperun_api_url); - let resp = client - .get(&url) - .bearer_auth(access_token) - .header("Content-Type", "application/json") - .header("Accept", "application/json") - .query(&[ - ("talk_id", talk_id.to_string()), - ("type", "a".to_string()), - ("only_view", "1".to_string()), - ]) - .send()?; - - let status = resp.status(); - if !status.is_success() { - return Err(anyhow!( - "Failed to fetch history for talk {}: status {}", - talk_id, - status - )); - } - - let json: Value = resp.json()?; - Ok(json) -} - -fn filter_chat( - chat_json: &Value, - filter_keywords: &[String], - min_messages: usize, - min_agent_msgs: usize, -) -> Option { - let histories = extract_histories_array(chat_json)?; - - if histories.len() < min_messages { - println!("Descartado - Chat com poucas mensagens."); - return None; - } - - let found = histories.iter().enumerate().find(|(_, msg)| { - let message = msg["message"].as_str().unwrap_or(""); - let is_template = msg["is_template"].as_bool().unwrap_or(false); - let is_transfer = message.contains( - "Atendimento transferido para a fila [NovaNet -> Atendimento -> NOC", - ); - is_transfer || is_template - }); - - let (pos, _) = match found { - Some(p) => p, - None => { - println!("Descartado - Não encontrou transferência ou template."); - return None; - } - }; - - if pos < min_agent_msgs { - println!("Descartado - Poucas mensagens com agente após transferência."); - return None; - } - - if chat_json["finished_at"].is_null() { - println!("Descartado - Chat não finalizado."); - return None; - } - - if chat_json["agent"]["user"]["name"] - .as_str() - .unwrap_or("") - == "PipeBot" - { - println!("Descartado - Chat finalizado pelo PipeBot."); - return None; - } - - let has_keyword = histories.iter().any(|msg| { - let text = msg["message"].as_str().unwrap_or(""); - filter_keywords - .iter() - .any(|kw| text.to_uppercase().contains(&kw.to_uppercase())) - }); - - if has_keyword { - println!("Descartado - Contém palavra do filtro."); - return None; - } - - Some(chat_json.clone()) -} - -fn compute_response_time(chat_json: &Value) -> Option { - let histories = extract_histories_array(chat_json)?; - - let trigger = histories.iter().enumerate().rev().find(|(_, msg)| { - let is_template = msg["is_template"].as_bool().unwrap_or(false); - let user = msg["user"]["name"].as_str().unwrap_or(""); - let message = msg["message"].as_str().unwrap_or(""); - let is_transfer = user == "PipeBot" - && message.contains("Atendimento entregue da fila de espera para o agente [NOC"); - is_template || is_transfer - }); - - let (trigger_pos, trigger_msg) = trigger?; - - let is_transfer_case = { - let user = trigger_msg["user"]["name"].as_str().unwrap_or(""); - let message = trigger_msg["message"].as_str().unwrap_or(""); - user == "PipeBot" - && message.contains("Atendimento entregue da fila de espera para o agente [NOC") - }; - - let (ref_pos, ref_msg) = if is_transfer_case { - (trigger_pos, trigger_msg) - } else { - if trigger_pos == 0 { - return None; - } - (trigger_pos - 1, &histories[trigger_pos - 1]) - }; - - let agent_msg = (0..ref_pos).rev().find_map(|i| { - let m = &histories[i]; - if m["type"].as_str() == Some("out") - && m["user"]["name"] - .as_str() - .map_or(false, |name| name.starts_with("NOC -")) - { - Some(m) - } else { - None - } - })?; - - let format = "%Y-%m-%d %H:%M:%S"; - let t_ref = chrono::NaiveDateTime::parse_from_str( - ref_msg["sent_at"].as_str()?, - format, - ) - .ok()?; - let t_agent = chrono::NaiveDateTime::parse_from_str( - agent_msg["sent_at"].as_str()?, - format, - ) - .ok()?; - - let response_time = (t_agent - t_ref).num_seconds() as f32; - - let name = agent_msg["user"]["name"].as_str().unwrap_or("").to_owned(); - let id = chat_json["tracking_number"].as_str().unwrap_or("").to_owned(); - let ref_date = ref_msg["sent_at"].as_str().unwrap_or("").to_owned(); - let agent_date = agent_msg["sent_at"].as_str().unwrap_or("").to_owned(); - - Some(format!( - "{};{};{};{};{}", - name, id, response_time, ref_date, agent_date - )) -} - -fn extract_histories_array(chat_json: &Value) -> Option<&[Value]> { - chat_json["talk_histories"] - .as_array() - .or_else(|| chat_json["talk_histories"]["data"].as_array()) - .map(|v| &**v) // <-- conversão de &Vec para &[Value] -} diff --git a/src/funcoes/send_mail_util.rs b/src/funcoes/send_mail_util.rs deleted file mode 100644 index e6dbb28..0000000 --- a/src/funcoes/send_mail_util.rs +++ /dev/null @@ -1,46 +0,0 @@ -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(); - } -} diff --git a/src/funcoes/zip_directory_util.rs b/src/funcoes/zip_directory_util.rs deleted file mode 100644 index c7ae699..0000000 --- a/src/funcoes/zip_directory_util.rs +++ /dev/null @@ -1,69 +0,0 @@ -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( - it: &mut dyn Iterator, - 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); - } -} diff --git a/src/old-version/.env.example b/src/old-version/.env.example deleted file mode 100644 index 1e36132..0000000 --- a/src/old-version/.env.example +++ /dev/null @@ -1,7 +0,0 @@ -OLLAMA_URL=localhost -OLLAMA_PORT=11432 -PIPERUN_API_URL=novanet.cxm.pipe.run -PIPERUN_CLIENT_ID=0 -PIPERUN_CLIENT_SECRET= -PIPERUN_BOT_USERNAME= -PIPERUN_BOT_PASSWORD= \ No newline at end of file diff --git a/src/old-version/groupped_repport_weekly.rs.save b/src/old-version/groupped_repport_weekly.rs.save deleted file mode 100644 index 89f0082..0000000 --- a/src/old-version/groupped_repport_weekly.rs.save +++ /dev/null @@ -1,493 +0,0 @@ -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, -} - -#[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, -} - -//inclusão de estrutura para agrupar o response_time.cvs -#[derive(Debug, serde::Deserialize)] -struct ResponseTimeRecord { - NOME: String, - ID_TALK: String, - #[serde(rename = "TEMPO DE RESPOSTA")] - TEMPO_DE_RESPOSTA: u32, - #[serde(rename = "TRANFERENCIA PELO BOT")] - TRANFERENCIA_PELO_BOT: String, - #[serde(rename = "PRIMEIRA RESPOSTA DO AGENTE")] - PRIMEIRA_RESPOSTA_DO_AGENTE: String, -} -//fim da inclusão - -fn main() { - match dotenv::dotenv().ok() { - Some(_) => println!("Environment variables loaded from .env file"), - None => eprintln!("Failed to load .env file, using defaults"), - } - - // Read environment variables - let OLLAMA_URL = env::var("OLLAMA_URL").unwrap_or("localhost".to_string()); - let OLLAMA_PORT = env::var("OLLAMA_PORT") - .unwrap_or("11432".to_string()) - .parse::() - .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::() - .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::(); - - 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: FIN - Lais Mota - 515578 - 20251020515578.csv - // id talk is the last information, so in the example is: 20251020515578 - let regex_filename = - //regex::Regex::new(r"(FIN - )((\s*\w+\s*)+) - (\d+) - (\d+).csv").unwrap(); - - 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::() as f32 - / columns.iter().len() as f32 - * 100.0; - columns.push(Column::new( - "PERCENTUAL DE EXELENCIA".into(), - [format!("{excelence_percentual:.2}")], - )); - - columns.push(Column::new("ID_TALK".into(), [talk_id.clone().as_str()])); - - let df = polars::frame::DataFrame::new(columns) - .expect("Failed to concatenate into a dataframe"); - - // return a tuple with the dataframe and the user name, so it can be correctly merged after - return Some((user_name.as_str().to_owned(), df)); - }) - .filter_map(|res| { - if res.is_ok() { - return Some(res.unwrap()); - } - return None; - }) - .into_group_map() - .into_iter() - .map(|(name, eval_dataframe_vec)| { - let groupped_df = eval_dataframe_vec - .iter() - .cloned() - .reduce(|acc, e| acc.vstack(&e).unwrap()) - .expect("Failed to concatenate dataframes"); - (name, groupped_df) - }) - .into_group_map(); - - dbg!(&groupped_by_user_on_day); - return Some(groupped_by_user_on_day); - }) - .filter_map(|res| { - if res.is_ok() { - return Some(res.unwrap()); - } - return None; - }) - .reduce(|mut acc, mut e| { - e.iter_mut().for_each(|(key, val)| { - if acc.contains_key(key) { - acc.get_mut(key) - .expect("Failed to obtain key that should already be present") - .append(val); - } else { - acc.insert(key.to_owned(), val.to_owned()); - } - }); - acc - }) - .and_then(|groupped_hashmap_df| { - let result = groupped_hashmap_df - .iter() - .map(|(key, val)| { - let dfs = val - .iter() - .cloned() - .reduce(|acc, e| acc.vstack(&e).unwrap()) - .expect("Failed to concatenate dataframes"); - (key.clone(), dfs) - }) - .collect_vec(); - return Some(result); - }); - - // Setup groupped folder - if !std::fs::exists(format!("./groupped/")).unwrap() { - std::fs::create_dir(format!("./groupped")).expect("Failed to create directory") - } - - // Setup previous week folder - if !std::fs::exists(format!( - "./groupped/{first_day_of_last_week} - {last_day_of_last_week}" - )) - .unwrap() - { - std::fs::create_dir(format!( - "./groupped/{first_day_of_last_week} - {last_day_of_last_week}" - )) - .expect("Failed to create directory") - } - - match groupped_values { - Some(mut val) => { - val.iter_mut().for_each(|(agent, groupped_evaluations)| { - let mut save_file_csv = std::fs::File::create(format!( - "./groupped/{first_day_of_last_week} - {last_day_of_last_week}/{agent}.csv" - )) - .expect("Could not create csv file for saving"); - CsvWriter::new(&mut save_file_csv) - .include_header(true) - .with_separator(b';') - .finish(groupped_evaluations) - .expect("Failed to save Groupped DataFrame to CSV File"); - }); - } - None => {} - } - -//inclusão nova para agrupar o response_time.csv - // Processar response_time.csv separadamente - let response_times_data = previous_week_folder_names - .iter() - .map(|folder_name| { - let folder_base_path = std::path::Path::new("./evaluations"); - let folder_date_path = folder_base_path.join(folder_name); - std::fs::read_dir(folder_date_path) - }) - .filter_map_ok(|files_inside_folder_on_date| { - let response_time_files = files_inside_folder_on_date - .filter_ok(|entry| { - let entry_file_name_as_str = entry - .file_name() - .into_string() - .expect("Failed to get filename as a String"); - - entry_file_name_as_str.ends_with("response_time.csv") - }) - .filter_map(|value| { - if value.is_ok() { - return Some(value.unwrap()); - } - None - }) - .map(|file_path| { - println!("Processing response time file: {:?}", file_path.path()); - - let mut rdr = csv::ReaderBuilder::new() - .delimiter(b';') - .has_headers(true) - .from_reader(std::fs::File::open(file_path.path()).unwrap()); - - let records: Vec = rdr - .deserialize() - .filter_map(Result::ok) - .collect(); - - records - }) - .flat_map(|records| records) - .collect_vec(); - - Some(response_time_files) - }) - .filter_map(|res| { - if res.is_ok() { - return Some(res.unwrap()); - } - return None; - }) - .flat_map(|records| records) - .collect_vec(); - // Salvar response times consolidados - if !response_times_data.is_empty() { - let response_time_file_path = format!( - "./groupped/{first_day_of_last_week} - {last_day_of_last_week}/response_times_consolidated.csv" - ); - - let mut wtr = csv::WriterBuilder::new() - .delimiter(b';') - .from_path(response_time_file_path) - .expect("Failed to create response times CSV"); - - // Escrever cabeçalho - wtr.write_record(&["NOME", "ID_TALK", "TEMPO DE RESPOSTA", "TRANFERENCIA PELO BOT", "PRIMEIRA RESPOSTA DO AGENTE"]) - .expect("Failed to write header"); - - for record in response_times_data { - wtr.write_record(&[ - &record.NOME, - &record.ID_TALK, - &record.TEMPO_DE_RESPOSTA.to_string(), - &record.TRANFERENCIA_PELO_BOT, - &record.PRIMEIRA_RESPOSTA_DO_AGENTE, - ]).expect("Failed to write record"); - } - - wtr.flush().expect("Failed to flush writer"); - println!("Response times consolidated successfully!"); - } else { - println!("No response time data found for the period."); - } -// --- FIM DA ADIÇÃO --- - -//fim da inclusão - - zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file( - std::path::Path::new(&format!( - "./groupped/{first_day_of_last_week} - {last_day_of_last_week}" - )), - std::path::Path::new(&format!( - "./groupped/{first_day_of_last_week} - {last_day_of_last_week}.zip" - )), - ); - - let recipients = "Wilson da Conceição Oliveira , nicolas.borges@nova.net.br"; - println!("Trying to send mail... {recipients}"); - send_mail_util::send_mail_util::send_email( - &format!( - "Relatório agrupado dos atendimentos da fila do Financeiro N2 - semana {first_day_of_last_week} - {last_day_of_last_week}" - ), - &BOT_EMAIL, - &BOT_EMAIL_PASSWORD, - recipients, - &format!("./groupped/{first_day_of_last_week} - {last_day_of_last_week}.zip"), - ); -}