hippox 0.5.0

🦛A reliable AI agent and skills orchestration runtime engine.
use anyhow::Result;
use quick_xml::{Reader, events::Event};
use serde_json::{Value, json};
use std::collections::HashMap;

use crate::executors::{
    ensure_dir, file_exists, read_file_content,
    types::{Skill, SkillParameter},
    validate_path, write_file_content,
};

#[derive(Debug)]
pub struct OdsReadSkill;

#[async_trait::async_trait]
impl Skill for OdsReadSkill {
    fn name(&self) -> &str {
        "ods_read"
    }

    fn description(&self) -> &str {
        "Read and extract data from OpenDocument Spreadsheet (.ods) files"
    }

    fn usage_hint(&self) -> &str {
        "Use this skill when the user wants to read OpenDocument spreadsheets (LibreOffice/OpenOffice Calc)"
    }

    fn parameters(&self) -> Vec<SkillParameter> {
        vec![
            SkillParameter {
                name: "path".to_string(),
                param_type: "string".to_string(),
                description: "Path to the ODS file".to_string(),
                required: true,
                default: None,
                example: Some(Value::String("spreadsheet.ods".to_string())),
                enum_values: None,
            },
            SkillParameter {
                name: "sheet".to_string(),
                param_type: "string".to_string(),
                description: "Sheet name or index (0-based)".to_string(),
                required: false,
                default: Some(Value::String("0".to_string())),
                example: Some(Value::String("Sheet1".to_string())),
                enum_values: None,
            },
            SkillParameter {
                name: "limit".to_string(),
                param_type: "integer".to_string(),
                description: "Maximum number of rows to read".to_string(),
                required: false,
                default: Some(Value::Number(100.into())),
                example: Some(Value::Number(50.into())),
                enum_values: None,
            },
        ]
    }

    fn example_call(&self) -> Value {
        json!({
            "action": "ods_read",
            "parameters": {
                "path": "spreadsheet.ods"
            }
        })
    }

    fn example_output(&self) -> String {
        "Sheet: Sheet1\nRow 1: [Value1, Value2]\nRow 2: [Value3, Value4]".to_string()
    }

    fn category(&self) -> &str {
        "document"
    }

    async fn execute(&self, parameters: &HashMap<String, Value>) -> Result<String> {
        let path = parameters
            .get("path")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing 'path' parameter"))?;
        let sheet_param = parameters
            .get("sheet")
            .and_then(|v| v.as_str())
            .unwrap_or("0");
        let limit = parameters
            .get("limit")
            .and_then(|v| v.as_u64())
            .unwrap_or(100) as usize;

        let validated_path = validate_path(path, None)?;
        if !file_exists(&validated_path.to_string_lossy()) {
            anyhow::bail!("ODS file not found: {}", path);
        }

        use quick_xml::Reader;
        use quick_xml::events::Event;
        use std::fs::File;
        use zip::ZipArchive;
        let file = File::open(&validated_path)?;
        let mut archive = ZipArchive::new(file)?;
        let mut content_xml = None;
        for i in 0..archive.len() {
            let entry = archive.by_index(i)?;
            if entry.name() == "content.xml" {
                let mut content = String::new();
                let mut reader = std::io::BufReader::new(entry);
                std::io::Read::read_to_string(&mut reader, &mut content)?;
                content_xml = Some(content);
                break;
            }
        }
        let content =
            content_xml.ok_or_else(|| anyhow::anyhow!("No content.xml found in ODS file"))?;
        let (sheets, sheet_names) = parse_ods_content(&content, limit)?;
        let sheet_data = if let Ok(idx) = sheet_param.parse::<usize>() {
            if idx < sheets.len() {
                &sheets[idx]
            } else {
                anyhow::bail!(
                    "Sheet index {} out of range (max: {})",
                    idx,
                    sheets.len() - 1
                )
            }
        } else {
            let sheet_name = sheet_param;
            if let Some(pos) = sheet_names.iter().position(|name| name == sheet_name) {
                &sheets[pos]
            } else {
                anyhow::bail!(
                    "Sheet '{}' not found. Available sheets: {:?}",
                    sheet_name,
                    sheet_names
                )
            }
        };
        let mut output = String::new();
        for (row_idx, row) in sheet_data.iter().enumerate() {
            output.push_str(&format!("Row {}: {:?}\n", row_idx + 1, row));
        }
        output.push_str(&format!("Total rows: {}", sheet_data.len()));
        Ok(output)
    }

    fn validate(&self, parameters: &HashMap<String, Value>) -> Result<()> {
        parameters
            .get("path")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing required parameter: path"))?;
        Ok(())
    }
}

#[derive(Debug)]
pub struct OdtReadSkill;

#[async_trait::async_trait]
impl Skill for OdtReadSkill {
    fn name(&self) -> &str {
        "odt_read"
    }

    fn description(&self) -> &str {
        "Read and extract text content from OpenDocument Text (.odt) files"
    }

    fn usage_hint(&self) -> &str {
        "Use this skill when the user wants to read OpenDocument text documents (LibreOffice/OpenOffice Writer)"
    }

