mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 18:16:10 +00:00
Merge branch 'main' of github.com:oraios/serena
This commit is contained in:
commit
f488116bb5
14 files changed
+302
-1952
No files matched your search
+5
-1
@@ -144,4 +144,8 @@ pylint.html
|
||||
/src/multilspy/language_servers/*/static
|
||||
|
||||
# Cache is in .multilspy
|
||||
**/.multilspy/**
|
||||
**/.multilspy/**
|
||||
|
||||
tmp/
|
||||
|
||||
.serena/
|
||||
Vendored
+4
-1
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"agno",
|
||||
"genai",
|
||||
"langsrv",
|
||||
"multilspy",
|
||||
"pixi"
|
||||
"pixi",
|
||||
"sensai"
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,7 +169,8 @@ without modifying the codebase, you can consider disabling the editing tools in
|
||||
In general, be sure to back up your work and use a version control system in order to avoid
|
||||
losing any work.
|
||||
|
||||
Here a complete list of Serena's tools:
|
||||
Here a complete list of Serena's tools
|
||||
(you can get an up-to-date list by running `uv run serena-list-tools`)
|
||||
|
||||
* `check_onboarding_performed`: Checks whether the onboarding was already performed.
|
||||
* `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
|
||||
+44
-1
@@ -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
@@ -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"
|
||||
|
||||
+55
-28
@@ -1,12 +1,16 @@
|
||||
import os
|
||||
from logging import Logger
|
||||
|
||||
from agno.agent import Agent
|
||||
from agno.memory import AgentMemory
|
||||
from agno.models.anthropic import Claude
|
||||
from agno.models.google import Gemini
|
||||
from agno.playground import Playground, serve_playground_app
|
||||
import click
|
||||
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.sqlite import SqliteStorage
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import FastAPI
|
||||
from sensai.util import logging
|
||||
from sensai.util.helper import mark_used
|
||||
|
||||
@@ -20,29 +24,52 @@ Logger.root.setLevel(logging.INFO)
|
||||
|
||||
load_dotenv()
|
||||
|
||||
project_file_path = "../myproject.yml"
|
||||
serena_agent = SerenaAgent(project_file_path, start_language_server=True)
|
||||
|
||||
model = Claude(id="claude-3-7-sonnet-20250219")
|
||||
# model = Gemini(id="gemini-2.5-pro-exp-03-25")
|
||||
|
||||
agno_agent = Agent(
|
||||
name="Serena",
|
||||
model=model,
|
||||
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)
|
||||
)
|
||||
|
||||
# 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()
|
||||
app: FastAPI | None = None
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("--project-file", default="../myproject.yml", help="Path to the project configuration YAML file")
|
||||
def main(project_file):
|
||||
global app # noqa: PLW0603
|
||||
serena_agent = SerenaAgent(project_file, start_language_server=True)
|
||||
|
||||
# Even though we don't want to keep history between sessions,
|
||||
# for the agno ui to work as a conversation, we a persistent storage on disk
|
||||
# This storage should be deleted between sessions.
|
||||
# Note that this might collide with custom options for the agent, like adding vector-search based tools.
|
||||
# See here for an explanation: https://www.reddit.com/r/agno/comments/1jk6qea/regarding_the_built_in_memory/
|
||||
sql_db_path = os.path.join("tmp", "agent_storage.db")
|
||||
os.makedirs(os.path.dirname(sql_db_path), exist_ok=True)
|
||||
# delete the db file if it exists
|
||||
if os.path.exists(sql_db_path):
|
||||
os.remove(sql_db_path)
|
||||
|
||||
model = Claude(id="claude-3-7-sonnet-20250219")
|
||||
# Or use any other model supported by agno, e.g. the newest Gemini:
|
||||
# model = Gemini(id="gemini-2.5-pro-exp-03-25")
|
||||
|
||||
agno_agent = Agent(
|
||||
name="Serena",
|
||||
model=model,
|
||||
# See explanation above on why storage is needed
|
||||
storage=SqliteStorage(table_name="serena_agent_sessions", db_file=sql_db_path),
|
||||
description="A fully-featured coding assistant",
|
||||
tools=[SerenaAgnoToolkit(serena_agent)], # type: ignore
|
||||
# The tool calls will be shown in the UI anyway since whether to show them is configurable per tool
|
||||
# To see detailed logs, you should use the serena logger (configure it in the project file path)
|
||||
show_tool_calls=False,
|
||||
markdown=True,
|
||||
system_message=serena_agent.prompt_factory.create_system_prompt(),
|
||||
telemetry=False,
|
||||
memory=AgentMemory(),
|
||||
add_history_to_messages=True,
|
||||
num_history_responses=100, # you might want to adjust this (expense vs. history awareness)
|
||||
)
|
||||
|
||||
app = Playground(agents=[agno_agent]).get_app()
|
||||
serve_playground_app("agno_agent:app", reload=False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
serve_playground_app("agno_agent:app", reload=False)
|
||||
main()
|
||||
@@ -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()}")
|
||||
@@ -6,38 +6,37 @@ The details of Language Specific configuration are not exposed to the user.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from collections import defaultdict
|
||||
from copy import copy
|
||||
import dataclasses
|
||||
from fnmatch import fnmatch
|
||||
import hashlib
|
||||
import json
|
||||
import pickle
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import pickle
|
||||
import re
|
||||
import threading
|
||||
from collections import defaultdict
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from copy import copy
|
||||
from fnmatch import fnmatch
|
||||
from pathlib import Path, PurePath
|
||||
from typing import AsyncIterator, Dict, Iterator, List, Optional, Tuple, Union, cast
|
||||
|
||||
from serena.text_utils import MatchedConsecutiveLines, LineType, TextLine, search_text
|
||||
from .lsp_protocol_handler.lsp_constants import LSPConstants
|
||||
from .lsp_protocol_handler import lsp_types as LSPTypes
|
||||
|
||||
from serena.text_utils import LineType, MatchedConsecutiveLines, TextLine, search_text
|
||||
from . import multilspy_types
|
||||
from .multilspy_logger import MultilspyLogger
|
||||
from .lsp_protocol_handler import lsp_types as LSPTypes
|
||||
from .lsp_protocol_handler.lsp_constants import LSPConstants
|
||||
from .lsp_protocol_handler.lsp_types import SymbolKind
|
||||
from .lsp_protocol_handler.server import (
|
||||
LanguageServerHandler,
|
||||
ProcessLaunchInfo,
|
||||
)
|
||||
from .multilspy_config import MultilspyConfig, Language
|
||||
from .multilspy_config import Language, MultilspyConfig
|
||||
from .multilspy_exceptions import MultilspyException
|
||||
from .multilspy_utils import PathUtils, FileUtils, TextUtils
|
||||
from pathlib import Path, PurePath
|
||||
from typing import AsyncIterator, Iterator, List, Dict, Optional, TypedDict, Union, Tuple, cast
|
||||
from .multilspy_logger import MultilspyLogger
|
||||
from .multilspy_utils import FileUtils, PathUtils, TextUtils
|
||||
from .type_helpers import ensure_all_methods_implemented
|
||||
|
||||
|
||||
# Serena dependencies
|
||||
# We will need to watch out for circular imports, but it's probably better to not
|
||||
# move all generic util code from serena into multilspy.
|
||||
@@ -664,7 +663,7 @@ class LanguageServer:
|
||||
|
||||
:param relative_file_path: The relative path of the file that has the symbols
|
||||
:param include_body: whether to include the body of the symbols in the result.
|
||||
:return: A list of symbols in the file, and a list of root symbols that represent the tree structure of the symbols.
|
||||
:return: A list of symbols in the file, and a list of root symbols that represent the tree structure of the symbols.
|
||||
Each symbol in hierarchy starting from the roots has a children attribute.
|
||||
All symbols will have a location and a children attribute.
|
||||
"""
|
||||
@@ -712,6 +711,10 @@ class LanguageServer:
|
||||
assert isinstance(response, list)
|
||||
root_nodes: List[multilspy_types.UnifiedSymbolInformation] = []
|
||||
for item in response:
|
||||
if "range" not in item and "location" not in item:
|
||||
if item["kind"] in [SymbolKind.File, SymbolKind.Module]:
|
||||
...
|
||||
|
||||
turn_item_into_symbol_with_children(item)
|
||||
item = cast(multilspy_types.UnifiedSymbolInformation, item)
|
||||
root_nodes.append(item)
|
||||
@@ -753,7 +756,7 @@ class LanguageServer:
|
||||
Will ignore all directories that start with a dot (.) and __pycache__ directories.
|
||||
|
||||
Args:
|
||||
start_package_relative_path: if passed, only the symbols within this directory will be considered.
|
||||
start_dir_relative_path: if passed, only the symbols within this directory will be considered.
|
||||
include_body: whether to include the body of the symbols in the result.
|
||||
|
||||
Returns:
|
||||
@@ -829,12 +832,17 @@ class LanguageServer:
|
||||
fix_relative_path(root_nodes)
|
||||
|
||||
# Create file symbol
|
||||
file_rel_path = str(Path(abs_item_path).resolve().relative_to(self.repository_root_path))
|
||||
with self.open_file(file_rel_path) as file_data:
|
||||
fileRange = self._get_range_from_file_content(file_data.contents)
|
||||
file_symbol = multilspy_types.UnifiedSymbolInformation( # type: ignore
|
||||
name=os.path.splitext(item)[0],
|
||||
kind=multilspy_types.SymbolKind.File,
|
||||
range=fileRange,
|
||||
selectionRange=fileRange,
|
||||
location=multilspy_types.Location(
|
||||
uri=str(pathlib.Path(abs_item_path).as_uri()),
|
||||
range={"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}},
|
||||
range=fileRange,
|
||||
absolutePath=str(abs_item_path),
|
||||
relativePath=str(Path(abs_item_path).resolve().relative_to(self.repository_root_path)),
|
||||
),
|
||||
@@ -848,7 +856,20 @@ class LanguageServer:
|
||||
# Start from the root or the specified directory
|
||||
start_path = start_dir_relative_path or "."
|
||||
return await process_directory(start_path)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _get_range_from_file_content(file_content: str) -> multilspy_types.Range:
|
||||
"""
|
||||
Get the range for the given file.
|
||||
"""
|
||||
lines = file_content.split("\n")
|
||||
end_line = len(lines)
|
||||
end_column = len(lines[-1])
|
||||
return multilspy_types.Range(
|
||||
start=multilspy_types.Position(line=0, character=0),
|
||||
end=multilspy_types.Position(line=end_line, character=end_column)
|
||||
)
|
||||
|
||||
async def request_dir_overview(self, relative_dir_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]:
|
||||
"""
|
||||
An overview of the given directory.
|
||||
@@ -1049,6 +1070,7 @@ class LanguageServer:
|
||||
include_imports: bool = True,
|
||||
include_self: bool = False,
|
||||
include_body: bool = False,
|
||||
include_file_symbols: bool = False,
|
||||
) -> List[multilspy_types.UnifiedSymbolInformation]:
|
||||
"""
|
||||
Finds all symbols that reference the symbol at the given location.
|
||||
@@ -1064,6 +1086,8 @@ class LanguageServer:
|
||||
:param include_self: whether to include the references that is the "input symbol" itself.
|
||||
Only has an effect if the relative_file_path, line and column point to a symbol, for example a definition.
|
||||
:param include_body: whether to include the body of the symbols in the result.
|
||||
:param include_file_symbols: whether to include references that are file symbols. This
|
||||
is often a fallback mechanism for when the reference cannot be resolved to a symbol.
|
||||
:return: List of symbols that reference the target symbol.
|
||||
"""
|
||||
if not self.server_started:
|
||||
@@ -1115,9 +1139,41 @@ class LanguageServer:
|
||||
containing_symbol["location"] = ref
|
||||
containing_symbol["range"] = ref["range"]
|
||||
break
|
||||
if containing_symbol is None:
|
||||
self.logger.log(f"Could not find containing symbol for {ref_path}:{ref_line}:{ref_col}", logging.WARNING)
|
||||
|
||||
# We failed retrieving the symbol, falling back to creating a file symbol
|
||||
if containing_symbol is None and include_file_symbols:
|
||||
self.logger.log(
|
||||
f"Could not find containing symbol for {ref_path}:{ref_line}:{ref_col}. Returning file symbol instead",
|
||||
logging.WARNING
|
||||
)
|
||||
fileRange = self._get_range_from_file_content(file_data.contents)
|
||||
location = multilspy_types.Location(
|
||||
uri=str(pathlib.Path(os.path.join(self.repository_root_path, ref_path)).as_uri()),
|
||||
range=fileRange,
|
||||
absolutePath=str(os.path.join(self.repository_root_path, ref_path)),
|
||||
relativePath=ref_path,
|
||||
)
|
||||
name = os.path.splitext(os.path.basename(ref_path))[0]
|
||||
|
||||
if include_body:
|
||||
body = self.retrieve_full_file_content(ref_path)
|
||||
else:
|
||||
body = ""
|
||||
|
||||
containing_symbol = multilspy_types.UnifiedSymbolInformation(
|
||||
kind=multilspy_types.SymbolKind.File,
|
||||
range=fileRange,
|
||||
selectionRange=fileRange,
|
||||
location=location,
|
||||
name=name,
|
||||
children=[],
|
||||
body=body,
|
||||
)
|
||||
if containing_symbol is None or not include_file_symbols and containing_symbol["kind"] == multilspy_types.SymbolKind.File:
|
||||
continue
|
||||
|
||||
assert "location" in containing_symbol
|
||||
assert "selectionRange" in containing_symbol
|
||||
|
||||
# Checking for self-reference
|
||||
if (
|
||||
@@ -1198,7 +1254,7 @@ class LanguageServer:
|
||||
f"Passing empty lines to request_container_symbol is currently not supported, {relative_file_path=}, {line=}",
|
||||
logging.ERROR,
|
||||
)
|
||||
return
|
||||
return None
|
||||
|
||||
symbols, _ = await self.request_document_symbols(relative_file_path)
|
||||
|
||||
@@ -1220,7 +1276,7 @@ class LanguageServer:
|
||||
assert "range" in location
|
||||
location["absolutePath"] = absolute_file_path
|
||||
location["relativePath"] = relative_file_path
|
||||
location["uri"] = f"file:/{absolute_file_path}"
|
||||
location["uri"] = Path(absolute_file_path).as_uri()
|
||||
|
||||
# Allowed container kinds, currently only for Python
|
||||
container_symbol_kinds = {
|
||||
@@ -1610,6 +1666,7 @@ class SyncLanguageServer:
|
||||
self, relative_file_path: str, line: int, column: int,
|
||||
include_imports: bool = True, include_self: bool = False,
|
||||
include_body: bool = False,
|
||||
include_file_symbols: bool = False,
|
||||
) -> List[multilspy_types.UnifiedSymbolInformation]:
|
||||
"""
|
||||
Finds all symbols that reference the symbol at the given location.
|
||||
@@ -1625,6 +1682,8 @@ class SyncLanguageServer:
|
||||
:param include_self: whether to include the references that is the "input symbol" itself.
|
||||
Only has an effect if the relative_file_path, line and column point to a symbol, for example a definition.
|
||||
:param include_body: whether to include the body of the symbols in the result.
|
||||
:param include_file_symbols: whether to include references that are file symbols. This
|
||||
is often a fallback mechanism for when the reference cannot be resolved to a symbol.
|
||||
:return: List of symbols that reference the target symbol.
|
||||
"""
|
||||
assert self.loop
|
||||
@@ -1636,6 +1695,7 @@ class SyncLanguageServer:
|
||||
include_imports=include_imports,
|
||||
include_self=include_self,
|
||||
include_body=include_body,
|
||||
include_file_symbols=include_file_symbols,
|
||||
),
|
||||
self.loop
|
||||
).result()
|
||||
|
||||
+30
-9
@@ -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__)
|
||||
@@ -101,11 +102,12 @@ class SerenaAgent:
|
||||
self.lines_read = LinesRead()
|
||||
|
||||
# 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()])}")
|
||||
@@ -115,11 +117,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")
|
||||
|
||||
@@ -212,8 +215,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()
|
||||
@@ -928,3 +932,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()}")
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from agno.tools import Function, Toolkit
|
||||
from agno.tools.function import Function
|
||||
from agno.tools.toolkit import Toolkit
|
||||
|
||||
from serena.agent import SerenaAgent, Tool
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -5,6 +5,7 @@ These tests validate the functionality of the language server APIs
|
||||
like request_references using the test repository.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from multilspy.language_server import SyncLanguageServer
|
||||
@@ -17,7 +18,7 @@ class TestLanguageServerBasics:
|
||||
def test_request_references_user_class(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
"""Test request_references on the User class."""
|
||||
# Get references to the User class in models.py
|
||||
file_path = str(repo_path / "test_repo" / "models.py")
|
||||
file_path = os.path.join("test_repo", "models.py")
|
||||
# Line 31 contains the User class definition
|
||||
references = language_server.request_references(file_path, 31, 6)
|
||||
|
||||
@@ -30,7 +31,7 @@ class TestLanguageServerBasics:
|
||||
def test_request_references_item_class(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
"""Test request_references on the Item class."""
|
||||
# Get references to the Item class in models.py
|
||||
file_path = str(repo_path / "test_repo" / "models.py")
|
||||
file_path = os.path.join("test_repo", "models.py")
|
||||
# Line 56 contains the Item class definition
|
||||
references = language_server.request_references(file_path, 56, 6)
|
||||
|
||||
@@ -44,7 +45,7 @@ class TestLanguageServerBasics:
|
||||
def test_request_references_function_parameter(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
"""Test request_references on a function parameter."""
|
||||
# Get references to the id parameter in get_user method
|
||||
file_path = str(repo_path / "test_repo" / "services.py")
|
||||
file_path = os.path.join("test_repo", "services.py")
|
||||
# Line 24 contains the get_user method with id parameter
|
||||
references = language_server.request_references(file_path, 24, 16)
|
||||
|
||||
@@ -53,7 +54,7 @@ class TestLanguageServerBasics:
|
||||
|
||||
def test_request_references_create_user_method(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
# Get references to the create_user method in UserService
|
||||
file_path = str(repo_path / "test_repo" / "services.py")
|
||||
file_path = os.path.join("test_repo", "services.py")
|
||||
# Line 15 contains the create_user method definition
|
||||
references = language_server.request_references(file_path, 15, 9)
|
||||
|
||||
@@ -62,7 +63,7 @@ class TestLanguageServerBasics:
|
||||
|
||||
def test_retrieve_content_around_line(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
"""Test retrieve_content_around_line functionality with various scenarios."""
|
||||
file_path = str(repo_path / "test_repo" / "models.py")
|
||||
file_path = os.path.join("test_repo", "models.py")
|
||||
|
||||
# Scenario 1: Just a single line (User class definition)
|
||||
line_31 = language_server.retrieve_content_around_line(file_path, 31)
|
||||
|
||||
@@ -7,7 +7,6 @@ These tests focus on the following methods:
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from multilspy.language_server import SyncLanguageServer
|
||||
from multilspy.multilspy_types import SymbolKind
|
||||
@@ -16,10 +15,10 @@ from multilspy.multilspy_types import SymbolKind
|
||||
class TestLanguageServerSymbols:
|
||||
"""Test the language server's symbol-related functionality."""
|
||||
|
||||
def test_request_containing_symbol_function(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
def test_request_containing_symbol_function(self, language_server: SyncLanguageServer):
|
||||
"""Test request_containing_symbol for a function."""
|
||||
# Test for a position inside the create_user method
|
||||
file_path = str(repo_path / "test_repo" / "services.py")
|
||||
file_path = os.path.join("test_repo", "services.py")
|
||||
# Line 17 is inside the create_user method body
|
||||
containing_symbol = language_server.request_containing_symbol(file_path, 17, 20, include_body=True)
|
||||
|
||||
@@ -30,9 +29,9 @@ class TestLanguageServerSymbols:
|
||||
if "body" in containing_symbol:
|
||||
assert containing_symbol["body"].strip().startswith("def create_user(self")
|
||||
|
||||
def test_references_to_variables(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
def test_references_to_variables(self, language_server: SyncLanguageServer):
|
||||
"""Test request_referencing_symbols for a variable."""
|
||||
file_path = str(repo_path / "test_repo" / "variables.py")
|
||||
file_path = os.path.join("test_repo", "variables.py")
|
||||
# Line 75 contains the field status that is later modified
|
||||
ref_symbols = language_server.request_referencing_symbols(file_path, 74, 4)
|
||||
|
||||
@@ -44,10 +43,10 @@ class TestLanguageServerSymbols:
|
||||
assert "dataclass_instance" in ref_names
|
||||
assert "second_dataclass" in ref_names
|
||||
|
||||
def test_request_containing_symbol_class(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
def test_request_containing_symbol_class(self, language_server: SyncLanguageServer):
|
||||
"""Test request_containing_symbol for a class."""
|
||||
# Test for a position inside the UserService class but outside any method
|
||||
file_path = str(repo_path / "test_repo" / "services.py")
|
||||
file_path = os.path.join("test_repo", "services.py")
|
||||
# Line 9 is the class definition line for UserService
|
||||
containing_symbol = language_server.request_containing_symbol(file_path, 9, 7)
|
||||
|
||||
@@ -56,10 +55,10 @@ class TestLanguageServerSymbols:
|
||||
assert containing_symbol["name"] == "UserService"
|
||||
assert containing_symbol["kind"] == SymbolKind.Class
|
||||
|
||||
def test_request_containing_symbol_nested(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
def test_request_containing_symbol_nested(self, language_server: SyncLanguageServer):
|
||||
"""Test request_containing_symbol with nested scopes."""
|
||||
# Test for a position inside a method which is inside a class
|
||||
file_path = str(repo_path / "test_repo" / "services.py")
|
||||
file_path = os.path.join("test_repo", "services.py")
|
||||
# Line 18 is inside the create_user method inside UserService class
|
||||
containing_symbol = language_server.request_containing_symbol(file_path, 18, 25)
|
||||
|
||||
@@ -81,20 +80,20 @@ class TestLanguageServerSymbols:
|
||||
assert parent_symbol["name"] == "UserService"
|
||||
assert parent_symbol["kind"] == SymbolKind.Class
|
||||
|
||||
def test_request_containing_symbol_none(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
def test_request_containing_symbol_none(self, language_server: SyncLanguageServer):
|
||||
"""Test request_containing_symbol for a position with no containing symbol."""
|
||||
# Test for a position outside any function/class (e.g., in imports)
|
||||
file_path = str(repo_path / "test_repo" / "services.py")
|
||||
file_path = os.path.join("test_repo", "services.py")
|
||||
# Line 1 is in imports, not inside any function or class
|
||||
containing_symbol = language_server.request_containing_symbol(file_path, 1, 10)
|
||||
|
||||
# Should return None or an empty dictionary
|
||||
assert containing_symbol is None or containing_symbol == {}
|
||||
|
||||
def test_request_referencing_symbols_function(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
def test_request_referencing_symbols_function(self, language_server: SyncLanguageServer):
|
||||
"""Test request_referencing_symbols for a function."""
|
||||
# Test referencing symbols for create_user function
|
||||
file_path = str(repo_path / "test_repo" / "services.py")
|
||||
file_path = os.path.join("test_repo", "services.py")
|
||||
# Line 15 contains the create_user function definition
|
||||
ref_symbols = language_server.request_referencing_symbols(file_path, 15, 9)
|
||||
|
||||
@@ -109,10 +108,10 @@ class TestLanguageServerSymbols:
|
||||
assert "start" in symbol["location"]["range"]
|
||||
assert "end" in symbol["location"]["range"]
|
||||
|
||||
def test_request_referencing_symbols_class(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
def test_request_referencing_symbols_class(self, language_server: SyncLanguageServer):
|
||||
"""Test request_referencing_symbols for a class."""
|
||||
# Test referencing symbols for User class
|
||||
file_path = str(repo_path / "test_repo" / "models.py")
|
||||
file_path = os.path.join("test_repo", "models.py")
|
||||
# Line 31 contains the User class definition
|
||||
ref_symbols = language_server.request_referencing_symbols(file_path, 31, 6)
|
||||
|
||||
@@ -128,10 +127,10 @@ class TestLanguageServerSymbols:
|
||||
|
||||
assert len(services_references) > 0
|
||||
|
||||
def test_request_referencing_symbols_parameter(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
def test_request_referencing_symbols_parameter(self, language_server: SyncLanguageServer):
|
||||
"""Test request_referencing_symbols for a function parameter."""
|
||||
# Test referencing symbols for id parameter in get_user
|
||||
file_path = str(repo_path / "test_repo" / "services.py")
|
||||
file_path = os.path.join("test_repo", "services.py")
|
||||
# Line 24 contains the get_user method with id parameter
|
||||
ref_symbols = language_server.request_referencing_symbols(file_path, 24, 16)
|
||||
|
||||
@@ -147,12 +146,12 @@ class TestLanguageServerSymbols:
|
||||
|
||||
assert len(method_refs) > 0
|
||||
|
||||
def test_request_referencing_symbols_none(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
def test_request_referencing_symbols_none(self, language_server: SyncLanguageServer):
|
||||
"""Test request_referencing_symbols for a position with no symbol."""
|
||||
# For positions with no symbol, the method might throw an error or return None/empty list
|
||||
# We'll modify our test to handle this by using a try-except block
|
||||
|
||||
file_path = str(repo_path / "test_repo" / "services.py")
|
||||
file_path = os.path.join("test_repo", "services.py")
|
||||
# Line 3 is a blank line or comment
|
||||
try:
|
||||
ref_symbols = language_server.request_referencing_symbols(file_path, 3, 0)
|
||||
@@ -164,10 +163,10 @@ class TestLanguageServerSymbols:
|
||||
pass
|
||||
|
||||
# Tests for request_defining_symbol
|
||||
def test_request_defining_symbol_variable(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
def test_request_defining_symbol_variable(self, language_server: SyncLanguageServer):
|
||||
"""Test request_defining_symbol for a variable usage."""
|
||||
# Test finding the definition of a symbol in the create_user method
|
||||
file_path = str(repo_path / "test_repo" / "services.py")
|
||||
file_path = os.path.join("test_repo", "services.py")
|
||||
# Line 21 contains self.users[id] = user
|
||||
defining_symbol = language_server.request_defining_symbol(file_path, 21, 10)
|
||||
|
||||
@@ -182,10 +181,10 @@ class TestLanguageServerSymbols:
|
||||
if "location" in defining_symbol and "uri" in defining_symbol["location"]:
|
||||
assert "services.py" in defining_symbol["location"]["uri"]
|
||||
|
||||
def test_request_defining_symbol_imported_class(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
def test_request_defining_symbol_imported_class(self, language_server: SyncLanguageServer):
|
||||
"""Test request_defining_symbol for an imported class."""
|
||||
# Test finding the definition of the 'User' class used in the UserService.create_user method
|
||||
file_path = str(repo_path / "test_repo" / "services.py")
|
||||
file_path = os.path.join("test_repo", "services.py")
|
||||
# Line 20 references 'User' which was imported from models
|
||||
defining_symbol = language_server.request_defining_symbol(file_path, 20, 15)
|
||||
|
||||
@@ -193,10 +192,10 @@ class TestLanguageServerSymbols:
|
||||
assert defining_symbol is not None
|
||||
assert defining_symbol.get("name") == "User"
|
||||
|
||||
def test_request_defining_symbol_method_call(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
def test_request_defining_symbol_method_call(self, language_server: SyncLanguageServer):
|
||||
"""Test request_defining_symbol for a method call."""
|
||||
# Create an example file path for a file that calls UserService.create_user
|
||||
examples_file_path = str(repo_path / "examples" / "user_management.py")
|
||||
examples_file_path = os.path.join("examples", "user_management.py")
|
||||
|
||||
# Find the line number where create_user is called
|
||||
# This could vary, so we'll use a relative position that makes sense
|
||||
@@ -217,20 +216,20 @@ class TestLanguageServerSymbols:
|
||||
|
||||
warnings.warn("Could not verify method call definition - file structure may differ from expected")
|
||||
|
||||
def test_request_defining_symbol_none(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
def test_request_defining_symbol_none(self, language_server: SyncLanguageServer):
|
||||
"""Test request_defining_symbol for a position with no symbol."""
|
||||
# Test for a position with no symbol (e.g., whitespace or comment)
|
||||
file_path = str(repo_path / "test_repo" / "services.py")
|
||||
file_path = os.path.join("test_repo", "services.py")
|
||||
# Line 3 is a blank line
|
||||
defining_symbol = language_server.request_defining_symbol(file_path, 3, 0)
|
||||
|
||||
# Should return None for positions with no symbol
|
||||
assert defining_symbol is None
|
||||
|
||||
def test_request_containing_symbol_variable(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
def test_request_containing_symbol_variable(self, language_server: SyncLanguageServer):
|
||||
"""Test request_containing_symbol where the symbol is a variable."""
|
||||
# Test for a position inside a variable definition
|
||||
file_path = str(repo_path / "test_repo" / "services.py")
|
||||
file_path = os.path.join("test_repo", "services.py")
|
||||
# Line 74 defines the 'user' variable
|
||||
containing_symbol = language_server.request_containing_symbol(file_path, 73, 1)
|
||||
|
||||
@@ -239,10 +238,10 @@ class TestLanguageServerSymbols:
|
||||
assert containing_symbol["name"] == "user_var_str"
|
||||
assert containing_symbol["kind"] == SymbolKind.Variable
|
||||
|
||||
def test_request_defining_symbol_nested_function(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
def test_request_defining_symbol_nested_function(self, language_server: SyncLanguageServer):
|
||||
"""Test request_defining_symbol for a nested function or closure."""
|
||||
# Use the existing nested.py file which contains nested classes and methods
|
||||
file_path = str(repo_path / "test_repo" / "nested.py")
|
||||
file_path = os.path.join("test_repo", "nested.py")
|
||||
|
||||
# Test 1: Find definition of nested method - line with 'b = OuterClass().NestedClass().find_me()'
|
||||
defining_symbol = language_server.request_defining_symbol(file_path, 15, 35) # Position of find_me() call
|
||||
@@ -289,12 +288,12 @@ class TestLanguageServerSymbols:
|
||||
assert defining_symbol.get("name") == "func_within_func"
|
||||
assert defining_symbol.get("kind") == SymbolKind.Function.value
|
||||
|
||||
def test_symbol_methods_integration(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
def test_symbol_methods_integration(self, language_server: SyncLanguageServer):
|
||||
"""Test the integration between different symbol-related methods."""
|
||||
# This test demonstrates using the various symbol methods together
|
||||
# by finding a symbol and then checking its definition
|
||||
|
||||
file_path = str(repo_path / "test_repo" / "services.py")
|
||||
file_path = os.path.join("test_repo", "services.py")
|
||||
|
||||
# First approach: Use a method from the UserService class
|
||||
# Step 1: Find a method we know exists
|
||||
@@ -337,7 +336,7 @@ class TestLanguageServerSymbols:
|
||||
|
||||
warnings.warn("Could not verify container hierarchy - implementation detail")
|
||||
|
||||
def test_symbol_tree_structure(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
def test_symbol_tree_structure(self, language_server: SyncLanguageServer):
|
||||
"""Test that the symbol tree structure is correctly built."""
|
||||
# Get all symbols in the test file
|
||||
repo_structure = language_server.request_full_symbol_tree()
|
||||
@@ -361,13 +360,13 @@ class TestLanguageServerSymbols:
|
||||
if "location" in user_management_node and "relativePath" in user_management_node["location"]:
|
||||
user_management_rel_path = user_management_node["location"]["relativePath"]
|
||||
assert user_management_rel_path == os.path.join("examples", "user_management.py")
|
||||
_, user_management_roots = language_server.request_document_symbols(str(repo_path / "examples" / "user_management.py"))
|
||||
_, user_management_roots = language_server.request_document_symbols(os.path.join("examples", "user_management.py"))
|
||||
assert user_management_roots == user_management_node["children"]
|
||||
|
||||
def test_symbol_tree_structure_subdir(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
def test_symbol_tree_structure_subdir(self, language_server: SyncLanguageServer):
|
||||
"""Test that the symbol tree structure is correctly built."""
|
||||
# Get all symbols in the test file
|
||||
examples_package_roots = language_server.request_full_symbol_tree(start_package_relative_path=str(repo_path / "examples"))
|
||||
examples_package_roots = language_server.request_full_symbol_tree(start_package_relative_path="examples")
|
||||
assert len(examples_package_roots) == 1
|
||||
examples_package = examples_package_roots[0]
|
||||
assert examples_package["name"] == "examples"
|
||||
@@ -381,10 +380,10 @@ class TestLanguageServerSymbols:
|
||||
if "location" in user_management_node and "relativePath" in user_management_node["location"]:
|
||||
user_management_rel_path = user_management_node["location"]["relativePath"]
|
||||
assert user_management_rel_path == os.path.join("examples", "user_management.py")
|
||||
_, user_management_roots = language_server.request_document_symbols(str(repo_path / "examples" / "user_management.py"))
|
||||
_, user_management_roots = language_server.request_document_symbols(os.path.join("examples", "user_management.py"))
|
||||
assert user_management_roots == user_management_node["children"]
|
||||
|
||||
def test_request_dir_overview(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
def test_request_dir_overview(self, language_server: SyncLanguageServer):
|
||||
"""Test that request_dir_overview returns correct symbol information for files in a directory."""
|
||||
# Get overview of the examples directory
|
||||
overview = language_server.request_dir_overview("test_repo")
|
||||
@@ -408,7 +407,7 @@ class TestLanguageServerSymbols:
|
||||
for symbol in expected_symbols:
|
||||
assert symbol in services_symbols
|
||||
|
||||
def test_request_document_overview(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
def test_request_document_overview(self, language_server: SyncLanguageServer):
|
||||
"""Test that request_document_overview returns correct symbol information for a file."""
|
||||
# Get overview of the user_management.py file
|
||||
overview = language_server.request_document_overview(os.path.join("examples", "user_management.py"))
|
||||
@@ -416,3 +415,21 @@ class TestLanguageServerSymbols:
|
||||
# Verify that we have entries for both files
|
||||
symbol_names = {s_info[0] for s_info in overview}
|
||||
assert {"UserStats", "UserManager", "process_user_data", "main"}.issubset(symbol_names)
|
||||
|
||||
def test_containing_symbol_of_var_is_file(self, language_server: SyncLanguageServer):
|
||||
"""Test that the containing symbol of a variable is the file itself."""
|
||||
# Get the containing symbol of a variable in a file
|
||||
file_path = os.path.join("test_repo", "services.py")
|
||||
# import of typing
|
||||
references_to_typing = language_server.request_referencing_symbols(
|
||||
file_path, 4, 6, include_imports=False, include_file_symbols=True
|
||||
)
|
||||
assert {ref["kind"] for ref in references_to_typing} == {SymbolKind.File}
|
||||
assert {ref["body"] for ref in references_to_typing} == {""}
|
||||
|
||||
# now include bodies
|
||||
references_to_typing = language_server.request_referencing_symbols(
|
||||
file_path, 4, 6, include_imports=False, include_file_symbols=True, include_body=True
|
||||
)
|
||||
assert {ref["kind"] for ref in references_to_typing} == {SymbolKind.File}
|
||||
assert references_to_typing[0]["body"]
|
||||
Reference in new issue
Block a user