mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-20 02:18:13 +00:00
Merge branch 'main' of github.com:oraios/serena
This commit is contained in:
commit
2033f14e2e
2 files changed
+89
-23
No files matched your search
+82
-21
@@ -35,6 +35,23 @@ from serena.util.shell import execute_shell_command
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
LOG_FORMAT = "%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s"
|
LOG_FORMAT = "%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s"
|
||||||
TTool = TypeVar("TTool", bound="Tool")
|
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:
|
class SerenaAgent:
|
||||||
@@ -82,19 +99,20 @@ class SerenaAgent:
|
|||||||
self.language_server = SyncLanguageServer.create(config, logger, self.project_root)
|
self.language_server = SyncLanguageServer.create(config, logger, self.project_root)
|
||||||
|
|
||||||
self.prompt_factory = PromptFactory()
|
self.prompt_factory = PromptFactory()
|
||||||
self.symbol_manager = SymbolManager(self.language_server)
|
self.symbol_manager = SymbolManager(self.language_server, self)
|
||||||
|
self.memories_manager = MemoriesManager(os.path.join(self.get_serena_managed_dir(), "memories"))
|
||||||
memories_dir = os.path.join(self.get_serena_managed_dir(), "memories")
|
self.lines_read = LinesRead()
|
||||||
self.memories_manager = MemoriesManager(memories_dir)
|
|
||||||
|
|
||||||
# find all tool classes and instantiate them
|
# find all tool classes and instantiate them
|
||||||
excluded_tools = project_config.get("excluded_tools", [])
|
excluded_tools = project_config.get("excluded_tools", [])
|
||||||
|
self._all_tools: dict[type[Tool], Tool] = {}
|
||||||
self.tools: dict[type[Tool], Tool] = {}
|
self.tools: dict[type[Tool], Tool] = {}
|
||||||
for tool_class in iter_tool_classes():
|
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:
|
if (tool_name := tool_class.get_name()) in excluded_tools:
|
||||||
log.info(f"Skipping tool {tool_name} because it is in the exclude list")
|
log.info(f"Skipping tool {tool_name} because it is in the exclude list")
|
||||||
continue
|
continue
|
||||||
tool_instance = tool_class(self)
|
|
||||||
self.tools[tool_class] = tool_instance
|
self.tools[tool_class] = tool_instance
|
||||||
log.info(f"Loaded tools: {', '.join([tool.get_name() for tool in self.tools.values()])}")
|
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()
|
self.language_server.start()
|
||||||
|
|
||||||
def get_tool(self, tool_class: type[TTool]) -> TTool:
|
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:
|
def print_tool_overview(self) -> None:
|
||||||
_print_tool_overview(self.tools.values())
|
_print_tool_overview(self.tools.values())
|
||||||
@@ -112,6 +130,9 @@ class SerenaAgent:
|
|||||||
def get_serena_managed_dir(self) -> str:
|
def get_serena_managed_dir(self) -> str:
|
||||||
return os.path.join(self.project_root, ".serena")
|
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:
|
def __del__(self) -> None:
|
||||||
"""
|
"""
|
||||||
Destructor to clean up the language server instance and GUI logger
|
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
|
self, relative_path: str, start_line: int = 0, end_line: int | None = None, max_answer_chars: int = _DEFAULT_MAX_ANSWER_LENGTH
|
||||||
) -> str:
|
) -> 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.
|
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.
|
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 relative_path: the relative path to the file to read
|
||||||
:param start_line: the start line of the range to read
|
:param start_line: the 0-based index of the first line to be retrieved.
|
||||||
:param end_line: the end line of the range to read. If None, the entire file will be read.
|
: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,
|
: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
|
no content will be returned. Don't adjust unless there is really no other way to get the content
|
||||||
required for the task.
|
required for the task.
|
||||||
@@ -282,7 +303,8 @@ class ReadFileTool(Tool):
|
|||||||
if end_line is None:
|
if end_line is None:
|
||||||
result_lines = result_lines[start_line:]
|
result_lines = result_lines[start_line:]
|
||||||
else:
|
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)
|
result = "\n".join(result_lines)
|
||||||
|
|
||||||
return self._limit_length(result, max_answer_chars)
|
return self._limit_length(result, max_answer_chars)
|
||||||
@@ -524,7 +546,7 @@ class ReplaceSymbolBodyTool(Tool):
|
|||||||
SymbolLocation(relative_path, line, column),
|
SymbolLocation(relative_path, line, column),
|
||||||
body=body,
|
body=body,
|
||||||
)
|
)
|
||||||
return "OK"
|
return SUCCESS_RESULT
|
||||||
|
|
||||||
|
|
||||||
class InsertAfterSymbolTool(Tool):
|
class InsertAfterSymbolTool(Tool):
|
||||||
@@ -548,11 +570,12 @@ class InsertAfterSymbolTool(Tool):
|
|||||||
:param column: the column
|
:param column: the column
|
||||||
:param body: the body/content to be inserted
|
:param body: the body/content to be inserted
|
||||||
"""
|
"""
|
||||||
|
location = SymbolLocation(relative_path, line, column)
|
||||||
self.symbol_manager.insert_after(
|
self.symbol_manager.insert_after(
|
||||||
SymbolLocation(relative_path, line, column),
|
location,
|
||||||
body=body,
|
body=body,
|
||||||
)
|
)
|
||||||
return "OK"
|
return SUCCESS_RESULT
|
||||||
|
|
||||||
|
|
||||||
class InsertBeforeSymbolTool(Tool):
|
class InsertBeforeSymbolTool(Tool):
|
||||||
@@ -581,7 +604,7 @@ class InsertBeforeSymbolTool(Tool):
|
|||||||
SymbolLocation(relative_path, line, column),
|
SymbolLocation(relative_path, line, column),
|
||||||
body=body,
|
body=body,
|
||||||
)
|
)
|
||||||
return "OK"
|
return SUCCESS_RESULT
|
||||||
|
|
||||||
|
|
||||||
class DeleteLinesTool(Tool):
|
class DeleteLinesTool(Tool):
|
||||||
@@ -596,15 +619,50 @@ class DeleteLinesTool(Tool):
|
|||||||
end_line: int,
|
end_line: int,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Deletes the given lines in the file. An editing operation, rarely used alone but can be useful in combination with
|
Deletes the given lines in the file.
|
||||||
other tools.
|
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 relative_path: the relative path to the file
|
||||||
:param start_line: the 0-based index of the first line to be deleted
|
: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 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)
|
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):
|
class InsertAtLineTool(Tool):
|
||||||
@@ -619,16 +677,19 @@ class InsertAtLineTool(Tool):
|
|||||||
content: str,
|
content: str,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Inserts the given content at the given line in the file. In general, symbolic insert operations like
|
Inserts the given content at the given line in the file, pushing existing content of the line down.
|
||||||
insert_after_symbol or insert_before_symbol should be preferred if you know which symbol you are looking for.
|
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).
|
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 relative_path: the relative path to the file
|
||||||
:param line: the 0-based index of the line to insert content at
|
: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)
|
self.symbol_manager.insert_at_line(relative_path, line, content)
|
||||||
return "OK"
|
return SUCCESS_RESULT
|
||||||
|
|
||||||
|
|
||||||
class CheckOnboardingPerformedTool(Tool):
|
class CheckOnboardingPerformedTool(Tool):
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ from collections.abc import Iterator, Sequence
|
|||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from dataclasses import asdict, dataclass
|
from dataclasses import asdict, dataclass
|
||||||
from typing import Any, Self
|
from typing import TYPE_CHECKING, Any, Self
|
||||||
|
|
||||||
from sensai.util.string import ToStringMixin
|
from sensai.util.string import ToStringMixin
|
||||||
|
|
||||||
from multilspy import SyncLanguageServer
|
from multilspy import SyncLanguageServer
|
||||||
from multilspy.multilspy_types import Position, SymbolKind, UnifiedSymbolInformation
|
from multilspy.multilspy_types import Position, SymbolKind, UnifiedSymbolInformation
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .agent import SerenaAgent
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -187,8 +190,9 @@ class Symbol(ToStringMixin):
|
|||||||
|
|
||||||
|
|
||||||
class SymbolManager:
|
class SymbolManager:
|
||||||
def __init__(self, lang_server: SyncLanguageServer) -> None:
|
def __init__(self, lang_server: SyncLanguageServer, agent: "SerenaAgent") -> None:
|
||||||
self.lang_server = lang_server
|
self.lang_server = lang_server
|
||||||
|
self.agent = agent
|
||||||
|
|
||||||
def _to_symbols(self, items: list[UnifiedSymbolInformation]) -> list[Symbol]:
|
def _to_symbols(self, items: list[UnifiedSymbolInformation]) -> list[Symbol]:
|
||||||
return [Symbol(s) for s in items]
|
return [Symbol(s) for s in items]
|
||||||
@@ -289,6 +293,7 @@ class SymbolManager:
|
|||||||
abs_path = os.path.join(root_path, relative_path)
|
abs_path = os.path.join(root_path, relative_path)
|
||||||
with open(abs_path, "w") as f:
|
with open(abs_path, "w") as f:
|
||||||
f.write(file_buffer.contents)
|
f.write(file_buffer.contents)
|
||||||
|
self.agent.mark_file_modified(relative_path)
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def _edited_symbol_location(self, location: SymbolLocation) -> Iterator[Symbol]:
|
def _edited_symbol_location(self, location: SymbolLocation) -> Iterator[Symbol]:
|
||||||
|
|||||||
Reference in new issue
Block a user