diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 35f7e430..2eac7512 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -350,6 +350,11 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): on the first run, which can take some time and require internet access. Others, like the Anthropic ones, may require an API key and rate limits may apply. """ + default_max_tool_answer_chars: int = 150_000 + """Used as default for tools where the apply method has a default maximal answer length. + Even though the value of the max_answer_chars can be changed when calling the tool, it may make sense to adjust this default + through the global configuration. + """ CONFIG_FILE = "serena_config.yml" CONFIG_FILE_DOCKER = "serena_config.docker.yml" # Docker-specific config file; auto-generated if missing, mounted via docker-compose for user customization @@ -453,6 +458,7 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): instance.token_count_estimator = loaded_commented_yaml.get( "token_count_estimator", RegisteredTokenCountEstimator.TIKTOKEN_GPT4O.name ) + instance.default_max_tool_answer_chars = loaded_commented_yaml.get("default_max_tool_answer_chars", 150_000) # re-save the configuration file if any migrations were performed if num_project_migrations > 0: diff --git a/src/serena/resources/serena_config.template.yml b/src/serena/resources/serena_config.template.yml index 1a9fdd24..9d7bce7c 100644 --- a/src/serena/resources/serena_config.template.yml +++ b/src/serena/resources/serena_config.template.yml @@ -48,6 +48,11 @@ jetbrains: False # NOTE: The plugin is yet unreleased. This is for Serena developers only. +default_max_tool_answer_chars: 150000 +# Used as default for tools where the apply method has a default maximal answer length. +# Even though the value of the max_answer_chars can be changed when calling the tool, it may make sense to adjust this default +# through the global configuration. + record_tool_usage_stats: False # whether to record tool usage statistics, they will be shown in the web dashboard if recording is active. diff --git a/src/serena/tools/cmd_tools.py b/src/serena/tools/cmd_tools.py index 214d54e2..ccacd122 100644 --- a/src/serena/tools/cmd_tools.py +++ b/src/serena/tools/cmd_tools.py @@ -2,7 +2,7 @@ Tools supporting the execution of (external) commands """ -from serena.tools import TOOL_DEFAULT_MAX_ANSWER_LENGTH, Tool, ToolMarkerCanEdit +from serena.tools import Tool, ToolMarkerCanEdit from serena.util.shell import execute_shell_command @@ -16,7 +16,7 @@ class ExecuteShellCommandTool(Tool, ToolMarkerCanEdit): command: str, cwd: str | None = None, capture_stderr: bool = True, - max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH, + max_answer_chars: int = -1, ) -> str: """ Execute a shell command and return its output. If there is a memory about suggested commands, read that first. @@ -26,7 +26,7 @@ class ExecuteShellCommandTool(Tool, ToolMarkerCanEdit): :param cwd: the working directory to execute the command in. If None, the project root will be used. :param capture_stderr: whether to capture and return stderr output :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 + no content will be returned. -1 means using the default value, don't adjust unless there is no other way to get the content required for the task. :return: a JSON object containing the command's stdout and optionally stderr output """ diff --git a/src/serena/tools/file_tools.py b/src/serena/tools/file_tools.py index e6c30658..e5b1467a 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, ToolMarkerOptional +from serena.tools import SUCCESS_RESULT, EditedFileContext, Tool, ToolMarkerCanEdit, ToolMarkerOptional from serena.util.file_system import scan_directory @@ -23,9 +23,7 @@ class ReadFileTool(Tool): Reads a file within the project directory. """ - def apply( - self, relative_path: str, start_line: int = 0, end_line: int | None = None, max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH - ) -> str: + def apply(self, relative_path: str, start_line: int = 0, end_line: int | None = None, max_answer_chars: int = -1) -> str: """ 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. @@ -89,15 +87,15 @@ class ListDirTool(Tool): Lists files and directories in the given directory (optionally with recursion). """ - def apply(self, relative_path: str, recursive: bool, max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH) -> str: + def apply(self, relative_path: str, recursive: bool, max_answer_chars: int = -1) -> str: """ Lists all non-gitignored files and directories in the given directory (optionally with recursion). :param relative_path: the relative path to the directory to list; pass "." to scan the project root :param recursive: whether to scan subdirectories recursively :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. + no content will be returned. -1 means the default value from the config will be used. + Don't adjust unless there is really no other way to get the content required for the task. :return: a JSON object with the names of directories and files within the given directory """ # Check if the directory exists before validation @@ -309,7 +307,7 @@ class SearchForPatternTool(Tool): paths_exclude_glob: str = "", relative_path: str = "", restrict_search_to_code_files: bool = False, - max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH, + max_answer_chars: int = -1, ) -> str: """ Offers a flexible search for arbitrary patterns in the codebase, including the @@ -350,7 +348,9 @@ class SearchForPatternTool(Tool): :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` is 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 + no content will be returned. + -1 means the default value from the config will be used. + 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. :param restrict_search_to_code_files: whether to restrict the search to only those files where diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index 8b9f2d1a..db3a97c7 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -1,6 +1,6 @@ import json -from serena.tools import TOOL_DEFAULT_MAX_ANSWER_LENGTH, Tool, ToolMarkerOptional, ToolMarkerSymbolicRead +from serena.tools import Tool, ToolMarkerOptional, ToolMarkerSymbolicRead from serena.tools.jetbrains_plugin_client import JetBrainsPluginClient @@ -15,7 +15,7 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): depth: int = 0, relative_path: str | None = None, include_body: bool = False, - max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH, + max_answer_chars: int = -1, ) -> str: """ Retrieves information on all symbols/code entities (classes, methods, etc.) based on the given `name_path`, @@ -57,6 +57,7 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): speed up the search as well as reduce the number of results. :param include_body: If True, include the symbol's source code. Use judiciously. :param max_answer_chars: max characters for the JSON result. If exceeded, no content is returned. + -1 means the default value from the config will be used. :return: JSON string: a list of symbols (with locations) matching the name. """ with JetBrainsPluginClient.from_project(self.project) as client: @@ -79,7 +80,7 @@ class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMark self, name_path: str, relative_path: str, - max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH, + max_answer_chars: int = -1, ) -> str: """ Finds symbols that reference the symbol at the given `name_path`. @@ -88,7 +89,8 @@ class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMark :param name_path: name path of the symbol for which to find references; matching logic as described in find symbol tool. :param relative_path: the relative path to the file containing the symbol for which to find references. Note that here you can't pass a directory but must pass a file. - :param max_answer_chars: max characters for the JSON result. If exceeded, no content is returned. + :param max_answer_chars: max characters for the JSON result. If exceeded, no content is returned. -1 means the + default value from the config will be used. :return: a list of JSON objects with the symbols referencing the requested symbol """ with JetBrainsPluginClient.from_project(self.project) as client: @@ -108,7 +110,7 @@ class JetBrainsGetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOp def apply( self, relative_path: str, - max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH, + max_answer_chars: int = -1, ) -> str: """ Gets an overview of the top-level symbols in the given file. @@ -119,6 +121,7 @@ class JetBrainsGetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOp :param relative_path: the relative path to the file to get the overview of :param max_answer_chars: max characters for the JSON result. If exceeded, no content is returned. + -1 means the default value from the config will be used. :return: a JSON object containing the symbols """ with JetBrainsPluginClient.from_project(self.project) as client: diff --git a/src/serena/tools/memory_tools.py b/src/serena/tools/memory_tools.py index 3cf611f0..63d6537e 100644 --- a/src/serena/tools/memory_tools.py +++ b/src/serena/tools/memory_tools.py @@ -1,6 +1,6 @@ import json -from serena.tools import TOOL_DEFAULT_MAX_ANSWER_LENGTH, Tool +from serena.tools import Tool class WriteMemoryTool(Tool): @@ -8,7 +8,7 @@ class WriteMemoryTool(Tool): Writes a named memory (for future reference) to Serena's project-specific memory store. """ - def apply(self, memory_name: str, content: str, max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH) -> str: + def apply(self, memory_name: str, content: str, max_answer_chars: int = -1) -> str: """ 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. @@ -26,7 +26,7 @@ class ReadMemoryTool(Tool): Reads the memory with the given name from Serena's project-specific memory store. """ - def apply(self, memory_file_name: str, max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH) -> str: + def apply(self, memory_file_name: str, max_answer_chars: int = -1) -> str: """ Read the content of a memory file. This tool should only be used if the information is relevant to the current task. You can infer whether the information diff --git a/src/serena/tools/symbol_tools.py b/src/serena/tools/symbol_tools.py index c57ec123..9cbc525a 100644 --- a/src/serena/tools/symbol_tools.py +++ b/src/serena/tools/symbol_tools.py @@ -11,7 +11,6 @@ from typing import Any from serena.tools import ( SUCCESS_RESULT, - TOOL_DEFAULT_MAX_ANSWER_LENGTH, Tool, ToolMarkerSymbolicEdit, ToolMarkerSymbolicRead, @@ -52,7 +51,7 @@ class GetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead): Gets an overview of the top-level symbols defined in a given file. """ - def apply(self, relative_path: str, max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH) -> str: + def apply(self, relative_path: str, max_answer_chars: int = -1) -> str: """ 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 @@ -60,8 +59,8 @@ class GetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead): :param relative_path: the relative path to the file 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. + no content will be returned. -1 means the default value from the config will be used. + Don't adjust unless there is really no other way to get the content required for the task. :return: a JSON object containing info about top-level symbols in the file """ symbol_retriever = self.create_language_server_symbol_retriever() @@ -92,7 +91,7 @@ class FindSymbolTool(Tool, ToolMarkerSymbolicRead): include_kinds: list[int] = [], # noqa: B006 exclude_kinds: list[int] = [], # noqa: B006 substring_matching: bool = False, - max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH, + max_answer_chars: int = -1, ) -> str: """ Retrieves information on all symbols/code entities (classes, methods, etc.) based on the given `name_path`, @@ -142,6 +141,7 @@ class FindSymbolTool(Tool, ToolMarkerSymbolicRead): If not provided, no kinds are excluded. :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. + -1 means the default value from the config will be used. :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 @@ -171,7 +171,7 @@ class FindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead): relative_path: str, include_kinds: list[int] = [], # noqa: B006 exclude_kinds: list[int] = [], # noqa: B006 - max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH, + max_answer_chars: int = -1, ) -> str: """ Finds references to the symbol at the given `name_path`. The result will contain metadata about the referencing symbols diff --git a/src/serena/tools/tools_base.py b/src/serena/tools/tools_base.py index 02241fbc..46afb365 100644 --- a/src/serena/tools/tools_base.py +++ b/src/serena/tools/tools_base.py @@ -70,9 +70,6 @@ class Component(ABC): return self.agent.lines_read -TOOL_DEFAULT_MAX_ANSWER_LENGTH = int(15e4) - - class ToolMarker: """ Base class for tool markers. @@ -214,8 +211,11 @@ class Tool(Component): params[param] = value log.info(f"{self.get_name_from_cls()}: {dict_string(params)}") - @staticmethod - def _limit_length(result: str, max_answer_chars: int) -> str: + def _limit_length(self, result: str, max_answer_chars: int) -> str: + if max_answer_chars == -1: + max_answer_chars = self.agent.serena_config.default_max_tool_answer_chars + if max_answer_chars <= 0: + raise ValueError(f"Must be positive or the default (-1), got: {max_answer_chars=}") if (n_chars := len(result)) > max_answer_chars: result = ( f"The answer is too long ({n_chars} characters). "