diff --git a/scripts/profile_tool_call.py b/scripts/profile_tool_call.py index e0e69981..8fe922a5 100644 --- a/scripts/profile_tool_call.py +++ b/scripts/profile_tool_call.py @@ -25,7 +25,6 @@ if __name__ == "__main__": serena_config = SerenaConfig.from_config_file() serena_config.log_level = logging.INFO - serena_config.jetbrains = False serena_config.gui_log_window_enabled = False serena_config.web_dashboard = False diff --git a/src/serena/agent.py b/src/serena/agent.py index e596dd1c..6f3c203b 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -18,7 +18,7 @@ from interprompt.jinja_template import JinjaTemplate from serena import serena_version from serena.analytics import RegisteredTokenCountEstimator, ToolUsageStats from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode -from serena.config.serena_config import SerenaConfig, ToolInclusionDefinition +from serena.config.serena_config import LanguageBackend, SerenaConfig, ToolInclusionDefinition from serena.dashboard import SerenaDashboardAPI from serena.ls_manager import LanguageServerManager from serena.project import Project @@ -226,7 +226,10 @@ class SerenaAgent: self._tool_usage_stats = ToolUsageStats(token_count_estimator) # log fundamental information - log.info(f"Starting Serena server (version={serena_version()}, process id={os.getpid()}, parent process id={os.getppid()})") + log.info( + f"Starting Serena server (version={serena_version()}, process id={os.getpid()}, parent process id={os.getppid()}; " + f"language backend={self.serena_config.language_backend.name})" + ) log.info("Configuration file: %s", self.serena_config.config_file_path) log.info("Available projects: {}".format(", ".join(self.serena_config.project_names))) log.info(f"Loaded tools ({len(self._all_tools)}): {', '.join([tool.get_name_from_cls() for tool in self._all_tools.values()])}") @@ -238,7 +241,7 @@ class SerenaAgent: tool_inclusion_definitions: list[ToolInclusionDefinition] = [self.serena_config, self._context] if self._context.single_project: tool_inclusion_definitions.extend(self._single_project_context_tool_inclusion_definitions(project)) - if self.serena_config.jetbrains: + if self.serena_config.language_backend == LanguageBackend.JETBRAINS: tool_inclusion_definitions.append(SerenaAgentMode.from_name_internal("jetbrains")) self._base_tool_set = ToolSet.default().apply(*tool_inclusion_definitions) @@ -514,7 +517,7 @@ class SerenaAgent: """ :return: whether this agent uses language server-based code analysis """ - return not self.serena_config.jetbrains + return self.serena_config.language_backend == LanguageBackend.LSP def _activate_project(self, project: Project) -> None: log.info(f"Activating {project.project_name} at {project.project_root}") diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 06e1c6d1..5c3b26fd 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -9,6 +9,7 @@ from collections.abc import Iterable from copy import deepcopy from dataclasses import dataclass, field from datetime import datetime +from enum import Enum from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING, Any, Optional, Self, TypeVar @@ -106,6 +107,26 @@ def get_serena_managed_in_project_dir(project_root: str | Path) -> str: return os.path.join(project_root, SERENA_MANAGED_DIR_NAME) +class LanguageBackend(Enum): + LSP = "LSP" + """ + Use the language server protocol (LSP), spawning freely available language servers + via the SolidLSP library that is part of Serena + """ + JETBRAINS = "JetBrains" + """ + Use the Serena plugin in your JetBrains IDE. + (requires the plugin to be installed and the project being worked on to be open in your IDE) + """ + + @staticmethod + def from_str(backend_str: str) -> "LanguageBackend": + for backend in LanguageBackend: + if backend.value.lower() == backend_str.lower(): + return backend + raise ValueError(f"Unknown language backend '{backend_str}': valid values are {[b.value for b in LanguageBackend]}") + + @dataclass(kw_only=True) class ProjectConfig(ToolInclusionDefinition, ToStringMixin): project_name: str @@ -362,9 +383,10 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): the path to the configuration file to which updates of the configuration shall be saved; if None, the configuration is not saved to disk """ - jetbrains: bool = False + + language_backend: LanguageBackend = LanguageBackend.LSP """ - whether to apply JetBrains mode + the language backend to use for code understanding features """ token_count_estimator: str = RegisteredTokenCountEstimator.CHAR_COUNT.name @@ -446,7 +468,7 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): # load list of known projects instance.projects = [] - num_project_migrations = 0 + num_migrations = 0 for path in loaded_commented_yaml["projects"]: path = Path(path).resolve() if not path.exists() or (path.is_dir() and not (path / ProjectConfig.rel_path_to_project_yml()).exists()): @@ -456,7 +478,7 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): path = cls._migrate_out_of_project_config_file(path) if path is None: continue - num_project_migrations += 1 + num_migrations += 1 project_config = ProjectConfig.load(path) project = RegisteredProject( project_root=str(path), @@ -469,7 +491,21 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): field_name = key return loaded_commented_yaml.get(key, get_dataclass_default(SerenaConfig, field_name)) - # set other configuration parameters + # determine language backend + language_backend = get_dataclass_default(SerenaConfig, "language_backend") + if "language_backend" in loaded_commented_yaml: + backend_str = loaded_commented_yaml["language_backend"] + language_backend = LanguageBackend.from_str(backend_str) + else: + # backward compatibility (migrate Boolean field "jetbrains") + if "jetbrains" in loaded_commented_yaml: + num_migrations += 1 + if loaded_commented_yaml["jetbrains"]: + language_backend = LanguageBackend.JETBRAINS + del loaded_commented_yaml["jetbrains"] + instance.language_backend = language_backend + + # set other configuration parameters (primitive types) instance.gui_log_window_enabled = get_value_or_default("gui_log_window", "gui_log_window_enabled") instance.web_dashboard_listen_address = get_value_or_default("web_dashboard_listen_address") instance.log_level = loaded_commented_yaml.get("log_level", loaded_commented_yaml.get("gui_log_level", logging.INFO)) @@ -479,16 +515,13 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): instance.trace_lsp_communication = get_value_or_default("trace_lsp_communication") instance.excluded_tools = get_value_or_default("excluded_tools") instance.included_optional_tools = get_value_or_default("included_optional_tools") - instance.jetbrains = get_value_or_default("jetbrains") instance.token_count_estimator = get_value_or_default("token_count_estimator") instance.default_max_tool_answer_chars = get_value_or_default("default_max_tool_answer_chars") instance.ls_specific_settings = get_value_or_default("ls_specific_settings") # re-save the configuration file if any migrations were performed - if num_project_migrations > 0: - log.info( - f"Migrated {num_project_migrations} project configurations from legacy format to in-project configuration; re-saving configuration" - ) + if num_migrations > 0: + log.info("Legacy configuration was migrated; re-saving configuration file") instance.save() return instance @@ -591,9 +624,15 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): """ if self.config_file_path is None: return + assert self.loaded_commented_yaml is not None, "Cannot save configuration without loaded YAML" + loaded_original_yaml = deepcopy(self.loaded_commented_yaml) - # projects are unique absolute paths - # we also canonicalize them before saving + + # convert project objects into list of paths loaded_original_yaml["projects"] = sorted({str(project.project_root) for project in self.projects}) + + # convert language backend to string + loaded_original_yaml["language_backend"] = self.language_backend.value + save_yaml(self.config_file_path, loaded_original_yaml, preserve_comments=True) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 9002c3bf..55d8b87b 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -9,6 +9,7 @@ from pydantic import BaseModel from sensai.util import logging from serena.analytics import ToolUsageStats +from serena.config.serena_config import LanguageBackend from serena.constants import SERENA_DASHBOARD_DIR from serena.task_executor import TaskExecutor from serena.util.logging import MemoryLogHandler @@ -443,7 +444,7 @@ class SerenaDashboardAPI: available_modes=available_modes, available_contexts=available_contexts, available_memories=available_memories, - jetbrains_mode=self._agent.serena_config.jetbrains, + jetbrains_mode=self._agent.serena_config.language_backend == LanguageBackend.JETBRAINS, languages=languages, encoding=encoding, ) diff --git a/src/serena/resources/serena_config.template.yml b/src/serena/resources/serena_config.template.yml index 974fe7e5..d197991d 100644 --- a/src/serena/resources/serena_config.template.yml +++ b/src/serena/resources/serena_config.template.yml @@ -1,3 +1,12 @@ +language_backend: LSP +# the language backend to use for code understanding and manipulation. +# Possible values are: +# * LSP: Use the language server protocol (LSP), spawning freely available language servers +# via the SolidLSP library that is part of Serena. +# * JetBrains: Use the Serena plugin in your JetBrains IDE. +# (requires the plugin to be installed and the project being worked on to be open +# in your IDE). + gui_log_window: False # whether to open a graphical window with Serena's logs. # This is mainly supported on Windows and (partly) on Linux; not available on macOS. @@ -42,7 +51,6 @@ ls_specific_settings: {} # (same entry as in project.yml) to the options. # Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available. # No documentation on options means no options are available. -# tool_timeout: 240 # timeout, in seconds, after which tool executions are terminated @@ -53,12 +61,6 @@ excluded_tools: [] included_optional_tools: [] # list of optional tools (which are disabled by default) to be included -jetbrains: False -# whether to enable JetBrains mode and use tools based on the Serena JetBrains IDE plugin -# instead of language server-based tools -# 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 @@ -73,14 +75,5 @@ token_count_estimator: CHAR_COUNT # data files on first run), or ANTHROPIC_CLAUDE_SONNET_4 which will use the (free of cost) Anthropic API to # estimate the token count using the Claude Sonnet 4 tokenizer. - -# MANAGED BY SERENA, KEEP AT THE BOTTOM OF THE YAML AND DON'T EDIT WITHOUT NEED -# The list of registered projects. -# To add a project, within a chat, simply ask Serena to "activate the project /path/to/project" or, -# if the project was previously added, "activate the project ". -# By default, the project's name will be the name of the directory containing the project, but you may change it -# by editing the (auto-generated) project configuration file `/path/project/project/.serena/project.yml` file. -# If you want to maintain full control of the project configuration, create the project.yml file manually and then -# instruct Serena to activate the project by its path for first-time activation. -# NOTE: Make sure there are no name collisions in the names of registered projects. +# The list of registered project paths (updated automatically). projects: []