use std::{any::Any, env, fmt::format, iter, time::Duration}; use chrono::{self, Timelike}; use dotenv; use ipaddress; use itertools::{self, Itertools}; use lettre::{self, message}; use reqwest; use serde_json::{self, json}; use std::io::prelude::*; pub mod send_mail_util; pub mod zip_directory_util; fn main() -> anyhow::Result<()> { match dotenv::dotenv().ok() { Some(_) => println!("Environment variables loaded from .env file"), None => eprintln!("Failed to load .env file, using defaults"), } // Read environment variables let OLLAMA_URL = env::var("OLLAMA_URL").unwrap_or("localhost".to_string()); let OLLAMA_PORT = env::var("OLLAMA_PORT") .unwrap_or("11432".to_string()) .parse::() .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") .filter(|keyword| !keyword.is_empty()) .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 -> Financeiro NVL2]", ); 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 found1 = filter_keywords.iter().any(|keyword| { message .to_uppercase() .find(&keyword.to_uppercase()) .is_some() }); let found2 = message_object["is_template"] .as_bool() .unwrap_or(true); let found = found1 || found2; found }); 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 -> Financeiro NVL2]"); let found = message.find("Atendimento entregue da fila de espera para o agente [FIN - "); 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() && message["user"]["name"].as_str().map_or(false, |name| name.starts_with("FIN -")) }) .take(1) .collect_vec(); //if msg[0] { // return None; //} let agent_first_message = msg[0]; // Calculate time difference between bot message and agent message let date_user_message_sent = agent_first_message["sent_at"].as_str().unwrap(); let format = "%Y-%m-%d %H:%M:%S"; let date_user_message_sent_parsed = match chrono::NaiveDateTime::parse_from_str(date_user_message_sent, format) { Ok(dt) => dt, Err(e) => { println!("Error parsing DateTime: {}", e); panic!("Failed parsing date") } }; let date_transfer_message_sent_parsed = match chrono::NaiveDateTime::parse_from_str( transfer_message["sent_at"].as_str().unwrap(), format, ) { Ok(dt) => dt, Err(e) => { println!("Error parsing DateTime: {}", e); panic!("Failed parsing date") } }; let response_time = (date_user_message_sent_parsed - date_transfer_message_sent_parsed) .as_seconds_f32(); let name = agent_first_message["user"]["name"] .as_str() .unwrap() .to_owned(); let id = json["tracking_number"].as_str().unwrap_or("").to_owned(); let bot_transfer_date = date_transfer_message_sent_parsed.to_owned(); let user_response_date = date_user_message_sent.to_owned(); println!( "response_time: {}s", (date_user_message_sent_parsed - date_transfer_message_sent_parsed) .as_seconds_f32() ); format!( "{};{};{};{};{}", name, id, response_time, bot_transfer_date, user_response_date ) }) .reduce(|acc, e| format!("{}\n{}", acc, e)) .unwrap_or("".to_string()); // return Ok(()); // Open file and write to it let header = "NOME;ID_TALK;TEMPO DE RESPOSTA;TRANFERENCIA PELO BOT;PRIMEIRA RESPOSTA DO AGENTE"; let mut response_time_file = std::fs::OpenOptions::new() .write(true) .open(format!( "./evaluations/{formatted_day_before}/response_time.csv" )) .expect("Failed to open response time file for write"); response_time_file .write_all(format!("{header}\n{response_time}").as_bytes()) .expect("Failed to write header to file"); filtered_chats.clone().skip(0).for_each(|result| { let json = result.unwrap(); let talk_histories = &json["talk_histories"]; let data = &talk_histories["data"]; let talk = talk_histories .as_array() .expect("Wrong message type received from talk histories") .iter() .rev() .map(|message_object| { let new_json_filtered = format!( "{{ message: {}, sent_at: {}, type: {}, user_name: {} }}", message_object["message"], message_object["sent_at"], message_object["type"], message_object["user"]["name"] ); // println!("{}", new_json_filtered); new_json_filtered }) .reduce(|acc, e| format!("{acc}\n{e}")) .expect("Error extracting talk"); println!("{prompt}\n {talk}"); let ollama_api_request = client .post(format!( "http://{OLLAMA_SANITIZED_IP}:{OLLAMA_PORT}/api/generate" )) .body( serde_json::json!({ "model": OLLAMA_AI_MODEL, "prompt": format!("{prompt} \n{talk}"), // "options": serde_json::json!({"temperature": 0.1}), "stream": false, }) .to_string(), ); let result = ollama_api_request.timeout(Duration::from_secs(3600)).send(); match result { Ok(response) => { println!("Response: {:?}", response); let response_json = response .json::() .expect("Failed to deserialize response to JSON"); println!("{}", response_json); let ai_response = response_json["response"] .as_str() .expect("Failed to get AI response as string"); println!("AI Response: {}", ai_response); let csv_response = ai_response.replace("```csv\n", "").replace("```", ""); // Save the CSV response to a file let user_name = &json["agent"]["user"]["name"] .as_str() .unwrap_or("unknown_user"); let talk_id = &json["id"].as_u64().unwrap_or(0); let tracking_number = &json["tracking_number"].as_str().unwrap_or(""); std::fs::write( format!( "./evaluations/{}/{} - {} - {}.csv", formatted_day_before, user_name, talk_id, tracking_number ), csv_response, ) .expect("Unable to write file"); std::fs::write( format!( "./evaluations/{}/{} - {} - {} - prompt.txt", formatted_day_before, user_name, talk_id, tracking_number ), format!("{prompt} \n{talk}"), ) .expect("Unable to write file"); } Err(error) => { println!("Error {error}"); } }; }); // Compress folder into zip let source_dir_str = format!("./evaluations/{formatted_day_before}"); let output_zip_file_str = format!("./evaluations/{formatted_day_before}.zip"); let source_dir = std::path::Path::new(source_dir_str.as_str()); let output_zip_file = std::path::Path::new(output_zip_file_str.as_str()); zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file(source_dir, output_zip_file); // Send folder to email //let recipients = "Wilson da Conceição Oliveira , Isadora G. Moura de Moura "; let recipients = "Wilson da Conceição Oliveira "; println!("Trying to send email... Recipients {recipients}"); send_mail_util::send_mail_util::send_email( &format!("Avaliacao atendimentos {formatted_day_before}"), &BOT_EMAIL, &BOT_EMAIL_PASSWORD, recipients, &output_zip_file_str, ); return Ok(()); } fn get_piperun_chats_on_date( PIPERUN_API_URL: &String, client: &reqwest::blocking::Client, access_token: &String, formatted_day_before_at_midnight: String, formatted_day_before_at_23_59_59: String, ) -> Vec { let start_of_talk_code: String = "talk_start".to_string(); let support_queue_id: String = "16".to_string(); // API V2 let report_type = "consolidated".to_string(); let page = "1".to_string(); let per_page = "15".to_string(); let talks_request = client .get(format!("https://{}/api/v2/reports/talks", PIPERUN_API_URL)) .bearer_auth(access_token) .header("Content-Type", "application/json") .header("Accept", "application/json") .query(&[ ("page", page.clone()), ("perPage", per_page.clone()), ("report_type", report_type.clone()), ("start_date", formatted_day_before_at_midnight.clone()), ("end_date", formatted_day_before_at_23_59_59.clone()), ("date_range_type", start_of_talk_code.clone()), ("queue_id[]", support_queue_id.clone()), ]); println!("Sending request for consolidated talks... {talks_request:?}"); let talks_response = talks_request.send(); let json_response = match talks_response { Ok(resp) => { if resp.status().is_success() { let json: serde_json::Value = resp.json().unwrap(); json } else { eprintln!("Failed to get consolidated talks: {}", resp.status()); let json: serde_json::Value = resp.json().unwrap(); eprintln!("Response body: {:?}", json); panic!("Failed to retrieve consolidated talks from Piperun API"); } } Err(e) => { eprintln!("Error: {e}"); panic!("Failed to send the request for talks to PipeRUN API"); } }; let mut aggregated_talks = json_response["data"] .as_array() .expect("Failed to parse messages as array") .to_owned(); let current_page = json_response["current_page"] .as_i64() .expect("Failed to obtain current page number"); let last_page = json_response["last_page"] .as_i64() .expect("Failed to obtain current page number"); if current_page == last_page { return aggregated_talks; } let mut all_other_messages = (current_page..last_page) .into_iter() .map(|page| { let page_to_request = page + 1; let talks_request = client .get(format!("https://{}/api/v2/reports/talks", PIPERUN_API_URL)) .bearer_auth(access_token) .header("Content-Type", "application/json") .header("Accept", "application/json") .query(&[ ("page", page_to_request.to_string()), ("perPage", per_page.clone()), ("report_type", report_type.clone()), ("start_date", formatted_day_before_at_midnight.clone()), ("end_date", formatted_day_before_at_23_59_59.clone()), ("date_range_type", start_of_talk_code.clone()), ("queue_id[]", support_queue_id.clone()), ]); println!("Sending request for consolidated talks... {talks_request:?}"); let talks_response = talks_request.send(); let json_response = match talks_response { Ok(resp) => { if resp.status().is_success() { let json: serde_json::Value = resp.json().unwrap(); json } else { eprintln!("Failed to get consolidated talks: {}", resp.status()); let json: serde_json::Value = resp.json().unwrap(); eprintln!("Response body: {:?}", json); panic!("Failed to retrieve consolidated talks from Piperun API"); } } Err(e) => { eprintln!("Error: {e}"); panic!("Failed to send the request for talks to PipeRUN API"); } }; let aggregated_talks = json_response["data"] .as_array() .expect("Failed to parse messages as array") .to_owned(); return aggregated_talks; }) .reduce(|mut this, mut acc| { acc.append(&mut this); acc }) .expect("Failed to concatenate all vectors of messages"); aggregated_talks.append(&mut all_other_messages); aggregated_talks }