feat: Add topic grouping support for memories (#1058)

This commit is contained in:
by22Jy authored and GitHub committed 2026-02-23 18:56:34 +01:00
1 parent 6214b46e7b
commit bc9bd9085e
2 files changed
+96 -29

No files matched your search

+61 -5
View File
@@ -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__(
+35 -24
View File
@@ -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)