mirror of
https://github.com/tiennm99/serena.git
synced 2026-07-15 06:19:37 +00:00
Merge branch 'main' of github.com:oraios/serena
This commit is contained in:
+82
-21
@@ -35,6 +35,23 @@ from serena.util.shell import execute_shell_command
|
||||
log = logging.getLogger(__name__)
|
||||
LOG_FORMAT = "%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s"
|
||||
TTool = TypeVar("TTool", bound="Tool")
|
||||
SUCCESS_RESULT = "OK"
|
||||
|
||||
|
||||
class LinesRead:
|
||||
def __init__(self) -> None:
|
||||
self.files = defaultdict(lambda: set())
|
||||
|
||||
def add_lines_read(self, relative_path: str, lines: tuple[int, int]) -> None:
|
||||
self.files[relative_path].add(lines)
|
||||
|
||||
def were_lines_read(self, relative_path: str, lines: tuple[int, int]) -> bool:
|
||||
lines_read_in_file = self.files[relative_path]
|
||||
return lines in lines_read_in_file
|
||||
|
||||
def invalidate_lines_read(self, relative_path: str) -> None:
|
||||
if relative_path in self.files:
|
||||
del self.files[relative_path]
|
||||
|
||||
|
||||
class SerenaAgent:
|
||||
@@ -82,19 +99,20 @@ class SerenaAgent:
|
||||
self.language_server = SyncLanguageServer.create(config, logger, self.project_root)
|
||||
|
||||
self.prompt_factory = PromptFactory()
|
||||
self.symbol_manager = SymbolManager(self.language_server)
|
||||
|
||||
memories_dir = os.path.join(self.get_serena_managed_dir(), "memories")
|
||||
self.memories_manager = MemoriesManager(memories_dir)
|
||||
self.symbol_manager = SymbolManager(self.language_server, self)
|
||||
self.memories_manager = MemoriesManager(os.path.join(self.get_serena_managed_dir(), "memories"))
|
||||
self.lines_read = LinesRead()
|
||||
|
||||
# find all tool classes and instantiate them
|
||||
excluded_tools = project_config.get("excluded_tools", [])
|
||||
self._all_tools: dict[type[Tool], Tool] = {}
|
||||
self.tools: dict[type[Tool], Tool] = {}
|
||||
for tool_class in iter_tool_classes():
|
||||
tool_instance = tool_class(self)
|
||||
self._all_tools[tool_class] = tool_instance
|
||||
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()])}")
|
||||
|
||||
@@ -104,7 +122,7 @@ class SerenaAgent:
|
||||
self.language_server.start()
|
||||
|
||||
def get_tool(self, tool_class: type[TTool]) -> TTool:
|
||||
return self.tools[tool_class] # type: ignore
|
||||
return self._all_tools[tool_class] # type: ignore
|
||||
|
||||
def print_tool_overview(self) -> None:
|
||||
_print_tool_overview(self.tools.values())
|
||||
@@ -112,6 +130,9 @@ class SerenaAgent:
|
||||
def get_serena_managed_dir(self) -> str:
|
||||
return os.path.join(self.project_root, ".serena")
|
||||
|
||||
def mark_file_modified(self, relativ_path: str) -> None:
|
||||
self.lines_read.invalidate_lines_read(relativ_path)
|
||||
|
||||
def __del__(self) -> None:
|
||||
"""
|
||||
Destructor to clean up the language server instance and GUI logger
|
||||
@@ -265,13 +286,13 @@ class ReadFileTool(Tool):
|
||||
self, relative_path: str, start_line: int = 0, end_line: int | None = None, max_answer_chars: int = _DEFAULT_MAX_ANSWER_LENGTH
|
||||
) -> str:
|
||||
"""
|
||||
Reads the given file or a chunk of it (between start_line and end_line). Generally, symbolic operations
|
||||
Reads the given file or a chunk of it. Generally, symbolic operations
|
||||
like find_symbol or find_referencing_symbols should be preferred if you know which symbols you are looking for.
|
||||
Reading the entire file is only recommended if there is no other way to get the content required for the task.
|
||||
|
||||
:param relative_path: the relative path to the file to read
|
||||
:param start_line: the start line of the range to read
|
||||
:param end_line: the end line of the range to read. If None, the entire file will be read.
|
||||
:param start_line: the 0-based index of the first line to be retrieved.
|
||||
:param end_line: the 0-based index of the last line to be retrieved (inclusive). If None, read until the end of the file.
|
||||
:param max_answer_chars: if the file (chunk) is longer than this number of characters,
|
||||
no content will be returned. Don't adjust unless there is really no other way to get the content
|
||||
required for the task.
|
||||
@@ -282,7 +303,8 @@ class ReadFileTool(Tool):
|
||||
if end_line is None:
|
||||
result_lines = result_lines[start_line:]
|
||||
else:
|
||||
result_lines = result_lines[start_line:end_line]
|
||||
self.agent.lines_read.add_lines_read(relative_path, (start_line, end_line))
|
||||
result_lines = result_lines[start_line:end_line+1]
|
||||
result = "\n".join(result_lines)
|
||||
|
||||
return self._limit_length(result, max_answer_chars)
|
||||
@@ -524,7 +546,7 @@ class ReplaceSymbolBodyTool(Tool):
|
||||
SymbolLocation(relative_path, line, column),
|
||||
body=body,
|
||||
)
|
||||
return "OK"
|
||||
return SUCCESS_RESULT
|
||||
|
||||
|
||||
class InsertAfterSymbolTool(Tool):
|
||||
@@ -548,11 +570,12 @@ class InsertAfterSymbolTool(Tool):
|
||||
:param column: the column
|
||||
:param body: the body/content to be inserted
|
||||
"""
|
||||
location = SymbolLocation(relative_path, line, column)
|
||||
self.symbol_manager.insert_after(
|
||||
SymbolLocation(relative_path, line, column),
|
||||
location,
|
||||
body=body,
|
||||
)
|
||||
return "OK"
|
||||
return SUCCESS_RESULT
|
||||
|
||||
|
||||
class InsertBeforeSymbolTool(Tool):
|
||||
@@ -581,7 +604,7 @@ class InsertBeforeSymbolTool(Tool):
|
||||
SymbolLocation(relative_path, line, column),
|
||||
body=body,
|
||||
)
|
||||
return "OK"
|
||||
return SUCCESS_RESULT
|
||||
|
||||
|
||||
class DeleteLinesTool(Tool):
|
||||
@@ -596,15 +619,50 @@ class DeleteLinesTool(Tool):
|
||||
end_line: int,
|
||||
) -> str:
|
||||
"""
|
||||
Deletes the given lines in the file. An editing operation, rarely used alone but can be useful in combination with
|
||||
other tools.
|
||||
Deletes the given lines in the file.
|
||||
Requires that the same range of lines was previously read using the `read_file` tool to verify correctness
|
||||
of the operation.
|
||||
|
||||
:param relative_path: the relative path to the file
|
||||
:param start_line: the 0-based index of the first line to be deleted
|
||||
:param end_line: the 0-based index of the last line to be deleted
|
||||
"""
|
||||
if not self.agent.lines_read.were_lines_read(relative_path, (start_line, end_line)):
|
||||
read_lines_tool = self.agent.get_tool(ReadFileTool)
|
||||
return f"Error: Must call `{read_lines_tool.get_name()}` first to read exactly the affected lines."
|
||||
self.symbol_manager.delete_lines(relative_path, start_line, end_line)
|
||||
return "OK"
|
||||
return SUCCESS_RESULT
|
||||
|
||||
|
||||
class ReplaceLinesTool(Tool):
|
||||
"""
|
||||
Replaces a range of lines within a file with new content.
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
relative_path: str,
|
||||
start_line: int,
|
||||
end_line: int,
|
||||
content: str,
|
||||
) -> str:
|
||||
"""
|
||||
Replaces the given range of lines in the given file.
|
||||
Requires that the same range of lines was previously read using the `read_file` tool to verify correctness
|
||||
of the operation.
|
||||
|
||||
:param relative_path: the relative path to the file
|
||||
:param start_line: the 0-based index of the first line to be deleted
|
||||
:param end_line: the 0-based index of the last line to be deleted
|
||||
:param content: the content to insert
|
||||
"""
|
||||
if not content.endswith("\n"):
|
||||
content += "\n"
|
||||
result = self.agent.get_tool(DeleteLinesTool).apply(relative_path, start_line, end_line)
|
||||
if result != SUCCESS_RESULT:
|
||||
return result
|
||||
self.agent.get_tool(InsertAtLineTool).apply(relative_path, start_line, content)
|
||||
return SUCCESS_RESULT
|
||||
|
||||
|
||||
class InsertAtLineTool(Tool):
|
||||
@@ -619,16 +677,19 @@ class InsertAtLineTool(Tool):
|
||||
content: str,
|
||||
) -> str:
|
||||
"""
|
||||
Inserts the given content at the given line in the file. In general, symbolic insert operations like
|
||||
insert_after_symbol or insert_before_symbol should be preferred if you know which symbol you are looking for.
|
||||
Inserts the given content at the given line in the file, pushing existing content of the line down.
|
||||
In general, symbolic insert operations like insert_after_symbol or insert_before_symbol should be preferred if you know which
|
||||
symbol you are looking for.
|
||||
However, this can also be useful for small targeted edits of the body of a longer symbol (without replacing the entire body).
|
||||
|
||||
:param relative_path: the relative path to the file
|
||||
:param line: the 0-based index of the line to insert content at
|
||||
:param content: the body/content to be inserted
|
||||
:param content: the content to be inserted
|
||||
"""
|
||||
if not content.endswith("\n"):
|
||||
content += "\n"
|
||||
self.symbol_manager.insert_at_line(relative_path, line, content)
|
||||
return "OK"
|
||||
return SUCCESS_RESULT
|
||||
|
||||
|
||||
class CheckOnboardingPerformedTool(Tool):
|
||||
|
||||
@@ -4,13 +4,16 @@ from collections.abc import Iterator, Sequence
|
||||
from contextlib import contextmanager
|
||||
from copy import copy
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Any, Self
|
||||
from typing import TYPE_CHECKING, Any, Self
|
||||
|
||||
from sensai.util.string import ToStringMixin
|
||||
|
||||
from multilspy import SyncLanguageServer
|
||||
from multilspy.multilspy_types import Position, SymbolKind, UnifiedSymbolInformation
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .agent import SerenaAgent
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -187,8 +190,9 @@ class Symbol(ToStringMixin):
|
||||
|
||||
|
||||
class SymbolManager:
|
||||
def __init__(self, lang_server: SyncLanguageServer) -> None:
|
||||
def __init__(self, lang_server: SyncLanguageServer, agent: "SerenaAgent") -> None:
|
||||
self.lang_server = lang_server
|
||||
self.agent = agent
|
||||
|
||||
def _to_symbols(self, items: list[UnifiedSymbolInformation]) -> list[Symbol]:
|
||||
return [Symbol(s) for s in items]
|
||||
@@ -289,6 +293,7 @@ class SymbolManager:
|
||||
abs_path = os.path.join(root_path, relative_path)
|
||||
with open(abs_path, "w") as f:
|
||||
f.write(file_buffer.contents)
|
||||
self.agent.mark_file_modified(relative_path)
|
||||
|
||||
@contextmanager
|
||||
def _edited_symbol_location(self, location: SymbolLocation) -> Iterator[Symbol]:
|
||||
|
||||
Reference in New Issue
Block a user