diff --git a/src/prompts/business/data-enrichment.bas b/src/prompts/business/data-enrichment.bas index 07e166d..558ee85 100644 --- a/src/prompts/business/data-enrichment.bas +++ b/src/prompts/business/data-enrichment.bas @@ -5,19 +5,21 @@ FOR EACH item IN items let website = WEBSITE OF item.company PRINT website - WAIT 10 + let page = GET website - let prompt = "Create a website for " + item.company + " with the following details: " + page + let prompt = "Build the same simulator , but for " + item.company + " using just *content about the company* from its website, so it is possible to create a good and useful emulator in the same langue as the content: " + page let alias = LLM "Return a single word for " + item.company + " like a token, no spaces, no special characters, no numbers, no uppercase letters." - CREATE SITE item.company + "bot", item.company, website, "site", prompt + let to = item.emailcto - let subject = "Simulador criado " + item.company - let body = "O simulador " + item.company + " foi criado com sucesso. Acesse o site: " + item.company + "bot" + let subject = "General Bots" + let body = "Oi, tudo bem? Criamos o simulador " + alias + " especificamente para vocês!" + "\n\n Acesse o site: https://sites.pragmatismo.com.br/" + alias + "\n\n" + "Para acessar o simulador, clique no link acima ou copie e cole no seu navegador." + "\n\n" + "Para iniciar, clique no ícone de Play." + "\n\n" + "Atenciosamente,\nDário Vieira" CREATE_DRAFT to, subject, body -NEXT item + + +NEXT item \ No newline at end of file diff --git a/src/services/config.rs b/src/services/config.rs index 0e9e8a8..3d0262b 100644 --- a/src/services/config.rs +++ b/src/services/config.rs @@ -8,6 +8,7 @@ pub struct AppConfig { pub database_custom: DatabaseConfig, pub email: EmailConfig, pub ai: AIConfig, + pub site_path: String, } #[derive(Clone)] @@ -141,6 +142,7 @@ impl AppConfig { database_custom, email, ai, + site_path: env::var("SITES_ROOT").unwrap() } } } \ No newline at end of file diff --git a/src/services/email.rs b/src/services/email.rs index 4918359..331ec85 100644 --- a/src/services/email.rs +++ b/src/services/email.rs @@ -289,6 +289,83 @@ pub async fn get_latest_email_from( } } } + +pub async fn fetch_latest_sent_to( + email_config: &EmailConfig, + to_email: &str, +) -> Result> { + // Establish connection + let tls = native_tls::TlsConnector::builder().build()?; + let client = imap::connect( + (email_config.server.as_str(), 993), + email_config.server.as_str(), + &tls, + )?; + + // Login + let mut session = client.login(&email_config.username, &email_config.password) + .map_err(|e| format!("Login failed: {:?}", e))?; + + // Try to select Archive folder first, then fall back to INBOX + if session.select("Sent").is_err() { + session.select("Sent Items")?; + } + + // Search for emails from the specified sender + let search_query = format!("TO \"{}\"", to_email); + let messages = session.search(&search_query)?; + + if messages.is_empty() { + session.logout()?; + return Err(format!("No emails found to {}", to_email).into()); + } + + // Get the latest message (highest sequence number) + let latest_seq = messages.iter().max().unwrap(); + + // Fetch the entire message + let messages = session.fetch(latest_seq.to_string(), "RFC822")?; + + let mut email_text = String::new(); + + for msg in messages.iter() { + let body = msg.body().ok_or("No body found in email")?; + + // Parse the complete email message + let parsed = parse_mail(body)?; + + // Extract headers + let headers = parsed.get_headers(); + let subject = headers.get_first_value("Subject").unwrap_or_default(); + let from = headers.get_first_value("From").unwrap_or_default(); + let date = headers.get_first_value("Date").unwrap_or_default(); + let to = headers.get_first_value("To").unwrap_or_default(); + + // Extract body text + let body_text = if let Some(body_part) = parsed.subparts.iter().find(|p| p.ctype.mimetype == "text/plain") { + body_part.get_body().unwrap_or_default() + } else { + parsed.get_body().unwrap_or_default() + }; + + // Format the email text ready for reply with headers + email_text = format!( + "--- Original Message ---\nFrom: {}\nTo: {}\nDate: {}\nSubject: {}\n\n{}\n\n--- Reply Above This Line ---\n\n", + from, to, date, subject, body_text + ); + + break; // We only want the first (and should be only) message + } + + session.logout()?; + + if email_text.is_empty() { + Err("Failed to extract email content".into()) + } else { + Ok(email_text) + } +} + pub async fn fetch_latest_email_from_sender( email_config: &EmailConfig, from_email: &str, @@ -366,6 +443,7 @@ pub async fn fetch_latest_email_from_sender( } + #[actix_web::post("/emails/send")] pub async fn send_email( payload: web::Json<(String, String, String)>, diff --git a/src/services/keywords/create_draft.rs b/src/services/keywords/create_draft.rs index 13305ee..7c64ac4 100644 --- a/src/services/keywords/create_draft.rs +++ b/src/services/keywords/create_draft.rs @@ -1,5 +1,5 @@ -use crate::services::email::SaveDraftRequest; -use crate::services::email::{fetch_latest_email_from_sender, save_email_draft}; +use crate::services::email::{fetch_latest_sent_to, SaveDraftRequest}; +use crate::services::email::{save_email_draft}; use crate::services::state::AppState; use rhai::Dynamic; use rhai::Engine; @@ -35,10 +35,10 @@ async fn execute_create_draft( subject: &str, reply_text: &str, ) -> Result { - let get_result = fetch_latest_email_from_sender(&state.config.clone().unwrap().email, to).await; + let get_result = fetch_latest_sent_to(&state.config.clone().unwrap().email, to).await; let email_body = if let Ok(get_result_str) = get_result { if !get_result_str.is_empty() { - get_result_str + reply_text + reply_text.to_string() + get_result_str.as_str() } else { "".to_string() } diff --git a/src/services/keywords/create_site.rs b/src/services/keywords/create_site.rs index 82ddfdb..3c87152 100644 --- a/src/services/keywords/create_site.rs +++ b/src/services/keywords/create_site.rs @@ -2,7 +2,8 @@ use rhai::Dynamic; use rhai::Engine; use std::error::Error; use std::fs; -use std::path::Path; +use std::path::{ PathBuf}; +use std::io::Read; use crate::services::state::AppState; use crate::services::utils; @@ -12,26 +13,25 @@ pub fn create_site_keyword(state: &AppState, engine: &mut Engine) { engine .register_custom_syntax( &[ - "CREATE", "SITE", "$expr$", ",", "$expr$", ",", "$expr$", ",", "$expr$", ",", - "$expr$", + "CREATE_SITE", "$expr$", ",", "$expr$", ",", "$expr$", + ], - true, // Statement + true, move |context, inputs| { - if inputs.len() < 5 { + if inputs.len() < 3 { return Err("Not enough arguments for CREATE SITE".into()); } - let _name = context.eval_expression_tree(&inputs[0])?; - - let _website = context.eval_expression_tree(&inputs[2])?; - let _template = context.eval_expression_tree(&inputs[3])?; - let prompt = context.eval_expression_tree(&inputs[4])?; - let ai_config = state_clone.config.as_ref().expect("Config must be initialized").ai.clone(); - // Use the same pattern as find_keyword - let fut = create_site(&ai_config, _name, prompt); + let alias = context.eval_expression_tree(&inputs[0])?; + let template_dir = context.eval_expression_tree(&inputs[1])?; + let prompt = context.eval_expression_tree(&inputs[2])?; + + let config = state_clone.config.as_ref().expect("Config must be initialized").clone(); + + let fut = create_site(&config, alias, template_dir, prompt); let result = tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(fut)) - .map_err(|e| format!("HTTP request failed: {}", e))?; + .map_err(|e| format!("Site creation failed: {}", e))?; Ok(Dynamic::from(result)) }, @@ -40,26 +40,51 @@ pub fn create_site_keyword(state: &AppState, engine: &mut Engine) { } async fn create_site( - ai_config: &crate::services::config::AIConfig, - _name: Dynamic, + config: &crate::services::config::AppConfig, + alias: Dynamic, + template_dir: Dynamic, prompt: Dynamic, -) -> Result> { +) -> Result> { + // Convert paths to platform-specific format + let base_path = PathBuf::from(&config.site_path); + let template_path = base_path.join(template_dir.to_string()); + let alias_path = base_path.join(alias.to_string()); - // Call the LLM to generate the HTML contents - let llm_result = utils::call_llm(&prompt.to_string(), &ai_config).await?; + // Create destination directory + fs::create_dir_all(&alias_path).map_err(|e| e.to_string())?; - // Create the directory structure - let base_path = "/opt/gbo/tenants/pragmatismo/proxy/data/websites/sites.pragmatismo.com.br"; - let site_name = format!("{}", _name.to_string()); - let full_path = format!("{}/{}", base_path, site_name); + // Process all HTML files in template directory + let mut combined_content = String::new(); + + for entry in fs::read_dir(&template_path).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let path = entry.path(); + + if path.extension().map_or(false, |ext| ext == "html") { + let mut file = fs::File::open(&path).map_err(|e| e.to_string())?; + let mut contents = String::new(); + file.read_to_string(&mut contents).map_err(|e| e.to_string())?; + + combined_content.push_str(&contents); + combined_content.push_str("\n\n--- TEMPLATE SEPARATOR ---\n\n"); + } + } - // Create directory if it doesn't exist - fs::create_dir_all(&full_path).map_err(|e| e.to_string())?; + // Combine template content with prompt + let full_prompt = format!( + "TEMPLATE FILES:\n{}\n\nPROMPT: {}\n\nGenerate a new HTML file cloning all previous TEMPLATE (keeping only the local _assets libraries use, no external resources), but turning this into this prompt:", + combined_content, + prompt.to_string() + ); - // Write the HTML file - let index_path = Path::new(&full_path).join("index.html"); + // Call LLM with the combined prompt + println!("Asking LLM to create site."); + let llm_result = utils::call_llm(&full_prompt, &config.ai).await?; + + // Write the generated HTML file + let index_path = alias_path.join("index.html"); fs::write(index_path, llm_result).map_err(|e| e.to_string())?; - println!("Site created at: {}", full_path); - Ok(full_path) -} + println!("Site created at: {}", alias_path.display()); + Ok(alias_path.to_string_lossy().into_owned()) +} \ No newline at end of file diff --git a/src/services/keywords/find.rs b/src/services/keywords/find.rs index 925c9af..f2de9b9 100644 --- a/src/services/keywords/find.rs +++ b/src/services/keywords/find.rs @@ -2,7 +2,6 @@ use rhai::Dynamic; use rhai::Engine; use serde_json::{json, Value}; use sqlx::{PgPool}; -use std::error::Error; use crate::services::state::AppState; use crate::services::utils; @@ -54,7 +53,7 @@ pub async fn execute_find( table_str, filter_str ); - let (where_clause, params) = parse_filter(filter_str).map_err(|e| e.to_string())?; + let (where_clause, params) = utils::parse_filter(filter_str).map_err(|e| e.to_string())?; let query = format!( "SELECT * FROM {} WHERE {} LIMIT 10", @@ -87,24 +86,3 @@ pub async fn execute_find( })) } -// Helper function to parse the filter string into SQL WHERE clause and parameters -fn parse_filter(filter_str: &str) -> Result<(String, Vec), Box> { - let parts: Vec<&str> = filter_str.split('=').collect(); - if parts.len() != 2 { - return Err("Invalid filter format. Expected 'KEY=VALUE'".into()); - } - - let column = parts[0].trim(); - let value = parts[1].trim(); - - // Validate column name to prevent SQL injection - if !column - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == '_') - { - return Err("Invalid column name in filter".into()); - } - - // Return the parameterized query part and the value separately - Ok((format!("{} = $1", column), vec![value.to_string()])) -} diff --git a/src/services/keywords/get.rs b/src/services/keywords/get.rs index fdb389f..e01652d 100644 --- a/src/services/keywords/get.rs +++ b/src/services/keywords/get.rs @@ -2,6 +2,8 @@ use reqwest; use crate::services::state::AppState; use std::error::Error; +use scraper::{Html, Selector}; + pub fn get_keyword(_state: &AppState, engine: &mut Engine) { engine.register_custom_syntax( @@ -29,7 +31,7 @@ pub fn get_keyword(_state: &AppState, engine: &mut Engine) { ).unwrap(); } -pub async fn execute_get(url: &str) -> Result> { +pub async fn _execute_get(url: &str) -> Result> { println!("Starting execute_get with URL: {}", url); let response = reqwest::get(url).await?; @@ -37,4 +39,28 @@ pub async fn execute_get(url: &str) -> Result Result> { + println!("Starting execute_get with URL: {}", url); + + let response = reqwest::get(url).await?; + let html_content = response.text().await?; + + // Parse HTML and extract text + let document = Html::parse_document(&html_content); + let selector = Selector::parse("body").unwrap(); // Focus on body content + let body = document.select(&selector).next().unwrap(); + let text_content = body.text().collect::>().join(" "); + + // Clean up the text (remove extra whitespace, newlines, etc.) + let cleaned_text = text_content + .replace('\n', " ") + .replace('\t', " ") + .split_whitespace() + .collect::>() + .join(" "); + + println!("GET request successful, extracted {} characters of text", cleaned_text.len()); + Ok(cleaned_text) +} diff --git a/src/services/keywords/get_website.rs b/src/services/keywords/get_website.rs index 8e53ae3..2fb1d1c 100644 --- a/src/services/keywords/get_website.rs +++ b/src/services/keywords/get_website.rs @@ -77,7 +77,8 @@ async fn perform_search( // Extract results let results = extract_search_results(&driver).await?; - + driver.quit().await?; + if !results.is_empty() { Ok(results[0].clone()) } else { diff --git a/src/services/keywords/llm_keyword.rs b/src/services/keywords/llm_keyword.rs index 9866aff..f6305f6 100644 --- a/src/services/keywords/llm_keyword.rs +++ b/src/services/keywords/llm_keyword.rs @@ -6,7 +6,7 @@ pub fn llm_keyword(state: &AppState, engine: &mut Engine) { let ai_config = state.config.clone().unwrap().ai.clone(); engine.register_custom_syntax( - &["LLM", "$string$"], // Syntax: LLM "text to process" + &["LLM", "$expr$"], // Syntax: LLM "text to process" false, // Expression, not statement move |context, inputs| { let text = context.eval_expression_tree(&inputs[0])?; @@ -16,7 +16,8 @@ pub fn llm_keyword(state: &AppState, engine: &mut Engine) { // Use the same pattern as GET - let fut = call_llm(&text_str, &ai_config); + let fut = call_llm( + &text_str, &ai_config); let result = tokio::task::block_in_place(|| { tokio::runtime::Handle::current().block_on(fut) }).map_err(|e| format!("LLM call failed: {}", e))?; diff --git a/src/services/keywords/set.rs b/src/services/keywords/set.rs index 0b9f235..de2d039 100644 --- a/src/services/keywords/set.rs +++ b/src/services/keywords/set.rs @@ -1,36 +1,120 @@ use rhai::Dynamic; use rhai::Engine; -use serde_json::json; +use serde_json::{json, Value}; +use sqlx::{PgPool}; +use std::error::Error; use crate::services::state::AppState; +use crate::services::utils; -pub fn set_keyword(_state: &AppState, engine: &mut Engine) { +pub fn set_keyword(state: &AppState, engine: &mut Engine) { + let db = state.db_custom.clone(); engine - .register_custom_syntax( - &["SET", "$expr$", ",", "$expr$", ",", "$expr$"], - true, // Statement - |context, inputs| { - let table_name = context.eval_expression_tree(&inputs[0])?; - let key_value = context.eval_expression_tree(&inputs[1])?; - let value = context.eval_expression_tree(&inputs[2])?; + .register_custom_syntax(&["SET", "$expr$", ",", "$expr$", ",", "$expr$"], false, { + let db = db.clone(); - let table_str = table_name.to_string(); - let key_str = key_value.to_string(); - let value_str = value.to_string(); + move |context, inputs| { + let table_name = context.eval_expression_tree(&inputs[0])?; + let filter = context.eval_expression_tree(&inputs[1])?; + let updates = context.eval_expression_tree(&inputs[2])?; + let binding = db.as_ref().unwrap(); - let result = json!({ - "command": "set", - "status": "success", - "table": table_str, - "key": key_str, - "value": value_str - }); - println!("SET executed: {}", result.to_string()); - Ok(Dynamic::UNIT) - }, - ) - .unwrap(); + // Use the current async context instead of creating a new runtime + let binding2 = table_name.to_string(); + let binding3 = filter.to_string(); + let binding4 = updates.to_string(); + let fut = execute_set(binding, &binding2, &binding3, &binding4); + // Use tokio::task::block_in_place + tokio::runtime::Handle::current().block_on + let result = + tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(fut)) + .map_err(|e| format!("DB error: {}", e))?; + + if let Some(rows_affected) = result.get("rows_affected") { + Ok(Dynamic::from(rows_affected.as_i64().unwrap_or(0))) + } else { + Err("No rows affected".into()) + } + } + }) + .unwrap(); } + +pub async fn execute_set( + pool: &PgPool, + table_str: &str, + filter_str: &str, + updates_str: &str, +) -> Result { + println!( + "Starting execute_set with table: {}, filter: {}, updates: {}", + table_str, filter_str, updates_str + ); + + // Parse the filter condition + let (where_clause, filter_params) = utils::parse_filter(filter_str).map_err(|e| e.to_string())?; + + // Parse the updates + let (set_clause, update_params) = parse_updates(updates_str).map_err(|e| e.to_string())?; + + // Combine all parameters (updates first, then filter) + let mut params = update_params; + params.extend(filter_params); + + let query = format!( + "UPDATE {} SET {} WHERE {}", + table_str, set_clause, where_clause + ); + println!("Executing query: {}", query); + + // Execute the update + let result = sqlx::query(&query) + .bind(¶ms[0]) // First update value + .bind(¶ms[1]) // Second update value if exists + .bind(¶ms[2]) // Filter value + .execute(pool) + .await + .map_err(|e| { + eprintln!("SQL execution error: {}", e); + e.to_string() + })?; + + println!("Update successful, affected {} rows", result.rows_affected()); + + Ok(json!({ + "command": "set", + "table": table_str, + "filter": filter_str, + "updates": updates_str, + "rows_affected": result.rows_affected() + })) +} + +// Helper function to parse the updates string into SQL SET clause and parameters +fn parse_updates(updates_str: &str) -> Result<(String, Vec), Box> { + let mut set_clauses = Vec::new(); + let mut params = Vec::new(); + + // Split multiple updates by comma + for update in updates_str.split(',') { + let parts: Vec<&str> = update.split('=').collect(); + if parts.len() != 2 { + return Err("Invalid update format. Expected 'KEY=VALUE'".into()); + } + + let column = parts[0].trim(); + let value = parts[1].trim(); + + // Validate column name to prevent SQL injection + if !column.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { + return Err("Invalid column name in update".into()); + } + + set_clauses.push(format!("{} = ${}", column, set_clauses.len() + 1)); + params.push(value.to_string()); + } + + Ok((set_clauses.join(", "), params)) +} \ No newline at end of file diff --git a/src/services/utils.rs b/src/services/utils.rs index 4d81419..4edde2a 100644 --- a/src/services/utils.rs +++ b/src/services/utils.rs @@ -216,3 +216,25 @@ pub async fn download_file(url: &str, output_path: &str) -> Result<(), Box Result<(String, Vec), Box> { + let parts: Vec<&str> = filter_str.split('=').collect(); + if parts.len() != 2 { + return Err("Invalid filter format. Expected 'KEY=VALUE'".into()); + } + + let column = parts[0].trim(); + let value = parts[1].trim(); + + // Validate column name to prevent SQL injection + if !column + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_') + { + return Err("Invalid column name in filter".into()); + } + + // Return the parameterized query part and the value separately + Ok((format!("{} = $1", column), vec![value.to_string()])) +}