From 7c1cc2ad0e2a8cce2b7e4c27f989564428d7a643 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 16 Jan 2026 15:58:22 +0100 Subject: [PATCH] Improve/extend SerenaConfig * Make the save function update all configuration settings * Consider all non-existing keys to be a migration, triggering a resave * Improve handling of settings that do not use the same name in the configuration file * Properly group SerenaConfig class members: - public setting (config file-mapped) - private setting (renamed to use _ prefix) - static member - methods * Add configuration setting 'jetbrains_plugin_server_adress', which is centrally propagated (avoiding injection) * Add 'fixed_tools' to template file (was previously a "hidden" setting) Relates to #941 --- src/serena/agent.py | 3 + src/serena/config/serena_config.py | 64 +++++++++++++++---- .../resources/serena_config.template.yml | 7 ++ src/serena/tools/jetbrains_plugin_client.py | 24 +++++-- 4 files changed, 80 insertions(+), 18 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index dfd50bc6..b3f205ec 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -185,6 +185,9 @@ class SerenaAgent: # obtain serena configuration using the decoupled factory function self.serena_config = serena_config or SerenaConfig.from_config_file() + # propagate configuration to other components + self.serena_config.propagate_settings() + # project-specific instances, which will be initialized upon project activation self._active_project: Project | None = None diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 62fc1b29..41ebf2ab 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -410,13 +410,8 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): web_dashboard: bool = True web_dashboard_open_on_launch: bool = True web_dashboard_listen_address: str = "127.0.0.1" + jetbrains_plugin_server_address: str = "127.0.0.1" tool_timeout: float = DEFAULT_TOOL_TIMEOUT - loaded_commented_yaml: CommentedMap | None = None - config_file_path: str | None = None - """ - the path to the configuration file to which updates of the configuration shall be saved; - if None, the configuration is not saved to disk - """ language_backend: LanguageBackend = LanguageBackend.LSP """ @@ -439,7 +434,30 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): ls_specific_settings: dict = field(default_factory=dict) """Advanced configuration option allowing to configure language server implementation specific options, see SolidLSPSettings for more info.""" + # *** fields that are NOT mapped to/from the configuration file *** + + _loaded_commented_yaml: CommentedMap | None = None + _config_file_path: str | None = None + """ + the path to the configuration file to which updates of the configuration shall be saved; + if None, the configuration is not saved to disk + """ + + # *** static members *** + CONFIG_FILE = "serena_config.yml" + CONFIG_FILE_RENAMED_KEYS = { + "gui_log_window_enabled": "gui_log_window", + } + """ + maps attributes to the keys used in the configuration file, in case they differ + """ + + # *** methods *** + + @property + def config_file_path(self) -> str | None: + return self._config_file_path def _tostring_includes(self) -> list[str]: return ["config_file_path"] @@ -494,7 +512,7 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): raise ValueError(f"Error loading Serena configuration from {config_file_path}: {e}") from e # create the configuration instance - instance = cls(loaded_commented_yaml=loaded_commented_yaml, config_file_path=config_file_path) + instance = cls(_loaded_commented_yaml=loaded_commented_yaml, _config_file_path=config_file_path) # read projects if "projects" not in loaded_commented_yaml: @@ -520,9 +538,11 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): ) instance.projects.append(project) - def get_value_or_default(key: str, field_name: str | None = None) -> Any: - if field_name is None: - field_name = key + def get_value_or_default(field_name: str) -> Any: + key = cls.CONFIG_FILE_RENAMED_KEYS.get(field_name, field_name) + nonlocal num_migrations + if key not in loaded_commented_yaml: + num_migrations += 1 return loaded_commented_yaml.get(key, get_dataclass_default(SerenaConfig, field_name)) # determine language backend @@ -540,11 +560,12 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): 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.gui_log_window_enabled = get_value_or_default("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)) instance.web_dashboard = get_value_or_default("web_dashboard") instance.web_dashboard_open_on_launch = get_value_or_default("web_dashboard_open_on_launch") + instance.jetbrains_plugin_server_address = get_value_or_default("jetbrains_plugin_server_address") instance.tool_timeout = get_value_or_default("tool_timeout") instance.trace_lsp_communication = get_value_or_default("trace_lsp_communication") instance.excluded_tools = get_value_or_default("excluded_tools") @@ -659,9 +680,18 @@ 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" + assert self._loaded_commented_yaml is not None, "Cannot save configuration without loaded YAML" - loaded_original_yaml = deepcopy(self.loaded_commented_yaml) + loaded_original_yaml = deepcopy(self._loaded_commented_yaml) + + # update fields with current values + # iterate over all dataclass fields + for field_info in dataclasses.fields(self): + field_name = field_info.name + if field_name in ["projects", "language_backend"] or field_name.startswith("_"): + continue + yaml_key = SerenaConfig.CONFIG_FILE_RENAMED_KEYS.get(field_name, field_name) + loaded_original_yaml[yaml_key] = getattr(self, field_name) # convert project objects into list of paths loaded_original_yaml["projects"] = sorted({str(project.project_root) for project in self.projects}) @@ -670,3 +700,11 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): loaded_original_yaml["language_backend"] = self.language_backend.value save_yaml(self.config_file_path, loaded_original_yaml, preserve_comments=True) + + def propagate_settings(self): + """ + Propagate settings from this configuration to individual components that are statically configured + """ + from serena.tools import JetBrainsPluginClient + + JetBrainsPluginClient.set_server_address(self.jetbrains_plugin_server_address) diff --git a/src/serena/resources/serena_config.template.yml b/src/serena/resources/serena_config.template.yml index ddfd930c..ddb41e35 100644 --- a/src/serena/resources/serena_config.template.yml +++ b/src/serena/resources/serena_config.template.yml @@ -47,6 +47,9 @@ web_dashboard_open_on_launch: True # may be higher if you have multiple instances running; try ports 24283, 24284, etc.) # See also: https://oraios.github.io/serena/02-usage/060_dashboard.html +jetbrains_plugin_server_address: 127.0.0.1 +# address where JetBrains plugin servers are running (only relevant when using the JetBrains language backend) + log_level: 20 # the minimum log level for the GUI log window and the dashboard (10 = debug, 20 = info, 30 = warning, 40 = error) @@ -70,6 +73,10 @@ excluded_tools: [] included_optional_tools: [] # list of optional tools (which are disabled by default) to be included +fixed_tools: [] +# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. +# This cannot be combined with non-empty excluded_tools or included_optional_tools. + 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 diff --git a/src/serena/tools/jetbrains_plugin_client.py b/src/serena/tools/jetbrains_plugin_client.py index a2eef52d..028fa66d 100644 --- a/src/serena/tools/jetbrains_plugin_client.py +++ b/src/serena/tools/jetbrains_plugin_client.py @@ -71,11 +71,17 @@ class JetBrainsPluginClient(ToStringMixin): """ the timeout used for request handling within the plugin (a constant in the plugin) """ - last_port: int | None = None + _last_port: int | None = None + """ + the last port that was successfully used to connect to a plugin instance in the current session + """ + _server_address: str = "127.0.0.1" + """ + the server address where to connect to the plugin service + """ def __init__(self, port: int, timeout: int = PLUGIN_REQUEST_TIMEOUT): self._port = port - self._base_url = f"http://127.0.0.1:{port}" self._timeout = timeout self._session = requests.Session() self._session.headers.update({"Content-Type": "application/json", "Accept": "application/json"}) @@ -91,6 +97,14 @@ class JetBrainsPluginClient(ToStringMixin): self._project_root = None self._plugin_version = None + @property + def _base_url(self) -> str: + return f"http://{self._server_address}:{self._port}" + + @classmethod + def set_server_address(cls, address: str) -> None: + cls._server_address = address + def _tostring_includes(self) -> list[str]: return ["_port", "_project_root", "_plugin_version"] @@ -98,8 +112,8 @@ class JetBrainsPluginClient(ToStringMixin): def from_project(cls, project: Project) -> Self: resolved_path = Path(project.project_root).resolve() - if cls.last_port is not None: - client = JetBrainsPluginClient(cls.last_port) + if cls._last_port is not None: + client = JetBrainsPluginClient(cls._last_port) if client.matches(resolved_path): return client @@ -107,7 +121,7 @@ class JetBrainsPluginClient(ToStringMixin): client = JetBrainsPluginClient(port) if client.matches(resolved_path): log.info("Found matching %s", client) - cls.last_port = port + cls._last_port = port return client raise ServerNotFoundError("Found no Serena service in a JetBrains IDE instance for the project at " + str(resolved_path))