diff --git a/lib/crewai-tools/src/crewai_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/__init__.py index 1ecf814fd..89d851a1b 100644 --- a/lib/crewai-tools/src/crewai_tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/__init__.py @@ -77,6 +77,7 @@ from crewai_tools.tools.generate_crewai_automation_tool.generate_crewai_automati GenerateCrewaiAutomationTool, ) from crewai_tools.tools.github_search_tool.github_search_tool import GithubSearchTool +from crewai_tools.tools.glob_tool.glob_tool import GlobTool from crewai_tools.tools.grep_tool.grep_tool import GrepTool from crewai_tools.tools.hyperbrowser_load_tool.hyperbrowser_load_tool import ( HyperbrowserLoadTool, @@ -231,6 +232,7 @@ __all__ = [ "FirecrawlSearchTool", "GenerateCrewaiAutomationTool", "GithubSearchTool", + "GlobTool", "GrepTool", "HyperbrowserLoadTool", "InvokeCrewAIAutomationTool", diff --git a/lib/crewai-tools/src/crewai_tools/tools/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/__init__.py index 1dfc614c5..96d12620d 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/tools/__init__.py @@ -66,6 +66,7 @@ from crewai_tools.tools.generate_crewai_automation_tool.generate_crewai_automati GenerateCrewaiAutomationTool, ) from crewai_tools.tools.github_search_tool.github_search_tool import GithubSearchTool +from crewai_tools.tools.glob_tool.glob_tool import GlobTool from crewai_tools.tools.grep_tool.grep_tool import GrepTool from crewai_tools.tools.hyperbrowser_load_tool.hyperbrowser_load_tool import ( HyperbrowserLoadTool, @@ -215,6 +216,7 @@ __all__ = [ "FirecrawlSearchTool", "GenerateCrewaiAutomationTool", "GithubSearchTool", + "GlobTool", "GrepTool", "HyperbrowserLoadTool", "InvokeCrewAIAutomationTool", diff --git a/lib/crewai-tools/src/crewai_tools/tools/glob_tool/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/glob_tool/__init__.py new file mode 100644 index 000000000..bede5a172 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/glob_tool/__init__.py @@ -0,0 +1,4 @@ +from crewai_tools.tools.glob_tool.glob_tool import GlobTool + + +__all__ = ["GlobTool"] diff --git a/lib/crewai/src/crewai/tools/agent_tools/glob_tool.py b/lib/crewai-tools/src/crewai_tools/tools/glob_tool/glob_tool.py similarity index 97% rename from lib/crewai/src/crewai/tools/agent_tools/glob_tool.py rename to lib/crewai-tools/src/crewai_tools/tools/glob_tool/glob_tool.py index b545bcda6..a96b15070 100644 --- a/lib/crewai/src/crewai/tools/agent_tools/glob_tool.py +++ b/lib/crewai-tools/src/crewai_tools/tools/glob_tool/glob_tool.py @@ -7,10 +7,9 @@ import os from pathlib import Path from typing import Literal +from crewai.tools import BaseTool from pydantic import BaseModel, Field -from crewai.tools.base_tool import BaseTool - MAX_FILES = 1000 MAX_OUTPUT_CHARS = 30_000 @@ -87,6 +86,11 @@ class GlobTool(BaseTool): Recursively searches for files matching a glob pattern within a directory. Useful for discovering files by name, extension, or path pattern. Complements GrepTool which searches by file content. + + Example: + >>> tool = GlobTool() + >>> result = tool.run(pattern="*.py", path="/path/to/project") + >>> result = tool.run(pattern="**/*.yaml", output_mode="detailed") """ name: str = "glob" diff --git a/lib/crewai-tools/tests/tools/glob_tool_test.py b/lib/crewai-tools/tests/tools/glob_tool_test.py new file mode 100644 index 000000000..fe012f08d --- /dev/null +++ b/lib/crewai-tools/tests/tools/glob_tool_test.py @@ -0,0 +1,182 @@ +"""Unit tests for GlobTool.""" + +from pathlib import Path + +import pytest + +from crewai_tools import GlobTool + + +@pytest.fixture +def sample_dir(tmp_path: Path) -> Path: + """Create a temp directory with sample files for testing.""" + # src/main.py + src = tmp_path / "src" + src.mkdir() + (src / "main.py").write_text("print('hello')") + (src / "utils.py").write_text("def helper(): pass") + + # src/components/button.tsx + components = src / "components" + components.mkdir() + (components / "button.tsx").write_text("export const Button = () => {}") + (components / "input.tsx").write_text("export const Input = () => {}") + + # tests/test_main.py + tests = tmp_path / "tests" + tests.mkdir() + (tests / "test_main.py").write_text("def test_hello(): pass") + (tests / "test_utils.py").write_text("def test_helper(): pass") + + # config files + (tmp_path / "config.yaml").write_text("key: value") + (tmp_path / "settings.json").write_text("{}") + + # hidden file + (tmp_path / ".hidden").write_text("secret") + + # empty directory + (tmp_path / "empty_dir").mkdir() + + return tmp_path + + +class TestGlobTool: + """Tests for GlobTool.""" + + def setup_method(self) -> None: + """Set up test fixtures.""" + self.tool = GlobTool() + + def test_tool_metadata(self) -> None: + """Test tool has correct name and description.""" + assert self.tool.name == "glob" + assert "pattern" in self.tool.description.lower() + + def test_args_schema(self) -> None: + """Test that args_schema has correct fields.""" + schema = self.tool.args_schema + fields = schema.model_fields + + assert "pattern" in fields + assert fields["pattern"].is_required() + + assert "path" in fields + assert not fields["path"].is_required() + + assert "output_mode" in fields + assert not fields["output_mode"].is_required() + + assert "include_hidden" in fields + assert not fields["include_hidden"].is_required() + + def test_find_python_files(self, sample_dir: Path) -> None: + """Test finding Python files with *.py pattern.""" + result = self.tool._run(pattern="*.py", path=str(sample_dir)) + assert "main.py" in result + assert "utils.py" in result + assert "test_main.py" in result + assert "test_utils.py" in result + + def test_find_specific_extension(self, sample_dir: Path) -> None: + """Test finding files with specific extension.""" + result = self.tool._run(pattern="*.tsx", path=str(sample_dir)) + assert "button.tsx" in result + assert "input.tsx" in result + assert "main.py" not in result + + def test_find_test_files(self, sample_dir: Path) -> None: + """Test finding test files with test_*.py pattern.""" + result = self.tool._run(pattern="test_*.py", path=str(sample_dir)) + assert "test_main.py" in result + assert "test_utils.py" in result + # Verify non-test files are not included (check for exact filename, not substring) + lines = result.split("\n") + file_lines = [l for l in lines if l.endswith(".py")] + assert not any(l.endswith("/main.py") or l == "main.py" for l in file_lines) + assert not any(l.endswith("/utils.py") or l == "utils.py" for l in file_lines) + + def test_recursive_pattern(self, sample_dir: Path) -> None: + """Test explicit recursive pattern **/*.py.""" + result = self.tool._run(pattern="**/*.py", path=str(sample_dir)) + assert "main.py" in result + assert "test_main.py" in result + + def test_hidden_files_excluded_by_default(self, sample_dir: Path) -> None: + """Test hidden files are excluded by default.""" + result = self.tool._run(pattern="*", path=str(sample_dir)) + assert ".hidden" not in result + + def test_hidden_files_included(self, sample_dir: Path) -> None: + """Test hidden files included when include_hidden=True.""" + result = self.tool._run( + pattern=".*", path=str(sample_dir), include_hidden=True + ) + assert ".hidden" in result + + def test_output_mode_paths(self, sample_dir: Path) -> None: + """Test paths output mode shows file paths.""" + result = self.tool._run( + pattern="*.py", path=str(sample_dir), output_mode="paths" + ) + # Should contain full paths + assert str(sample_dir) in result or "main.py" in result + + def test_output_mode_detailed(self, sample_dir: Path) -> None: + """Test detailed output mode shows sizes.""" + result = self.tool._run( + pattern="*.py", path=str(sample_dir), output_mode="detailed" + ) + # Should contain size indicators + assert "B" in result # Bytes indicator + + def test_output_mode_tree(self, sample_dir: Path) -> None: + """Test tree output mode shows directory structure.""" + result = self.tool._run( + pattern="*.py", path=str(sample_dir), output_mode="tree" + ) + assert str(sample_dir) in result + + def test_dirs_only(self, sample_dir: Path) -> None: + """Test dirs_only=True only returns directories.""" + result = self.tool._run( + pattern="*", + path=str(sample_dir), + dirs_only=True, + files_only=False, + ) + assert "src" in result or "tests" in result or "empty_dir" in result + # Should not contain file extensions + assert ".py" not in result + assert ".yaml" not in result + + def test_path_not_found(self) -> None: + """Test error message when path doesn't exist.""" + result = self.tool._run(pattern="*.py", path="/nonexistent/path") + assert "Error" in result + assert "does not exist" in result + + def test_path_is_file(self, sample_dir: Path) -> None: + """Test error message when path is a file, not directory.""" + file_path = sample_dir / "config.yaml" + result = self.tool._run(pattern="*.py", path=str(file_path)) + assert "Error" in result + assert "not a directory" in result + + def test_no_matches(self, sample_dir: Path) -> None: + """Test message when no files match pattern.""" + result = self.tool._run(pattern="*.xyz", path=str(sample_dir)) + assert "No files found" in result + + def test_found_count_in_output(self, sample_dir: Path) -> None: + """Test that result includes count of found files.""" + result = self.tool._run(pattern="*.py", path=str(sample_dir)) + assert "Found" in result + assert "file(s)" in result + + def test_run_with_kwargs(self, sample_dir: Path) -> None: + """Test _run ignores extra kwargs.""" + result = self.tool._run( + pattern="*.py", path=str(sample_dir), extra_arg="ignored" + ) + assert "main.py" in result diff --git a/lib/crewai/src/crewai/tools/agent_tools/__init__.py b/lib/crewai/src/crewai/tools/agent_tools/__init__.py index d4c93778f..53c47739b 100644 --- a/lib/crewai/src/crewai/tools/agent_tools/__init__.py +++ b/lib/crewai/src/crewai/tools/agent_tools/__init__.py @@ -1,10 +1 @@ """Agent tools for crewAI.""" - -from crewai.tools.agent_tools.glob_tool import GlobTool -from crewai.tools.agent_tools.grep_tool import GrepTool - - -__all__ = [ - "GlobTool", - "GrepTool", -]