diff --git a/PROMPT.txt b/PROMPT.txt index 9285ccc..e025079 100644 --- a/PROMPT.txt +++ b/PROMPT.txt @@ -8,14 +8,9 @@ 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. -05 (USO DO PORTUGUÊS) – ESTE CRITÉRIO SE APLICA SOMENTE ÀS MENSAGENS ENVIADAS PELO AGENTE (TYPE = "OUT"). -AO AVALIAR ESTE CRITÉRIO, IGNORE COMPLETAMENTE QUALQUER ERRO COMETIDO PELO CLIENTE. -NÃO ANALISE MENSAGENS COM TYPE = "IN" E NÃO ANALISE MENSAGENS DO BOT AUTOMÁTICO. - -O AGENTE DEVE UTILIZAR CORRETAMENTE O PORTUGUÊS NAS MENSAGENS TYPE OUT, INCLUINDO PONTUAÇÃO, ACENTUAÇÃO E INICIO DE FRASES COM LETRA MAIÚSCULA. - +05 (USO DO PORTUGUÊS) – O AGENTE DEVE PONTUAR CORRETAMENTE, ACENTUAR CORRETAMENTE E INICIAR AS FRASES COM LETRA MAIÚSCULA. É PERMITIDO O USO DE LINGUAGEM REGIONAL COMO “TU”, “TEU”, “TUA”, POIS ISSO FAZ PARTE DA IDENTIDADE DA EMPRESA. -IMPORTANTE: NÃO AVALIE MENSAGENS DO CLIENTE (TYPE IN), NÃO AVALIE MENSAGENS DO PIPEBOT, NEM AVALIE MENSAGENS TYPE “SYSTEM”. AVALIE SOMENTE AS MENSAGENS TYPE OUT. +ESTE CRITÉRIO SE APLICA SOMENTE ÀS MENSAGENS ENVIADAS PELO AGENTE (YPE OUT). AO AVALIAR ESTE CRITÉRIO, IGNORE COMPLETAMENTE QUALQUER ERRO COMETIDO PELO CLIENTE. 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?". @@ -46,6 +41,8 @@ Em cada categoria, atribua 0 quando o agente não tiver atendido o critétio e 1 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 A seguir estão as mensagens do atendimento, em JSON, avalie e retorne apenas um CSV. diff --git a/src/main_old.rs b/src/main_old.rs deleted file mode 100644 index 1e5aa07..0000000 --- a/src/main_old.rs +++ /dev/null @@ -1,665 +0,0 @@ -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 -> 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).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 "; - 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 = "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"); - - 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 -}