mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-14 12:17:02 +00:00
Add facade method metadata and API scope configuration for the REPL
Facade methods are now declared explicitly via the facade_method decorator, which attaches FacadeMethodInfo (optional, beta, can_edit) mirroring the tool markers; undecorated methods are never exposed. All LspApi and JetBrainsApi methods are decorated accordingly (JetBrains methods are non-optional, as the facade only exists with the JetBrains backend). Which facades/methods are enabled is determined by an ApiScope, which is built by applying ApiInclusionDefinitions (included_apis, excluded_apis, referencing facades or facade methods such as "lsp" or "lsp.find_symbol") from the global configuration, the context, the active modes and the project configuration, in that order. Optional methods and all methods of an excluded facade must be included explicitly; other methods are enabled unless excluded. For read-only projects, editing methods are excluded. The entrypoint omits facades which are not enabled. The REPL is re-created whenever the active tools are updated (mode switch, project activation), as it depends on the same configuration. The new settings are read from project.yml and documented in all configuration templates.
This commit is contained in:
1 parent
91c2ec8dc2
commit
eabc7e60d6
14 files changed
+382
-65
No files matched your search
@@ -7,7 +7,7 @@ from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
|
||||
from serena.facades.api.jb import JetBrainsApi
|
||||
from serena.facades.facade import Facade
|
||||
from serena.facades.facade import ApiScope, Facade
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -26,7 +26,7 @@ def client() -> MagicMock:
|
||||
|
||||
|
||||
def test_facade_exposes_all_jetbrains_operations(agent: MagicMock) -> None:
|
||||
facade = Facade.from_api(JetBrainsApi(agent))
|
||||
facade = Facade.from_api(JetBrainsApi(agent), ApiScope())
|
||||
assert facade.name == "jb"
|
||||
assert set(facade.enabled_method_names) == {
|
||||
"find_symbol",
|
||||
|
||||
@@ -8,6 +8,7 @@ from serena import __version__
|
||||
from serena.agent import Tool, ToolRegistry
|
||||
from serena.config.context_mode import SerenaAgentContext
|
||||
from serena.config.serena_config import SerenaConfig
|
||||
from serena.facades.facade import ApiScope
|
||||
from serena.facades.repl import SerenaRepl
|
||||
from serena.mcp import SerenaMCPFactory
|
||||
|
||||
@@ -26,7 +27,7 @@ class MockAgent:
|
||||
|
||||
@staticmethod
|
||||
def get_repl() -> SerenaRepl:
|
||||
return SerenaRepl([])
|
||||
return SerenaRepl([], ApiScope())
|
||||
|
||||
|
||||
class BaseMockTool(Tool):
|
||||
|
||||
@@ -7,8 +7,9 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from serena.config.serena_config import ApiInclusionDefinition
|
||||
from serena.facades.api.lsp import LspApi
|
||||
from serena.facades.facade import Facade, FacadeApi
|
||||
from serena.facades.facade import ApiScope, Facade, FacadeApi, FacadeMethodInfo, facade_method
|
||||
from serena.facades.repl import SerenaRepl
|
||||
from serena.tools import SerenaReplTool
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
@@ -20,7 +21,7 @@ class TestReplExecution:
|
||||
|
||||
@pytest.fixture
|
||||
def repl(self) -> SerenaRepl:
|
||||
return SerenaRepl([Facade.from_api(LspApi(MagicMock()))])
|
||||
return SerenaRepl([Facade.from_api(LspApi(MagicMock()), ApiScope())], ApiScope())
|
||||
|
||||
def test_return_statement_defines_result(self, repl: SerenaRepl) -> None:
|
||||
assert repl.execute("x = 20\ny = 22\nreturn x + y") == "42"
|
||||
@@ -59,28 +60,81 @@ class TestFacade:
|
||||
def __init__(self, agent: MagicMock) -> None:
|
||||
super().__init__(agent, name="dummy", description="a dummy facade")
|
||||
|
||||
@facade_method()
|
||||
def add(self, a: int, b: int) -> int:
|
||||
"""Adds two numbers."""
|
||||
return a + b
|
||||
|
||||
@facade_method(can_edit=True)
|
||||
def secret(self) -> str:
|
||||
return "hidden"
|
||||
|
||||
def serena_internal_(self) -> str:
|
||||
"""Public within Serena, but not LLM-facing."""
|
||||
@facade_method(optional=True, beta=True)
|
||||
def extra(self) -> str:
|
||||
return "extra"
|
||||
|
||||
def undecorated(self) -> str:
|
||||
"""Public within Serena, but not exposed, since it is not decorated."""
|
||||
return "internal"
|
||||
|
||||
def _internal(self) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _scope(*definitions: ApiInclusionDefinition, **kwargs: list[str]) -> ApiScope:
|
||||
"""
|
||||
:param definitions: definitions to apply in order
|
||||
:param kwargs: an additional definition (`included_apis`/`excluded_apis`) to apply last
|
||||
"""
|
||||
scope = ApiScope()
|
||||
for definition in definitions:
|
||||
scope.process(definition)
|
||||
if kwargs:
|
||||
scope.process(ApiInclusionDefinition(**kwargs))
|
||||
return scope
|
||||
|
||||
def test_api_scope_facade_exclusion_and_method_inclusion(self) -> None:
|
||||
# excluding the facade disables everything
|
||||
facade = Facade.from_api(self.DummyApi(MagicMock()), self._scope(excluded_apis=["dummy"]))
|
||||
assert facade.enabled_method_names == []
|
||||
|
||||
# an excluded facade is opt-in: a method inclusion enables exactly that method
|
||||
facade = Facade.from_api(self.DummyApi(MagicMock()), self._scope(excluded_apis=["dummy"], included_apis=["dummy.add"]))
|
||||
assert facade.enabled_method_names == ["add"]
|
||||
|
||||
def test_entrypoint_omits_excluded_facades(self) -> None:
|
||||
def create_repl(scope: ApiScope) -> SerenaRepl:
|
||||
return SerenaRepl([Facade.from_api(self.DummyApi(MagicMock()), scope)], scope)
|
||||
|
||||
assert "s.dummy" in create_repl(ApiScope()).execute("s.info()")
|
||||
assert "s.dummy" not in create_repl(self._scope(excluded_apis=["dummy"])).execute("s.info()")
|
||||
# a method inclusion keeps the facade available (with just that method)
|
||||
overview = create_repl(self._scope(excluded_apis=["dummy"], included_apis=["dummy.add"])).execute("s.info()")
|
||||
assert "s.dummy" in overview and "methods: add" in overview
|
||||
|
||||
def test_api_scope_later_definitions_take_precedence(self) -> None:
|
||||
scope = self._scope(
|
||||
ApiInclusionDefinition(included_apis=["dummy.extra"]),
|
||||
ApiInclusionDefinition(excluded_apis=["dummy.extra", "dummy.add"]),
|
||||
ApiInclusionDefinition(included_apis=["dummy.add"]),
|
||||
)
|
||||
facade = Facade.from_api(self.DummyApi(MagicMock()), scope)
|
||||
assert set(facade.enabled_method_names) == {"add", "secret"}
|
||||
|
||||
def test_api_scope_read_only_excludes_editing_methods(self) -> None:
|
||||
scope = self._scope(included_apis=["dummy.secret"])
|
||||
scope.exclude_editing()
|
||||
facade = Facade.from_api(self.DummyApi(MagicMock()), scope)
|
||||
assert facade.enabled_method_names == ["add"]
|
||||
|
||||
def test_enabled_methods_delegate_to_implementation(self) -> None:
|
||||
facade = Facade.from_api(self.DummyApi(MagicMock()))
|
||||
facade = Facade.from_api(self.DummyApi(MagicMock()), ApiScope())
|
||||
assert facade.add(1, 2) == 3
|
||||
assert "dummy.add(a: int, b: int) -> int" in facade.describe()
|
||||
assert "Adds two numbers." in facade.describe_method("add")
|
||||
|
||||
def test_disabled_methods_are_inaccessible_and_undocumented(self) -> None:
|
||||
facade = Facade.from_api(self.DummyApi(MagicMock()), enabled_methods=["add"])
|
||||
facade = Facade.from_api(self.DummyApi(MagicMock()), self._scope(excluded_apis=["dummy.secret"]))
|
||||
assert facade.add(1, 2) == 3
|
||||
with pytest.raises(AttributeError):
|
||||
facade.secret()
|
||||
@@ -89,18 +143,36 @@ class TestFacade:
|
||||
assert "secret" not in facade.describe()
|
||||
assert "_internal" not in facade.describe()
|
||||
|
||||
def test_trailing_underscore_members_are_not_llm_facing(self) -> None:
|
||||
facade = Facade.from_api(self.DummyApi(MagicMock()))
|
||||
assert self.DummyApi(MagicMock()).serena_internal_() == "internal" # usable from within Serena
|
||||
def test_undecorated_methods_are_not_exposed(self) -> None:
|
||||
facade = Facade.from_api(self.DummyApi(MagicMock()), ApiScope())
|
||||
assert self.DummyApi(MagicMock()).undecorated() == "internal" # usable from within Serena
|
||||
with pytest.raises(AttributeError):
|
||||
facade.serena_internal_()
|
||||
facade.undecorated()
|
||||
with pytest.raises(ValueError):
|
||||
facade.get_method("serena_internal_")
|
||||
assert "serena_internal_" not in facade.describe()
|
||||
assert "serena_internal_" not in facade.enabled_method_names
|
||||
facade.get_method("undecorated")
|
||||
assert "undecorated" not in facade.describe()
|
||||
assert "undecorated" not in facade.enabled_method_names
|
||||
|
||||
def test_optional_methods_are_disabled_by_default(self) -> None:
|
||||
facade = Facade.from_api(self.DummyApi(MagicMock()), ApiScope())
|
||||
assert "extra" not in facade.enabled_method_names
|
||||
with pytest.raises(AttributeError):
|
||||
facade.extra()
|
||||
facade.get_method("extra").enabled = True
|
||||
assert facade.extra() == "extra"
|
||||
|
||||
# an explicit inclusion enables it
|
||||
facade = Facade.from_api(self.DummyApi(MagicMock()), self._scope(included_apis=["dummy.extra"]))
|
||||
assert "extra" in facade.enabled_method_names
|
||||
|
||||
def test_method_info_mirrors_decorator(self) -> None:
|
||||
facade = Facade.from_api(self.DummyApi(MagicMock()), ApiScope())
|
||||
assert facade.get_method("add").info == FacadeMethodInfo(name="add")
|
||||
assert facade.get_method("secret").info.can_edit
|
||||
assert facade.get_method("extra").info == FacadeMethodInfo(name="extra", optional=True, beta=True)
|
||||
|
||||
def test_enablement_can_be_changed(self) -> None:
|
||||
facade = Facade.from_api(self.DummyApi(MagicMock()))
|
||||
facade = Facade.from_api(self.DummyApi(MagicMock()), ApiScope())
|
||||
facade.get_method("secret").enabled = False
|
||||
with pytest.raises(AttributeError):
|
||||
facade.secret()
|
||||
|
||||
Reference in new issue
Block a user