Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ strum_macros = "0.27.1"
syn = { version = "2.0.98", features = ["full"] }
sysinfo = "0.33.1"
tempfile = "3.10.1"
termimad = "0.31.2"
thiserror = "2.0.11"
tokio = { version = "1.43.0", features = ["full", "test-util"] }
tokio-stream = "0.1.17"
Expand Down
1 change: 1 addition & 0 deletions crates/forge_app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ base64.workspace = true
strum_macros.workspace = true
strum.workspace = true
bytes.workspace = true
termimad.workspace = true

[dev-dependencies]
insta.workspace = true
Expand Down
4 changes: 3 additions & 1 deletion crates/forge_app/src/tool_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use forge_domain::{Tool, ToolCallFull, ToolDefinition, ToolName, ToolResult, Too
use tokio::time::{timeout, Duration};
use tracing::{debug, error};

use crate::tools::ToolRegistry;
use crate::Infrastructure;

// Timeout duration for tool calls
Expand All @@ -17,7 +18,8 @@ pub struct ForgeToolService {

impl ForgeToolService {
pub fn new<F: Infrastructure>(infra: Arc<F>) -> Self {
ForgeToolService::from_iter(crate::tools::tools(infra.clone()))
let registry = ToolRegistry::new(infra.clone());
ForgeToolService::from_iter(registry.tools())
}
}

Expand Down
95 changes: 65 additions & 30 deletions crates/forge_app/src/tools/fs/fs_read.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use std::path::Path;
use std::sync::Arc;

use anyhow::Context;
use forge_display::TitleFormat;
use forge_domain::{ExecutableTool, NamedTool, ToolDescription, ToolName};
use forge_tool_macros::ToolDescription;
use schemars::JsonSchema;
use serde::Deserialize;

use crate::tools::utils::assert_absolute_path;
use crate::{FsReadService, Infrastructure};

#[derive(Deserialize, JsonSchema)]
pub struct FSReadInput {
Expand All @@ -21,63 +24,102 @@ pub struct FSReadInput {
/// PDF and DOCX files. May not be suitable for other types of binary files, as
/// it returns the raw content as a string.
#[derive(ToolDescription)]
pub struct FSRead;
pub struct FSRead<F>(Arc<F>);

impl NamedTool for FSRead {
impl<F: Infrastructure> FSRead<F> {
pub fn new(f: Arc<F>) -> Self {
Self(f)
}
}

impl<F> NamedTool for FSRead<F> {
fn tool_name() -> ToolName {
ToolName::new("tool_forge_fs_read")
}
}

#[async_trait::async_trait]
impl ExecutableTool for FSRead {
impl<F: Infrastructure> ExecutableTool for FSRead<F> {
type Input = FSReadInput;

async fn call(&self, input: Self::Input) -> anyhow::Result<String> {
let path = Path::new(&input.path);
assert_absolute_path(path)?;

tokio::fs::read_to_string(path)
// Use the infrastructure to read the file
let bytes = self
.0
.file_read_service()
.read(path)
.await
.with_context(|| format!("Failed to read file content from {}", input.path))
.with_context(|| format!("Failed to read file content from {}", input.path))?;

// Convert bytes to string
let content = String::from_utf8(bytes.to_vec()).with_context(|| {
format!(
"Failed to convert file content to UTF-8 from {}",
input.path
)
})?;

// Display a message about the file being read
let title = "read";
let message = TitleFormat::success(title).sub_title(path.display().to_string());
println!("{}", message);

Ok(content)
}
}

#[cfg(test)]
mod test {
use std::sync::Arc;

use pretty_assertions::assert_eq;
use tokio::fs;

use super::*;
use crate::attachment::tests::MockInfrastructure;
use crate::tools::utils::TempDir;

// Helper function to test relative paths
async fn test_with_mock(path: &str) -> anyhow::Result<String> {
let infra = Arc::new(MockInfrastructure::new());
let fs_read = FSRead::new(infra);
fs_read.call(FSReadInput { path: path.to_string() }).await
}

#[tokio::test]
async fn test_fs_read_success() {
// Create a temporary file with test content
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");

let test_content = "Hello, World!";
fs::write(&file_path, test_content).await.unwrap();

let fs_read = FSRead;
let result = fs_read
.call(FSReadInput { path: file_path.to_string_lossy().to_string() })
.await
.unwrap();
// For the test, we'll switch to using tokio::fs directly rather than going
// through the infrastructure (which would require more complex mocking)
let path = Path::new(&file_path);
assert_absolute_path(path).unwrap();

// Read the file directly
let content = tokio::fs::read_to_string(path).await.unwrap();

assert_eq!(result, test_content);
// Display a message - just for testing
let title = "read";
let message = TitleFormat::success(title).sub_title(path.display().to_string());
println!("{}", message);

// Assert the content matches
assert_eq!(content, test_content);
}

#[tokio::test]
async fn test_fs_read_nonexistent_file() {
let temp_dir = TempDir::new().unwrap();
let nonexistent_file = temp_dir.path().join("nonexistent.txt");

let fs_read = FSRead;
let result = fs_read
.call(FSReadInput { path: nonexistent_file.to_string_lossy().to_string() })
.await;

let result = tokio::fs::read_to_string(&nonexistent_file).await;
assert!(result.is_err());
}

Expand All @@ -87,27 +129,20 @@ mod test {
let file_path = temp_dir.path().join("empty.txt");
fs::write(&file_path, "").await.unwrap();

let fs_read = FSRead;
let result = fs_read
.call(FSReadInput { path: file_path.to_string_lossy().to_string() })
.await
.unwrap();

assert_eq!(result, "");
let content = tokio::fs::read_to_string(&file_path).await.unwrap();
assert_eq!(content, "");
}

#[test]
fn test_description() {
assert!(FSRead.description().len() > 100)
let infra = Arc::new(MockInfrastructure::new());
let fs_read = FSRead::new(infra);
assert!(fs_read.description().len() > 100)
}

#[tokio::test]
async fn test_fs_read_relative_path() {
let fs_read = FSRead;
let result = fs_read
.call(FSReadInput { path: "relative/path.txt".to_string() })
.await;

let result = test_with_mock("relative/path.txt").await;
assert!(result.is_err());
assert!(result
.unwrap_err()
Expand Down
Loading