diff --git a/src/main.rs b/src/main.rs index cf68ad4..1d9fce6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,12 +5,12 @@ use std::{any::Any, env, fmt::format, iter, time::Duration}; use chrono::{self, Timelike}; use dotenv; +use ipaddress; +use itertools::{self, Itertools}; +use lettre; use reqwest; use serde_json::{self, json}; -use ipaddress; use zip; -use lettre; - fn main() { match dotenv::dotenv().ok() { @@ -35,17 +35,19 @@ fn main() { 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!") + 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::() .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::() - .unwrap_or(12); + 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::() + .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!"); - + 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); @@ -65,7 +67,7 @@ fn main() { } else { format!("[{}]", OLLAMA_URL.clone()) } - }, + } Err(e) => OLLAMA_URL.clone(), }; @@ -167,17 +169,14 @@ fn main() { if !std::fs::exists(format!("{formatted_day_before}")).unwrap() { std::fs::create_dir(format!("{formatted_day_before}")).expect("Failed to create directory") } - - let start_of_talk_code: String = "1".to_string(); + + 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 talks_request = client - .get(format!( - "https://{}/api/v2/reports/talks", - PIPERUN_API_URL - )) + .get(format!("https://{}/api/v2/reports/talks", PIPERUN_API_URL)) .bearer_auth(&access_token) .header("Content-Type", "application/json") .header("Accept", "application/json") @@ -189,23 +188,6 @@ fn main() { ("queue_id[]", support_queue_id), ]); - /* - // Get the list of consolidated talks from the day before - let talks_request = client - .get(format!( - "https://{}/api/reports/talks/consolidated", - PIPERUN_API_URL - )) - .bearer_auth(&access_token) - .header("Content-Type", "application/json") - .header("Accept", "application/json") - .query(&[ - ("start_date", formatted_day_before_at_midnight), - ("end_date", formatted_day_before_at_23_59_59), - ("date_range_type", start_of_talk_code), - ("queue_id[]", support_queue_id), - ]); - */ println!("Sending request for consolidated talks... {talks_request:?}"); let talks_response = talks_request.send(); let talks = match talks_response { @@ -276,11 +258,11 @@ fn main() { let talk_histories = &json["talk_histories"]; let data = &talk_histories["data"]; - // Further filtering + // 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;} - // Find the last transfer + // Filter chats that have less that specified ammount of talks with support agent form the last queue transfer let found = talk_histories.as_array().expect("Wrong message type received from talk histories").into_iter().enumerate().find(|(pos, message_object)|{ let 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]"); @@ -295,12 +277,29 @@ fn main() { } }; + // Filter chats that pass by multiple agents + let number_of_agents_except_pipebot = talk_histories.as_array().expect("Wrong message type received from talk histories").into_iter() + .map(|message| &message["user"]["name"]) + .filter(|name| name.as_str().unwrap() != "PipeBot".to_string()) + .unique() + .count(); + if number_of_agents_except_pipebot > 1 {return;} + + // Filter stop notification active + let found_stop_notification = 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("ATENÇÃO CLIENTE COM NOTIFICAÇÃO DE PARADA ATIVA"); + found.is_some() + }); + + if found_stop_notification.is_some() {return;} + + // Filter Bot finished chats if json["agent"]["user"]["name"].as_str().unwrap_or("unknown_user") == "PipeBot" {return;} let talk = talk_histories.as_array().expect("Wrong message type received from talk histories").iter().rev().map(|message_object| { - // println!("Message obj: {:?}", message_object) - // println!("{:?}", message_object["user"]["name"]); + let new_json_filtered = format!( "{{ message: {}, @@ -317,7 +316,7 @@ fn main() { new_json_filtered }).reduce(|acc, e| {format!("{acc}\n{e}")}).expect("Error extracting talk"); -let system = +let system = " SEGUINDO OS CRITÉRIOS QUE VÃO ESTAR ABAIXO, AVALIE ATENDIMENTOS DE SUPORTE PRESTADOS VIA CHAT. @@ -329,10 +328,9 @@ SEGUINDO OS CRITÉRIOS QUE VÃO ESTAR ABAIXO, AVALIE ATENDIMENTOS DE SUPORTE PRE 04 (USO DO PORTUGUÊS) - O AGENTE DEVE UTILIZAR CORRETAMENTE O PORTUGUÊS. LEMBRANDO QUE SOMOS UMA EMPRESA REGIONAL, UTILIZAMOS UMA LINGUAGEM MAIS INFORMAL. NÃO ESTÁ ERRADO SE O AGENTE UTILIZAR 'TU', 'TEU', 'TUA'. -05 (PACIÊNCIA E EDUCAÇÃO) - O AGENTE DEVE SER PACIENTE E EDUCADO, INCLUIMDO O USO DE AGRADECIMENTOS, SAUDAÇÕES E LINGUAGEM RESPEITOSA. +05 (PACIÊNCIA E EDUCAÇÃO) - O AGENTE DEVE SER PACIENTE E EDUCADO, UTILIZANDO O USO DE AGRADECIMENTOS, SAUDAÇÕES, PEDIDOS DE DESCULPAS E LINGUAGEM RESPEITOSA, COMO POR EXEMPLO 'VOU VERIFICAR, AGUARDE UM MOMENTO POR GENTILEZA', 'DESCULPE, MAS NÃO TE COMPREENDI, PODERIA EXPLICAR DE NOVO?', 'OBRIGADO PELAS INFORMAÇÕES'. 06 (DISPONIBILIDADE) - O AGENTE DEVE SE COLOCAR À DISPOSIÇÃO DO CLIENTE, DEIXANDO CLARO QUE A EMPRESA ESTÁ SEMPRE DISPONÍVEL PARA AJUDAR. - ---------- As mensagens do chat estão estruturadas no formato JSON com os campos: @@ -351,13 +349,13 @@ A seguir estão as mensagens do atendimento, em JSON, avalie e retorne apenas um "; // println!("{system}\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!("{system} \n{talk}"), - "options": serde_json::json!({"temperature": 0.1}), + // "options": serde_json::json!({"temperature": 0.1}), "stream": false, }).to_string() ); @@ -376,28 +374,33 @@ A seguir estão as mensagens do atendimento, em JSON, avalie e retorne apenas um 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!("./{}/{} - {} - {}.csv", formatted_day_before, user_name, talk_id, tracking_number), csv_response).expect("Unable to write file"); - std::fs::write(format!("./{}/{} - {} - {} - prompt.txt", formatted_day_before, user_name, talk_id, tracking_number), format!("{system} \n{talk}")).expect("Unable to write file"); - + 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!("{system} \n{talk}")).expect("Unable to write file"); }, Err(error) => {println!("Error {error}");} }; }); - // Compress folder into zip - let source_dir_str = format!("./{formatted_day_before}"); - let output_zip_file_str = format!("./{formatted_day_before}.zip"); - let source_dir = Path::new(source_dir_str.as_str()); - let output_zip_file = Path::new(output_zip_file_str.as_str()); - doit(source_dir, output_zip_file, zip::CompressionMethod::Stored); + // 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 = Path::new(source_dir_str.as_str()); + let output_zip_file = Path::new(output_zip_file_str.as_str()); + doit(source_dir, output_zip_file, zip::CompressionMethod::Stored); - // Send folder to email - println!("Trying to send email..."); - send_email(&formatted_day_before, &BOT_EMAIL, &BOT_EMAIL_PASSWORD, "Wilson da Conceição Oliveira ", &output_zip_file_str); + // Send folder to email + println!("Trying to send email..."); + send_email( + &formatted_day_before, + &BOT_EMAIL, + &BOT_EMAIL_PASSWORD, + "Wilson da Conceição Oliveira ", + &output_zip_file_str, + ); } use std::io::prelude::*; @@ -412,8 +415,7 @@ fn zip_dir( prefix: &Path, writer: T, method: zip::CompressionMethod, -) -where +) where T: Write + Seek, { let mut zip = zip::ZipWriter::new(writer); @@ -428,13 +430,15 @@ where let name = path.strip_prefix(prefix).unwrap(); let path_as_string = name .to_str() - .map(str::to_owned).expect("Failed to parse path"); + .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"); + 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"); @@ -444,7 +448,8 @@ where // 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.add_directory(path_as_string, options) + .expect("Failed to add directory"); } } zip.finish().expect("Failed to ZIP"); @@ -464,40 +469,49 @@ fn doit(src_dir: &Path, dst_file: &Path, method: zip::CompressionMethod) { zip_dir(&mut it.filter_map(|e| e.ok()), src_dir, file, method); } - - use lettre::{ - message::header::ContentType, + Message, SmtpTransport, Transport, message::Attachment, message::MultiPart, message::SinglePart, + message::header::ContentType, transport::smtp::authentication::{Credentials, Mechanism}, - Message, SmtpTransport, Transport, }; -fn send_email(day_before:&str, bot_email:&str, bot_email_password:&str, to:&str, zip_file_name:&str) { +fn send_email( + day_before: &str, + bot_email: &str, + bot_email_password: &str, + to: &str, + zip_file_name: &str, +) { let filebody = std::fs::read(zip_file_name).unwrap(); let content_type = ContentType::parse("application/zip").unwrap(); let attachment = Attachment::new(zip_file_name.to_string()).body(filebody, content_type); let email = Message::builder() - .from(format!("PipeRUN bot <{bot_email}>").parse().unwrap()) - .reply_to(format!("PipeRUN bot <{bot_email}>").parse().unwrap()) - .to(format!("{to}").parse().unwrap()) - .subject(format!("Avaliacao atendimentos {day_before}")) - .multipart(MultiPart::mixed() - .singlepart(SinglePart::builder() - .header(ContentType::TEXT_PLAIN) - .body(String::from("Avaliacao dos atendimentos")) - ) - .singlepart( - attachment - ) - ).unwrap() ; + .from(format!("PipeRUN bot <{bot_email}>").parse().unwrap()) + .reply_to(format!("PipeRUN bot <{bot_email}>").parse().unwrap()) + .to(format!("{to}").parse().unwrap()) + .subject(format!("Avaliacao atendimentos {day_before}")) + .multipart( + MultiPart::mixed() + .singlepart( + SinglePart::builder() + .header(ContentType::TEXT_PLAIN) + .body(String::from("Avaliacao dos atendimentos")), + ) + .singlepart(attachment), + ) + .unwrap(); // Create the SMTPS transport - let sender = SmtpTransport::from_url(&format!("smtps://{bot_email}:{bot_email_password}@mail.nova.net.br")).unwrap().build(); + 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(); -} \ No newline at end of file +}