    fn parameters(&self) -> Vec<SkillParameter> {
        vec![SkillParameter {
            name: "path".to_string(),
            param_type: "string".to_string(),
            description: "Path to the ODT file".to_string(),
            required: true,
            default: None,
            example: Some(Value::String("document.odt".to_string())),
            enum_values: None,
        }]
    }

    fn example_call(&self) -> Value {
        json!({
            "action": "odt_read",
            "parameters": {
                "path": "document.odt"
            }
        })
    }

    fn example_output(&self) -> String {
        "Document content extracted from OpenDocument text file...".to_string()
    }

    fn category(&self) -> &str {
        "document"
    }

    async fn execute(&self, parameters: &HashMap<String, Value>) -> Result<String> {
        let path = parameters
            .get("path")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing 'path' parameter"))?;
        let validated_path = validate_path(path, None)?;
        if !file_exists(&validated_path.to_string_lossy()) {
            anyhow::bail!("ODT file not found: {}", path);
        }
        use quick_xml::Reader;
        use quick_xml::events::Event;
        use std::fs::File;
        use zip::ZipArchive;
        let file = File::open(&validated_path)?;
        let mut archive = ZipArchive::new(file)?;
        let mut content_xml = None;
        for i in 0..archive.len() {
            let entry = archive.by_index(i)?;
            if entry.name() == "content.xml" {
                let mut content = String::new();
                let mut reader = std::io::BufReader::new(entry);
                std::io::Read::read_to_string(&mut reader, &mut content)?;
                content_xml = Some(content);
                break;
            }
        }
        let content =
            content_xml.ok_or_else(|| anyhow::anyhow!("No content.xml found in ODT file"))?;
        let text = extract_text_from_odt_xml(&content);
        Ok(text)
    }

    fn validate(&self, parameters: &HashMap<String, Value>) -> Result<()> {
        parameters
            .get("path")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing required parameter: path"))?;
        Ok(())
    }
}

fn parse_ods_content(xml: &str, limit: usize) -> Result<(Vec<Vec<Vec<String>>>, Vec<String>)> {
    let mut reader = Reader::from_str(xml);
    reader.config_mut().trim_text(true); 
    let mut sheets = Vec::new();
    let mut sheet_names = Vec::new();
    let mut current_sheet = Vec::new();
    let mut current_row = Vec::new();
    let mut in_table = false;
    let mut in_row = false;
    let mut in_cell = false;
    let mut cell_value = String::new();
    let mut current_sheet_name = String::new();
    let mut buf = Vec::new();
    loop {
        match reader.read_event_into(&mut buf) {
            Ok(Event::Start(ref e)) => {
                match e.name().as_ref() {
                    b"table:table" => {
                        in_table = true;
                        for attr in e.attributes() {
                            if let Ok(attr) = attr {
                                if attr.key.as_ref() == b"table:name" {
                                    if let Ok(name) = attr.unescape_value() {
                                        current_sheet_name = name.to_string();
                                    }
                                }
                            }
                        }
                        current_sheet.clear();
                    }
                    b"table:table-row" => {
                        in_row = true;
                        current_row.clear();
                    }
                    b"table:table-cell" => {
                        in_cell = true;
                        cell_value.clear();
                    }
                    _ => {}
                }
            }
            Ok(Event::Text(e)) => {
                if in_cell {
                    if let Ok(text) = e.decode() {
                        cell_value.push_str(&text);
                    }
                }
            }
            Ok(Event::End(ref e)) => match e.name().as_ref() {
                b"table:table-cell" => {
                    in_cell = false;
                    current_row.push(cell_value.trim().to_string());
                }
                b"table:table-row" => {
                    in_row = false;
                    if !current_row.is_empty() && current_sheet.len() < limit {
                        current_sheet.push(current_row.clone());
                    }
                }
                b"table:table" => {
                    if !current_sheet.is_empty() {
                        sheets.push(current_sheet.clone());
                        sheet_names.push(current_sheet_name.clone());
                    }
                    in_table = false;
                }
                _ => {}
            },
            Ok(Event::Eof) => break,
            Err(e) => {
                eprintln!("Error parsing XML: {}", e);
                break;
            }
            _ => {}
        }
        buf.clear();
    }
    Ok((sheets, sheet_names))
}

fn extract_text_from_odt_xml(xml: &str) -> String {
    let mut reader = Reader::from_str(xml);
    reader.config_mut().trim_text(true);
    let mut text_parts = Vec::new();
    let mut in_text = false;
    let mut buf = Vec::new();
    loop {
        match reader.read_event_into(&mut buf) {
            Ok(Event::Start(ref e)) => {
                if e.name().as_ref() == b"text:p" || e.name().as_ref() == b"text:h" {
                    text_parts.push("\n".to_string());
                }
            }
            Ok(Event::Text(e)) => {
                if let Ok(text) = e.decode() {
                    let trimmed = text.trim();
                    if !trimmed.is_empty() {
                        text_parts.push(trimmed.to_string());
                    }
                }
            }
            Ok(Event::Eof) => break,
            Err(e) => {
                eprintln!("Error parsing XML: {}", e);
                break;
            }
            _ => {}
        }
        buf.clear();
    }
    text_parts.join(" ")
}