- First run to generate e-mail.
Some checks are pending
GBCI / build (push) Waiting to run

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-08-02 00:07:54 -03:00
parent e15da79204
commit 50bfad7642
11 changed files with 311 additions and 92 deletions

View file

@ -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

View file

@ -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()
}
}
}

View file

@ -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<String, Box<dyn std::error::Error>> {
// 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)>,

View file

@ -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<String, String> {
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()
}

View file

@ -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<String, Box<dyn Error + Send + Sync>> {
) -> Result<String, Box<dyn Error + Send + Sync>> {
// 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())
}

View file

@ -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<String>), Box<dyn Error>> {
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()]))
}

View file

@ -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<String, Box<dyn Error + Send + Sync>> {
pub async fn _execute_get(url: &str) -> Result<String, Box<dyn Error + Send + Sync>> {
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<String, Box<dyn Error + Send + Syn
println!("GET request successful, got {} bytes", content.len());
Ok(format!("Secure content fetched: {}", content))
}
}
pub async fn execute_get(url: &str) -> Result<String, Box<dyn Error + Send + Sync>> {
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::<Vec<_>>().join(" ");
// Clean up the text (remove extra whitespace, newlines, etc.)
let cleaned_text = text_content
.replace('\n', " ")
.replace('\t', " ")
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
println!("GET request successful, extracted {} characters of text", cleaned_text.len());
Ok(cleaned_text)
}

View file

@ -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 {

View file

@ -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))?;

View file

@ -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<Value, String> {
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(&params[0]) // First update value
.bind(&params[1]) // Second update value if exists
.bind(&params[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<String>), Box<dyn Error>> {
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))
}

View file

@ -216,3 +216,25 @@ pub async fn download_file(url: &str, output_path: &str) -> Result<(), Box<dyn s
Ok(())
}
// Helper function to parse the filter string into SQL WHERE clause and parameters
pub fn parse_filter(filter_str: &str) -> Result<(String, Vec<String>), Box<dyn Error>> {
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()]))
}