Merge pull request #850 from oraios/backend-param

SerenaConfig: Use "language_backend" instead of "jetbrains", allowing override via CLI
This commit is contained in:
Dominik Jain authored and GitHub committed 2025-12-17 14:22:54 +01:00
commit 1e3edb6a3b
8 files changed
+97 -49

No files matched your search

-1
View File
@@ -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
+7 -4
View File
@@ -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}")
+19 -3
View File
@@ -18,7 +18,7 @@ from tqdm import tqdm
from serena.agent import SerenaAgent
from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode
from serena.config.serena_config import ProjectConfig, SerenaConfig, SerenaPaths
from serena.config.serena_config import LanguageBackend, ProjectConfig, SerenaConfig, SerenaPaths
from serena.constants import (
DEFAULT_CONTEXT,
DEFAULT_MODES,
@@ -154,6 +154,12 @@ class TopLevelCommands(AutoRegisteringGroup):
show_default=True,
help="Built-in mode names or paths to custom mode YAMLs.",
)
@click.option(
"--language-backend",
type=click.Choice([lb.value for lb in LanguageBackend]),
default=None,
help="Override the configured language backend.",
)
@click.option(
"--transport",
type=click.Choice(["stdio", "sse", "streamable-http"]),
@@ -161,8 +167,16 @@ class TopLevelCommands(AutoRegisteringGroup):
show_default=True,
help="Transport protocol.",
)
@click.option("--host", type=str, default="0.0.0.0", show_default=True)
@click.option("--port", type=int, default=8000, show_default=True)
@click.option(
"--host",
type=str,
default="0.0.0.0",
show_default=True,
help="Listen address for the MCP server (when using corresponding transport).",
)
@click.option(
"--port", type=int, default=8000, show_default=True, help="Listen port for the MCP server (when using corresponding transport)."
)
@click.option("--enable-web-dashboard", type=bool, is_flag=False, default=None, help="Override dashboard setting in config.")
@click.option("--enable-gui-log-window", type=bool, is_flag=False, default=None, help="Override GUI log window setting in config.")
@click.option(
@@ -185,6 +199,7 @@ class TopLevelCommands(AutoRegisteringGroup):
project_from_cwd: bool | None,
context: str,
modes: tuple[str, ...],
language_backend: str | None,
transport: Literal["stdio", "sse", "streamable-http"],
host: str,
port: int,
@@ -227,6 +242,7 @@ class TopLevelCommands(AutoRegisteringGroup):
host=host,
port=port,
modes=modes,
language_backend=LanguageBackend.from_str(language_backend) if language_backend else None,
enable_web_dashboard=enable_web_dashboard,
enable_gui_log_window=enable_gui_log_window,
log_level=log_level,
+3 -11
View File
@@ -70,7 +70,7 @@ class SerenaAgentMode(ToolInclusionDefinition, ToStringMixin):
own_yaml_path = os.path.join(SERENAS_OWN_MODE_YAMLS_DIR, fname)
if not os.path.exists(own_yaml_path):
raise FileNotFoundError(
f"Mode {name} not found in {SerenaPaths().user_modes_dir} or in {SERENAS_OWN_MODE_YAMLS_DIR}. "
f"Mode {name} not found in {SerenaPaths().user_modes_dir} or in {SERENAS_OWN_MODE_YAMLS_DIR}."
f"Available modes:\n{cls.list_registered_mode_names()}"
)
return own_yaml_path
@@ -78,16 +78,8 @@ class SerenaAgentMode(ToolInclusionDefinition, ToStringMixin):
@classmethod
def from_name(cls, name: str) -> Self:
"""Load a registered Serena mode."""
try:
mode_path = cls.get_path(name)
return cls.from_yaml(mode_path)
except:
try:
# Allow loading an internal mode if it is explicitly requested
return cls.from_name_internal(name)
except:
pass
raise
mode_path = cls.get_path(name)
return cls.from_yaml(mode_path)
@classmethod
def from_name_internal(cls, name: str) -> Self:
+51 -12
View File
@@ -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)
+2 -1
View File
@@ -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,
)
+5
View File
@@ -23,6 +23,7 @@ from serena.agent import (
SerenaConfig,
)
from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode
from serena.config.serena_config import LanguageBackend
from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES, SERENA_LOG_FORMAT
from serena.tools import Tool
from serena.util.exception import show_fatal_exception_safe
@@ -251,6 +252,7 @@ class SerenaMCPFactory:
host: str = "0.0.0.0",
port: int = 8000,
modes: Sequence[str] = DEFAULT_MODES,
language_backend: LanguageBackend | None = None,
enable_web_dashboard: bool | None = None,
enable_gui_log_window: bool | None = None,
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None,
@@ -263,6 +265,7 @@ class SerenaMCPFactory:
:param host: The host to bind to
:param port: The port to bind to
:param modes: List of mode names or paths to mode files
:param language_backend: the language backend to use, overriding the configuration setting.
:param enable_web_dashboard: Whether to enable the web dashboard. If not specified, will take the value from the serena configuration.
:param enable_gui_log_window: Whether to enable the GUI log window. It currently does not work on macOS, and setting this to True will be ignored then.
If not specified, will take the value from the serena configuration.
@@ -286,6 +289,8 @@ class SerenaMCPFactory:
config.trace_lsp_communication = trace_lsp_communication
if tool_timeout is not None:
config.tool_timeout = tool_timeout
if language_backend is not None:
config.language_backend = language_backend
modes_instances = [SerenaAgentMode.load(mode) for mode in modes]
self._instantiate_agent(config, modes_instances)
+10 -17
View File
@@ -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 <project name>".
# 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: []