use super::Handler;
use crate::agent::views::ActionResult;
use crate::error::{BrowsingError, Result};
use crate::tools::views::{ActionContext, ActionParams};
use async_trait::async_trait;
use std::path::Path;
use tokio::fs;
use tracing::info;
pub struct FileHandler;
#[async_trait]
impl Handler for FileHandler {
async fn handle(
&self,
params: &ActionParams<'_>,
_context: &mut ActionContext<'_>,
) -> Result<ActionResult> {
let action_type = params.get_action_type().unwrap_or("unknown");
match action_type {
"write_file" => self.write_file(params).await,
"read_file" => self.read_file(params).await,
"replace_file" => self.replace_file(params).await,
_ => Err(BrowsingError::Tool(format!(
"Unknown file action: {action_type}"
))),
}
}
}
impl FileHandler {
async fn write_file(&self, params: &ActionParams<'_>) -> Result<ActionResult> {
let path = params.get_required_str("path")?;
let content = params.get_required_str("content")?;
if path.contains("..") || path.contains('~') {
return Err(BrowsingError::Tool(
"Invalid file path: path traversal not allowed".into(),
));
}
let path = Path::new(path);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await.map_err(|e| {
BrowsingError::Tool(format!("Failed to create directory: {e}"))
})?;
}
fs::write(path, content).await.map_err(|e| {
BrowsingError::Tool(format!("Failed to write file: {e}"))
})?;
let memory = format!("Wrote {} bytes to file {}", content.len(), path.display());
info!("[file] {}", memory);
Ok(ActionResult {
extracted_content: Some(format!(
"Successfully wrote {} bytes to {}",
content.len(),
path.display()
)),
long_term_memory: Some(memory),
..Default::default()
})
}
async fn read_file(&self, params: &ActionParams<'_>) -> Result<ActionResult> {
let path = params.get_required_str("path")?;
if path.contains("..") || path.contains('~') {
return Err(BrowsingError::Tool(
"Invalid file path: path traversal not allowed".into(),
));
}
let path = Path::new(path);
if !path.exists() {
return Err(BrowsingError::Tool(format!(
"File {} does not exist",
path.display()
)));
}
if !path.is_file() {
return Err(BrowsingError::Tool(format!(
"Path {} is not a file",
path.display()
)));
}
let content = fs::read_to_string(path).await.map_err(|e| {
BrowsingError::Tool(format!("Failed to read file: {e}"))
})?;
let memory = format!(
"Read {} bytes from file {}",
content.len(),
path.display()
);
info!("[file] {}", memory);
Ok(ActionResult {
extracted_content: Some(content.clone()),
long_term_memory: Some(memory),
..Default::default()
})
}
async fn replace_file(&self, params: &ActionParams<'_>) -> Result<ActionResult> {
let path = params.get_required_str("path")?;
let old_text = params.get_required_str("old_text")?;
let new_text = params.get_required_str("new_text")?;
if path.contains("..") || path.contains('~') {
return Err(BrowsingError::Tool(
"Invalid file path: path traversal not allowed".into(),
));
}
let path = Path::new(path);
if !path.exists() {
return Err(BrowsingError::Tool(format!(
"File {} does not exist",
path.display()
)));
}
if !path.is_file() {
return Err(BrowsingError::Tool(format!(
"Path {} is not a file",
path.display()
)));
}
let content = fs::read_to_string(path).await.map_err(|e| {
BrowsingError::Tool(format!("Failed to read file for replacement: {e}"))
})?;
let updated = content.replace(old_text, new_text);
let replacements = content.matches(old_text).count();
fs::write(path, updated).await.map_err(|e| {
BrowsingError::Tool(format!("Failed to write replaced content: {e}"))
})?;
let memory = format!(
"Replaced {} occurrence(s) of '{}' with '{}' in {}",
replacements,
old_text,
new_text,
path.display()
);
info!("[file] {}", memory);
Ok(ActionResult {
extracted_content: Some(format!("Replaced {replacements} occurrence(s)")),
long_term_memory: Some(memory),
..Default::default()
})
}
}