From 99757f35ebd53173c397379d4884c70cacc3981e Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Sun, 3 Aug 2025 00:14:11 +0200 Subject: [PATCH] Made more tools optional and shortened several docstrings --- docs/serena_on_chatgpt.md | 3 +- scripts/demo_run_tools.py | 10 ++--- src/serena/config/context_mode.py | 3 +- .../config/contexts/context.template.yml | 3 ++ .../resources/config/contexts/desktop-app.yml | 3 ++ .../config/contexts/ide-assistant.yml | 5 --- .../resources/config/modes/mode.template.yml | 3 ++ src/serena/tools/cmd_tools.py | 11 +---- src/serena/tools/config_tools.py | 8 ++-- src/serena/tools/file_tools.py | 20 +++------ src/serena/tools/memory_tools.py | 11 +---- src/serena/tools/symbol_tools.py | 44 +++++++++---------- src/serena/tools/workflow_tools.py | 2 +- 13 files changed, 52 insertions(+), 74 deletions(-) diff --git a/docs/serena_on_chatgpt.md b/docs/serena_on_chatgpt.md index 64f7e248..ec872696 100644 --- a/docs/serena_on_chatgpt.md +++ b/docs/serena_on_chatgpt.md @@ -18,7 +18,7 @@ Run the following command to launch Serena as http server (assuming port 8000): ```bash uvx mcpo --port 8000 --api-key -- \ uvx --from git+https://github.com/oraios/serena \ - serena start-mcp-server --project $(pwd) + serena start-mcp-server --context chatgpt --project $(pwd) ``` - `--api-key` is required to secure the server. @@ -63,6 +63,7 @@ Your server is now securely exposed to the internet. ``` "servers": ["url": ""], ``` + **Important**: don't include a trailing slash at the end of the URL! ChatGPT will read the schema and create functions automatically. diff --git a/scripts/demo_run_tools.py b/scripts/demo_run_tools.py index 394fe553..d52cfb00 100644 --- a/scripts/demo_run_tools.py +++ b/scripts/demo_run_tools.py @@ -9,7 +9,7 @@ from pprint import pprint from serena.agent import SerenaAgent from serena.config.serena_config import SerenaConfig from serena.constants import REPO_ROOT -from serena.tools import FindFileTool, FindReferencingSymbolsTool, SearchForPatternTool +from serena.tools import FindFileTool, FindReferencingSymbolsTool, GetSymbolsOverviewTool, SearchForPatternTool if __name__ == "__main__": agent = SerenaAgent(project=REPO_ROOT, serena_config=SerenaConfig(gui_log_window_enabled=False, web_dashboard=False)) @@ -18,13 +18,9 @@ if __name__ == "__main__": find_refs_tool = agent.get_tool(FindReferencingSymbolsTool) find_file_tool = agent.get_tool(FindFileTool) search_pattern_tool = agent.get_tool(SearchForPatternTool) + overview_tool = agent.get_tool(GetSymbolsOverviewTool) result = agent.execute_task( - lambda: search_pattern_tool.apply( - r"def request_full_.*?\).*?\)", - restrict_search_to_code_files=False, - relative_path="src/solidlsp", - paths_include_glob="**/ls.py", - ) + lambda: overview_tool.apply("src/solidlsp/ls.py"), ) pprint(json.loads(result)) diff --git a/src/serena/config/context_mode.py b/src/serena/config/context_mode.py index e04f3383..fbe34091 100644 --- a/src/serena/config/context_mode.py +++ b/src/serena/config/context_mode.py @@ -3,8 +3,7 @@ Context and Mode configuration loader """ import os -from copy import copy -from dataclasses import asdict, dataclass, field +from dataclasses import dataclass, field from enum import Enum from pathlib import Path from typing import TYPE_CHECKING, Self diff --git a/src/serena/resources/config/contexts/context.template.yml b/src/serena/resources/config/contexts/context.template.yml index f9dee588..c01a1177 100644 --- a/src/serena/resources/config/contexts/context.template.yml +++ b/src/serena/resources/config/contexts/context.template.yml @@ -3,6 +3,9 @@ description: Description of the context, not used in the code. prompt: Prompt that will form part of the system prompt/initial instructions for agents started in this context. excluded_tools: [] +# several tools are excluded by default and have to be explicitly included by the user +included_optional_tools: [] + # mapping of tool names to an override of their descriptions (the default description is the docstring of the Tool's apply method). # Sometimes, tool descriptions are too long (e.g., for ChatGPT), or users may want to override them for another reason. tool_description_overrides: {} \ No newline at end of file diff --git a/src/serena/resources/config/contexts/desktop-app.yml b/src/serena/resources/config/contexts/desktop-app.yml index 8105ca8d..c3adcfb4 100644 --- a/src/serena/resources/config/contexts/desktop-app.yml +++ b/src/serena/resources/config/contexts/desktop-app.yml @@ -11,4 +11,7 @@ prompt: | a diagram in addition to your text-based communication. Note that in the chat interface you have various rendering options for text, html, and mermaid diagrams, as has been explained to you in your initial instructions. excluded_tools: [] +included_optional_tools: + - switch_modes + tool_description_overrides: {} diff --git a/src/serena/resources/config/contexts/ide-assistant.yml b/src/serena/resources/config/contexts/ide-assistant.yml index 14d65130..27a13673 100644 --- a/src/serena/resources/config/contexts/ide-assistant.yml +++ b/src/serena/resources/config/contexts/ide-assistant.yml @@ -19,13 +19,8 @@ prompt: | excluded_tools: - create_text_file - read_file - - delete_lines - - replace_lines - - insert_at_line - execute_shell_command - prepare_for_new_conversation - - summarize_changes - - get_current_config tool_description_overrides: {} diff --git a/src/serena/resources/config/modes/mode.template.yml b/src/serena/resources/config/modes/mode.template.yml index 431afdeb..6d836ca3 100644 --- a/src/serena/resources/config/modes/mode.template.yml +++ b/src/serena/resources/config/modes/mode.template.yml @@ -2,3 +2,6 @@ description: Description of the mode, not used in the code. prompt: Prompt that will form part of the message sent to the model when activating this mode. excluded_tools: [] + +# several tools are excluded by default and have to be explicitly included by the user +included_optional_tools: [] \ No newline at end of file diff --git a/src/serena/tools/cmd_tools.py b/src/serena/tools/cmd_tools.py index 0db0da08..214d54e2 100644 --- a/src/serena/tools/cmd_tools.py +++ b/src/serena/tools/cmd_tools.py @@ -19,15 +19,8 @@ class ExecuteShellCommandTool(Tool, ToolMarkerCanEdit): max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH, ) -> str: """ - Execute a shell command and return its output. - - IMPORTANT: you should always consider the memory about suggested shell commands before using this tool. - If this memory was not loaded in the current conversation, you should load it using the `read_memory` tool - before using this tool. - - You should have at least once looked at the suggested shell commands from the corresponding memory - created during the onboarding process before using this tool. - Never execute unsafe shell commands like `rm -rf /` or similar! Generally be very careful with deletions. + Execute a shell command and return its output. If there is a memory about suggested commands, read that first. + Never execute unsafe shell commands like `rm -rf /` or similar! :param command: the shell command to execute :param cwd: the working directory to execute the command in. If None, the project root will be used. diff --git a/src/serena/tools/config_tools.py b/src/serena/tools/config_tools.py index 51b782ad..8a3ddeb7 100644 --- a/src/serena/tools/config_tools.py +++ b/src/serena/tools/config_tools.py @@ -1,7 +1,7 @@ import json from serena.config.context_mode import SerenaAgentMode -from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject +from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject): @@ -35,7 +35,7 @@ class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject): return result_str -class RemoveProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject): +class RemoveProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional): """ Removes a project from the Serena configuration. """ @@ -50,7 +50,7 @@ class RemoveProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject): return f"Successfully removed project '{project_name}' from configuration." -class SwitchModesTool(Tool): +class SwitchModesTool(Tool, ToolMarkerOptional): """ Activates modes by providing a list of their names """ @@ -71,7 +71,7 @@ class SwitchModesTool(Tool): return result_str -class GetCurrentConfigTool(Tool): +class GetCurrentConfigTool(Tool, ToolMarkerOptional): """ Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. """ diff --git a/src/serena/tools/file_tools.py b/src/serena/tools/file_tools.py index f79ffe06..b5770ab5 100644 --- a/src/serena/tools/file_tools.py +++ b/src/serena/tools/file_tools.py @@ -14,7 +14,7 @@ from fnmatch import fnmatch from pathlib import Path from serena.text_utils import search_files -from serena.tools import SUCCESS_RESULT, TOOL_DEFAULT_MAX_ANSWER_LENGTH, EditedFileContext, Tool, ToolMarkerCanEdit +from serena.tools import SUCCESS_RESULT, TOOL_DEFAULT_MAX_ANSWER_LENGTH, EditedFileContext, Tool, ToolMarkerCanEdit, ToolMarkerOptional from serena.util.file_system import scan_directory @@ -29,7 +29,6 @@ class ReadFileTool(Tool): """ Reads the given file or a chunk of it. Generally, symbolic operations like find_symbol or find_referencing_symbols should be preferred if you know which symbols you are looking for. - Reading the entire file is only recommended if there is no other way to get the content required for the task. :param relative_path: the relative path to the file to read :param start_line: the 0-based index of the first line to be retrieved. @@ -60,14 +59,7 @@ class CreateTextFileTool(Tool, ToolMarkerCanEdit): def apply(self, relative_path: str, content: str) -> str: """ - Write a new file (or overwrite an existing file). For existing files, it is strongly recommended - to use symbolic operations like replace_symbol_body or insert_after_symbol/insert_before_symbol, if possible. - 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. + Write a new file or overwrite an existing file. :param relative_path: the relative path to the file to create :param content: the (utf-8-encoded) content to write to the file @@ -200,7 +192,7 @@ class ReplaceRegexTool(Tool, ToolMarkerCanEdit): return SUCCESS_RESULT -class DeleteLinesTool(Tool, ToolMarkerCanEdit): +class DeleteLinesTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional): """ Deletes a range of lines within a file. """ @@ -228,7 +220,7 @@ class DeleteLinesTool(Tool, ToolMarkerCanEdit): return SUCCESS_RESULT -class ReplaceLinesTool(Tool, ToolMarkerCanEdit): +class ReplaceLinesTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional): """ Replaces a range of lines within a file with new content. """ @@ -259,7 +251,7 @@ class ReplaceLinesTool(Tool, ToolMarkerCanEdit): return SUCCESS_RESULT -class InsertAtLineTool(Tool, ToolMarkerCanEdit): +class InsertAtLineTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional): """ Inserts content at a given line in a file. """ @@ -351,7 +343,7 @@ class SearchForPatternTool(Tool): For example, for finding classes or methods from a name pattern. Setting to False is a better choice if you also want to search in non-code files, like in html or yaml files, which is why it is the default. - :return: A JSON object mapping file paths to lists of matched consecutive lines (with context, if requested). + :return: A mapping of file paths to lists of matched consecutive lines. """ abs_path = os.path.join(self.get_project_root(), relative_path) if not os.path.exists(abs_path): diff --git a/src/serena/tools/memory_tools.py b/src/serena/tools/memory_tools.py index 63bba4fe..3cf611f0 100644 --- a/src/serena/tools/memory_tools.py +++ b/src/serena/tools/memory_tools.py @@ -10,15 +10,8 @@ class WriteMemoryTool(Tool): def apply(self, memory_name: str, content: str, max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH) -> str: """ - Write some information about this project that can be useful for future tasks to a memory. - Use markdown formatting for the content. - The information should be short and to the point. - The memory name should be meaningful, such that from the name you can infer what the information is about. - It is better to have multiple small memories than to have a single large one because - memories will be read one by one and we only ever want to read relevant memories. - - This tool is either called during the onboarding process or when you have identified - something worth remembering about the project from the past conversation. + Write some information about this project that can be useful for future tasks to a memory in md format. + The memory name should be meaningful. """ if len(content) > max_answer_chars: raise ValueError( diff --git a/src/serena/tools/symbol_tools.py b/src/serena/tools/symbol_tools.py index 4d2a5ffc..0dfb41a3 100644 --- a/src/serena/tools/symbol_tools.py +++ b/src/serena/tools/symbol_tools.py @@ -4,6 +4,7 @@ Language server-related tools import dataclasses import json +import os from collections.abc import Sequence from copy import copy from typing import Any @@ -33,10 +34,7 @@ class RestartLanguageServerTool(Tool): def apply(self) -> str: """Use this tool only on explicit user request or after confirmation. - It may be necessary to restart the language server if the user performs edits - not through Serena, so the language server state becomes outdated and further editing attempts lead to errors. - - If such editing errors happen, you should suggest using this tool. + It may be necessary to restart the language server if it hangs. """ self.agent.reset_language_server() return SUCCESS_RESULT @@ -49,24 +47,28 @@ class GetSymbolsOverviewTool(Tool): def apply(self, relative_path: str, max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH) -> str: """ - Gets an overview of the given file or directory. - For each analyzed file, we list the top-level symbols in the file (name_path, kind). - Use this tool to get a high-level understanding of the code symbols. - Calling this is often a good idea before more targeted reading, searching or editing operations on the code symbols. - Before requesting a symbol overview, it is usually a good idea to narrow down the scope of the overview - by first understanding the basic directory structure of the repository that you can get from memories - or by using the `list_dir` and `find_file` tools (or similar). + Use this tool to get a high-level understanding of the code symbols in a file. + This should be the first tool to call when you want to understand a new file, unless you already know + what you are looking for. :param relative_path: the relative path to the file or directory to get the overview of :param max_answer_chars: if the overview 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. If the overview is too long, you should use a smaller directory instead, (e.g. a subdirectory). - :return: a JSON object mapping relative paths of all contained files to info about top-level symbols in the file (name_path, kind). + :return: a JSON object containing info about top-level symbols in the file """ symbol_retriever = self.create_language_server_symbol_retriever() - result = symbol_retriever.get_symbol_overview(relative_path) - result_json_str = json.dumps({k: [dataclasses.asdict(i) for i in l] for k, l in result.items()}) + file_path = os.path.join(self.project.project_root, relative_path) + + # The symbol overview is capable of working with both files and directories, + # but we want to ensure that the user provides a file path. + if not os.path.exists(file_path): + raise FileNotFoundError(f"File or directory {relative_path} does not exist in the project.") + if os.path.isdir(file_path): + raise ValueError(f"Expected a file path, but got a directory path: {relative_path}. ") + result = symbol_retriever.get_symbol_overview(relative_path)[relative_path] + result_json_str = json.dumps([dataclasses.asdict(i) for i in result]) return self._limit_length(result_json_str, max_answer_chars) @@ -132,7 +134,7 @@ class FindSymbolTool(Tool): :param exclude_kinds: Optional. List of LSP symbol kind integers to exclude. Takes precedence over `include_kinds`. :param substring_matching: If True, use substring matching for the last segment of `name`. :param max_answer_chars: Max characters for the JSON result. If exceeded, no content is returned. - :return: JSON string: a list of symbols (with locations) matching the name. + :return: a list of symbols (with locations) matching the name. """ parsed_include_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in include_kinds] if include_kinds else None parsed_exclude_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in exclude_kinds] if exclude_kinds else None @@ -164,10 +166,8 @@ class FindReferencingSymbolsTool(Tool): max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH, ) -> str: """ - Finds symbols that reference the symbol at the given `name_path`. The result will contain metadata about the referencing symbols - as well as a short code snippet around the reference (unless `include_body` is True, then the short snippet will be omitted). - Note that among other kinds of references, this function can be used to find (direct) subclasses of a class, - as subclasses are referencing symbols that have the kind class. + Finds references to the symbol at the given `name_path`. The result will contain metadata about the referencing symbols + as well as a short code snippet around the reference. :param name_path: for finding the symbol to find references for, same logic as in the `find_symbol` tool. :param relative_path: the relative path to the file containing the symbol for which to find references. @@ -269,9 +269,9 @@ class InsertBeforeSymbolTool(Tool, ToolMarkerCanEdit): body: str, ) -> str: """ - Inserts the given body/content before the beginning of the definition of the given symbol (via the symbol's location). - A typical use case is to insert a new class, function, method, field or variable assignment. - It also can be used to insert a new import statement before the first symbol in the file. + Inserts the given content before the beginning of the definition of the given symbol (via the symbol's location). + A typical use case is to insert a new class, function, method, field or variable assignment; or + a new import statement before the first symbol in the file. :param name_path: name path of the symbol before which to insert content (definitions in the `find_symbol` tool apply) :param relative_path: the relative path to the file containing the symbol diff --git a/src/serena/tools/workflow_tools.py b/src/serena/tools/workflow_tools.py index 38cc19be..be78d03d 100644 --- a/src/serena/tools/workflow_tools.py +++ b/src/serena/tools/workflow_tools.py @@ -96,7 +96,7 @@ class ThinkAboutWhetherYouAreDoneTool(Tool): return self.prompt_factory.create_think_about_whether_you_are_done() -class SummarizeChangesTool(Tool): +class SummarizeChangesTool(Tool, ToolMarkerOptional): """ Provides instructions for summarizing the changes made to the codebase. """