use std::io::Write;
use std::sync::Arc;
use agent_contracts::backend::capability::export::ExportFileRequest;
use agent_contracts::backend::capability::path::{ResolveBase, ResolvePathRequest};
use agent_contracts::runtime::runtime_view::RuntimeView;
use agent_contracts::tool::{ToolExecutor, ToolSpecView};
use agent_types::interaction::InteractionRequest;
use agent_types::tool::call_types::FinalToolCall;
use agent_types::tool::execution_types::{RawToolOutcome, ToolExecutionError, ToolExecutorOutput};
use async_trait::async_trait;
use tempfile::NamedTempFile;
use super::super::input::SendFileInput;
use super::super::spec::SendFileToolSpec;
pub struct SendFileToolExecutor {
spec: Arc<SendFileToolSpec>,
}
impl SendFileToolExecutor {
pub fn new(spec: Arc<SendFileToolSpec>) -> Self {
Self { spec }
}
}
const KIBIBYTE_BYTES: u64 = 1024;
const MEBIBYTE_BYTES: u64 = KIBIBYTE_BYTES * 1024;
fn format_file_size(size_bytes: u64) -> String {
if size_bytes >= MEBIBYTE_BYTES {
format!("{:.1}MB", size_bytes as f64 / (MEBIBYTE_BYTES as f64))
} else if size_bytes >= KIBIBYTE_BYTES {
format!("{:.1}KB", size_bytes as f64 / (KIBIBYTE_BYTES as f64))
} else {
format!("{size_bytes}B")
}
}
#[async_trait]
impl ToolExecutor for SendFileToolExecutor {
fn spec(&self) -> &dyn ToolSpecView {
self.spec.as_ref()
}
async fn invoke(
&self,
call: &FinalToolCall,
runtime: &dyn RuntimeView,
) -> Result<ToolExecutorOutput, ToolExecutionError> {
let input: SendFileInput = serde_json::from_value(call.input.clone()).map_err(|e| {
ToolExecutionError::ExecutionFailed {
message: format!("failed to parse send_file input: {e}"),
}
})?;
let file_sender = match runtime.channel_file_sender() {
Some(sender) => sender,
None => {
return Ok(ToolExecutorOutput::Completed {
raw_outcome: RawToolOutcome::Error {
message: "send_file is only available in channel sessions (e.g. Feishu)."
.to_string(),
},
});
}
};
let operation_backend = match runtime.operation_backend() {
Some(backend) => backend,
None => {
return Ok(ToolExecutorOutput::Completed {
raw_outcome: RawToolOutcome::Error {
message:
"send_file requires operation backend access, but none is configured"
.to_string(),
},
});
}
};
let resolved_path = match operation_backend
.paths()
.resolve_path(ResolvePathRequest {
raw_path: input.file_path.trim().to_string(),
base: ResolveBase::WorkspaceRoot,
})
.await
{
Ok(path) => path,
Err(error) => {
return Ok(ToolExecutorOutput::Completed {
raw_outcome: RawToolOutcome::Error {
message: format!(
"Failed to resolve file path \"{}\": {error}",
input.file_path
),
},
});
}
};
let exported_file = match operation_backend
.export()
.export_file(ExportFileRequest {
path: resolved_path,
preferred_name: None,
})
.await
{
Ok(file) => file,
Err(error) => {
return Ok(ToolExecutorOutput::Completed {
raw_outcome: RawToolOutcome::Error {
message: format!("Failed to export file \"{}\": {error}", input.file_path),
},
});
}
};
let metadata = exported_file.metadata().clone();
let file_name = metadata.file_name.as_str();
let file_size = metadata
.size_bytes
.map(format_file_size)
.unwrap_or_else(|| "unknown size".to_string());
let confirm_prompt = format!("确认发送文件 \"{}\" ({}) 给您?", file_name, file_size);
let confirm_request = InteractionRequest::Confirm {
prompt: confirm_prompt,
source: None,
};
let response = runtime.interaction().ask(&confirm_request).await;
let confirmed = matches!(
response,
agent_types::interaction::InteractionResponse::Confirmed { allowed: true }
);
if !confirmed {
return Ok(ToolExecutorOutput::Completed {
raw_outcome: RawToolOutcome::Success {
output: "User cancelled file sending.".to_string(),
},
});
}
let mut reader = match exported_file.open_read().await {
Ok(reader) => reader,
Err(error) => {
return Ok(ToolExecutorOutput::Completed {
raw_outcome: RawToolOutcome::Error {
message: format!(
"Failed to read exported file \"{}\": {error}",
input.file_path
),
},
});
}
};
let mut temp_file = match NamedTempFile::new() {
Ok(file) => file,
Err(error) => {
return Ok(ToolExecutorOutput::Completed {
raw_outcome: RawToolOutcome::Error {
message: format!("Failed to create temp file for send_file: {error}"),
},
});
}
};
let mut buffer = Vec::new();
if let Err(error) = tokio::io::AsyncReadExt::read_to_end(&mut reader, &mut buffer).await {
return Ok(ToolExecutorOutput::Completed {
raw_outcome: RawToolOutcome::Error {
message: format!("Failed to read exported file stream: {error}"),
},
});
}
if let Err(error) = temp_file.write_all(buffer.as_slice()) {
return Ok(ToolExecutorOutput::Completed {
raw_outcome: RawToolOutcome::Error {
message: format!("Failed to write temp file for send_file: {error}"),
},
});
}
if let Err(error) = temp_file.flush() {
return Ok(ToolExecutorOutput::Completed {
raw_outcome: RawToolOutcome::Error {
message: format!("Failed to flush temp file for send_file: {error}"),
},
});
}
let temp_path = match temp_file.path().to_str() {
Some(path) => path.to_string(),
None => {
return Ok(ToolExecutorOutput::Completed {
raw_outcome: RawToolOutcome::Error {
message: format!(
"Temp file path is not valid UTF-8: {}",
temp_file.path().display()
),
},
});
}
};
match file_sender
.send_file(temp_path.as_str(), input.label.as_deref())
.await
{
Ok(_) => Ok(ToolExecutorOutput::Completed {
raw_outcome: RawToolOutcome::Success {
output: format!("File \"{}\" sent successfully.", file_name),
},
}),
Err(error) => Ok(ToolExecutorOutput::Completed {
raw_outcome: RawToolOutcome::Error {
message: format!("Failed to send file: {error}"),
},
}),
}
}
}