mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 16:15:29 +00:00
Improve read-only memory handling, introducing MemoriesList
This commit is contained in:
1 parent
57fc582606
commit
4dc617ae6b
8 files changed
+65
-38
No files matched your search
+7
-4
@@ -96,17 +96,16 @@ initial_prompt: |
|
||||
rather than mere functions (i.e. use the strategy pattern, for example).
|
||||
You avoid the use of low-level data structures in all cases where an object-oriented abstraction would be more appropriate.
|
||||
For simple data storage, you use dataclasses instead of dictionaries or tuples.
|
||||
|
||||
|
||||
You structure function implementations into functional blocks that are separated by blank lines.
|
||||
Atop each functional block, you write an elliptical phrase (starting with lower-case letter) that describes the purpose of the
|
||||
block in a concise manner.
|
||||
|
||||
|
||||
Docstrings: You consistently use reStructuredText.
|
||||
|
||||
|
||||
Comments:
|
||||
When describing parameters, methods/functions and classes, you use a precise style, where the initial (elliptical) phrase
|
||||
clearly defines *what* it is. Any details then follow in subsequent sentences.
|
||||
|
||||
# the encoding used by text files in the project
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
encoding: utf-8
|
||||
@@ -143,3 +142,7 @@ symbol_info_budget:
|
||||
# Note: the backend is fixed at startup. If a project with a different backend
|
||||
# is activated post-init, an error will be returned.
|
||||
language_backend:
|
||||
|
||||
# list of regex patterns which, when matched, mark a memory entry as read‑only.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
read_only_memory_patterns: []
|
||||
+4
-4
@@ -13,7 +13,7 @@ from typing import TYPE_CHECKING, Optional, TypeVar
|
||||
|
||||
from sensai.util import logging
|
||||
from sensai.util.logging import LogTime
|
||||
from sensai.util.string import TextBuilder, list_string
|
||||
from sensai.util.string import TextBuilder, dict_string
|
||||
|
||||
from interprompt.jinja_template import JinjaTemplate
|
||||
from serena import serena_version
|
||||
@@ -603,17 +603,17 @@ class SerenaAgent:
|
||||
def create_system_prompt(self) -> str:
|
||||
available_tools = self._active_tools
|
||||
available_markers = available_tools.tool_marker_names
|
||||
global_memory_names = MemoriesManager(
|
||||
global_memories = MemoriesManager(
|
||||
serena_data_folder=None, read_only_memory_patterns=self.serena_config.read_only_memory_patterns
|
||||
).list_global_memories()
|
||||
global_memories_list = list_string(global_memory_names) if global_memory_names else ""
|
||||
global_memories_str = dict_string(global_memories.to_dict()) if len(global_memories) > 0 else ""
|
||||
log.info("Generating system prompt with available_tools=(see active tools), available_markers=%s", available_markers)
|
||||
system_prompt = self.prompt_factory.create_system_prompt(
|
||||
context_system_prompt=self._format_prompt(self._context.prompt),
|
||||
mode_system_prompts=[self._format_prompt(mode.prompt) for mode in self.get_active_modes()],
|
||||
available_tools=available_tools.tool_names,
|
||||
available_markers=available_markers,
|
||||
global_memories_list=global_memories_list,
|
||||
global_memories_list=global_memories_str,
|
||||
)
|
||||
|
||||
# If a project is active at startup, append its activation message
|
||||
|
||||
@@ -501,7 +501,7 @@ class SerenaDashboardAPI:
|
||||
# Get available memories if ReadMemoryTool is active
|
||||
available_memories = None
|
||||
if self._agent.tool_is_active("read_memory") and project is not None:
|
||||
available_memories = project.memories_manager.list_memories()
|
||||
available_memories = project.memories_manager.list_memories().get_full_list()
|
||||
|
||||
# Get list of languages for the active project
|
||||
languages = []
|
||||
|
||||
+46
-19
@@ -45,8 +45,8 @@ class MemoriesManager:
|
||||
self._read_only_memory_patterns = [re.compile(pattern) for pattern in set(read_only_memory_patterns)]
|
||||
|
||||
def _is_read_only_memory(self, name: str) -> bool:
|
||||
for read_only_memory in self._read_only_memory_patterns:
|
||||
if read_only_memory.match(name):
|
||||
for pattern in self._read_only_memory_patterns:
|
||||
if pattern.fullmatch(name):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -87,7 +87,7 @@ class MemoriesManager:
|
||||
return self._project_memory_dir / filename
|
||||
|
||||
def _check_write_access(self, name: str, is_tool_context: bool) -> None:
|
||||
# in tool context, global memory write access can be disabled
|
||||
# in tool context, memories can be read-only
|
||||
if is_tool_context and self._is_read_only_memory(name):
|
||||
raise PermissionError(f"Attempted to write to read_only memory: '{name}')")
|
||||
|
||||
@@ -105,38 +105,64 @@ class MemoriesManager:
|
||||
f.write(content)
|
||||
return f"Memory {name} written."
|
||||
|
||||
def _list_memories(self, search_dir: Path, base_dir: Path, prefix: str = "") -> list[str]:
|
||||
class MemoriesList:
|
||||
def __init__(self) -> None:
|
||||
self.memories: list[str] = []
|
||||
self.read_only_memories: list[str] = []
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.memories) + len(self.read_only_memories)
|
||||
|
||||
def add(self, memory_name: str, is_read_only: bool) -> None:
|
||||
if is_read_only:
|
||||
self.read_only_memories.append(memory_name)
|
||||
else:
|
||||
self.memories.append(memory_name)
|
||||
|
||||
def extend(self, other: "MemoriesManager.MemoriesList") -> None:
|
||||
self.memories.extend(other.memories)
|
||||
self.read_only_memories.extend(other.read_only_memories)
|
||||
|
||||
def to_dict(self) -> dict[str, list[str]]:
|
||||
result = {}
|
||||
if self.memories:
|
||||
result["memories"] = sorted(self.memories)
|
||||
if self.read_only_memories:
|
||||
result["read_only_memories"] = sorted(self.read_only_memories)
|
||||
return result
|
||||
|
||||
def get_full_list(self) -> list[str]:
|
||||
return sorted(self.memories + self.read_only_memories)
|
||||
|
||||
def _list_memories(self, search_dir: Path, base_dir: Path, prefix: str = "") -> MemoriesList:
|
||||
result = self.MemoriesList()
|
||||
if not search_dir.exists():
|
||||
return []
|
||||
results = []
|
||||
read_only_postfix = " (read only)"
|
||||
return result
|
||||
for md_file in search_dir.rglob("*.md"):
|
||||
rel = str(md_file.relative_to(base_dir).with_suffix("")).replace(os.sep, "/")
|
||||
memory_name = prefix + rel
|
||||
if self._is_read_only_memory(memory_name):
|
||||
memory_name += read_only_postfix
|
||||
results.append(memory_name)
|
||||
return results
|
||||
result.add(memory_name, is_read_only=self._is_read_only_memory(memory_name))
|
||||
return result
|
||||
|
||||
def list_global_memories(self, subtopic: str = "") -> list[str]:
|
||||
def list_global_memories(self, subtopic: str = "") -> MemoriesList:
|
||||
dir_path = self._global_memory_dir
|
||||
if subtopic:
|
||||
dir_path = dir_path / subtopic.replace("/", os.sep)
|
||||
return self._list_memories(dir_path, self._global_memory_dir, self.GLOBAL_TOPIC + "/")
|
||||
|
||||
def list_project_memories(self, topic: str = "") -> list[str]:
|
||||
def list_project_memories(self, topic: str = "") -> MemoriesList:
|
||||
assert self._project_memory_dir is not None, "Project dir was not passed at initialization"
|
||||
dir_path = self._project_memory_dir
|
||||
if topic:
|
||||
dir_path = dir_path / topic.replace("/", os.sep)
|
||||
return self._list_memories(dir_path, self._project_memory_dir)
|
||||
|
||||
def list_memories(self, topic: str = "") -> list[str]:
|
||||
def list_memories(self, topic: str = "") -> MemoriesList:
|
||||
"""
|
||||
Lists all memories, optionally filtered by topic.
|
||||
If the topic is omitted, both global and project-specific memories are returned.
|
||||
"""
|
||||
memories: list[str]
|
||||
memories: MemoriesManager.MemoriesList
|
||||
|
||||
if topic:
|
||||
if self._is_global(topic):
|
||||
@@ -146,9 +172,10 @@ class MemoriesManager:
|
||||
else:
|
||||
memories = self.list_project_memories(topic=topic)
|
||||
else:
|
||||
memories = self.list_project_memories() + self.list_global_memories()
|
||||
memories = self.list_project_memories()
|
||||
memories.extend(self.list_global_memories())
|
||||
|
||||
return sorted(memories)
|
||||
return memories
|
||||
|
||||
def delete_memory(self, name: str, is_tool_context: bool) -> str:
|
||||
self._check_write_access(name, is_tool_context)
|
||||
@@ -219,7 +246,7 @@ class Project(ToStringMixin):
|
||||
self._serena_data_folder = serena_config.get_project_serena_folder(self.project_root)
|
||||
log.info("Serena project data folder: %s", self._serena_data_folder)
|
||||
|
||||
read_only_memory_patterns = project_config.read_only_memory_patterns
|
||||
read_only_memory_patterns = serena_config.read_only_memory_patterns + project_config.read_only_memory_patterns
|
||||
self.memories_manager = MemoriesManager(self._serena_data_folder, read_only_memory_patterns=read_only_memory_patterns)
|
||||
|
||||
self.language_server_manager: LanguageServerManager | None = None
|
||||
@@ -320,7 +347,7 @@ class Project(ToStringMixin):
|
||||
project_memories = self.memories_manager.list_project_memories()
|
||||
if project_memories:
|
||||
msg += (
|
||||
f"\nAvailable project memories: {json.dumps(project_memories)}\n"
|
||||
f"\nAvailable project memories: {json.dumps(project_memories.to_dict())}\n"
|
||||
+ "Use the `read_memory` tool to read these memories later if they are relevant to the task."
|
||||
)
|
||||
if self.project_config.initial_prompt:
|
||||
|
||||
@@ -124,5 +124,6 @@ initial_prompt: ""
|
||||
# If null or missing, use the setting from the global configuration.
|
||||
symbol_info_budget:
|
||||
|
||||
# Extends the same setting from the global configuration, merging the two lists.
|
||||
# list of regex patterns which, when matched, mark a memory entry as read‑only.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
read_only_memory_patterns: []
|
||||
@@ -68,8 +68,8 @@ ls_specific_settings: {}
|
||||
# These patterns are merged additively with each project's own ignored_paths.
|
||||
ignored_paths: []
|
||||
|
||||
# Mark memories as read-only through regex patterns.
|
||||
# For example, "global/.*" will mark global memories as read-only.
|
||||
# list of regex patterns which, when matched, mark a memory entry as read‑only.
|
||||
# For example, "global/.*" will mark all global memories as read-only.
|
||||
# You can extend the list on a per-project basis in the project.yml configuration file.
|
||||
read_only_memory_patterns: []
|
||||
|
||||
|
||||
@@ -51,15 +51,11 @@ class ListMemoriesTool(Tool):
|
||||
List available memories. Any memory can be read using the `read_memory` tool.
|
||||
"""
|
||||
|
||||
def list_memories(self, topic: str = "") -> list[str]:
|
||||
return self.memories_manager.list_memories(topic)
|
||||
|
||||
def apply(self, topic: str = "") -> str:
|
||||
"""
|
||||
List available memories, optionally filtered by topic. Memories marked as read-only cannot be
|
||||
edited or deleted.
|
||||
Lists available memories, optionally filtered by topic.
|
||||
"""
|
||||
return self._to_json(self.memories_manager.list_memories(topic))
|
||||
return self._to_json(self.memories_manager.list_memories(topic).to_dict())
|
||||
|
||||
|
||||
class DeleteMemoryTool(Tool, ToolMarkerCanEdit):
|
||||
|
||||
@@ -517,4 +517,4 @@ class TestMemoriesManagerCustomPath:
|
||||
manager.save_memory("topic_a", "content a", is_tool_context=False)
|
||||
manager.save_memory("topic_b", "content b", is_tool_context=False)
|
||||
memories = manager.list_project_memories()
|
||||
assert sorted(memories) == ["topic_a", "topic_b"]
|
||||
assert sorted(memories.get_full_list()) == ["topic_a", "topic_b"]
|
||||
Reference in new issue
Block a user