mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 20:15:20 +00:00
feat: Add topic grouping support for memories (#1058)
This commit is contained in:
1 parent
6214b46e7b
commit
bc9bd9085e
2 files changed
+96
-29
No files matched your search
+61
-5
@@ -9,7 +9,11 @@ import pathspec
|
||||
from sensai.util.logging import LogTime
|
||||
from sensai.util.string import ToStringMixin
|
||||
|
||||
from serena.config.serena_config import DEFAULT_TOOL_TIMEOUT, ProjectConfig, get_serena_managed_in_project_dir
|
||||
from serena.config.serena_config import (
|
||||
DEFAULT_TOOL_TIMEOUT,
|
||||
ProjectConfig,
|
||||
get_serena_managed_in_project_dir,
|
||||
)
|
||||
from serena.constants import SERENA_FILE_ENCODING, SERENA_MANAGED_DIR_NAME
|
||||
from serena.ls_manager import LanguageServerFactory, LanguageServerManager
|
||||
from serena.text_utils import MatchedConsecutiveLines, search_files
|
||||
@@ -31,9 +35,19 @@ class MemoriesManager:
|
||||
self._encoding = SERENA_FILE_ENCODING
|
||||
|
||||
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.
|
||||
# Strip .md extension if present
|
||||
name = name.replace(".md", "")
|
||||
filename = f"{name}.md"
|
||||
|
||||
# Split by "/" to handle subdirectories
|
||||
parts = name.split("/")
|
||||
filename = f"{parts[-1]}.md"
|
||||
|
||||
if len(parts) > 1:
|
||||
# Create subdirectory path
|
||||
subdir = self._memory_dir / "/".join(parts[:-1])
|
||||
subdir.mkdir(parents=True, exist_ok=True)
|
||||
return subdir / filename
|
||||
|
||||
return self._memory_dir / filename
|
||||
|
||||
def load_memory(self, name: str) -> str:
|
||||
@@ -49,14 +63,56 @@ class MemoriesManager:
|
||||
f.write(content)
|
||||
return f"Memory {name} written."
|
||||
|
||||
def list_memories(self) -> list[str]:
|
||||
return [f.name.replace(".md", "") for f in self._memory_dir.iterdir() if f.is_file()]
|
||||
def list_memories(self, topic: str = "") -> list[str]:
|
||||
"""
|
||||
List memories, optionally filtered by topic.
|
||||
"""
|
||||
memories = []
|
||||
|
||||
if topic:
|
||||
# Only list memories in specified subdirectory
|
||||
search_dir = self._memory_dir / topic.replace("/", os.sep)
|
||||
if not search_dir.exists():
|
||||
return []
|
||||
else:
|
||||
search_dir = self._memory_dir
|
||||
|
||||
# Recursively find all .md files
|
||||
for md_file in search_dir.rglob("*.md"):
|
||||
# Calculate relative path as memory name
|
||||
rel_path = md_file.relative_to(self._memory_dir)
|
||||
name = str(rel_path.with_suffix("")).replace(os.sep, "/")
|
||||
memories.append(name)
|
||||
|
||||
# Sort alphabetically by name
|
||||
return sorted(memories)
|
||||
|
||||
def delete_memory(self, name: str) -> str:
|
||||
memory_file_path = self.get_memory_file_path(name)
|
||||
if not memory_file_path.exists():
|
||||
return f"Memory {name} not found."
|
||||
memory_file_path.unlink()
|
||||
return f"Memory {name} deleted."
|
||||
|
||||
def rename_memory(self, old_name: str, new_name: str) -> str:
|
||||
"""
|
||||
Rename or move a memory file.
|
||||
"""
|
||||
old_path = self.get_memory_file_path(old_name)
|
||||
new_path = self.get_memory_file_path(new_name)
|
||||
|
||||
if not old_path.exists():
|
||||
raise FileNotFoundError(f"Memory {old_name} not found.")
|
||||
if new_path.exists():
|
||||
raise FileExistsError(f"Memory {new_name} already exists.")
|
||||
|
||||
# Ensure target directory exists
|
||||
new_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Move/rename the file
|
||||
old_path.rename(new_path)
|
||||
return f"Memory renamed from {old_name} to {new_name}."
|
||||
|
||||
|
||||
class Project(ToStringMixin):
|
||||
def __init__(
|
||||
|
||||
@@ -8,21 +8,23 @@ class WriteMemoryTool(Tool, ToolMarkerCanEdit):
|
||||
Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
"""
|
||||
|
||||
def apply(self, memory_file_name: str, content: str, max_answer_chars: int = -1) -> str:
|
||||
def apply(self, memory_name: str, content: str, max_chars: int = -1) -> str:
|
||||
"""
|
||||
Write some information (utf-8-encoded) about this project that can be useful for future tasks to a memory in md format.
|
||||
The memory name should be meaningful.
|
||||
The memory name should be meaningful and can include "/" to organize into topics (e.g., "auth/login/logic").
|
||||
|
||||
:param max_chars: the maximum number of characters to write. By default, determined by the config,
|
||||
change only if instructed to do so.
|
||||
"""
|
||||
# NOTE: utf-8 encoding is configured in the MemoriesManager
|
||||
if max_answer_chars == -1:
|
||||
max_answer_chars = self.agent.serena_config.default_max_tool_answer_chars
|
||||
if len(content) > max_answer_chars:
|
||||
if max_chars == -1:
|
||||
max_chars = self.agent.serena_config.default_max_tool_answer_chars
|
||||
if len(content) > max_chars:
|
||||
raise ValueError(
|
||||
f"Content for {memory_file_name} is too long. Max length is {max_answer_chars} characters. "
|
||||
+ "Please make the content shorter."
|
||||
f"Content for {memory_name} is too long. Max length is {max_chars} characters. " + "Please make the content shorter."
|
||||
)
|
||||
|
||||
return self.memories_manager.save_memory(memory_file_name, content)
|
||||
return self.memories_manager.save_memory(memory_name, content)
|
||||
|
||||
|
||||
class ReadMemoryTool(Tool):
|
||||
@@ -30,14 +32,13 @@ class ReadMemoryTool(Tool):
|
||||
Reads the memory with the given name from Serena's project-specific memory store.
|
||||
"""
|
||||
|
||||
def apply(self, memory_file_name: str, max_answer_chars: int = -1) -> str:
|
||||
def apply(self, memory_name: str) -> str:
|
||||
"""
|
||||
Read the content of a memory file. This tool should only be used if the information
|
||||
is relevant to the current task. You can infer whether the information
|
||||
is relevant from the memory file name.
|
||||
Read the content of a memory. Should only be used if the information
|
||||
is relevant to the current task, with relevance inferred from the memory name.
|
||||
You should not read the same memory file multiple times in the same conversation.
|
||||
"""
|
||||
return self.memories_manager.load_memory(memory_file_name)
|
||||
return self.memories_manager.load_memory(memory_name)
|
||||
|
||||
|
||||
class ListMemoriesTool(Tool):
|
||||
@@ -45,11 +46,11 @@ class ListMemoriesTool(Tool):
|
||||
Lists memories in Serena's project-specific memory store.
|
||||
"""
|
||||
|
||||
def apply(self) -> str:
|
||||
def apply(self, topic: str = "") -> str:
|
||||
"""
|
||||
List available memories. Any memory can be read using the `read_memory` tool.
|
||||
List available memories, optionally filtered by topic.
|
||||
"""
|
||||
return self._to_json(self.memories_manager.list_memories())
|
||||
return self._to_json(self.memories_manager.list_memories(topic))
|
||||
|
||||
|
||||
class DeleteMemoryTool(Tool, ToolMarkerCanEdit):
|
||||
@@ -57,19 +58,29 @@ class DeleteMemoryTool(Tool, ToolMarkerCanEdit):
|
||||
Deletes a memory from Serena's project-specific memory store.
|
||||
"""
|
||||
|
||||
def apply(self, memory_file_name: str) -> str:
|
||||
def apply(self, memory_name: str) -> str:
|
||||
"""
|
||||
Delete a memory file. Should only happen if a user asks for it explicitly,
|
||||
for example by saying that the information retrieved from a memory file is no longer correct
|
||||
or no longer relevant for the project.
|
||||
Delete a memory, only call if instructed explicitly or permission was granted by the user.
|
||||
"""
|
||||
return self.memories_manager.delete_memory(memory_file_name)
|
||||
return self.memories_manager.delete_memory(memory_name)
|
||||
|
||||
|
||||
class RenameMemoryTool(Tool, ToolMarkerCanEdit):
|
||||
"""
|
||||
Renames or moves a memory in Serena's project-specific memory store.
|
||||
"""
|
||||
|
||||
def apply(self, old_name: str, new_name: str) -> str:
|
||||
"""
|
||||
Rename or move a memory, use "/" in the name to organize into topics.
|
||||
"""
|
||||
return self.memories_manager.rename_memory(old_name, new_name)
|
||||
|
||||
|
||||
class EditMemoryTool(Tool, ToolMarkerCanEdit):
|
||||
def apply(
|
||||
self,
|
||||
memory_file_name: str,
|
||||
memory_name: str,
|
||||
needle: str,
|
||||
repl: str,
|
||||
mode: Literal["literal", "regex"],
|
||||
@@ -77,7 +88,7 @@ class EditMemoryTool(Tool, ToolMarkerCanEdit):
|
||||
r"""
|
||||
Replaces content matching a regular expression in a memory.
|
||||
|
||||
:param memory_file_name: the name of the memory
|
||||
:param memory_name: the name of the memory
|
||||
:param needle: the string or regex pattern to search for.
|
||||
If `mode` is "literal", this string will be matched exactly.
|
||||
If `mode` is "regex", this string will be treated as a regular expression (syntax of Python's `re` module,
|
||||
@@ -86,5 +97,5 @@ class EditMemoryTool(Tool, ToolMarkerCanEdit):
|
||||
:param mode: either "literal" or "regex", specifying how the `needle` parameter is to be interpreted.
|
||||
"""
|
||||
replace_content_tool = self.agent.get_tool(ReplaceContentTool)
|
||||
rel_path = self.memories_manager.get_memory_file_path(memory_file_name).relative_to(self.get_project_root())
|
||||
rel_path = self.memories_manager.get_memory_file_path(memory_name).relative_to(self.get_project_root())
|
||||
return replace_content_tool.replace_content(str(rel_path), needle, repl, mode=mode, require_not_ignored=False)
|
||||
Reference in new issue
Block a user