mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 16:15:33 +00:00
Add configuration facade (cfg) with get_current_config and open_dashboard
Introduce ConfigApi, exposing Serena's configuration and session state: get_current_config and open_dashboard (both non-editing). The corresponding tools delegate to the API via ConfigApiMixin. As on the tool side, opening the dashboard is only offered if the dashboard is enabled and not opened automatically; the condition is shared (_is_dashboard_openable) and applied to the API scope via a NamedApiInclusionDefinition, the API-side counterpart of NamedToolInclusionDefinition.
This commit is contained in:
1 parent
6716193d71
commit
05603b2706
7 files changed
+118
-11
No files matched your search
@@ -53,5 +53,7 @@ Code runs as a function body (`return` defines the result); a single expression
|
||||
- Python code can always modify the system; the REPL tool is inherently fully privileged, regardless of
|
||||
facade scope or the project's `read_only` setting (which only makes Serena's own API refuse edits).
|
||||
A "read-only REPL" is not feasible and must not be promised.
|
||||
- Session/project management (activate_project, initial_instructions, dashboard, ...) stays tool-only;
|
||||
facades expose operations on the active project.
|
||||
- Project activation (activate_project) and initial_instructions stay tool-only (activation rebuilds the REPL);
|
||||
Serena's configuration/session state (config overview, dashboard; later e.g. modes) lives in the `cfg` facade.
|
||||
Computed conditions (read-only project, dashboard not openable) are applied to the API scope in
|
||||
`SerenaAgent.get_repl` via `exclude_editing()`/`NamedApiInclusionDefinition`, mirroring the tool side.
|
||||
+15
-1
@@ -36,6 +36,7 @@ from serena.config.serena_config import (
|
||||
ModeSelectionDefinition,
|
||||
ModeSelectionDefinitionWithAddedModes,
|
||||
ModeSelectionDefinitionWithBaseModes,
|
||||
NamedApiInclusionDefinition,
|
||||
NamedToolInclusionDefinition,
|
||||
RegisteredProject,
|
||||
SerenaConfig,
|
||||
@@ -48,6 +49,7 @@ from serena.ls_manager import LanguageServerManager
|
||||
from serena.memories.memory_manager import MemoryManager
|
||||
from serena.project import Project
|
||||
from serena.prompt_factory import SerenaPromptFactory
|
||||
from serena.repl.api.cfg_api import ConfigApi
|
||||
from serena.repl.api.edit_api import EditApi
|
||||
from serena.repl.api.fs_api import FsApi
|
||||
from serena.repl.api.jb_api import JetBrainsApi
|
||||
@@ -760,6 +762,15 @@ class SerenaAgent:
|
||||
except Exception as e:
|
||||
log.debug(f"Failed to send usage info: {e}")
|
||||
|
||||
@staticmethod
|
||||
def _is_dashboard_openable(serena_config: SerenaConfig) -> bool:
|
||||
"""
|
||||
:param serena_config: the configuration
|
||||
:return: whether the web dashboard is available and opening it is a meaningful operation
|
||||
(i.e. it is enabled and not opened automatically)
|
||||
"""
|
||||
return serena_config.web_dashboard and not serena_config.web_dashboard_open_on_launch and not serena_config.gui_log_window
|
||||
|
||||
@classmethod
|
||||
def _create_base_toolset(
|
||||
cls,
|
||||
@@ -789,7 +800,7 @@ class SerenaAgent:
|
||||
|
||||
# determine whether to include the OpenDashboardTool based on the Serena configuration
|
||||
tool_inclusion_definitions: list[ToolInclusionDefinition] = []
|
||||
if serena_config.web_dashboard and not serena_config.web_dashboard_open_on_launch and not serena_config.gui_log_window:
|
||||
if cls._is_dashboard_openable(serena_config):
|
||||
tool_inclusion_definitions.append(
|
||||
NamedToolInclusionDefinition(name="OpenDashboard", included_optional_tools=[OpenDashboardTool.get_name_from_cls()])
|
||||
)
|
||||
@@ -1281,9 +1292,12 @@ class SerenaAgent:
|
||||
api_scope.process(self._active_project.project_config)
|
||||
if self._active_project.project_config.read_only:
|
||||
api_scope.exclude_editing()
|
||||
if not self._is_dashboard_openable(self.serena_config):
|
||||
api_scope.process(NamedApiInclusionDefinition(name="Dashboard", excluded_apis=["cfg.open_dashboard"]))
|
||||
|
||||
# gather facades
|
||||
facades = [
|
||||
Facade.from_api(ConfigApi(self), api_scope),
|
||||
Facade.from_api(FsApi(self), api_scope),
|
||||
Facade.from_api(EditApi(self), api_scope),
|
||||
Facade.from_api(MemoryApi(self), api_scope),
|
||||
|
||||
@@ -190,6 +190,14 @@ class ApiInclusionDefinition:
|
||||
excluded_apis: Sequence[str] = ()
|
||||
|
||||
|
||||
@dataclass
|
||||
class NamedApiInclusionDefinition(ApiInclusionDefinition):
|
||||
name: str | None = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"ApiInclusionDefinition[{self.name}]"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModeSelectionDefinition:
|
||||
default_modes: Sequence[str] | None = None
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""
|
||||
The implementation of operations concerning Serena's configuration and session state.
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from serena.tools import GetCurrentConfigTool, OpenDashboardTool
|
||||
|
||||
from ..facade import FacadeApi, facade_method
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from serena.agent import SerenaAgent
|
||||
|
||||
|
||||
class ConfigApi(FacadeApi):
|
||||
def __init__(self, agent: "SerenaAgent") -> None:
|
||||
super().__init__(agent, name="cfg", description="Serena's configuration and session state (incl. the dashboard)")
|
||||
|
||||
@facade_method(corresponding_tool=GetCurrentConfigTool)
|
||||
def get_current_config(self) -> str:
|
||||
"""
|
||||
Provides the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
|
||||
:return: the configuration overview
|
||||
"""
|
||||
return self._agent.get_current_config_overview()
|
||||
|
||||
@facade_method(corresponding_tool=OpenDashboardTool)
|
||||
def open_dashboard(self) -> str:
|
||||
"""
|
||||
Opens the Serena web dashboard in the default web browser.
|
||||
The dashboard provides logs, session information, and tool usage statistics.
|
||||
|
||||
:return: a message indicating whether the dashboard could be opened
|
||||
"""
|
||||
if self._agent.open_dashboard():
|
||||
return f"Serena web dashboard has been opened in the user's default web browser: {self._agent.get_dashboard_url()}"
|
||||
else:
|
||||
return f"Serena web dashboard could not be opened automatically; tell the user to open it via {self._agent.get_dashboard_url()}"
|
||||
@@ -1,11 +1,29 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from sensai.util.helper import mark_used
|
||||
|
||||
from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from serena.repl.api.cfg_api import ConfigApi
|
||||
|
||||
class OpenDashboardTool(Tool, ToolMarkerOptional, ToolMarkerDoesNotRequireActiveProject):
|
||||
|
||||
class ConfigApiMixin:
|
||||
"""
|
||||
Mixin for tools which delegate to the configuration API.
|
||||
The API is imported locally, since the API module refers to the tools (as corresponding tools).
|
||||
"""
|
||||
|
||||
def _api(self) -> "ConfigApi":
|
||||
from serena.repl.api.cfg_api import ConfigApi
|
||||
|
||||
tool = cast(Tool, cast(object, self))
|
||||
return ConfigApi(tool.agent)
|
||||
|
||||
|
||||
class OpenDashboardTool(Tool, ToolMarkerOptional, ToolMarkerDoesNotRequireActiveProject, ConfigApiMixin):
|
||||
"""
|
||||
Opens the Serena web dashboard in the default web browser.
|
||||
The dashboard provides logs, session information, and tool usage statistics.
|
||||
@@ -15,10 +33,7 @@ class OpenDashboardTool(Tool, ToolMarkerOptional, ToolMarkerDoesNotRequireActive
|
||||
"""
|
||||
Opens the Serena web dashboard in the default web browser.
|
||||
"""
|
||||
if self.agent.open_dashboard():
|
||||
return f"Serena web dashboard has been opened in the user's default web browser: {self.agent.get_dashboard_url()}"
|
||||
else:
|
||||
return f"Serena web dashboard could not be opened automatically; tell the user to open it via {self.agent.get_dashboard_url()}"
|
||||
return self._api().open_dashboard()
|
||||
|
||||
|
||||
class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
|
||||
@@ -56,7 +71,7 @@ class RemoveProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerO
|
||||
return f"Successfully removed project '{project_name}' from configuration."
|
||||
|
||||
|
||||
class GetCurrentConfigTool(Tool):
|
||||
class GetCurrentConfigTool(Tool, ConfigApiMixin):
|
||||
"""
|
||||
Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
"""
|
||||
@@ -65,4 +80,4 @@ class GetCurrentConfigTool(Tool):
|
||||
"""
|
||||
Print the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
"""
|
||||
return self.agent.get_current_config_overview()
|
||||
return self._api().get_current_config()
|
||||
@@ -268,7 +268,10 @@ class TestAgentInterface:
|
||||
fixed_toolset = {"serena_repl", "initial_instructions", "activate_project"}
|
||||
assert {t.get_name() for t in agent.get_exposed_tool_instances()} == fixed_toolset
|
||||
assert set(agent.get_active_tool_names()) == fixed_toolset
|
||||
assert "s.mem" in agent.get_repl().entrypoint.overview()
|
||||
overview = agent.get_repl().entrypoint.overview()
|
||||
assert "s.mem" in overview
|
||||
# the dashboard is disabled in the test configuration, so opening it is not offered
|
||||
assert "s.cfg" in overview and "open_dashboard" not in overview
|
||||
|
||||
# after activation: the toolset is unchanged, the REPL reflects the project's API exclusions
|
||||
agent.activate_project_from_path_or_name(name)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
Tests for the configuration facade API.
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from serena.repl.api.cfg_api import ConfigApi
|
||||
from serena.repl.facade import ApiScope, Facade
|
||||
|
||||
|
||||
def test_facade_exposes_config_operations() -> None:
|
||||
facade = Facade.from_api(ConfigApi(MagicMock()), ApiScope())
|
||||
assert facade.name == "cfg"
|
||||
assert set(facade.enabled_method_names) == {"get_current_config", "open_dashboard"}
|
||||
assert not any(facade.get_method(name).info.can_edit for name in facade.enabled_method_names)
|
||||
|
||||
|
||||
def test_operations_delegate_to_agent() -> None:
|
||||
agent = MagicMock()
|
||||
agent.get_current_config_overview.return_value = "overview"
|
||||
agent.open_dashboard.return_value = False
|
||||
agent.get_dashboard_url.return_value = "http://localhost:1"
|
||||
api = ConfigApi(agent)
|
||||
assert api.get_current_config() == "overview"
|
||||
assert "http://localhost:1" in api.open_dashboard()
|
||||
Reference in new issue
Block a user