Made tool selection configurable

More things:
- list tools using .subclasses instead of inspect
- entrypoint for listing tools
- util for listing selected tools
- documentation
This commit is contained in:
Michael Panchenko
2025-04-01 18:34:15 +02:00
parent 4eb4a26374
commit efbea42e39
7 changed files with 113 additions and 49 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ We thus built Serena with the prospect of being able to cancel most other subscr
"mcpServers": {
"serena": {
"command": "/abs/path/to/uv",
"args": ["run", "--directory", "/abs/path/to/serena", "serena", "/abs/path/to/myproject.yml"]
"args": ["run", "--directory", "/abs/path/to/serena", "serena-mcp-server", "/abs/path/to/myproject.yml"]
}
}
}
+44 -1
View File
@@ -4,7 +4,50 @@ project_root: /path/to/project
language: python
# list of directories to ignore: either names (e.g. "temp") or relative paths (e.g. "build/foo")
ignored_dirs: [".git", "temp"]
# whether to open a graphical window with Serena's logs (not supported on MacOS)
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run serena-list-tools`.
#
# check_onboarding_performed
# create_text_file
# delete_lines
# delete_memory
# execute_shell_command
# find_referencing_symbols
# find_symbol
# get_dir_overview
# get_document_overview
# insert_after_symbol
# insert_at_line
# insert_before_symbol
# list_dir
# list_memories
# onboarding
# prepare_for_new_conversation
# read_file
# read_memory
# replace_symbol_body
# search_in_all_code
# summarize_changes
# think_about_collected_information
# think_about_task_adherence
# think_about_whether_you_are_done
# write_memory
excluded_tools: []
# Whether to open a graphical window with Serena's logs (not supported on MacOS).
# This is useful both for troubleshooting and for monitoring the tool calls,
# especially when using the agno playground, since the tool calls are not always shown,
# and the input params are never shown in the agno UI.
# When used as MCP server for Claude Desktop, the logs are primarily for troubleshooting.
# Note: unfortunately, the various entities starting the Serena server or agent do so in
# mysterious ways, often starting multiple instances of the process without shutting down
# previous instances. This leads to multiple log windows being opened, and only the last
# window being updated. Since we can't control how agno or Claude Desktop starts Serena, we have to live with this limitation for now.
gui_log_window: True
# minimum log level for the GUI log window (10 = debug, 20 = info, 30 = warning, 40 = error)
gui_log_level: 20
+2 -1
View File
@@ -33,7 +33,8 @@ dependencies = [
]
[project.scripts]
serena = "serena.mcp:start_mcp_server"
serena-mcp-server = "serena.mcp:start_mcp_server"
serena-list-tools = "serena.agent:print_tool_overview"
[project.license]
text = "MIT"
+24 -27
View File
@@ -1,19 +1,13 @@
import os
from logging import Logger
from agno.agent.agent import Agent
from agno.memory.agent import AgentMemory
from agno.models.anthropic.claude import Claude
from agno.models.google.gemini import Gemini
from agno.playground.playground import Playground
from agno.playground.serve import serve_playground_app
from agno.storage.agent.sqlite import SqliteAgentStorage
from dotenv import load_dotenv
from sensai.util import logging
from sensai.util.helper import mark_used
from serena.agent import SerenaAgent
from serena.agno import SerenaAgnoToolkit
mark_used(Gemini, Claude)
@@ -36,27 +30,30 @@ os.makedirs(os.path.dirname(sql_db_path), exist_ok=True)
if os.path.exists(sql_db_path):
os.remove(sql_db_path)
model = Claude(id="claude-3-7-sonnet-20250219")
# model = Gemini(id="gemini-2.5-pro-exp-03-25")
for tool_name in serena_agent.list_tool_names():
print(tool_name)
agno_agent = Agent(
name="Serena",
model=model,
storage=SqliteAgentStorage(table_name="serena_agent_sessions", db_file=sql_db_path),
description="A fully-featured coding assistant",
tools=[SerenaAgnoToolkit(serena_agent)], # type: ignore
show_tool_calls=False,
markdown=True,
system_message=serena_agent.prompt_factory.create_system_prompt(),
read_tool_call_history=False,
telemetry=False,
memory=AgentMemory(),
add_history_to_messages=True,
num_history_responses=100, # you might want to adjust this (expense vs. history awareness)
)
# model = Claude(id="claude-3-7-sonnet-20250219")
# # model = Gemini(id="gemini-2.5-pro-exp-03-25")
# The app object must be in the module scope so that the server can access it for hot reloading
app = Playground(agents=[agno_agent]).get_app()
# agno_agent = Agent(
# name="Serena",
# model=model,
# storage=SqliteAgentStorage(table_name="serena_agent_sessions", db_file=sql_db_path),
# description="A fully-featured coding assistant",
# tools=[SerenaAgnoToolkit(serena_agent)], # type: ignore
# show_tool_calls=False,
# markdown=True,
# system_message=serena_agent.prompt_factory.create_system_prompt(),
# read_tool_call_history=False,
# telemetry=False,
# memory=AgentMemory(),
# add_history_to_messages=True,
# num_history_responses=100, # you might want to adjust this (expense vs. history awareness)
# )
if __name__ == "__main__":
serve_playground_app("agno_agent:app", reload=False)
# # The app object must be in the module scope so that the server can access it for hot reloading
# app = Playground(agents=[agno_agent]).get_app()
# if __name__ == "__main__":
# serve_playground_app("agno_agent:app", reload=False)
-10
View File
@@ -1,10 +0,0 @@
from serena.agent import SerenaAgent
if __name__ == "__main__":
agent = SerenaAgent("myproject.yml")
tools = {}
for tool in agent.tools.values():
tools[tool.get_name()] = tool
for tool_name in sorted(tools.keys()):
tool = tools[tool_name]
print(f" * `{tool_name}`: {tool.get_tool_description().strip()}")
+30 -9
View File
@@ -11,7 +11,7 @@ import traceback
import warnings
from abc import ABC
from collections import defaultdict
from collections.abc import Callable, Iterator
from collections.abc import Callable, Generator, Iterable, Iterator
from contextlib import contextmanager
from logging import Logger
from pathlib import Path
@@ -29,6 +29,7 @@ from serena.gui_log_viewer import GuiLogViewer, GuiLogViewerHandler
from serena.llm.prompt_factory import PromptFactory
from serena.symbol import SymbolLocation, SymbolManager
from serena.util.file_system import scan_directory
from serena.util.inspection import iter_subclasses
from serena.util.shell import execute_shell_command
log = logging.getLogger(__name__)
@@ -87,11 +88,12 @@ class SerenaAgent:
self.memories_manager = MemoriesManager(memories_dir)
# find all tool classes and instantiate them
excluded_tools = project_config.get("excluded_tools", [])
self.tools: dict[type[Tool], Tool] = {}
tool_classes = [
cls for name, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass) if issubclass(cls, Tool) and cls is not Tool
]
for tool_class in tool_classes:
for tool_class in iter_tool_classes():
if (tool_name := tool_class.get_name()) in excluded_tools:
log.info(f"Skipping tool {tool_name} because it is in the exclude list")
continue
tool_instance = tool_class(self)
self.tools[tool_class] = tool_instance
log.info(f"Loaded tools: {', '.join([tool.get_name() for tool in self.tools.values()])}")
@@ -101,11 +103,12 @@ class SerenaAgent:
log.info("Starting the language server ...")
self.language_server.start()
self._is_initialized = True
def get_tool(self, tool_class: type[TTool]) -> TTool:
return self.tools[tool_class] # type: ignore
def print_tool_overview(self) -> None:
_print_tool_overview(self.tools.values())
def get_serena_managed_dir(self) -> str:
return os.path.join(self.project_root, ".serena")
@@ -195,8 +198,9 @@ class Tool(Component):
raise Exception(f"{self} does not define method apply")
return apply_fn
def get_tool_description(self) -> str:
docstring = self.__class__.__doc__
@classmethod
def get_tool_description(cls) -> str:
docstring = cls.__doc__
if docstring is None:
return ""
return docstring.strip()
@@ -878,3 +882,20 @@ class ExecuteShellCommandTool(Tool):
result = execute_shell_command(command, cwd=_cwd, capture_stderr=capture_stderr)
result = result.json()
return self._limit_length(result, max_answer_chars)
def iter_tool_classes() -> Generator[type[Tool], None, None]:
return iter_subclasses(Tool)
def print_tool_overview() -> None:
_print_tool_overview(iter_tool_classes())
def _print_tool_overview(tools: Iterable[type[Tool] | Tool]) -> None:
tool_dict: dict[str, type[Tool] | Tool] = {}
for tool in tools:
tool_dict[tool.get_name()] = tool
for tool_name in sorted(tool_dict.keys()):
tool = tool_dict[tool_name]
print(f" * `{tool_name}`: {tool.get_tool_description().strip()}")
+12
View File
@@ -0,0 +1,12 @@
from collections.abc import Generator
from typing import TypeVar
T = TypeVar("T")
def iter_subclasses(cls: type[T], recursive: bool = True) -> Generator[type[T], None, None]:
"""Iterate over all subclasses of a class. If recursive is True, also iterate over all subclasses of all subclasses."""
for subclass in cls.__subclasses__():
yield subclass
if recursive:
yield from iter_subclasses(subclass, recursive)