diff --git a/src/serena/project.py b/src/serena/project.py index c1227523..3cd5eb08 100644 --- a/src/serena/project.py +++ b/src/serena/project.py @@ -26,21 +26,21 @@ class MemoriesManager: self._memory_dir.mkdir(parents=True, exist_ok=True) self._encoding = SERENA_FILE_ENCODING - def _get_memory_file_path(self, name: str) -> Path: + def get_memory_file_path(self, name: str) -> Path: # strip all .md from the name. Models tend to get confused, sometimes passing the .md extension and sometimes not. name = name.replace(".md", "") filename = f"{name}.md" return self._memory_dir / filename def load_memory(self, name: str) -> str: - memory_file_path = self._get_memory_file_path(name) + memory_file_path = self.get_memory_file_path(name) if not memory_file_path.exists(): return f"Memory file {name} not found, consider creating it with the `write_memory` tool if you need it." with open(memory_file_path, encoding=self._encoding) as f: return f.read() def save_memory(self, name: str, content: str) -> str: - memory_file_path = self._get_memory_file_path(name) + memory_file_path = self.get_memory_file_path(name) with open(memory_file_path, "w", encoding=self._encoding) as f: f.write(content) return f"Memory {name} written." @@ -49,7 +49,7 @@ class MemoriesManager: return [f.name.replace(".md", "") for f in self._memory_dir.iterdir() if f.is_file()] def delete_memory(self, name: str) -> str: - memory_file_path = self._get_memory_file_path(name) + memory_file_path = self.get_memory_file_path(name) memory_file_path.unlink() return f"Memory {name} deleted." diff --git a/src/serena/tools/file_tools.py b/src/serena/tools/file_tools.py index 6a7479bf..1d091535 100644 --- a/src/serena/tools/file_tools.py +++ b/src/serena/tools/file_tools.py @@ -195,7 +195,23 @@ class ReplaceRegexTool(Tool, ToolMarkerCanEdit): If this is set to False and the regex matches multiple occurrences, an error will be returned (and you may retry with a revised, more specific regex). """ - self.project.validate_relative_path(relative_path, require_not_ignored=True) + return self.replace_regex( + relative_path, regex, repl, allow_multiple_occurrences=allow_multiple_occurrences, require_not_ignored=True + ) + + def replace_regex( + self, + relative_path: str, + regex: str, + repl: str, + allow_multiple_occurrences: bool = False, + require_not_ignored: bool = True, + ) -> str: + """ + Performs the regex replacement, with additional options not exposed in the tool. + This function can be used internally by other tools. + """ + self.project.validate_relative_path(relative_path, require_not_ignored=require_not_ignored) with EditedFileContext(relative_path, self.agent) as context: original_content = context.get_original_content() updated_content, n = re.subn(regex, repl, original_content, flags=re.DOTALL | re.MULTILINE) diff --git a/src/serena/tools/memory_tools.py b/src/serena/tools/memory_tools.py index f005c8af..c6e2b93b 100644 --- a/src/serena/tools/memory_tools.py +++ b/src/serena/tools/memory_tools.py @@ -1,6 +1,6 @@ import json -from serena.tools import Tool +from serena.tools import ReplaceRegexTool, Tool class WriteMemoryTool(Tool): @@ -64,3 +64,26 @@ class DeleteMemoryTool(Tool): or no longer relevant for the project. """ return self.memories_manager.delete_memory(memory_file_name) + + +class EditMemoryTool(Tool): + def apply( + self, + memory_file_name: str, + regex: str, + repl: str, + ) -> str: + r""" + Replaces content matching a regular expression in a memory. + + :param memory_file_name: the name of the memory + :param regex: a Python-style regular expression (flags DOTALL and MULTILINE enabled, i.e. + '.' matches all characters, multi-line matching is enabled). + Apply the usual escaping as needed for reserved characters in Python-style regex. + :param repl: the string to replace the matched content with, which may contain + backreferences like \1, \2, etc. for groups matched by the regex. + Insert new content verbatim, except for backslashes, which have to be escaped. + """ + replace_regex_tool = self.agent.get_tool(ReplaceRegexTool) + rel_path = self.memories_manager.get_memory_file_path(memory_file_name).relative_to(self.get_project_root()) + return replace_regex_tool.replace_regex(str(rel_path), regex, repl, require_not_ignored=False)