use std::ffi::OsStr; use std::{any::Any, env, fmt::format, iter, time::Duration}; use chrono::{self, Timelike}; use dotenv; use ipaddress; use itertools::{self, Itertools}; use lettre::message::Mailboxes; use lettre::{self, message}; use reqwest; use serde_json::{self, json}; use zip; 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::() .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::() .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::() .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 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").collect::>(); 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::(value).expect("Failed to parse the JSON") ["id"] .clone() .to_string() }) .collect::>(); 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::().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) .take(10) .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::().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 = 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 let recipients = "Wilson da Conceição Oliveira , Isadora G. Moura de Moura "; println!("Trying to send email... Recipients {recipients}"); send_email( &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 { 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"); 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 } use std::io::prelude::*; use zip::{result::ZipError, write::SimpleFileOptions}; use std::fs::File; use std::path::{Path, PathBuf}; use walkdir::{DirEntry, WalkDir}; fn zip_dir( 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"); } fn doit(src_dir: &Path, dst_file: &Path, method: zip::CompressionMethod) { if !Path::new(src_dir).is_dir() { panic!("src_dir must be a directory"); } let path = Path::new(dst_file); let file = File::create(path).unwrap(); let walkdir = WalkDir::new(src_dir); let it = walkdir.into_iter(); zip_dir(&mut it.filter_map(|e| e.ok()), src_dir, file, method); } use lettre::{ Message, SmtpTransport, Transport, message::Attachment, message::MultiPart, message::SinglePart, message::header::ContentType, transport::smtp::authentication::{Credentials, Mechanism}, }; fn send_email( day_before: &str, bot_email: &str, bot_email_password: &str, to: &str, zip_file_name: &str, ) { let filebody = std::fs::read(zip_file_name).unwrap(); let content_type = ContentType::parse("application/zip").unwrap(); let attachment = Attachment::new(zip_file_name.to_string()).body(filebody, content_type); let mailboxes : Mailboxes = to.parse().unwrap(); let to_header: message::header::To = mailboxes.into(); let email = Message::builder() .from(format!("PipeRUN bot <{bot_email}>").parse().unwrap()) .reply_to(format!("PipeRUN bot <{bot_email}>").parse().unwrap()) .mailbox(to_header) .subject(format!("Avaliacao atendimentos {day_before}")) .multipart( MultiPart::mixed() .singlepart( SinglePart::builder() .header(ContentType::TEXT_PLAIN) .body(String::from("Avaliacao dos atendimentos")), ) .singlepart(attachment), ) .unwrap(); // Create the SMTPS transport let sender = SmtpTransport::from_url(&format!( "smtps://{bot_email}:{bot_email_password}@mail.nova.net.br" )) .unwrap() .build(); // Send the email via remote relay sender.send(&email).unwrap(); }