From 291ee91f5b3b5ee21cf68549f6efce70cbc9a6f5 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 5 Apr 2025 19:39:23 +0200 Subject: [PATCH 1/6] New tool: GetReferencingCodeExtractsTool --- README.md | 3 ++- src/serena/agent.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7564734..0a3d437 100644 --- a/README.md +++ b/README.md @@ -560,6 +560,7 @@ Here the full list of Serena's default tools with a short description (the outpu * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). * `get_dir_overview`: Gets an overview of the top-level symbols defined in all files within a given directory. * `get_document_overview`: Gets an overview of the top-level symbols defined in a given file. + * `get_referencing_code_extracts`: Gets the code blocks that reference the symbol at the given location. * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. * `insert_at_line`: Inserts content at a given line in a file. * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. @@ -569,6 +570,7 @@ Here the full list of Serena's default tools with a short description (the outpu * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). * `read_file`: Reads a file within the project directory. * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. + * `replace_lines`: Replaces a range of lines within a file with new content. * `replace_symbol_body`: Replaces the full definition of a symbol. * `search_in_all_code`: Performs a search for a pattern in all code files (and only in code files) in the project. * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. @@ -576,4 +578,3 @@ Here the full list of Serena's default tools with a short description (the outpu * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. - diff --git a/src/serena/agent.py b/src/serena/agent.py index 88983da..3ac5a57 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -536,6 +536,45 @@ class FindReferencingSymbolsTool(Tool): return self._limit_length(result, max_answer_chars) +class GetReferencingCodeExtractsTool(Tool): + """ + Gets the code blocks that reference the symbol at the given location. + """ + + def apply( + self, + relative_path: str, + line: int, + column: int, + context_lines_before: int = 0, + context_lines_after: int = 0, + max_answer_chars: int = _DEFAULT_MAX_ANSWER_LENGTH, + ) -> str: + """ + Returns short code extracts where the symbol at the given location is referenced. + + Contrary to the find_referencing_symbols tool, this tool returns references that are not symbols but instead + code extracts that may or may not be contained in a symbol (for example, file-level calls). + It may make sense to use this tool if you want to get a quick and dirty overview of the code that references + the symbol. Usually just looking at the code extracts is not enough to understand the context, + unless the case you are investigating is very simple, + or you already have read the relevant symbols using the find_referencing_symbols tool and + now want to get an overview of how the referenced symbol (at the given location) is used in them. + + :param relative_path: the relative path to the file containing the symbol + :param line: the line number + :param column: the column + :param context_lines_before: the number of lines to include before the reference + :param context_lines_after: the number of lines to include after the reference + """ + matches = self.language_server.request_references_with_content( + relative_path, line, column, context_lines_before, context_lines_after + ) + result = [match.to_display_string() for match in matches] + result_json_str = json.dumps(result) + return self._limit_length(result_json_str, max_answer_chars) + + class ReplaceSymbolBodyTool(Tool): """ Replaces the full definition of a symbol. From 385deb05c9375e45eef02312784cacac49adee1b Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 5 Apr 2025 19:56:27 +0200 Subject: [PATCH 2/6] Docstring, minor --- src/serena/agent.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 3ac5a57..bcbe4bb 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -553,10 +553,10 @@ class GetReferencingCodeExtractsTool(Tool): """ Returns short code extracts where the symbol at the given location is referenced. - Contrary to the find_referencing_symbols tool, this tool returns references that are not symbols but instead + Contrary to the `find_referencing_symbols` tool, this tool returns references that are not symbols but instead code extracts that may or may not be contained in a symbol (for example, file-level calls). It may make sense to use this tool if you want to get a quick and dirty overview of the code that references - the symbol. Usually just looking at the code extracts is not enough to understand the context, + the symbol. Usually, just looking at the code extracts is not enough to understand the context, unless the case you are investigating is very simple, or you already have read the relevant symbols using the find_referencing_symbols tool and now want to get an overview of how the referenced symbol (at the given location) is used in them. @@ -564,8 +564,12 @@ class GetReferencingCodeExtractsTool(Tool): :param relative_path: the relative path to the file containing the symbol :param line: the line number :param column: the column - :param context_lines_before: the number of lines to include before the reference - :param context_lines_after: the number of lines to include after the reference + :param context_lines_before: the number of lines to include before the line containing the reference + :param context_lines_after: the number of lines to include after the line containing the reference + :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 + make a stricter query. """ matches = self.language_server.request_references_with_content( relative_path, line, column, context_lines_before, context_lines_after From 6052f7b0aea67828ca2431c83d7e95707c33ecc5 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 6 Apr 2025 13:22:39 +0200 Subject: [PATCH 3/6] Minor docstring changes --- CHANGELOG.md | 4 ++++ src/serena/agent.py | 21 +++++++++++++-------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7128943..599b9d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ # Changelog +## 06.04.2025 +- New tool: FindReferencingCodeSnippets +- Adjusted prompt in CreateTextFileTool to prevent writing partial content (see [here](https://www.reddit.com/r/ClaudeAI/comments/1jpavtm/comment/mloek1x/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button)). + ## 01.04.2025: Initial Release \ No newline at end of file diff --git a/src/serena/agent.py b/src/serena/agent.py index bcbe4bb..f9175b6 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -332,6 +332,10 @@ class CreateTextFileTool(Tool): You can also use insert_at_line to insert content at a specific line for existing files if the symbolic operations are not the right choice for what you want to do. + If ever used on an existing file, the content has to be the complete content of that file (so it + may never end with something like "The remaining content of the file is left unchanged."). + For operations that just replace a part of a file, use the replace_lines or the symbolic editing tools instead. + :param relative_path: the relative path to the file to create :param content: the (utf-8-encoded) content to write to the file :return: a message indicating success or failure @@ -536,9 +540,9 @@ class FindReferencingSymbolsTool(Tool): return self._limit_length(result, max_answer_chars) -class GetReferencingCodeExtractsTool(Tool): +class FindReferencingCodeSnippetsTool(Tool): """ - Gets the code blocks that reference the symbol at the given location. + Finds code snippets in which the symbol at the given location is referenced. """ def apply( @@ -551,19 +555,20 @@ class GetReferencingCodeExtractsTool(Tool): max_answer_chars: int = _DEFAULT_MAX_ANSWER_LENGTH, ) -> str: """ - Returns short code extracts where the symbol at the given location is referenced. + Returns short code snippets where the symbol at the given location is referenced. Contrary to the `find_referencing_symbols` tool, this tool returns references that are not symbols but instead - code extracts that may or may not be contained in a symbol (for example, file-level calls). - It may make sense to use this tool if you want to get a quick and dirty overview of the code that references - the symbol. Usually, just looking at the code extracts is not enough to understand the context, + code snippets that may or may not be contained in a symbol (for example, file-level calls). + It may make sense to use this tool to get a quick overview of the code that references + the symbol. Usually, just looking at code snippets is not enough to understand the full context, unless the case you are investigating is very simple, or you already have read the relevant symbols using the find_referencing_symbols tool and now want to get an overview of how the referenced symbol (at the given location) is used in them. + The size of the snippets is controlled by the context_lines_before and context_lines_after parameters. :param relative_path: the relative path to the file containing the symbol - :param line: the line number - :param column: the column + :param line: the line number of the symbol to find references for + :param column: the column of the symbol to find references for :param context_lines_before: the number of lines to include before the line containing the reference :param context_lines_after: the number of lines to include after the line containing the reference :param max_answer_chars: if the output is longer than this number of characters, From 8c103c57f7e501f3eef1f8a78238052200253f65 Mon Sep 17 00:00:00 2001 From: Michael Panchenko <35432522+MischaPanch@users.noreply.github.com> Date: Sun, 6 Apr 2025 13:33:34 +0200 Subject: [PATCH 4/6] Typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0a3d437..4ba7603 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ Serena can read, write and execute code, read logs and the terminal output. That's it! Save the config and then restart Claude Desktop. -Note: on Windows and MacOS there are official Claude Desktop applications by Anthropic, for Linux there is an [open-source +Note: on Windows and macOS there are official Claude Desktop applications by Anthropic, for Linux there is an [open-source community version](https://github.com/aaddrick/claude-desktop-debian). ⚠️ Be sure to fully quit the Claude Desktop application, as closing Claude will just minimize it to the system tray – at least on Windows. From e8558a7c12ea76cce056a38e046b2b2ab504f2ca Mon Sep 17 00:00:00 2001 From: ShadowBeast Date: Sun, 6 Apr 2025 17:48:57 +0300 Subject: [PATCH 5/6] Add Serena version to server startup logs This change adds the Serena version number to the server startup log message, making it easier to identify which version is running when reviewing logs. --- src/serena/agent.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index f9175b6..4b949b3 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -24,6 +24,7 @@ from multilspy import SyncLanguageServer from multilspy.multilspy_config import Language, MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger from multilspy.multilspy_types import SymbolKind +from serena import __version__ from serena.gui_log_viewer import GuiLogViewer, GuiLogViewerHandler from serena.llm.prompt_factory import PromptFactory from serena.symbol import SymbolLocation, SymbolManager @@ -88,7 +89,7 @@ class SerenaAgent: Logger.root.addHandler(self._gui_log_handler) log.info( - f"Starting serena server for project {project_file_path} (language={self.language}, root={self.project_root}); " + f"Starting serena server v{__version__} for project {project_file_path} (language={self.language}, root={self.project_root}); " f"process id={os.getpid()}, parent process id={os.getppid()}" ) From ba03ec431fb11682fb5743e6a95143b606f0378f Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 6 Apr 2025 22:23:06 +0200 Subject: [PATCH 6/6] FindSymbolTool: allow passing a file for restricting search, not just a directory --- CHANGELOG.md | 1 + src/multilspy/language_server.py | 38 ++++++++++++------------- src/serena/agent.py | 8 ++++-- src/serena/symbol.py | 8 ++++-- test/multilspy/test_symbol_retrieval.py | 2 +- 5 files changed, 30 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 599b9d9..9bd34d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,5 +3,6 @@ ## 06.04.2025 - New tool: FindReferencingCodeSnippets - Adjusted prompt in CreateTextFileTool to prevent writing partial content (see [here](https://www.reddit.com/r/ClaudeAI/comments/1jpavtm/comment/mloek1x/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button)). +- FindSymbolTool: allow passing a file for restricting search, not just a directory (Gemini was too dumb to pass directories) ## 01.04.2025: Initial Release \ No newline at end of file diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 41fc43c..ae5fa59 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -746,7 +746,7 @@ class LanguageServer: self._cache_has_changed = True return result - async def request_full_symbol_tree(self, start_dir_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]: + async def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]: """ Will go through all files in the project and build a tree of symbols. Note: this may be slow the first time it is called. @@ -755,19 +755,16 @@ class LanguageServer: that are within the repository. Will ignore all directories that start with a dot (.) and __pycache__ directories. - Args: - start_dir_relative_path: if passed, only the symbols within this directory will be considered. - include_body: whether to include the body of the symbols in the result. + :param within_relative_path: pass a relative path to only consider symbols within this path. + If a file is passed, only the symbols within this file will be considered. + If a directory is passed, all files within this directory will be considered. + :param include_body: whether to include the body of the symbols in the result. - Returns: - A list of root symbols representing the top-level packages/modules in the project. + :return: A list of root symbols representing the top-level packages/modules in the project. """ - if not self.server_started: - self.logger.log( - "request_full_symbol_tree called before Language Server started", - logging.ERROR, - ) - raise MultilspyException("Language Server not started") + if within_relative_path is not None and os.path.isfile(within_relative_path): + _, root_nodes = await self.request_document_symbols(within_relative_path, include_body=include_body) + return root_nodes # Helper function to check if a path should be ignored def should_ignore_dir(path: str) -> bool: @@ -854,7 +851,7 @@ class LanguageServer: return result # Start from the root or the specified directory - start_path = start_dir_relative_path or "." + start_path = within_relative_path or "." return await process_directory(start_path) @staticmethod @@ -1579,7 +1576,7 @@ class SyncLanguageServer: ).result() return result - def request_full_symbol_tree(self, start_package_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]: + def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]: """ Will go through all files in the project and build a tree of symbols. Note: this may be slow the first time it is called. @@ -1588,15 +1585,16 @@ class SyncLanguageServer: that are within the repository. Will ignore all directories that start with a dot (.) and __pycache__ directories. - Args: - start_package_relative_path: if passed, only the symbols within this directory will be considered. - include_body: whether to include the body of the symbols in the result. + :param within_relative_path: pass a relative path to only consider symbols within this path. + If a file is passed, only the symbols within this file will be considered. + If a directory is passed, all files within this directory will be considered. + If None, the entire codebase will be considered. + :param include_body: whether to include the body of the symbols in the result. - Returns: - A list of root symbols representing the top-level packages/modules in the project. + :return: A list of root symbols representing the top-level packages/modules in the project. """ result = asyncio.run_coroutine_threadsafe( - self.language_server.request_full_symbol_tree(start_package_relative_path, include_body), self.loop + self.language_server.request_full_symbol_tree(within_relative_path, include_body), self.loop ).result() return result diff --git a/src/serena/agent.py b/src/serena/agent.py index 4b949b3..9ea4c03 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -430,11 +430,11 @@ class FindSymbolTool(Tool): self, name: str, depth: int = 0, + within_relative_path: str | None = None, include_body: bool = False, include_kinds: list[int] | None = None, exclude_kinds: list[int] | None = None, substring_matching: bool = False, - dir_relative_path: str | None = None, max_answer_chars: int = _DEFAULT_MAX_ANSWER_LENGTH, ) -> str: """ @@ -450,7 +450,9 @@ class FindSymbolTool(Tool): (e.g. depth 1 will retrieve methods and attributes for the case where the symbol refers to a class). Provide a non-zero depth if you intend to subsequently query symbols that are contained in the retrieved symbol. - :param dir_relative_path: pass a directory relative path to only consider symbols within this directory. + :param within_relative_path: pass a relative path to only consider symbols within this path. + If a file is passed, only the symbols within this file will be considered. + If a directory is passed, all files within this directory will be considered. If None, the entire codebase will be considered. :param include_body: whether to include the body of all symbols in the result. You should only use this if you actually need the body of the symbol for the task at hand (for example, for a deep analysis @@ -479,7 +481,7 @@ class FindSymbolTool(Tool): include_kinds=include_kinds, exclude_kinds=exclude_kinds, substring_matching=substring_matching, - dir_relative_path=dir_relative_path, + within_relative_path=within_relative_path, ) symbol_dicts = [s.to_dict(kind=True, location=True, depth=depth, include_body=include_body) for s in symbols] result = json.dumps(symbol_dicts) diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 9c698f4..1819b1a 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -200,7 +200,7 @@ class SymbolManager: def find_by_name( self, name: str, - dir_relative_path: str | None = None, + within_relative_path: str | None = None, include_body: bool = False, include_kinds: Sequence[SymbolKind] | None = None, exclude_kinds: Sequence[SymbolKind] | None = None, @@ -210,7 +210,9 @@ class SymbolManager: Find all symbols that match the given name. :param name: the name of the symbol to find - :param dir_relative_path: pass a directory relative path to only consider symbols within this directory. + :param within_relative_path: pass a relative path to only consider symbols within this path. + If a file is passed, only the symbols within this file will be considered. + If a directory is passed, all files within this directory will be considered. If None, the entire codebase will be considered. :param include_body: whether to include the body of all symbols in the result. Note: you can filter out the bodies of the children if you set include_children_body=False @@ -224,7 +226,7 @@ class SymbolManager: :return: a list of symbols that match the given name """ symbols: list[Symbol] = [] - symbol_roots = self.lang_server.request_full_symbol_tree(start_package_relative_path=dir_relative_path, include_body=include_body) + symbol_roots = self.lang_server.request_full_symbol_tree(within_relative_path=within_relative_path, include_body=include_body) for root in symbol_roots: symbols.extend( Symbol(root).find(name, include_kinds=include_kinds, exclude_kinds=exclude_kinds, substring_matching=substring_matching) diff --git a/test/multilspy/test_symbol_retrieval.py b/test/multilspy/test_symbol_retrieval.py index dd630b5..14fe1fc 100644 --- a/test/multilspy/test_symbol_retrieval.py +++ b/test/multilspy/test_symbol_retrieval.py @@ -366,7 +366,7 @@ class TestLanguageServerSymbols: def test_symbol_tree_structure_subdir(self, language_server: SyncLanguageServer): """Test that the symbol tree structure is correctly built.""" # Get all symbols in the test file - examples_package_roots = language_server.request_full_symbol_tree(start_package_relative_path="examples") + examples_package_roots = language_server.request_full_symbol_tree(within_relative_path="examples") assert len(examples_package_roots) == 1 examples_package = examples_package_roots[0] assert examples_package["name"] == "examples"