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
This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2026-01-16 16:03:05 +01:00
1 parent 6c4d6e5b67
commit 7c1cc2ad0e
4 files changed
+80 -18

No files matched your search

+3
View File
@@ -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
+51 -13
View File
@@ -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)
@@ -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
+19 -5
View File
@@ -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))