use crate::DriverCallback;
use crate::DriverContext;
use crate::{
DriverCategory,
types::{Driver, DriverParameter},
};
use crate::{DriverError, DriverResult, file_exists, validate_path};
use serde_json::{Value, json};
use std::collections::HashMap;
use tracing::{debug, info};
#[derive(Debug)]
pub struct OdsReadDriver;
#[async_trait::async_trait]
impl Driver for OdsReadDriver {
fn name(&self) -> &str {
return "ods_read";
}
fn description(&self) -> &str {
return "Read and extract data from OpenDocument Spreadsheet (.ods) files";
}
fn usage_hint(&self) -> &str {
return "Use this skill when the user wants to read OpenDocument spreadsheets (LibreOffice/OpenOffice Calc)";
}
fn parameters(&self) -> Vec<DriverParameter> {
return vec![
DriverParameter {
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,
},
DriverParameter {
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,
},
DriverParameter {
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) -> DriverResult<Value> {
return Ok(json!({
"action": "ods_read",
"parameters": {
"path": "spreadsheet.ods"
}
}));
}
fn example_output(&self) -> String {
return "Sheet: Sheet1\nRow 1: [Value1, Value2]\nRow 2: [Value3, Value4]".to_string();
}
fn category(&self) -> DriverCategory {
return DriverCategory::Document;
}
async fn execute(
&self,
parameters: &HashMap<String, Value>,
_callback: Option<&dyn DriverCallback>,
_context: Option<&DriverContext>,
) -> DriverResult<String> {
debug!("Executing ods_read driver");
let path = parameters.get("path").and_then(|v| v.as_str()).ok_or_else(|| DriverError::missing_parameter("path"))?;
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;
debug!("Reading ODS file: {}, sheet: {}, limit: {}", path, sheet_param, limit);
let validated_path = validate_path(path, None).map_err(|e| DriverError::execution(format!("Invalid path: {}", e)))?;
if !file_exists(&validated_path.to_string_lossy()) {
return Err(DriverError::execution(format!("ODS file not found: {}", path)));
}
use std::fs::File;
use zip::ZipArchive;
let file = File::open(&validated_path).map_err(|e| DriverError::execution(format!("Failed to open file: {}", e)))?;
let mut archive = ZipArchive::new(file).map_err(|e| DriverError::execution(format!("Failed to open ODS archive: {}", e)))?;
let mut content_xml = None;
for i in 0..archive.len() {
let entry = archive.by_index(i).map_err(|e| DriverError::execution(format!("Failed to read archive entry: {}", e)))?;
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)
.map_err(|e| DriverError::execution(format!("Failed to read content.xml: {}", e)))?;
content_xml = Some(content);
break;
}
}
let content = content_xml.ok_or_else(|| DriverError::execution("No content.xml found in ODS file".to_string()))?;
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 {
return Err(DriverError::execution(format!("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 {
return Err(DriverError::execution(format!("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()));
info!("ODS read completed: {} ({} rows)", path, sheet_data.len());
return Ok(output);
}
fn validate(&self, parameters: &HashMap<String, Value>) -> DriverResult<()> {
parameters.get("path").and_then(|v| v.as_str()).ok_or_else(|| DriverError::missing_parameter("path"))?;
return Ok(());
}
}
#[derive(Debug)]
pub struct OdtReadDriver;
#[async_trait::async_trait]
impl Driver for OdtReadDriver {
fn name(&self) -> &str {
return "odt_read";
}
fn description(&self) -> &str {
return "Read and extract text content from OpenDocument Text (.odt) files";
}
fn usage_hint(&self) -> &str {
return "Use this skill when the user wants to read OpenDocument text documents (LibreOffice/OpenOffice Writer)";
}
fn parameters(&self) -> Vec<DriverParameter> {
return vec![DriverParameter {
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) -> DriverResult<Value> {
return Ok(json!({
"action": "odt_read",
"parameters": {
"path": "document.odt"
}
}));
}
fn example_output(&self) -> String {
return "Document content extracted from OpenDocument text file...".to_string();
}
fn category(&self) -> DriverCategory {
return DriverCategory::Document;
}
async fn execute(
&self,
parameters: &HashMap<String, Value>,
_callback: Option<&dyn DriverCallback>,
_context: Option<&DriverContext>,
) -> DriverResult<String> {
debug!("Executing odt_read driver");
let path = parameters.get("path").and_then(|v| v.as_str()).ok_or_else(|| DriverError::missing_parameter("path"))?;
debug!("Reading ODT file: {}", path);
let validated_path = validate_path(path, None).map_err(|e| DriverError::execution(format!("Invalid path: {}", e)))?;
if !file_exists(&validated_path.to_string_lossy()) {
return Err(DriverError::execution(format!("ODT file not found: {}", path)));
}
use std::fs::File;
use zip::ZipArchive;
let file = File::open(&validated_path).map_err(|e| DriverError::execution(format!("Failed to open file: {}", e)))?;
let mut archive = ZipArchive::new(file).map_err(|e| DriverError::execution(format!("Failed to open ODT archive: {}", e)))?;
let mut content_xml = None;
for i in 0..archive.len() {
let entry = archive.by_index(i).map_err(|e| DriverError::execution(format!("Failed to read archive entry: {}", e)))?;
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)
.map_err(|e| DriverError::execution(format!("Failed to read content.xml: {}", e)))?;
content_xml = Some(content);
break;
}
}
let content = content_xml.ok_or_else(|| DriverError::execution("No content.xml found in ODT file".to_string()))?;
let text = extract_text_from_odt_xml(&content);
info!("ODT read completed: {} ({} characters)", path, text.len());
return Ok(text);
}
fn validate(&self, parameters: &HashMap<String, Value>) -> DriverResult<()> {
parameters.get("path").and_then(|v| v.as_str()).ok_or_else(|| DriverError::missing_parameter("path"))?;
return Ok(());
}
}
fn parse_ods_content(xml: &str, limit: usize) -> DriverResult<(Vec<Vec<Vec<String>>>, Vec<String>)> {
use quick_xml::{Reader, events::Event};
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) => {
debug!("Error parsing XML: {}", e);
break;
}
_ => {}
}
buf.clear();
}
return Ok((sheets, sheet_names));
}
fn extract_text_from_odt_xml(xml: &str) -> String {
use quick_xml::{Reader, events::Event};
let mut reader = Reader::from_str(xml);
reader.config_mut().trim_text(true);
let mut text_parts = Vec::new();
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) => {
debug!("Error parsing XML: {}", e);
break;
}
_ => {}
}
buf.clear();
}
return text_parts.join(" ");
}