diff --git a/src/serena/repl/api/edit_api.py b/src/serena/repl/api/edit_api.py index a96f5cc4..fa1fa186 100644 --- a/src/serena/repl/api/edit_api.py +++ b/src/serena/repl/api/edit_api.py @@ -6,6 +6,16 @@ The implementation of editing operations, which are independent of the language from typing import TYPE_CHECKING, Literal from serena.code_editor import EditedFileContext +from serena.tools import ( + DeleteLinesTool, + InsertAfterSymbolTool, + InsertAtLineTool, + InsertBeforeSymbolTool, + ReplaceContentTool, + ReplaceInFilesTool, + ReplaceLinesTool, + ReplaceSymbolBodyTool, +) from serena.util.text_utils import ContentReplacer, MultiFileReplacement, ReplacementOccurrence, ReplacementRejectedError from ..facade import SUCCESS_RESULT, FacadeApi, facade_method @@ -68,7 +78,7 @@ class EditApi(FacadeApi): # file-level operations - @facade_method(can_edit=True) + @facade_method(can_edit=True, corresponding_tool=ReplaceContentTool) def replace_content( self, relative_path: str, @@ -106,7 +116,7 @@ class EditApi(FacadeApi): context.set_updated_content(replacer.replace(context.get_original_content(), needle, repl)) return SUCCESS_RESULT - @facade_method(can_edit=True) + @facade_method(can_edit=True, corresponding_tool=ReplaceInFilesTool) def replace_in_files( self, needle: str, @@ -186,7 +196,7 @@ class EditApi(FacadeApi): # line-level operations - @facade_method(optional=True, can_edit=True) + @facade_method(optional=True, can_edit=True, corresponding_tool=DeleteLinesTool) def delete_lines(self, relative_path: str, start_line: int, end_line: int) -> str: """ Deletes the given lines in the file. @@ -200,7 +210,7 @@ class EditApi(FacadeApi): self._create_code_editor().delete_lines(relative_path, start_line, end_line) return SUCCESS_RESULT - @facade_method(optional=True, can_edit=True) + @facade_method(optional=True, can_edit=True, corresponding_tool=ReplaceLinesTool) def replace_lines(self, relative_path: str, start_line: int, end_line: int, content: str) -> str: """ Replaces the given range of lines in the given file. @@ -217,7 +227,7 @@ class EditApi(FacadeApi): code_editor.insert_at_line(relative_path, start_line, self._normalize_inserted_content(content)) return SUCCESS_RESULT - @facade_method(optional=True, can_edit=True) + @facade_method(optional=True, can_edit=True, corresponding_tool=InsertAtLineTool) def insert_at_line(self, relative_path: str, line: int, content: str) -> str: """ Inserts the given content at the given line in the file, pushing existing content of the line down. @@ -239,7 +249,7 @@ class EditApi(FacadeApi): # symbol-level operations - @facade_method(can_edit=True) + @facade_method(can_edit=True, corresponding_tool=ReplaceSymbolBodyTool) def replace_symbol_body(self, name_path: str, relative_path: str, body: str) -> str: """ Replaces the body of the given symbol. @@ -257,7 +267,7 @@ class EditApi(FacadeApi): self._create_code_editor().replace_body(name_path, relative_file_path=relative_path, body=body) return SUCCESS_RESULT - @facade_method(can_edit=True) + @facade_method(can_edit=True, corresponding_tool=InsertAfterSymbolTool) def insert_after_symbol(self, name_path: str, relative_path: str, body: str) -> str: """ Inserts code after a class/method/function definition. @@ -272,7 +282,7 @@ class EditApi(FacadeApi): self._create_code_editor().insert_after_symbol(name_path, relative_file_path=relative_path, body=body) return SUCCESS_RESULT - @facade_method(can_edit=True) + @facade_method(can_edit=True, corresponding_tool=InsertBeforeSymbolTool) def insert_before_symbol(self, name_path: str, relative_path: str, body: str) -> str: """ Inserts the given content before the beginning of the definition of the given symbol (via the symbol's location). diff --git a/src/serena/repl/api/fs_api.py b/src/serena/repl/api/fs_api.py index afd4efca..4272a4ba 100644 --- a/src/serena/repl/api/fs_api.py +++ b/src/serena/repl/api/fs_api.py @@ -9,6 +9,7 @@ from fnmatch import fnmatch from pathlib import Path from typing import TYPE_CHECKING +from serena.tools import CreateTextFileTool, FindFileTool, ListDirTool, ReadFileTool, SearchForPatternTool from serena.util.file_system import scan_directory from serena.util.text_utils import MatchedConsecutiveLines from solidlsp.ls_utils import TextUtils @@ -163,7 +164,7 @@ class FsApi(FacadeApi): def __init__(self, agent: "SerenaAgent") -> None: super().__init__(agent, name="fs", description="the project's files as units (as opposed to their content, see `edit`)") - @facade_method() + @facade_method(corresponding_tool=ReadFileTool) def read_file(self, relative_path: str, start_line: int = 0, end_line: int | None = None, max_answer_chars: int = -1) -> FileContent: """ Reads the given file or a range of its lines. @@ -183,7 +184,7 @@ class FsApi(FacadeApi): lines = lines[start_line:] if end_line is None else lines[start_line : end_line + 1] return FileContent(lines, FileContentRenderer(self._agent, max_answer_chars)) - @facade_method(can_edit=True) + @facade_method(can_edit=True, corresponding_tool=CreateTextFileTool) def create_text_file(self, relative_path: str, content: str) -> str: """ Writes a new file or overwrites an existing file with the given content. @@ -211,7 +212,7 @@ class FsApi(FacadeApi): answer += " Overwrote existing file." return answer - @facade_method() + @facade_method(corresponding_tool=ListDirTool) def list_dir( self, relative_path: str, recursive: bool, skip_ignored_files: bool = False, max_answer_chars: int = -1 ) -> DirectoryListing: @@ -240,7 +241,7 @@ class FsApi(FacadeApi): ) return DirectoryListing(dirs, files, DirectoryListingRenderer(self._agent, max_answer_chars)) - @facade_method() + @facade_method(corresponding_tool=FindFileTool) def find_file(self, file_mask: str, relative_path: str) -> list[str]: """ Finds files matching the given file mask within the given relative path. @@ -269,7 +270,7 @@ class FsApi(FacadeApi): ) return files - @facade_method() + @facade_method(corresponding_tool=SearchForPatternTool) def search_for_pattern( self, substring_pattern: str, diff --git a/src/serena/repl/api/jb_api.py b/src/serena/repl/api/jb_api.py index b3449f91..e8ffdb62 100644 --- a/src/serena/repl/api/jb_api.py +++ b/src/serena/repl/api/jb_api.py @@ -13,6 +13,21 @@ from serena.code_editor import JetBrainsCodeEditor from serena.jetbrains.jetbrains_plugin_client import JetBrainsPluginClient from serena.jetbrains.jetbrains_types import SymbolDTO, SymbolDTOUtil from serena.symbol import JetBrainsSymbolDictGrouper +from serena.tools import ( + JetBrainsDebugTool, + JetBrainsFindDeclarationTool, + JetBrainsFindImplementationsTool, + JetBrainsFindReferencingSymbolsTool, + JetBrainsFindSymbolTool, + JetBrainsGetSymbolsOverviewTool, + JetBrainsInlineSymbol, + JetBrainsListInspectionsTool, + JetBrainsMoveTool, + JetBrainsRenameTool, + JetBrainsRunInspectionsTool, + JetBrainsSafeDeleteTool, + JetBrainsTypeHierarchyTool, +) from serena.util.text_utils import find_text_coordinates from ..facade import FacadeApi, facade_method @@ -177,7 +192,7 @@ class JetBrainsApi(FacadeApi): # read operations - @facade_method() + @facade_method(corresponding_tool=JetBrainsFindSymbolTool) def find_symbol( self, name_path_pattern: str, @@ -261,7 +276,7 @@ class JetBrainsApi(FacadeApi): raise ValueError(f"Matched {n_matches}>{max_matches=} symbols.\n" + renderer.render_identifiers(collection)) return collection - @facade_method() + @facade_method(corresponding_tool=JetBrainsFindReferencingSymbolsTool) def find_referencing_symbols(self, name_path: str, relative_path: str, max_answer_chars: int = -1) -> JetBrainsSymbolCollection: """ Finds all symbols that reference the given symbol — its callers / usages / dependents, i.e. the @@ -294,7 +309,7 @@ class JetBrainsApi(FacadeApi): renderer = JetBrainsReferencesRenderer(self._agent, max_answer_chars, grouper=self.references_grouper_) return JetBrainsSymbolCollection(symbol_dicts, renderer) - @facade_method() + @facade_method(corresponding_tool=JetBrainsGetSymbolsOverviewTool) def get_symbols_overview( self, relative_path: str, depth: int = -1, max_answer_chars: int = -1, include_file_documentation: bool = False ) -> JetBrainsSymbolsOverview: @@ -340,7 +355,7 @@ class JetBrainsApi(FacadeApi): result[rel_path].append(name_path) return dict(result) - @facade_method() + @facade_method(corresponding_tool=JetBrainsTypeHierarchyTool) def get_type_hierarchy( self, name_path: str, @@ -379,7 +394,7 @@ class JetBrainsApi(FacadeApi): result["levels_not_included"] = levels_not_included return self._json_object(result, max_answer_chars) - @facade_method() + @facade_method(corresponding_tool=JetBrainsFindDeclarationTool) def find_declaration(self, relative_path: str, regex: str, include_body: bool = False) -> JetBrainsSymbolCollection: r""" Finds the declaration of a symbol. @@ -402,7 +417,7 @@ class JetBrainsApi(FacadeApi): ) return JetBrainsSymbolCollection(response["symbols"], JetBrainsSymbolCollectionRenderer(self._agent, -1)) - @facade_method() + @facade_method(corresponding_tool=JetBrainsFindImplementationsTool) def find_implementations(self, relative_path: str, name_path: str) -> JetBrainsSymbolCollection: """ Finds the implementations of a symbol. @@ -417,7 +432,7 @@ class JetBrainsApi(FacadeApi): # edit operations - @facade_method(can_edit=True) + @facade_method(can_edit=True, corresponding_tool=JetBrainsRenameTool) def rename( self, relative_path: str, @@ -448,7 +463,7 @@ class JetBrainsApi(FacadeApi): ) return self._json_object(result) - @facade_method(beta=True, can_edit=True) + @facade_method(beta=True, can_edit=True, corresponding_tool=JetBrainsMoveTool) def move( self, relative_path: str, @@ -489,7 +504,7 @@ class JetBrainsApi(FacadeApi): ) return self._json_object(result) - @facade_method(beta=True, can_edit=True) + @facade_method(beta=True, can_edit=True, corresponding_tool=JetBrainsSafeDeleteTool) def safe_delete( self, relative_path: str, name_path: str | None = None, delete_even_if_used: bool = False, propagate: bool = False ) -> JsonObject: @@ -516,7 +531,7 @@ class JetBrainsApi(FacadeApi): ) return self._json_object(result) - @facade_method(beta=True, can_edit=True) + @facade_method(beta=True, can_edit=True, corresponding_tool=JetBrainsInlineSymbol) def inline_symbol(self, name_path: str, relative_path: str, keep_definition: bool = False) -> JsonObject: """ Inlines a symbol (usually a method/function, but also classes may be amenable to inlining, @@ -537,7 +552,7 @@ class JetBrainsApi(FacadeApi): # inspections - @facade_method() + @facade_method(corresponding_tool=JetBrainsRunInspectionsTool) def run_inspections( self, relative_path: str, @@ -573,7 +588,7 @@ class JetBrainsApi(FacadeApi): ) return self._json_object(result, max_answer_chars) - @facade_method() + @facade_method(corresponding_tool=JetBrainsListInspectionsTool) def list_inspections( self, language: str | None = None, group_path_contains: str | None = None, max_answer_chars: int = -1 ) -> JsonObject: @@ -603,7 +618,7 @@ class JetBrainsApi(FacadeApi): """ return self._agent.prompt_factory.create_info_jet_brains_debug_repl() - @facade_method(beta=True) + @facade_method(beta=True, corresponding_tool=JetBrainsDebugTool) def debug_eval(self, expression: str, repl_key: str = "default") -> str: """ Provides debugging functionality (run configs, breakpoints, stepping, inspection, and evaluation) diff --git a/src/serena/repl/api/lsp_api.py b/src/serena/repl/api/lsp_api.py index 115f9772..28015197 100644 --- a/src/serena/repl/api/lsp_api.py +++ b/src/serena/repl/api/lsp_api.py @@ -18,6 +18,18 @@ from serena.symbol import ( ReferenceInLanguageServerSymbol, SymbolDictGrouper, ) +from serena.tools import ( + FindDeclarationTool, + FindImplementationsTool, + FindReferencingSymbolsTool, + FindSymbolTool, + GetDiagnosticsForFileTool, + GetDiagnosticsForSymbolTool, + GetSymbolsOverviewTool, + RenameSymbolTool, + RestartLanguageServerTool, + SafeDeleteSymbol, +) from serena.util.text_utils import TextOutputUtils, find_text_coordinates from solidlsp.lsp_protocol_handler.lsp_types import SymbolKind @@ -310,7 +322,7 @@ class LspApi(FacadeApi): # language server management - @facade_method(optional=True) + @facade_method(optional=True, corresponding_tool=RestartLanguageServerTool) def restart_language_server(self) -> str: """ Restarts the language server(s). Use this only on explicit user request or after confirmation; @@ -323,7 +335,7 @@ class LspApi(FacadeApi): # read operations - @facade_method() + @facade_method(corresponding_tool=GetSymbolsOverviewTool) def get_symbols_overview(self, relative_path: str, depth: int = -1, max_answer_chars: int = -1) -> LspSymbolCollection: """ Gets an overview of the top-level symbols defined in the given file (classes, methods, fields) — its @@ -369,7 +381,7 @@ class LspApi(FacadeApi): ) return LspSymbolCollection(symbols, renderer) - @facade_method() + @facade_method(corresponding_tool=FindSymbolTool) def find_symbol( self, name_path_pattern: str, @@ -462,7 +474,7 @@ class LspApi(FacadeApi): return symbol_collection - @facade_method() + @facade_method(corresponding_tool=FindReferencingSymbolsTool) def find_referencing_symbols( self, name_path: str, @@ -496,7 +508,7 @@ class LspApi(FacadeApi): ) return LspReferenceCollection(references, LspReferenceCollectionRenderer(self._agent, max_answer_chars, self.references_grouper_)) - @facade_method() + @facade_method(corresponding_tool=FindImplementationsTool) def find_implementations( self, name_path: str, @@ -532,7 +544,7 @@ class LspApi(FacadeApi): output_params = SymbolOutputParams(kind=True, relative_path=True, body_location=True, include_info=include_info) return LspSymbolCollection(symbols, LspSymbolCollectionRenderer(self._agent, max_answer_chars, symbol_retriever, output_params)) - @facade_method() + @facade_method(corresponding_tool=FindDeclarationTool) def find_declaration( self, relative_path: str, @@ -587,7 +599,7 @@ class LspApi(FacadeApi): collection_renderer = LspSymbolCollectionRenderer(self._agent, -1, symbol_retriever, output_params) return LspSymbol(defining_symbol, LspSymbolRenderer(self._agent, -1, collection_renderer)) - @facade_method() + @facade_method(corresponding_tool=GetDiagnosticsForFileTool) def get_diagnostics_for_file( self, relative_path: str, start_line: int = 0, end_line: int = -1, min_severity: int = 4, max_answer_chars: int = -1 ) -> LspDiagnostics: @@ -621,7 +633,7 @@ class LspApi(FacadeApi): return self._create_diagnostics(grouped_diagnostics, max_answer_chars) - @facade_method(optional=True) + @facade_method(optional=True, corresponding_tool=GetDiagnosticsForSymbolTool) def get_diagnostics_for_symbol( self, name_path: str, @@ -665,7 +677,7 @@ class LspApi(FacadeApi): # edit operations - @facade_method(can_edit=True) + @facade_method(can_edit=True, corresponding_tool=RenameSymbolTool) def rename_symbol(self, name_path: str, relative_path: str, new_name: str) -> str: """ Renames the symbol with the given `name_path` to `new_name` throughout the entire codebase. @@ -680,7 +692,7 @@ class LspApi(FacadeApi): self._get_project().ls_sync_file_system_changes() return self._create_ls_code_editor().rename_symbol(name_path, relative_path=relative_path, new_name=new_name) - @facade_method(can_edit=True) + @facade_method(can_edit=True, corresponding_tool=SafeDeleteSymbol) def safe_delete_symbol(self, name_path_pattern: str, relative_path: str) -> str: """ Deletes the symbol if it is safe to do so (i.e., if there are no references to it) diff --git a/src/serena/repl/api/mem_api.py b/src/serena/repl/api/mem_api.py index 60fc305c..d6e41f76 100644 --- a/src/serena/repl/api/mem_api.py +++ b/src/serena/repl/api/mem_api.py @@ -8,6 +8,15 @@ import platform from typing import TYPE_CHECKING, Literal from serena.memories.memory_manager import MemoryManager +from serena.tools import ( + DeleteMemoryTool, + EditMemoryTool, + ListMemoriesTool, + OnboardingTool, + ReadMemoryTool, + RenameMemoryTool, + WriteMemoryTool, +) from ..facade import FacadeApi, facade_method from ..representable import Renderer, RepresentableViaRenderer @@ -56,7 +65,7 @@ class MemoryApi(FacadeApi): def _get_memory_manager(self) -> MemoryManager: return self._get_project().memory_manager - @facade_method() + @facade_method(corresponding_tool=ListMemoriesTool) def list_memories(self, topic: str = "") -> MemoryList: """ Lists the available memories, optionally filtered by topic. @@ -66,7 +75,7 @@ class MemoryApi(FacadeApi): """ return MemoryList(self._get_memory_manager().list_memories(topic), MemoryListRenderer(self._agent, -1)) - @facade_method() + @facade_method(corresponding_tool=ReadMemoryTool) def read_memory(self, memory_name: str) -> str: """ Reads a memory that is likely to be relevant to the current task, inferring relevance e.g. from the name. @@ -76,7 +85,7 @@ class MemoryApi(FacadeApi): """ return self._get_memory_manager().load_memory(memory_name) - @facade_method(can_edit=True) + @facade_method(can_edit=True, corresponding_tool=WriteMemoryTool) def write_memory(self, memory_name: str, content: str, max_chars: int = -1) -> str: """ Writes information about this project that can be useful for future tasks in md format. @@ -98,7 +107,7 @@ class MemoryApi(FacadeApi): ) return self._get_memory_manager().save_memory(memory_name, content, is_tool_context=True) - @facade_method(can_edit=True) + @facade_method(can_edit=True, corresponding_tool=EditMemoryTool) def edit_memory( self, memory_name: str, @@ -125,7 +134,7 @@ class MemoryApi(FacadeApi): memory_name, needle, repl, mode, allow_multiple_occurrences, is_tool_context=True, regex_multiline=True ) - @facade_method(can_edit=True) + @facade_method(can_edit=True, corresponding_tool=RenameMemoryTool) def rename_memory(self, old_name: str, new_name: str) -> str: """ Renames or moves a memory; use "/" in the name to organize into topics. @@ -144,7 +153,7 @@ class MemoryApi(FacadeApi): log.info(f"Updated {n_references_updated} references to memory {old_name} to {new_name}") return renaming_message - @facade_method(can_edit=True) + @facade_method(can_edit=True, corresponding_tool=DeleteMemoryTool) def delete_memory(self, memory_name: str) -> str: """ Deletes a memory; only call this if instructed explicitly or permission was granted by the user. @@ -154,7 +163,7 @@ class MemoryApi(FacadeApi): """ return self._get_memory_manager().delete_memory(memory_name, is_tool_context=True) - @facade_method() + @facade_method(corresponding_tool=OnboardingTool) def onboarding(self) -> str: """ Provides the instructions for performing onboarding (identifying the project structure and essential tasks, diff --git a/src/serena/repl/api/shell_api.py b/src/serena/repl/api/shell_api.py index 54922ace..56ba2679 100644 --- a/src/serena/repl/api/shell_api.py +++ b/src/serena/repl/api/shell_api.py @@ -6,6 +6,7 @@ The implementation of shell command execution. import os.path from typing import TYPE_CHECKING +from serena.tools import ExecuteShellCommandTool from serena.util.shell import ShellCommandResult, execute_shell_command from ..facade import FacadeApi, facade_method @@ -54,7 +55,7 @@ class ShellApi(FacadeApi): def __init__(self, agent: "SerenaAgent") -> None: super().__init__(agent, name="shell", description="execution of shell commands") - @facade_method(can_edit=True) + @facade_method(can_edit=True, corresponding_tool=ExecuteShellCommandTool) def execute_shell_command( self, command: str, cwd: str | None = None, capture_stderr: bool = True, max_answer_chars: int = -1 ) -> ShellCommandOutput: diff --git a/src/serena/repl/facade.py b/src/serena/repl/facade.py index a20080d7..16a7fa4c 100644 --- a/src/serena/repl/facade.py +++ b/src/serena/repl/facade.py @@ -17,6 +17,7 @@ from serena.project import Project if TYPE_CHECKING: from serena.agent import SerenaAgent from serena.code_editor import CodeEditor + from serena.tools import Tool log = logging.getLogger(__name__) TCallable = TypeVar("TCallable", bound=Callable[..., Any]) @@ -39,12 +40,22 @@ class FacadeMethodInfo: """whether the method is in beta (not yet fully stable)""" can_edit: bool = False """whether the method can modify the codebase (relevant for read-only contexts)""" + corresponding_tool: "type[Tool] | None" = None + """the classic tool offering the same functionality, if any""" + + def get_corresponding_tool_name(self) -> str | None: + """ + :return: the name of the corresponding tool, or None if there is none + """ + return self.corresponding_tool.get_name_from_cls() if self.corresponding_tool is not None else None _FACADE_METHOD_INFO_ATTR = "__facade_method_info__" -def facade_method(*, optional: bool = False, beta: bool = False, can_edit: bool = False) -> Callable[[TCallable], TCallable]: +def facade_method( + *, optional: bool = False, beta: bool = False, can_edit: bool = False, corresponding_tool: "type[Tool] | None" = None +) -> Callable[[TCallable], TCallable]: """ Marks a method of a `FacadeApi` as exposed through the facade, attaching the given metadata. The decorator only annotates the method (it does not wrap it), such that signature and docstring remain intact. @@ -52,11 +63,15 @@ def facade_method(*, optional: bool = False, beta: bool = False, can_edit: bool :param optional: whether the method is disabled by default and must be enabled explicitly :param beta: whether the method is in beta :param can_edit: whether the method can modify the codebase + :param corresponding_tool: the classic tool offering the same functionality, if any :return: the decorator """ def decorator(method: TCallable) -> TCallable: - setattr(method, _FACADE_METHOD_INFO_ATTR, FacadeMethodInfo(name=method.__name__, optional=optional, beta=beta, can_edit=can_edit)) + info = FacadeMethodInfo( + name=method.__name__, optional=optional, beta=beta, can_edit=can_edit, corresponding_tool=corresponding_tool + ) + setattr(method, _FACADE_METHOD_INFO_ATTR, info) return method return decorator diff --git a/src/serena/tools/cmd_tools.py b/src/serena/tools/cmd_tools.py index 6ad18914..51357995 100644 --- a/src/serena/tools/cmd_tools.py +++ b/src/serena/tools/cmd_tools.py @@ -3,18 +3,23 @@ Tools supporting the execution of (external) commands """ # SPDX-License-Identifier: GPL-3.0-or-later -from typing import cast +from typing import TYPE_CHECKING, cast -from serena.repl.api.shell_api import ShellApi from serena.tools import Tool, ToolMarkerCanEdit +if TYPE_CHECKING: + from serena.repl.api.shell_api import ShellApi + class ShellApiMixin: """ - Mixin for tools which delegate to the shell API + Mixin for tools which delegate to the shell API. + The API is imported locally, since the API module refers to the tools (as corresponding tools). """ - def _api(self) -> ShellApi: + def _api(self) -> "ShellApi": + from serena.repl.api.shell_api import ShellApi + tool = cast(Tool, cast(object, self)) return ShellApi(tool.agent) diff --git a/src/serena/tools/file_tools.py b/src/serena/tools/file_tools.py index f9b256ba..fd61ed54 100644 --- a/src/serena/tools/file_tools.py +++ b/src/serena/tools/file_tools.py @@ -7,29 +7,37 @@ File and file system-related tools, specifically for """ # SPDX-License-Identifier: GPL-3.0-or-later -from typing import Literal, cast +from typing import TYPE_CHECKING, Literal, cast -from serena.repl.api.edit_api import EditApi -from serena.repl.api.fs_api import FsApi from serena.tools import EditingToolWithDiagnostics, Tool, ToolMarkerOptional +if TYPE_CHECKING: + from serena.repl.api.edit_api import EditApi + from serena.repl.api.fs_api import FsApi + class EditApiMixin: """ - Mixin for tools which delegate to the editing API + Mixin for tools which delegate to the editing API. + The API is imported locally, since the API module refers to the tools (as corresponding tools). """ - def _api(self) -> EditApi: + def _api(self) -> "EditApi": + from serena.repl.api.edit_api import EditApi + tool = cast(Tool, cast(object, self)) return EditApi(tool.agent) class FsApiMixin: """ - Mixin for tools which delegate to the file system API + Mixin for tools which delegate to the file system API. + The API is imported locally, since the API module refers to the tools (as corresponding tools). """ - def _api(self) -> FsApi: + def _api(self) -> "FsApi": + from serena.repl.api.fs_api import FsApi + tool = cast(Tool, cast(object, self)) return FsApi(tool.agent) diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index 0e877d5a..9985957d 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -1,26 +1,28 @@ # SPDX-License-Identifier: GPL-3.0-or-later import logging -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Literal, cast -from serena.repl.api.jb_api import JetBrainsApi +from serena.symbol import SymbolDictGrouper from serena.tools import Tool, ToolMarkerBeta, ToolMarkerOptional, ToolMarkerSymbolicEdit, ToolMarkerSymbolicRead if TYPE_CHECKING: - from serena.agent import SerenaAgent + from serena.repl.api.jb_api import JetBrainsApi log = logging.getLogger(__name__) class JetBrainsApiMixin: """ - Mixin for tools which delegate to the JetBrains API + Mixin for tools which delegate to the JetBrains API. + The API is imported locally, since the API module refers to the tools (as corresponding tools). """ - agent: "SerenaAgent" + def _api(self) -> "JetBrainsApi": + from serena.repl.api.jb_api import JetBrainsApi - def _api(self) -> JetBrainsApi: - return JetBrainsApi(self.agent) + tool = cast(Tool, cast(object, self)) + return JetBrainsApi(tool.agent) class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional, JetBrainsApiMixin): @@ -28,7 +30,11 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional, Performs a global (or local) search for symbols using the JetBrains backend """ - symbol_dict_grouper = JetBrainsApi.find_symbol_grouper_ + @property + def symbol_dict_grouper(self) -> SymbolDictGrouper: + from serena.repl.api.jb_api import JetBrainsApi + + return JetBrainsApi.find_symbol_grouper_ def apply( self, @@ -217,7 +223,11 @@ class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMark Finds symbols that reference the given symbol using the JetBrains backend """ - symbol_dict_grouper = JetBrainsApi.references_grouper_ + @property + def symbol_dict_grouper(self) -> SymbolDictGrouper: + from serena.repl.api.jb_api import JetBrainsApi + + return JetBrainsApi.references_grouper_ def apply( self, @@ -245,7 +255,11 @@ class JetBrainsGetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOp Retrieves an overview of the top-level symbols within a specified file using the JetBrains backend """ - symbol_dict_grouper = JetBrainsApi.overview_grouper_ + @property + def symbol_dict_grouper(self) -> SymbolDictGrouper: + from serena.repl.api.jb_api import JetBrainsApi + + return JetBrainsApi.overview_grouper_ def apply( self, diff --git a/src/serena/tools/memory_tools.py b/src/serena/tools/memory_tools.py index 6c2e966a..cfbbf3e4 100644 --- a/src/serena/tools/memory_tools.py +++ b/src/serena/tools/memory_tools.py @@ -1,17 +1,22 @@ # SPDX-License-Identifier: GPL-3.0-or-later -from typing import Literal, cast +from typing import TYPE_CHECKING, Literal, cast -from serena.repl.api.mem_api import MemoryApi from serena.tools import Tool, ToolMarkerCanEdit +if TYPE_CHECKING: + from serena.repl.api.mem_api import MemoryApi + class MemoryApiMixin: """ - Mixin for tools which delegate to the memory API + Mixin for tools which delegate to the memory API. + The API is imported locally, since the API module refers to the tools (as corresponding tools). """ - def _api(self) -> MemoryApi: + def _api(self) -> "MemoryApi": + from serena.repl.api.mem_api import MemoryApi + tool = cast(Tool, cast(object, self)) return MemoryApi(tool.agent) diff --git a/src/serena/tools/symbol_tools.py b/src/serena/tools/symbol_tools.py index 76022951..9712b821 100644 --- a/src/serena/tools/symbol_tools.py +++ b/src/serena/tools/symbol_tools.py @@ -3,9 +3,9 @@ Language server-related tools """ # SPDX-License-Identifier: GPL-3.0-or-later -from typing import cast +from typing import TYPE_CHECKING, cast -from serena.repl.api.lsp_api import LspApi +from serena.symbol import SymbolDictGrouper from serena.tools import ( EditingToolWithDiagnostics, Tool, @@ -15,13 +15,19 @@ from serena.tools import ( from serena.tools.file_tools import EditApiMixin from serena.tools.tools_base import ToolMarkerOptional +if TYPE_CHECKING: + from serena.repl.api.lsp_api import LspApi + class LspApiMixin: """ - Mixin for tools which delegate to the language server API + Mixin for tools which delegate to the language server API. + The API is imported locally, since the API module refers to the tools (as corresponding tools). """ - def _api(self) -> LspApi: + def _api(self) -> "LspApi": + from serena.repl.api.lsp_api import LspApi + tool = cast(Tool, cast(object, self)) return LspApi(tool.agent) @@ -41,7 +47,11 @@ class GetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead, LspApiMixin): Gets an overview of the top-level symbols defined in a given file. """ - symbol_dict_grouper = LspApi.overview_grouper_ + @property + def symbol_dict_grouper(self) -> SymbolDictGrouper: + from serena.repl.api.lsp_api import LspApi + + return LspApi.overview_grouper_ def apply(self, relative_path: str, depth: int = -1, max_answer_chars: int = -1) -> str: """ @@ -65,7 +75,11 @@ class FindSymbolTool(Tool, ToolMarkerSymbolicRead, LspApiMixin): Performs a global (or local) search using the language server backend. """ - symbol_dict_grouper = LspApi.find_symbol_dict_grouper_ + @property + def symbol_dict_grouper(self) -> SymbolDictGrouper: + from serena.repl.api.lsp_api import LspApi + + return LspApi.find_symbol_dict_grouper_ def apply( self, @@ -146,7 +160,11 @@ class FindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, LspApiMixin): Finds symbols that reference the given symbol """ - symbol_dict_grouper = LspApi.references_grouper_ + @property + def symbol_dict_grouper(self) -> SymbolDictGrouper: + from serena.repl.api.lsp_api import LspApi + + return LspApi.references_grouper_ def apply( self, diff --git a/test/serena/test_repl_tool.py b/test/serena/test_repl_tool.py index a2923915..fc04c434 100644 --- a/test/serena/test_repl_tool.py +++ b/test/serena/test_repl_tool.py @@ -11,7 +11,7 @@ from serena.config.serena_config import ApiInclusionDefinition from serena.repl.api.lsp_api import LspApi from serena.repl.facade import ApiScope, Facade, FacadeApi, FacadeMethodInfo, facade_method from serena.repl.repl import SerenaRepl -from serena.tools import SerenaReplTool +from serena.tools import FindSymbolTool, SerenaReplTool from solidlsp.ls_config import LanguageServerId from test.conftest import agent_for_project_context @@ -165,6 +165,15 @@ class TestFacade: facade = Facade.from_api(self.DummyApi(MagicMock()), self._scope(included_apis=["dummy.extra"])) assert "extra" in facade.enabled_method_names + def test_corresponding_tool(self) -> None: + facade = Facade.from_api(self.DummyApi(MagicMock()), ApiScope()) + assert facade.get_method("add").info.get_corresponding_tool_name() is None + + lsp_facade = Facade.from_api(LspApi(MagicMock()), ApiScope()) + info = lsp_facade.get_method("find_symbol").info + assert info.corresponding_tool is FindSymbolTool + assert info.get_corresponding_tool_name() == "find_symbol" + def test_method_info_mirrors_decorator(self) -> None: facade = Facade.from_api(self.DummyApi(MagicMock()), ApiScope()) assert facade.get_method("add").info == FacadeMethodInfo(name="add")