From 81e0a324a5dc7fbba90d93c222ac679c90b387c3 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 30 Jun 2025 19:43:17 +0200 Subject: [PATCH] Possibility to restrict paths to search for The LLM also tries passing the relative_path param to the tool --- scripts/demo_run_tools.py | 12 +++-- src/serena/agent.py | 33 +++++++----- .../language_servers/solargraph/solargraph.py | 1 + src/solidlsp/ls.py | 54 ++++++++++++------- 4 files changed, 63 insertions(+), 37 deletions(-) diff --git a/scripts/demo_run_tools.py b/scripts/demo_run_tools.py index 8822db7a..7679372a 100644 --- a/scripts/demo_run_tools.py +++ b/scripts/demo_run_tools.py @@ -20,10 +20,16 @@ class InMemorySerenaConfig(SerenaConfigBase): if __name__ == "__main__": - agent = SerenaAgent(project=REPO_ROOT) + agent = SerenaAgent(project=REPO_ROOT, serena_config=InMemorySerenaConfig()) # apply a tool find_refs_tool = agent.get_tool(FindReferencingSymbolsTool) - print("Finding the symbol 'SyncLanguageServer'\n") - result = agent.execute_task(lambda: find_refs_tool.apply(name_path="SolidLanguageServer", relative_path="src/solidlsp/ls.py")) + find_file_tool = agent.get_tool(FindFileTool) + search_pattern_tool = agent.get_tool(SearchForPatternTool) + + result = agent.execute_task( + lambda: search_pattern_tool.apply( + ".*Pyright.*|.*Omnisharp.*", restrict_search_to_code_files=False, relative_path="src/solidlsp/ls.py" + ) + ) pprint(json.loads(result)) diff --git a/src/serena/agent.py b/src/serena/agent.py index 0098e83a..25d2944a 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -2178,6 +2178,7 @@ class SearchForPatternTool(Tool): context_lines_after: int = 0, paths_include_glob: str | None = None, paths_exclude_glob: str | None = None, + relative_path: str = "", restrict_search_to_code_files: bool = False, max_answer_chars: int = _DEFAULT_MAX_ANSWER_LENGTH, ) -> str: @@ -2191,6 +2192,8 @@ class SearchForPatternTool(Tool): :param context_lines_after: Number of lines of context to include after each match :param paths_include_glob: optional glob pattern specifying files to include in the search; if not provided, search globally. :param paths_exclude_glob: optional glob pattern specifying files to exclude from the search (takes precedence over paths_include_glob). + :param relative_path: only subpaths of this path (relative to the repo root) will be analyzed. If a path to a single + file is passed, only that will be searched. The path must exist, otherwise a FileNotFoundError will be raised. :param max_answer_chars: if the output is longer than this number of characters, no content will be returned. Don't adjust unless there is really no other way to get the content required for the task. Instead, if the output is too long, you should @@ -2203,34 +2206,36 @@ class SearchForPatternTool(Tool): which is why it is the default. :return: A JSON object mapping file paths to lists of matched consecutive lines (with context, if requested). """ - # this was previously a kwarg and was true by default - # However, the LLM doesn't really know which files are taken into account by the language server - # and which onees + abs_path = os.path.join(self.get_project_root(), relative_path) + if not os.path.exists(abs_path): + raise FileNotFoundError(f"Relative path {relative_path} does not exist.") + if restrict_search_to_code_files: matches = self.language_server.search_files_for_pattern( pattern=pattern, + relative_path=relative_path, context_lines_before=context_lines_before, context_lines_after=context_lines_after, paths_include_glob=paths_include_glob, paths_exclude_glob=paths_exclude_glob, ) else: - # we walk through all files in the project starting from the root - project_root = self.get_project_root() - rel_paths_to_search = [] - for root, dirs, files in os.walk(project_root): - # don't explore ignored dirs - dirs[:] = [d for d in dirs if not self.agent.path_is_gitignored(os.path.join(root, d))] - for file in files: - file_path = os.path.join(root, file) - if not self.agent.path_is_gitignored(file_path): - relative_path = os.path.relpath(file_path, project_root) - rel_paths_to_search.append(relative_path) + if os.path.isfile(abs_path): + rel_paths_to_search = [relative_path] + else: + dirs, rel_paths_to_search = scan_directory( + path=abs_path, + recursive=True, + is_ignored_dir=self.agent.path_is_gitignored, + is_ignored_file=self.agent.path_is_gitignored, + relative_to=self.get_project_root(), + ) # TODO (maybe): not super efficient to walk through the files again and filter if glob patterns are provided # but it probably never matters and this version required no further refactoring matches = search_files( rel_paths_to_search, pattern, + root_path=self.get_project_root(), paths_include_glob=paths_include_glob, paths_exclude_glob=paths_exclude_glob, ) diff --git a/src/solidlsp/language_servers/solargraph/solargraph.py b/src/solidlsp/language_servers/solargraph/solargraph.py index 2eea3ec7..171cc888 100644 --- a/src/solidlsp/language_servers/solargraph/solargraph.py +++ b/src/solidlsp/language_servers/solargraph/solargraph.py @@ -10,6 +10,7 @@ import pathlib import stat import subprocess import threading + from overrides import override from solidlsp.ls import SolidLanguageServer diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index f77ed994..a4d8f920 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -5,7 +5,6 @@ import logging import os import pathlib import pickle -import re import subprocess import threading from abc import ABC, abstractmethod @@ -20,6 +19,7 @@ import pathspec import tqdm from serena.text_utils import MatchedConsecutiveLines, search_files +from serena.util.file_system import match_path from solidlsp import ls_types from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_exceptions import LanguageServerException @@ -697,11 +697,13 @@ class SolidLanguageServer(ABC): for ref in references ] - def retrieve_full_file_content(self, relative_file_path: str) -> str: + def retrieve_full_file_content(self, file_path: str) -> str: """ Retrieve the full content of the given file. """ - with self.open_file(relative_file_path) as file_data: + if os.path.isabs(file_path): + file_path = os.path.relpath(file_path, self.repository_root_path) + with self.open_file(file_path) as file_data: return file_data.contents def retrieve_content_around_line( @@ -1188,8 +1190,11 @@ class SolidLanguageServer(ABC): symbol_body = symbol_body[symbol_start_column:] return symbol_body - def request_parsed_files(self) -> list[str]: - """Retrieves relative paths of all files analyzed by the Language Server.""" + def request_parsed_files(self, relative_path: str = "") -> list[str]: + """Retrieves relative paths of all files analyzed by the Language Server. + + :param relative_path: will only retrieve files that are subpaths of this. + """ if not self.server_started: self.logger.log( "request_parsed_files called before Language Server started", @@ -1197,23 +1202,30 @@ class SolidLanguageServer(ABC): ) raise LanguageServerException("Language Server not started") rel_file_paths = [] - for root, dirs, files in os.walk(self.repository_root_path, followlinks=True): - dirs[:] = [d for d in dirs if not self.is_ignored_path(os.path.join(root, d))] - for file in files: - rel_file_path = os.path.relpath(os.path.join(root, file), start=self.repository_root_path) - try: - if not self.is_ignored_path(rel_file_path): - rel_file_paths.append(rel_file_path) - except FileNotFoundError: - self.logger.log( - f"File {rel_file_path} not found (possibly due it being a symlink), skipping it in request_parsed_files", - logging.WARNING, - ) - return rel_file_paths + start_path = os.path.join(self.repository_root_path, relative_path) + if not os.path.exists(start_path): + raise FileNotFoundError(f"Relative path {start_path} not found.") + if os.path.isfile(start_path): + return [relative_path] + else: + for root, dirs, files in os.walk(start_path, followlinks=True): + dirs[:] = [d for d in dirs if not self.is_ignored_path(os.path.join(root, d))] + for file in files: + rel_file_path = os.path.relpath(os.path.join(root, file), start=self.repository_root_path) + try: + if not self.is_ignored_path(rel_file_path): + rel_file_paths.append(rel_file_path) + except FileNotFoundError: + self.logger.log( + f"File {rel_file_path} not found (possibly due it being a symlink), skipping it in request_parsed_files", + logging.WARNING, + ) + return rel_file_paths def search_files_for_pattern( self, - pattern: re.Pattern | str, + pattern: str, + relative_path: str = "", context_lines_before: int = 0, context_lines_after: int = 0, paths_include_glob: str | None = None, @@ -1223,6 +1235,7 @@ class SolidLanguageServer(ABC): Search for a pattern across all files analyzed by the Language Server. :param pattern: Regular expression pattern to search for, either as a compiled Pattern or string + :param relative_path: :param context_lines_before: Number of lines of context to include before each match :param context_lines_after: Number of lines of context to include after each match :param paths_include_glob: Glob pattern to filter which files to include in the search @@ -1232,11 +1245,12 @@ class SolidLanguageServer(ABC): if isinstance(pattern, str): pattern = re.compile(pattern) - relative_file_paths = self.request_parsed_files() + relative_file_paths = self.request_parsed_files(relative_path=relative_path) return search_files( relative_file_paths, pattern, file_reader=self.retrieve_full_file_content, + root_path=self.repository_root_path, context_lines_before=context_lines_before, context_lines_after=context_lines_after, paths_include_glob=paths_include_glob,