From 25140394514e296878f06e8cb6f5717084354a95 Mon Sep 17 00:00:00 2001 From: Jean Silva Date: Sun, 15 Feb 2026 17:29:18 +0100 Subject: [PATCH 1/2] feat(search): add fast file-finder tool using fd and fzf Add FileFinderTool to codebase_explorer to provide fast file name searching powered by the fd utility and optional ranking via fzf. The tool runs fd to list project files, uses fzf -f to rank matches non-interactively, and falls back to case-insensitive substring matching if fzf is unavailable. Results are returned as paths relative to the project root and capped to the top 30 entries. Handle missing tools and subprocess errors with clear error messages. Also: - import subprocess in codebase_explorer. - adjust CLI output formatting: simplify MCP error suggestion line, change the agent list display to a magenta registry line, and print agent execution results without Panel to avoid nested panels. - update Dockerfile comments to note installation of search tools (ripgrep, fd-find, fzf). These changes add a performant, user-facing file search feature and clean up CLI presentation for clearer console output. --- Dockerfile | 16 +++++ .../src/agentic_framework/cli.py | 10 +-- .../agentic_framework/core/developer_agent.py | 16 +++-- .../src/agentic_framework/tools/__init__.py | 2 + .../agentic_framework/tools/code_searcher.py | 32 ++++++---- .../tools/codebase_explorer.py | 63 +++++++++++++++++++ 6 files changed, 119 insertions(+), 20 deletions(-) diff --git a/Dockerfile b/Dockerfile index af0ff48..3865e27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,22 @@ WORKDIR /app # Copy from the official installer: https://github.com/astral-sh/uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ +# Install Search Tools +# ripgrep: ultra-fast text searching +# fd-find: user-friendly alternative to 'find' +# fzf: general-purpose command-line fuzzy finder +RUN apt-get update && apt-get install -y --no-install-recommends \ + ripgrep \ + fd-find \ + fzf \ + && rm -rf /var/lib/apt/lists/* + +# Note: In Debian/Ubuntu, the 'fd' executable is renamed to 'fdfind'. +# We create a symbolic link so the agent can just call 'fd'. +RUN ln -s $(which fdfind) /usr/local/bin/fd + +# Python Environment Setup + # Set environment variables ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ diff --git a/agentic-framework/src/agentic_framework/cli.py b/agentic-framework/src/agentic_framework/cli.py index 48afae0..abc7422 100644 --- a/agentic-framework/src/agentic_framework/cli.py +++ b/agentic-framework/src/agentic_framework/cli.py @@ -6,7 +6,6 @@ import typer from dotenv import load_dotenv from rich.console import Console -from rich.panel import Panel from agentic_framework.constants import LOGS_DIR from agentic_framework.mcp import MCPConnectionError, MCPProvider @@ -57,7 +56,7 @@ def _handle_mcp_connection_error(error: MCPConnectionError) -> None: elif error.__cause__: console.print(f"[red] cause: {error.__cause__}[/red]") - console.print("\n[yellow]Suggestion:[/yellow] Ensure the MCP server URL is correct and you have network access.") + console.print("[yellow]Suggestion:[/yellow] Ensure the MCP server URL is correct and you have network access.") if "web-fetch" in error.server_name: console.print("[yellow]Note:[/yellow] web-fetch requires a valid remote URL. Check mcp/config.py") @@ -93,7 +92,9 @@ def execute_agent(agent_name: str, input_text: str, timeout_sec: int) -> str: def list_agents() -> None: """List all available agents.""" agents = AgentRegistry.list_agents() - console.print(Panel(f"[bold green]Available Agents:[/bold green] {', '.join(agents)}", title="Registry")) + console.print( + f"[bold magenta]Registry:[/bold magenta] [bold green]Available Agents:[/bold green] {', '.join(agents)}\n" + ) @app.callback(invoke_without_command=True) @@ -127,7 +128,8 @@ def command( try: result = execute_agent(agent_name=agent_name, input_text=input_text, timeout_sec=timeout_sec) - console.print(Panel(result, title=f"[bold green]Result from {agent_name}[/bold green]")) + console.print(f"[bold green]Result from {agent_name}:[/bold green]") + console.print(result) except typer.Exit: raise except TimeoutError as error: diff --git a/agentic-framework/src/agentic_framework/core/developer_agent.py b/agentic-framework/src/agentic_framework/core/developer_agent.py index 836dac3..afc53f1 100644 --- a/agentic-framework/src/agentic_framework/core/developer_agent.py +++ b/agentic-framework/src/agentic_framework/core/developer_agent.py @@ -7,6 +7,7 @@ from agentic_framework.registry import AgentRegistry from agentic_framework.tools import ( CodeSearcher, + FileFinderTool, FileFragmentReaderTool, FileOutlinerTool, StructureExplorerTool, @@ -25,11 +26,12 @@ def system_prompt(self) -> str: return """You are a Principal Software Engineer assistant. Your goal is to help the user understand and maintain their codebase. You have access to several specialized tools for: - 1. Discovering the project structure. - 2. Extracting class and function outlines from Python files. - 3. Reading specific fragments of a file. - 4. Searching the codebase for patterns using ripgrep. + 1. Discovering the project structure and finding files by name (`find_files`). + 2. Extracting class and function outlines from Python files (`get_file_outline`). + 3. Reading specific fragments of a file (`read_file_fragment`). + 4. Searching the codebase for patterns using ripgrep (`code_search`). + When you need to find a specific file by name, use `find_files`. When asked about the project structure, start with `discover_structure`. When asked to explain a file, start with `get_file_outline` to get an overview. Use `read_file_fragment` to read specific lines if you need more detail. @@ -42,6 +44,7 @@ def system_prompt(self) -> str: def local_tools(self) -> Sequence[Any]: # Initialize tool instances with project root searcher = CodeSearcher(str(BASE_DIR)) + finder = FileFinderTool(str(BASE_DIR)) explorer = StructureExplorerTool(str(BASE_DIR)) outliner = FileOutlinerTool(str(BASE_DIR)) reader = FileFragmentReaderTool(str(BASE_DIR)) @@ -52,6 +55,11 @@ def local_tools(self) -> Sequence[Any]: name=searcher.name, description=searcher.description, ), + StructuredTool.from_function( + func=finder.invoke, + name=finder.name, + description=finder.description, + ), StructuredTool.from_function( func=explorer.invoke, name=explorer.name, diff --git a/agentic-framework/src/agentic_framework/tools/__init__.py b/agentic-framework/src/agentic_framework/tools/__init__.py index f681878..f85c404 100644 --- a/agentic-framework/src/agentic_framework/tools/__init__.py +++ b/agentic-framework/src/agentic_framework/tools/__init__.py @@ -3,6 +3,7 @@ FileFragmentReaderTool, FileOutlinerTool, StructureExplorerTool, + FileFinderTool, ) from .example import CalculatorTool, WeatherTool from .web_search import WebSearchTool @@ -15,4 +16,5 @@ "StructureExplorerTool", "FileOutlinerTool", "FileFragmentReaderTool", + "FileFinderTool", ] diff --git a/agentic-framework/src/agentic_framework/tools/code_searcher.py b/agentic-framework/src/agentic_framework/tools/code_searcher.py index d0b6ecf..f176a2a 100644 --- a/agentic-framework/src/agentic_framework/tools/code_searcher.py +++ b/agentic-framework/src/agentic_framework/tools/code_searcher.py @@ -1,5 +1,5 @@ import subprocess -from typing import Any, Dict, List, Union +from typing import Any, List, Union from agentic_framework.interfaces.base import Tool @@ -22,20 +22,28 @@ def invoke(self, input_str: str) -> Any: """Executes a search across the codebase. input_str is the search query.""" return self.grep_search(input_str) - def grep_search(self, query: str, file_type: str = "py") -> Union[List[str], List[Dict[str, Any]]]: + def grep_search(self, query: str, glob: str = "*") -> Union[List[str], str]: """ - Executes a search across the codebase. - Returns matches with line numbers and snippets. + Executes a search across the codebase using ripgrep. + Returns matches in vimgrep format (file:line:col:text). """ try: - # -n: line numbers, -C 1: context, -t: type - cmd = ["rg", "--json", "-t", file_type, query, self.root_dir] + # --vimgrep: file:line:col:text + # --smart-case: case-insensitive unless query has uppercase + # --iglob: filter by glob pattern + # --max-columns: limit long lines to avoid token blowup + cmd = ["rg", "--vimgrep", "--smart-case", "--max-columns", "500", "--iglob", glob, query, self.root_dir] result = subprocess.run(cmd, capture_output=True, text=True, check=False) - # Note: In a production framework, you'd parse the JSON stream from rg - # For this MVP, we return a simplified string representation - if result.returncode != 0: - return [] - return result.stdout.splitlines()[:20] # Limit to top 20 for token safety + if result.returncode == 0: + lines = result.stdout.splitlines() + if not lines: + return "No matches found." + return lines[:30] # Increased to 30 for more context + + if result.stderr: + return f"Error executing rg: {result.stderr}" + return "No matches found." + except FileNotFoundError: - return ["Error: ripgrep (rg) is not installed on the host system."] + return "Error: ripgrep (rg) is not installed. Please install it to use this tool." diff --git a/agentic-framework/src/agentic_framework/tools/codebase_explorer.py b/agentic-framework/src/agentic_framework/tools/codebase_explorer.py index bfaf749..4687770 100644 --- a/agentic-framework/src/agentic_framework/tools/codebase_explorer.py +++ b/agentic-framework/src/agentic_framework/tools/codebase_explorer.py @@ -1,4 +1,5 @@ import re +import subprocess from pathlib import Path from typing import Any, Dict, List, Optional, Union @@ -134,3 +135,65 @@ def invoke(self, input_str: str) -> Any: return "".join(lines[max(0, start_line - 1) : end_line]) except Exception as e: return f"Error: {e}" + + +class FileFinderTool(CodebaseExplorer, Tool): + """Tool to find files by name using 'fd'.""" + + @property + def name(self) -> str: + return "find_files" + + @property + def description(self) -> str: + return ( + "Fast file search by name using 'fd'. Returns paths relative to project root. " + "Input is a search pattern (regex or simple string)." + ) + + def invoke(self, pattern: str) -> Any: + try: + # First, get candidates using fd + # -H: hidden files, -I: ignore .gitignore for faster full list if needed, + # but usually we want to respect it, so let's stick to standard fd. + # We'll list all files and let fzf rank them. + fd_cmd = ["fd", "--color", "never", "-H", ".", str(self.root_dir)] + fd_result = subprocess.run(fd_cmd, capture_output=True, text=True, check=False) + + if fd_result.returncode != 0: + if fd_result.stderr: + return f"Error executing fd: {fd_result.stderr}" + return "No files found." + + all_files = fd_result.stdout + if not all_files: + return "No files found." + + # Use fzf to rank the files based on the pattern + # fzf -f performs a non-interactive fuzzy search + try: + fzf_cmd = ["fzf", "-f", pattern] + fzf_result = subprocess.run(fzf_cmd, input=all_files, capture_output=True, text=True, check=False) + if fzf_result.returncode == 0: + ranked_files = fzf_result.stdout.splitlines() + else: + # Fallback to simple substring match if fzf returns nothing or fails + ranked_files = [f for f in all_files.splitlines() if pattern.lower() in f.lower()] + except FileNotFoundError: + # Fallback if fzf is missing + ranked_files = [f for f in all_files.splitlines() if pattern.lower() in f.lower()] + + # Convert absolute paths back to relative and limit results + relative_files = [] + for f in ranked_files[:30]: # Limit to top 30 + try: + relative_files.append(str(Path(f).relative_to(self.root_dir))) + except ValueError: + relative_files.append(f) + + if not relative_files: + return "No matches found for the given pattern." + + return relative_files + except FileNotFoundError: + return "Error: Required search tools (fd) not found." From f0ea5958a6f2df753d5344daf8de1629cbd7dc55 Mon Sep 17 00:00:00 2001 From: Jean Silva Date: Sun, 15 Feb 2026 17:39:16 +0100 Subject: [PATCH 2/2] refactor(tools): reorder imports to group FileFinderTool Reorder the imports in tools.__init__ to place FileFinderTool with the other file-related tools (FileFragmentReaderTool, FileOutlinerTool, StructureExplorerTool). This fixes an import ordering inconsistency introduced earlier and keeps related file tools grouped together for better readability and maintainability. --- agentic-framework/src/agentic_framework/tools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agentic-framework/src/agentic_framework/tools/__init__.py b/agentic-framework/src/agentic_framework/tools/__init__.py index f85c404..6da34c0 100644 --- a/agentic-framework/src/agentic_framework/tools/__init__.py +++ b/agentic-framework/src/agentic_framework/tools/__init__.py @@ -1,9 +1,9 @@ from .code_searcher import CodeSearcher from .codebase_explorer import ( + FileFinderTool, FileFragmentReaderTool, FileOutlinerTool, StructureExplorerTool, - FileFinderTool, ) from .example import CalculatorTool, WeatherTool from .web_search import WebSearchTool