From 8108f8cd28f17a410779cfab2b09fffaec7b1938 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 2 Jun 2025 14:37:21 +0200 Subject: [PATCH 01/13] Allow overriding config entries by passing args --- src/serena/agent.py | 17 ++++++++++++++++- src/serena/mcp.py | 45 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 4c50174b..d669399c 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -21,7 +21,7 @@ from functools import cached_property from logging import Logger from pathlib import Path from types import TracebackType -from typing import TYPE_CHECKING, Any, Self, TypeVar, Union +from typing import TYPE_CHECKING, Any, Literal, Self, TypeVar, Union, cast import yaml from overrides import override @@ -475,6 +475,9 @@ class SerenaAgent: serena_config: SerenaConfigBase | None = None, context: SerenaAgentContext | None = None, modes: list[SerenaAgentMode] | None = None, + enable_web_dashboard: bool | None = None, + enable_gui_log_window: bool | None = None, + gui_log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, ): """ :param project: the project to load immediately or None to not load any project; may be a path to the project or a name of @@ -485,9 +488,21 @@ class SerenaAgent: :param modes: list of modes in which the agent is operating (they will be combined), None for default modes. The modes may adjust prompts, tool availability, and tool descriptions. :param serena_config: the Serena configuration or None to read the configuration from the default location. + :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. + :param gui_log_level: Log level for the GUI log window. If not specified, will take the value from the serena configuration. """ # obtain serena configuration self.serena_config = serena_config or SerenaConfig.from_config_file() + if enable_web_dashboard is not None: + self.serena_config.web_dashboard = enable_web_dashboard + if enable_gui_log_window is not None: + self.serena_config.gui_log_window_enabled = enable_gui_log_window + if gui_log_level is not None: + gui_log_level = cast(Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], gui_log_level.upper()) + # transform to int + self.serena_config.gui_log_window_level = logging.getLevelNamesMapping()[gui_log_level] # open GUI log window if enabled self._gui_log_handler: Union["GuiLogViewerHandler", None] = None # noqa diff --git a/src/serena/mcp.py b/src/serena/mcp.py index a8c21911..b30d1984 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -102,6 +102,9 @@ def create_mcp_server_and_agent( port: int = 8000, context: str = DEFAULT_CONTEXT, modes: Sequence[str] = DEFAULT_MODES, + enable_web_dashboard: bool | None = None, + enable_gui_log_window: bool | None = None, + gui_log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, ) -> tuple[FastMCP, SerenaAgent]: """ Create an MCP server. @@ -113,6 +116,10 @@ def create_mcp_server_and_agent( :param port: The port to bind to :param context: The context name or path to context file :param modes: List of mode names or paths to mode files + :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. + :param gui_log_level: Log level for the GUI log window. If not specified, will take the value from the serena configuration. """ mcp: FastMCP | None = None context_instance = SerenaAgentContext.load(context) @@ -125,6 +132,9 @@ def create_mcp_server_and_agent( # project_activation_callback=update_tools context=context_instance, modes=modes_instances, + enable_web_dashboard=enable_web_dashboard, + enable_gui_log_window=enable_gui_log_window, + gui_log_level=gui_log_level, ) except Exception as e: show_fatal_exception_safe(e) @@ -237,6 +247,27 @@ PROJECT_TYPE = ProjectType() default=8000, help="Port to bind to (for SSE transport).", ) +@click.option( + "--enable-web-dashboard", + type=bool, + is_flag=False, + default=None, + help="Whether to enable the web dashboard. If not specified, will take the value from the serena configuration.", +) +@click.option( + "--enable-gui-log-window", + type=bool, + is_flag=False, + default=None, + help="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.", +) +@click.option( + "--gui-log-level", + type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), + default=None, + help="Log level for the GUI log window. If not specified, will take the value from the serena configuration.", +) def start_mcp_server( project_file_opt: str | None, project_file_arg: str | None, @@ -245,6 +276,9 @@ def start_mcp_server( transport: Literal["stdio", "sse"] = "stdio", host: str = "0.0.0.0", port: int = 8000, + enable_web_dashboard: bool | None = None, + enable_gui_log_window: bool | None = None, + gui_log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, ) -> None: """Starts the Serena MCP server. By default, will not activate any project at startup. If you want to start with an already active project, use --project to pass the project name or path. @@ -256,7 +290,16 @@ def start_mcp_server( # This is for backward compatibility with the old CLI, should be removed in the future! project_file = project_file_arg if project_file_arg is not None else project_file_opt - mcp_server, agent = create_mcp_server_and_agent(project=project_file, host=host, port=port, context=context, modes=modes) + mcp_server, agent = create_mcp_server_and_agent( + project=project_file, + host=host, + port=port, + context=context, + modes=modes, + enable_web_dashboard=enable_web_dashboard, + enable_gui_log_window=enable_gui_log_window, + gui_log_level=gui_log_level, + ) # log after server creation such that the log appears in the GUI if project_file_arg is not None: From e944dca0cc1405c7a7e522c1feb79bf984536e1d Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 2 Jun 2025 14:41:44 +0200 Subject: [PATCH 02/13] Autogenerate serena_config.yml if it's missing --- src/serena/agent.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index d669399c..15124b55 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -295,19 +295,32 @@ class SerenaConfig(SerenaConfigBase): loaded_commented_yaml: CommentedMap CONFIG_FILE = "serena_config.yml" + _TEMPLATE_FILE = "serena_config.template.yml" + + @classmethod + def autogenerate(cls) -> None: + log.info("Autogenerating Serena configuration file") + if os.path.exists(cls.get_config_file_path()): + raise FileExistsError( + f"Serena configuration file already exists at {cls.get_config_file_path()}. Please remove it if you want to autogenerate a new one." + ) + loaded_commented_yaml = load_yaml(cls._TEMPLATE_FILE, preserve_comments=True) + save_yaml(cls.get_config_file_path(), loaded_commented_yaml, preserve_comments=True) @classmethod def get_config_file_path(cls) -> str: return os.path.join(serena_root_path(), cls.CONFIG_FILE) @classmethod - def from_config_file(cls) -> "SerenaConfig": + def from_config_file(cls, generate_if_missing: bool = True) -> "SerenaConfig": """ Static constructor to create SerenaConfig from the configuration file """ config_file = cls.get_config_file_path() if not os.path.exists(config_file): - raise FileNotFoundError(f"Serena configuration file not found: {config_file}") + if not generate_if_missing: + raise FileNotFoundError(f"Serena configuration file not found: {config_file}") + cls.autogenerate() log.info(f"Loading Serena configuration from {config_file}") try: From 94befeb995d68ead6426ef3c78a38c856bfe26d0 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 2 Jun 2025 19:42:46 +0200 Subject: [PATCH 03/13] Prompting, minor --- src/serena/agent.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 0810448e..8d64df11 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -734,13 +734,13 @@ class SerenaAgent: # Active modes active_mode_names = [mode.name for mode in self.get_active_modes()] - result_str += "Active modes: {}\n".format(", ".join(active_mode_names)) + result_str += "Active modes: {}\n".format(", ".join(active_mode_names)) + "\n" # Available but not active modes all_available_modes = SerenaAgentMode.list_registered_mode_names() inactive_modes = [mode for mode in all_available_modes if mode not in active_mode_names] if inactive_modes: - result_str += "Available but not active modes: {}\n".format(", ".join(inactive_modes)) + result_str += "Available but not active modes: {}\n".format(", ".join(inactive_modes)) + "\n" # Active tools result_str += "Active tools (after all exclusions from the project, context, and modes):\n" @@ -1942,6 +1942,8 @@ class InitialInstructionsTool(Tool): """ Get the initial instructions for the current coding project. You should always call this tool before starting to work (including using any other tool) on any programming task! + The only exception is when a user asks you to activate a project, in which case you should call the `activate_project` first + instead and then call this tool. """ return self.agent.create_system_prompt() From a7144e232fdc52c9ae8de9b5a05f7cc5d25b6a49 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 2 Jun 2025 19:52:59 +0200 Subject: [PATCH 04/13] pyproject [ci skip] --- pyproject.toml | 3 +-- uv.lock | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f66c7e58..ecac1ef3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,6 @@ dependencies = [ "dotenv>=0.9.9", "pathspec>=0.12.1", "psutil>=7.0.0", - "agno>=1.2.15", "docstring_parser>=0.16", "joblib>=1.5.1", ] @@ -73,7 +72,7 @@ google = [ Homepage = "https://github.com/oraios/serena" [tool.hatch.build.targets.wheel] -packages = ["src/serena", "src/multilspy"] +packages = ["src/serena", "src/multilspy", "src/interpropmt"] [tool.black] line-length = 140 diff --git a/uv.lock b/uv.lock index c098ce28..8be9884e 100644 --- a/uv.lock +++ b/uv.lock @@ -970,7 +970,6 @@ name = "serena" version = "0.1.0" source = { editable = "." } dependencies = [ - { name = "agno" }, { name = "docstring-parser" }, { name = "dotenv" }, { name = "fastapi" }, @@ -1016,7 +1015,6 @@ google = [ [package.metadata] requires-dist = [ - { name = "agno", specifier = ">=1.2.15" }, { name = "agno", marker = "extra == 'agno'", specifier = ">=1.2.6" }, { name = "anthropic", marker = "extra == 'anthropic'", specifier = ">=0.49.0" }, { name = "black", extras = ["jupyter"], marker = "extra == 'dev'", specifier = ">=23.7.0" }, From dd0c961916a5809a114d09bc19b49b430e589a4a Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 2 Jun 2025 19:55:13 +0200 Subject: [PATCH 05/13] typo [ci skip] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ecac1ef3..d865d89e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,7 @@ google = [ Homepage = "https://github.com/oraios/serena" [tool.hatch.build.targets.wheel] -packages = ["src/serena", "src/multilspy", "src/interpropmt"] +packages = ["src/serena", "src/multilspy", "src/interprompt"] [tool.black] line-length = 140 From d1f5856bac6e3f99ad484a6b672783e725d4fef0 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 2 Jun 2025 20:01:58 +0200 Subject: [PATCH 06/13] Include config dir and template yamls in package [ci skip] --- pyproject.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index d865d89e..0958b95b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,6 +74,14 @@ Homepage = "https://github.com/oraios/serena" [tool.hatch.build.targets.wheel] packages = ["src/serena", "src/multilspy", "src/interprompt"] +[tool.hatch.build] +include = [ + "config/**", + "serena_config.template.yml", + "project.template.yml" +] + + [tool.black] line-length = 140 target-version = [ From 3a9cde185ab2ab1c95d2256747764d633d1b25e1 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 2 Jun 2025 20:06:10 +0200 Subject: [PATCH 07/13] Better error message [ci skip] --- src/serena/config.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/serena/config.py b/src/serena/config.py index 57b7a686..0b9a85de 100644 --- a/src/serena/config.py +++ b/src/serena/config.py @@ -121,7 +121,13 @@ class SerenaAgentContext: try: return cls.from_name(str(name_or_path)) except FileNotFoundError: - return cls.from_yaml(name_or_path) + try: + return cls.from_yaml(name_or_path) + except FileNotFoundError as e: + raise FileNotFoundError( + f"Context {name_or_path} not found in {CONTEXT_YAMLS_DIR}. You can load a custom context by using from_yaml() instead.\n" + f"Available contexts:\n{cls.list_registered_context_names()}" + ) from e @classmethod def list_registered_context_names(cls) -> list[str]: From a1e3bb19b6fbb774176e0a13fc1d62b51d40f24c Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 2 Jun 2025 20:24:29 +0200 Subject: [PATCH 08/13] Moved yamls to src/serena so that they're packaged [ci skip] --- pyproject.toml | 8 -------- src/serena/agent.py | 5 ++--- src/serena/constants.py | 10 ++++++---- .../serena/resources/config}/contexts/agent.yml | 0 .../serena/resources/config}/contexts/desktop-app.yml | 0 .../resources/config}/contexts/ide-assistant.yml | 0 .../serena/resources/config}/modes/editing.yml | 0 .../serena/resources/config}/modes/interactive.yml | 0 .../serena/resources/config}/modes/no-onboarding.yml | 0 .../serena/resources/config}/modes/onboarding.yml | 0 .../serena/resources/config}/modes/one-shot.yml | 0 .../serena/resources/config}/modes/planning.yml | 0 .../config}/prompt_templates/simple_tool_outputs.yml | 0 .../config}/prompt_templates/system_prompt.yml | 0 .../serena/resources/project.template.yml | 0 .../serena/resources/serena_config.template.yml | 0 16 files changed, 8 insertions(+), 15 deletions(-) rename {config => src/serena/resources/config}/contexts/agent.yml (100%) rename {config => src/serena/resources/config}/contexts/desktop-app.yml (100%) rename {config => src/serena/resources/config}/contexts/ide-assistant.yml (100%) rename {config => src/serena/resources/config}/modes/editing.yml (100%) rename {config => src/serena/resources/config}/modes/interactive.yml (100%) rename {config => src/serena/resources/config}/modes/no-onboarding.yml (100%) rename {config => src/serena/resources/config}/modes/onboarding.yml (100%) rename {config => src/serena/resources/config}/modes/one-shot.yml (100%) rename {config => src/serena/resources/config}/modes/planning.yml (100%) rename {config => src/serena/resources/config}/prompt_templates/simple_tool_outputs.yml (100%) rename {config => src/serena/resources/config}/prompt_templates/system_prompt.yml (100%) rename project.template.yml => src/serena/resources/project.template.yml (100%) rename serena_config.template.yml => src/serena/resources/serena_config.template.yml (100%) diff --git a/pyproject.toml b/pyproject.toml index 0958b95b..d865d89e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,14 +74,6 @@ Homepage = "https://github.com/oraios/serena" [tool.hatch.build.targets.wheel] packages = ["src/serena", "src/multilspy", "src/interprompt"] -[tool.hatch.build] -include = [ - "config/**", - "serena_config.template.yml", - "project.template.yml" -] - - [tool.black] line-length = 140 target-version = [ diff --git a/src/serena/agent.py b/src/serena/agent.py index 8d64df11..1b94c8a0 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -36,7 +36,7 @@ from multilspy.multilspy_logger import MultilspyLogger from multilspy.multilspy_types import SymbolKind from serena import serena_root_path, serena_version from serena.config import SerenaAgentContext, SerenaAgentMode -from serena.constants import PROJECT_TEMPLATE_FILE, SERENA_MANAGED_DIR_NAME +from serena.constants import PROJECT_TEMPLATE_FILE, SELENA_CONFIG_TEMPLATE_FILE, SERENA_MANAGED_DIR_NAME from serena.dashboard import MemoryLogHandler, SerenaDashboardAPI from serena.prompt_factory import PromptFactory, SerenaPromptFactory from serena.symbol import SymbolManager @@ -295,7 +295,6 @@ class SerenaConfig(SerenaConfigBase): loaded_commented_yaml: CommentedMap CONFIG_FILE = "serena_config.yml" - _TEMPLATE_FILE = "serena_config.template.yml" @classmethod def autogenerate(cls) -> None: @@ -304,7 +303,7 @@ class SerenaConfig(SerenaConfigBase): raise FileExistsError( f"Serena configuration file already exists at {cls.get_config_file_path()}. Please remove it if you want to autogenerate a new one." ) - loaded_commented_yaml = load_yaml(cls._TEMPLATE_FILE, preserve_comments=True) + loaded_commented_yaml = load_yaml(SELENA_CONFIG_TEMPLATE_FILE, preserve_comments=True) save_yaml(cls.get_config_file_path(), loaded_commented_yaml, preserve_comments=True) @classmethod diff --git a/src/serena/constants.py b/src/serena/constants.py index 5b138e90..b4bb586f 100644 --- a/src/serena/constants.py +++ b/src/serena/constants.py @@ -1,15 +1,17 @@ from pathlib import Path _repo_root_path = Path(__file__).parent.parent.parent.resolve() +_serena_pkg_path = Path(__file__).parent.resolve() REPO_ROOT = str(_repo_root_path) -PROMPT_TEMPLATES_DIR = str(_repo_root_path / "config" / "prompt_templates") -CONTEXT_YAMLS_DIR = str(_repo_root_path / "config" / "contexts") -MODE_YAMLS_DIR = str(_repo_root_path / "config" / "modes") +PROMPT_TEMPLATES_DIR = str(_serena_pkg_path / "resources" / "config" / "prompt_templates") +CONTEXT_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "contexts") +MODE_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "modes") SERENA_MANAGED_DIR_NAME = ".serena" DEFAULT_CONTEXT = "desktop-app" DEFAULT_MODES = ("interactive", "editing") -PROJECT_TEMPLATE_FILE = str(_repo_root_path / "project.template.yml") +PROJECT_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "project.template.yml") +SELENA_CONFIG_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "serena_config.template.yml") diff --git a/config/contexts/agent.yml b/src/serena/resources/config/contexts/agent.yml similarity index 100% rename from config/contexts/agent.yml rename to src/serena/resources/config/contexts/agent.yml diff --git a/config/contexts/desktop-app.yml b/src/serena/resources/config/contexts/desktop-app.yml similarity index 100% rename from config/contexts/desktop-app.yml rename to src/serena/resources/config/contexts/desktop-app.yml diff --git a/config/contexts/ide-assistant.yml b/src/serena/resources/config/contexts/ide-assistant.yml similarity index 100% rename from config/contexts/ide-assistant.yml rename to src/serena/resources/config/contexts/ide-assistant.yml diff --git a/config/modes/editing.yml b/src/serena/resources/config/modes/editing.yml similarity index 100% rename from config/modes/editing.yml rename to src/serena/resources/config/modes/editing.yml diff --git a/config/modes/interactive.yml b/src/serena/resources/config/modes/interactive.yml similarity index 100% rename from config/modes/interactive.yml rename to src/serena/resources/config/modes/interactive.yml diff --git a/config/modes/no-onboarding.yml b/src/serena/resources/config/modes/no-onboarding.yml similarity index 100% rename from config/modes/no-onboarding.yml rename to src/serena/resources/config/modes/no-onboarding.yml diff --git a/config/modes/onboarding.yml b/src/serena/resources/config/modes/onboarding.yml similarity index 100% rename from config/modes/onboarding.yml rename to src/serena/resources/config/modes/onboarding.yml diff --git a/config/modes/one-shot.yml b/src/serena/resources/config/modes/one-shot.yml similarity index 100% rename from config/modes/one-shot.yml rename to src/serena/resources/config/modes/one-shot.yml diff --git a/config/modes/planning.yml b/src/serena/resources/config/modes/planning.yml similarity index 100% rename from config/modes/planning.yml rename to src/serena/resources/config/modes/planning.yml diff --git a/config/prompt_templates/simple_tool_outputs.yml b/src/serena/resources/config/prompt_templates/simple_tool_outputs.yml similarity index 100% rename from config/prompt_templates/simple_tool_outputs.yml rename to src/serena/resources/config/prompt_templates/simple_tool_outputs.yml diff --git a/config/prompt_templates/system_prompt.yml b/src/serena/resources/config/prompt_templates/system_prompt.yml similarity index 100% rename from config/prompt_templates/system_prompt.yml rename to src/serena/resources/config/prompt_templates/system_prompt.yml diff --git a/project.template.yml b/src/serena/resources/project.template.yml similarity index 100% rename from project.template.yml rename to src/serena/resources/project.template.yml diff --git a/serena_config.template.yml b/src/serena/resources/serena_config.template.yml similarity index 100% rename from serena_config.template.yml rename to src/serena/resources/serena_config.template.yml From bf2573ee536fed80d8bb0bd3d8ea1fc6d2b10cf5 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 2 Jun 2025 21:25:52 +0200 Subject: [PATCH 09/13] Moved dashboard to src/serena. Stopped using serena_root_path (in favor of constants.py) --- src/serena/__init__.py | 7 ------- src/serena/agent.py | 6 +++--- src/serena/agno.py | 6 +++--- src/serena/constants.py | 2 ++ src/serena/dashboard.py | 5 ++--- .../serena/resources/dashboard}/dashboard.js | 0 .../serena/resources/dashboard}/index.html | 0 .../serena/resources/dashboard}/jquery.min.js | 0 .../serena/resources/dashboard}/serena-icon-16.png | Bin .../serena/resources/dashboard}/serena-icon-32.png | Bin .../serena/resources/dashboard}/serena-icon-48.png | Bin .../serena/resources/dashboard}/serena-logs.png | Bin 12 files changed, 10 insertions(+), 16 deletions(-) rename {dashboard => src/serena/resources/dashboard}/dashboard.js (100%) rename {dashboard => src/serena/resources/dashboard}/index.html (100%) rename {dashboard => src/serena/resources/dashboard}/jquery.min.js (100%) rename {dashboard => src/serena/resources/dashboard}/serena-icon-16.png (100%) rename {dashboard => src/serena/resources/dashboard}/serena-icon-32.png (100%) rename {dashboard => src/serena/resources/dashboard}/serena-icon-48.png (100%) rename {dashboard => src/serena/resources/dashboard}/serena-logs.png (100%) diff --git a/src/serena/__init__.py b/src/serena/__init__.py index 3e1a38d7..85f7b173 100644 --- a/src/serena/__init__.py +++ b/src/serena/__init__.py @@ -1,12 +1,5 @@ __version__ = "2025-05-21" - -def serena_root_path() -> str: - from pathlib import Path - - return str(Path(__file__).parent.parent.parent.absolute()) - - def serena_version() -> str: """ :return: the version of the package, including git status if available. diff --git a/src/serena/agent.py b/src/serena/agent.py index 1b94c8a0..a937cac5 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -34,9 +34,9 @@ from multilspy import SyncLanguageServer from multilspy.multilspy_config import Language, MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger from multilspy.multilspy_types import SymbolKind -from serena import serena_root_path, serena_version +from serena import serena_version from serena.config import SerenaAgentContext, SerenaAgentMode -from serena.constants import PROJECT_TEMPLATE_FILE, SELENA_CONFIG_TEMPLATE_FILE, SERENA_MANAGED_DIR_NAME +from serena.constants import PROJECT_TEMPLATE_FILE, SELENA_CONFIG_TEMPLATE_FILE, SERENA_MANAGED_DIR_NAME, REPO_ROOT from serena.dashboard import MemoryLogHandler, SerenaDashboardAPI from serena.prompt_factory import PromptFactory, SerenaPromptFactory from serena.symbol import SymbolManager @@ -308,7 +308,7 @@ class SerenaConfig(SerenaConfigBase): @classmethod def get_config_file_path(cls) -> str: - return os.path.join(serena_root_path(), cls.CONFIG_FILE) + return os.path.join(REPO_ROOT, cls.CONFIG_FILE) @classmethod def from_config_file(cls, generate_if_missing: bool = True) -> "SerenaConfig": diff --git a/src/serena/agno.py b/src/serena/agno.py index f6684eca..1248d1a2 100644 --- a/src/serena/agno.py +++ b/src/serena/agno.py @@ -14,7 +14,7 @@ from agno.tools.toolkit import Toolkit from dotenv import load_dotenv from sensai.util.logging import LogTime -from serena import serena_root_path +from serena.constants import REPO_ROOT from serena.agent import SerenaAgent, Tool, show_fatal_exception_safe from serena.config import SerenaAgentContext @@ -65,7 +65,7 @@ class SerenaAgnoAgentProvider: return cls._agent # change to Serena root - os.chdir(serena_root_path()) + os.chdir(REPO_ROOT) load_dotenv() @@ -94,7 +94,7 @@ class SerenaAgnoAgentProvider: # If project file path is relative, make it absolute by joining with project root if not project_file.is_absolute(): # Get the project root directory (parent of scripts directory) - project_root = Path(serena_root_path()) + project_root = Path(REPO_ROOT) project_file = project_root / args_project_file # Ensure the path is normalized and absolute diff --git a/src/serena/constants.py b/src/serena/constants.py index b4bb586f..9e0980fe 100644 --- a/src/serena/constants.py +++ b/src/serena/constants.py @@ -7,6 +7,8 @@ REPO_ROOT = str(_repo_root_path) PROMPT_TEMPLATES_DIR = str(_serena_pkg_path / "resources" / "config" / "prompt_templates") CONTEXT_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "contexts") MODE_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "modes") +SERENA_DASHBOARD_DIR = str(_serena_pkg_path / "resources" / "dashboard") +SERENA_ICON_DIR = str(_serena_pkg_path / "resources" / "icons") SERENA_MANAGED_DIR_NAME = ".serena" diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index d52040b5..4b0419ac 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -10,7 +10,7 @@ from fastapi.staticfiles import StaticFiles from pydantic import BaseModel from sensai.util import logging -from serena import serena_root_path +from serena.constants import SERENA_DASHBOARD_DIR log = logging.getLogger(__name__) @@ -75,8 +75,7 @@ class SerenaDashboardAPI: self._setup_routes() def _setup_routes(self) -> None: - static_dir = os.path.join(serena_root_path(), "dashboard") - self._app.mount("/dashboard", StaticFiles(directory=static_dir), name="dashboard") + self._app.mount("/dashboard", StaticFiles(directory=SERENA_DASHBOARD_DIR), name="dashboard") self._app.add_api_route("/get_log_messages", self._get_log_messages, methods=["POST"], response_model=ResponseLog) self._app.add_api_route("/get_tool_names", self._get_tool_names, methods=["GET"], response_model=ResponseToolNames) diff --git a/dashboard/dashboard.js b/src/serena/resources/dashboard/dashboard.js similarity index 100% rename from dashboard/dashboard.js rename to src/serena/resources/dashboard/dashboard.js diff --git a/dashboard/index.html b/src/serena/resources/dashboard/index.html similarity index 100% rename from dashboard/index.html rename to src/serena/resources/dashboard/index.html diff --git a/dashboard/jquery.min.js b/src/serena/resources/dashboard/jquery.min.js similarity index 100% rename from dashboard/jquery.min.js rename to src/serena/resources/dashboard/jquery.min.js diff --git a/dashboard/serena-icon-16.png b/src/serena/resources/dashboard/serena-icon-16.png similarity index 100% rename from dashboard/serena-icon-16.png rename to src/serena/resources/dashboard/serena-icon-16.png diff --git a/dashboard/serena-icon-32.png b/src/serena/resources/dashboard/serena-icon-32.png similarity index 100% rename from dashboard/serena-icon-32.png rename to src/serena/resources/dashboard/serena-icon-32.png diff --git a/dashboard/serena-icon-48.png b/src/serena/resources/dashboard/serena-icon-48.png similarity index 100% rename from dashboard/serena-icon-48.png rename to src/serena/resources/dashboard/serena-icon-48.png diff --git a/dashboard/serena-logs.png b/src/serena/resources/dashboard/serena-logs.png similarity index 100% rename from dashboard/serena-logs.png rename to src/serena/resources/dashboard/serena-logs.png From ba6652cd745874cf18a8f38fb731d67aace41409 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 2 Jun 2025 21:37:17 +0200 Subject: [PATCH 10/13] Extended planning mode, minor extension in editing mode prompt --- src/serena/resources/config/modes/editing.yml | 5 +++-- src/serena/resources/config/modes/planning.yml | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/serena/resources/config/modes/editing.yml b/src/serena/resources/config/modes/editing.yml index 431fe55b..901f32e8 100644 --- a/src/serena/resources/config/modes/editing.yml +++ b/src/serena/resources/config/modes/editing.yml @@ -16,7 +16,7 @@ prompt: | You can get information about available symbols by using the `get_symbols_overview` tool for finding top-level symbols in a file or directory, or by using `find_symbol` if you already know the symbol's name path. You generally try to read as little code as possible while still solving your task, meaning you only read the bodies when you need to, and after you have found the symbol you want to edit. - For example, if you are working with python code and alread know that you need to read the body of the constructor of the class Foo, you can directly + For example, if you are working with python code and already know that you need to read the body of the constructor of the class Foo, you can directly use `find_symbol` with the name path `Foo/__init__` and `include_body=True`. If you don't know yet which methods in `Foo` you need to read or edit, you can use `find_symbol` with the name path `Foo`, `include_body=False` and `depth=1` to get all (top-level) methods of `Foo` before proceeding to read the desired methods with `include_body=True`. @@ -37,12 +37,13 @@ prompt: | then use your knowledge of the codebase to write the regex, if you haven't collected enough information of this content yet. You are extremely good at regex, so you never need to check whether the replacement produced the correct result. In particular, you know what to escape and what not to escape, and you know how to use wildcards. + Also, the regex tool never adds any indentation (contrary to the symbolic editing tools), so you have to take care to add the correct indentation + when using it to insert code. Moreover, the replacement tool will fail if it can't perform the desired replacement, and this is all the feedback you need. Your overall goal for replacement operations is to use relatively short regexes, since I want you to minimize the number of output tokens. For replacements of larger chunks of code, this means you intelligently make use of wildcards for the middle part and of characteristic snippets for the before/after parts that uniquely identify the chunk. - For small replacements, up to a single line, you follow the following rules: 1. If the snippet to be replaced is likely to be unique within the file, you perform the replacement by directly using the escaped version of the diff --git a/src/serena/resources/config/modes/planning.yml b/src/serena/resources/config/modes/planning.yml index 53e2989b..c76501e8 100644 --- a/src/serena/resources/config/modes/planning.yml +++ b/src/serena/resources/config/modes/planning.yml @@ -8,7 +8,19 @@ prompt: | it may be enough to read a single file or a small set of symbols. You can achieve the intelligent reading of code by using the symbolic tools for getting an overview of symbols and the relations between them, and then only reading the bodies of symbols that are necessary to answer the question - or complete the task. You can also use the standard tools like list_dir and search_for_pattern if you need to. + or complete the task. You can also use the standard tools like list_dir, find_file and search_for_pattern if you need to. + + Symbols are identified by their name path and relative file path, see the description of the `find_symbols` tool for more details + on how the `name_path` matches symbols. + You can get information about available symbols by using the `get_symbols_overview` tool for finding top-level symbols in a file + or directory, or by using `find_symbol` if you already know the symbol's name path. You generally try to read as little code as possible + while still solving your task, meaning you only read the bodies when you need to, and after you have found the symbol you want to edit. + For example, if you are working with python code and already know that you need to read the body of the constructor of the class Foo, you can directly + use `find_symbol` with the name path `Foo/__init__` and `include_body=True`. If you don't know yet which methods in `Foo` you need to read or edit, + you can use `find_symbol` with the name path `Foo`, `include_body=False` and `depth=1` to get all (top-level) methods of `Foo` before proceeding + to read the desired methods with `include_body=True` + You can unterstand relationships between symbols by using the `find_referencing_symbols` tool. + You generally have access to memories and it may be useful for you to read them, but also only if they help you to answer the question or complete the task. You can infer which memories are relevant to the current task by reading the memory names and descriptions. @@ -21,3 +33,4 @@ excluded_tools: - replace_lines - insert_at_line - execute_shell_command + - replace_regex From 38f33b8f868b2f45a4a341f45e61d2942dc60358 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 2 Jun 2025 21:49:37 +0200 Subject: [PATCH 11/13] Readme, format --- README.md | 36 +++++++++++++++++++++++++++++------- src/serena/__init__.py | 1 + src/serena/agent.py | 2 +- src/serena/agno.py | 2 +- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c8948dcf..6d48de76 100644 --- a/README.md +++ b/README.md @@ -173,19 +173,31 @@ Serena can be used in various ways, below you will find instructions for selecte ### Setup -1. Install `uv` (instructions [here](https://docs.astral.sh/uv/getting-started/installation/)) -2. Clone the repository. -3. Copy `serena_config.template.yml` to `serena_config.yml` +Serena is managed by `uv`, so you will need to [install it](https://docs.astral.sh/uv/getting-started/installation/)). + +Then you can either + +1. Clone the repository and cd into it. +2. Optionally create a config file from the template and adjust it according to your preferences. ```shell - cp serena_config.template.yml serena_config.yml + cp src/serena/resources/serena_config.template.yml serena_config.yml ``` - + If you just want the default config, you can skip this part, and a config file will be created when you first run Serena. + +or use `uvx` to run Serena directly by relying on + +```shell +uvx --from git+https://github.com/oraios/serena serena-mcp-server.exe +``` + +in your MCP config (delete the `.exe` on macOS or Linux). + You can now add Serena to your MCP client as described below for various clients and [activate your first project](#project-activation). > In the default configuration, Serena will start a small dashboard on localhost that will display logs and allow shutting down the > MCP server (since many clients fail to cleanup processes, leaving zombies behind). If you don't want that, simply set `web_dashboard` to `False` -> in your `serena_config.yml`. +> in your `serena_config.yml` or pass ### Configuration @@ -195,7 +207,7 @@ Serena's behavior (like available projects, active tools and prompts) is configu 2. In the arguments passed to the `serena-mcp-server` in your client's config (see below), which will apply to all sessions started by the respective client. In particular, the [context](#contexts) parameter should be set appropriately for Serena to be best adjusted to existing tools and capabilities of your client. - See for a detailed explanation. + See for a detailed explanation. You can override all entries from the `serena_config.yml` through command line arguments. 3. In the `.serena/project.yml` file within your project. This will hold project-level configuration that is used whenever that project is activated. 4. Through the currently active set of [modes](#modes). @@ -253,6 +265,16 @@ That's it! Save the config and then restart Claude Desktop. You are ready for ac uv run serena-mcp-server --help ``` +ℹ️ You can use Serena without cloning or configuring it explicitly by + +{ + "mcpServers": { + "serena": { + "command": "/abs/path/to/uv", + "args": ["run", "--directory", "/abs/path/to/serena", "serena-mcp-server"] + } + } +} #### Troubleshooting diff --git a/src/serena/__init__.py b/src/serena/__init__.py index 85f7b173..1a6b2383 100644 --- a/src/serena/__init__.py +++ b/src/serena/__init__.py @@ -1,5 +1,6 @@ __version__ = "2025-05-21" + def serena_version() -> str: """ :return: the version of the package, including git status if available. diff --git a/src/serena/agent.py b/src/serena/agent.py index a937cac5..039174c7 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -36,7 +36,7 @@ from multilspy.multilspy_logger import MultilspyLogger from multilspy.multilspy_types import SymbolKind from serena import serena_version from serena.config import SerenaAgentContext, SerenaAgentMode -from serena.constants import PROJECT_TEMPLATE_FILE, SELENA_CONFIG_TEMPLATE_FILE, SERENA_MANAGED_DIR_NAME, REPO_ROOT +from serena.constants import PROJECT_TEMPLATE_FILE, REPO_ROOT, SELENA_CONFIG_TEMPLATE_FILE, SERENA_MANAGED_DIR_NAME from serena.dashboard import MemoryLogHandler, SerenaDashboardAPI from serena.prompt_factory import PromptFactory, SerenaPromptFactory from serena.symbol import SymbolManager diff --git a/src/serena/agno.py b/src/serena/agno.py index 1248d1a2..c9150693 100644 --- a/src/serena/agno.py +++ b/src/serena/agno.py @@ -14,9 +14,9 @@ from agno.tools.toolkit import Toolkit from dotenv import load_dotenv from sensai.util.logging import LogTime -from serena.constants import REPO_ROOT from serena.agent import SerenaAgent, Tool, show_fatal_exception_safe from serena.config import SerenaAgentContext +from serena.constants import REPO_ROOT log = logging.getLogger(__name__) From be8d62128f828cf166419728a336ced2feab6a72 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 2 Jun 2025 21:52:23 +0200 Subject: [PATCH 12/13] GH: don't run java and rust tests for the time being --- .github/workflows/pytest.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 1a3b040a..8e97440d 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -55,10 +55,6 @@ jobs: run: uv pip install -e ".[dev]" - name: Test with pytest shell: bash - # Currently java Language Server is not working on macos + # Currently, java and rust seem to cause problems in CI run: | - if [ "${{ matrix.os }}" = "macos-latest" ]; then - uv run pytest -m "not java" - else - uv run poe test - fi + uv run pytest -m "not java and not rust" From 795ffda43538e73c15c8c77a2911cf8e32906691 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 2 Jun 2025 22:01:23 +0200 Subject: [PATCH 13/13] Update snapshot (whitespace issues) --- test/serena/__snapshots__/test_symbol_editing.ambr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/serena/__snapshots__/test_symbol_editing.ambr b/test/serena/__snapshots__/test_symbol_editing.ambr index 4fa7309d..2b698a22 100644 --- a/test/serena/__snapshots__/test_symbol_editing.ambr +++ b/test/serena/__snapshots__/test_symbol_editing.ambr @@ -629,7 +629,7 @@ # Instance variable with type annotation self.typed_instance_var: list[str] = ["item1", "item2"] - + def modify_instance_var(self): # This body has been replaced self.instance_var = "Replaced!" @@ -694,7 +694,7 @@ constructor(value: number) { this.value = value; } - + function printValue() { // This body has been replaced console.warn("New value: " + this.value);