bgustscraper 0.2.0

Advanced semantic scraping engine with AI-driven compliance checks and legal terms validation
Documentation
use anyhow::Result;
use std::fs;
use std::time::Duration;
use chrono::Local;
use crate::schedule_config::{ScheduleConfig, JobConfig, WindowConfig};
use crate::worker::{PlaywrightWorker, ScrapingEngine, ScrapeOptions};
use crate::clustering;
use crate::cache::LocalCache;

pub async fn run_scheduler_daemon() -> Result<()> {
    println!("⏰ [SCHEDULER] Iniciando Daemon del Planificador de Tareas...");
    let config = ScheduleConfig::load_or_create()?;
    println!("⏰ [SCHEDULER] Cargadas {} tareas de agenda recurrentes.", config.jobs.len());

    loop {
        // Ejecutar comprobaciones de ventanas horarias
        if let Err(e) = check_and_execute_jobs(&config).await {
            eprintln!("⚠️ [SCHEDULER] Error durante la comprobación de tareas: {}", e);
        }

        // Despertar cada 60 segundos
        tokio::time::sleep(Duration::from_secs(60)).await;
    }
}

async fn check_and_execute_jobs(config: &ScheduleConfig) -> Result<()> {
    let now = Local::now();
    let weekday_str = now.format("%a").to_string(); // "Mon", "Tue", etc.
    let current_time_str = now.format("%H:%M").to_string(); // "HH:MM"
    let date_str = now.format("%Y-%m-%d").to_string(); // "YYYY-MM-DD"

    for job in &config.jobs {
        // 1. Validar si el día de la semana está habilitado
        if !job.days.contains(&weekday_str) {
            continue;
        }

        // 2. Revisar cada una de las ventanas horarias configuradas para la tarea
        for window in &job.windows {
            if is_time_in_window(&current_time_str, &window.start, &window.end) {
                let cache = LocalCache::new()?;
                let schedule_key = format!("schedule:{}:{}:{}", job.name, date_str, window.name);
                
                // 3. Control de Estado: Validar si ya fue ejecutada hoy en esta ventana
                match cache.get(&schedule_key) {
                    Ok(Some(status)) if status == "EJECUTADO" => {
                        // Ya se ejecutó hoy en esta ventana. Omitir.
                        continue;
                    }
                    _ => {
                        // Proceder con la ejecución
                        println!("⏰ [SCHEDULER] Ventana horaria '{}' activa para la tarea '{}'. Iniciando ejecución...", window.name, job.name);
                        execute_scheduled_job(job, window, &date_str, &cache).await?;
                    }
                }
            }
        }
    }

    Ok(())
}

fn is_time_in_window(current: &str, start: &str, end: &str) -> bool {
    current >= start && current <= end
}

async fn execute_scheduled_job(
    job: &JobConfig,
    window: &WindowConfig,
    date_str: &str,
    cache: &LocalCache,
) -> Result<()> {
    let mut worker = PlaywrightWorker::new(true, ScrapingEngine::Playwright)?;
    
    println!("⏰ [SCHEDULER] Navegando y extrayendo de: {}", job.url);
    let scrape_opts = ScrapeOptions {
        deep_scan: false,
        semantic_embeddings: Some(true),
        text: None,
        prompt: None,
    };
    
    let resp = worker.scrape(&job.url, Some(scrape_opts))?;
    
    if resp.status == "success" {
        let markdown = resp.markdown.clone().unwrap_or_default();

        // 1. Compresión semántica K-Means
        let compressed_text = if let (Some(sentences), Some(embeddings)) = (&resp.sentences, &resp.embeddings) {
            println!("⏰ [SCHEDULER] Comprimiendo texto por clustering K-Means...");
            let summary_sentences = clustering::compress_text_by_clustering(sentences, embeddings, 12);
            summary_sentences.join("\n")
        } else {
            markdown.chars().take(2000).collect()
        };

        // 2. Extracción semántica estructurada con Qwen local
        println!("⏰ [SCHEDULER] Ejecutando extracción estructurada JSON...");
        let extract_opts = ScrapeOptions {
            deep_scan: false,
            semantic_embeddings: None,
            text: Some(compressed_text),
            prompt: Some(job.extract_prompt.clone()),
        };
        
        let extract_resp = worker.scrape("extract", Some(extract_opts))?;
        if extract_resp.status == "success" {
            if let Some(data) = extract_resp.data {
                // Escribir JSON final en la ruta configurada
                fs::create_dir_all("downloads")?;
                fs::write(&job.output_path, serde_json::to_string_pretty(&data)?)?;
                println!("⏰ [SCHEDULER] Tarea programada '{}' completada de forma exitosa. Guardado en: {}", job.name, job.output_path);
                
                // Registrar el estado en caché local redb para evitar ejecuciones adicionales
                let schedule_key = format!("schedule:{}:{}:{}", job.name, date_str, window.name);
                cache.set(&schedule_key, "EJECUTADO")?;
            }
        } else {
            eprintln!("⚠️ [SCHEDULER] La extracción semántica falló: {}", extract_resp.message.unwrap_or_default());
        }
    } else {
        eprintln!("⚠️ [SCHEDULER] Fallo al navegar a la URL: {}", resp.message.unwrap_or_default());
    }

    worker.exit()?;
    Ok(())
}