bgustscraper 0.2.0

Advanced semantic scraping engine with AI-driven compliance checks and legal terms validation
Documentation
use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};
use crate::worker::{PlaywrightWorker, ScrapeOptions};
use url::Url;

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SemanticDocument {
    pub name: String,
    pub url: String,
    pub extension: String,
    pub summary: String,
    pub category: String,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SemanticExtractionResult {
    pub documents: Vec<SemanticDocument>,
}

const DOC_EXTRACT_PROMPT: &str = r#"
Extrae una lista de todos los documentos, archivos, reportes, decretos o leyes descargables mencionados en el texto.
Para cada documento, extrae obligatoriamente los siguientes campos:
- name: El título real, limpio y descriptivo del documento (ej. "Ley Orgánica del Ambiente").
- url: El enlace o URL de descarga mencionado (debe ser el enlace exacto provisto en el texto).
- extension: La extensión del archivo en minúsculas (pdf, xlsx, csv, doc, etc.).
- summary: Un resumen de una sola frase sobre el contenido del documento.
- category: Categoría (ej. "Ley", "Reglamento", "Decreto", "Estadística", "Estudio").

Formato de salida estricto (JSON):
{
  "documents": [
    {
      "name": "Nombre descriptivo",
      "url": "https://enlace-de-descarga",
      "extension": "pdf",
      "summary": "Resumen del contenido...",
      "category": "Ley"
    }
  ]
}
Si no se menciona ningún documento descargable, responde estrictamente con {"documents": []}.
"#;

pub fn extract_semantic_documents(
    worker: &mut PlaywrightWorker,
    compressed_text: &str,
    base_url: &str,
) -> Result<Vec<SemanticDocument>> {
    let options = ScrapeOptions {
        deep_scan: false,
        semantic_embeddings: None,
        text: Some(compressed_text.to_string()),
        prompt: Some(DOC_EXTRACT_PROMPT.to_string()),
    };

    let response = worker.scrape("extract", Some(options))?;

    if response.status != "success" {
        return Err(anyhow!(
            "El extractor semántico falló: {}",
            response.message.unwrap_or_else(|| "Error desconocido".to_string())
        ));
    }

    let raw_data = response
        .data
        .ok_or_else(|| anyhow!("El worker no devolvió datos estructurados"))?;

    let mut extraction: SemanticExtractionResult = serde_json::from_value(raw_data)
        .map_err(|e| anyhow!("Error al deserializar la extracción semántica de documentos: {}", e))?;

    // Normalizar URLs relativas usando la URL base de la página
    let base = Url::parse(base_url)?;
    for doc in &mut extraction.documents {
        if let Ok(full_url) = base.join(&doc.url) {
            doc.url = full_url.to_string();
        }
    }

    Ok(extraction.documents)
}