refactor: drop memory/ prefix from git memory repo file paths and update core memory rendering [LET-7356] (#9395)
This commit is contained in:
committed by
Caren Thomas
parent
5fd5a6dd07
commit
bbc648909b
@@ -190,11 +190,49 @@ class Memory(BaseModel, validate_assignment=True):
|
|||||||
s.write("\n")
|
s.write("\n")
|
||||||
s.write("\n</memory_blocks>")
|
s.write("\n</memory_blocks>")
|
||||||
|
|
||||||
|
def _render_memory_blocks_git(self, s: StringIO):
|
||||||
|
"""Render memory blocks as individual file tags with YAML frontmatter.
|
||||||
|
|
||||||
|
Each block is rendered as <label.md>---frontmatter---value</label.md>,
|
||||||
|
matching the format stored in the git repo. Labels without a 'system/'
|
||||||
|
prefix get one added automatically.
|
||||||
|
"""
|
||||||
|
renderable = self._get_renderable_blocks()
|
||||||
|
if not renderable:
|
||||||
|
return
|
||||||
|
|
||||||
|
for idx, block in enumerate(renderable):
|
||||||
|
label = block.label or "block"
|
||||||
|
# Ensure system/ prefix
|
||||||
|
if not label.startswith("system/"):
|
||||||
|
label = f"system/{label}"
|
||||||
|
tag = f"{label}.md"
|
||||||
|
value = block.value or ""
|
||||||
|
|
||||||
|
s.write(f"\n\n<{tag}>\n")
|
||||||
|
|
||||||
|
# Build frontmatter (same fields as serialize_block)
|
||||||
|
front_lines = []
|
||||||
|
if block.description:
|
||||||
|
front_lines.append(f"description: {block.description}")
|
||||||
|
if block.limit is not None:
|
||||||
|
front_lines.append(f"limit: {block.limit}")
|
||||||
|
if getattr(block, "read_only", False):
|
||||||
|
front_lines.append("read_only: true")
|
||||||
|
|
||||||
|
if front_lines:
|
||||||
|
s.write("---\n")
|
||||||
|
s.write("\n".join(front_lines))
|
||||||
|
s.write("\n---\n")
|
||||||
|
|
||||||
|
s.write(f"{value}\n")
|
||||||
|
s.write(f"</{tag}>")
|
||||||
|
|
||||||
def _render_memory_filesystem(self, s: StringIO):
|
def _render_memory_filesystem(self, s: StringIO):
|
||||||
"""Render a filesystem tree view of all memory blocks.
|
"""Render a filesystem tree view of all memory blocks.
|
||||||
|
|
||||||
Only rendered for git-memory-enabled agents. Shows all blocks
|
Only rendered for git-memory-enabled agents. Uses box-drawing
|
||||||
(system and non-system) as a tree with char counts and descriptions.
|
characters (├──, └──, │) like the Unix `tree` command.
|
||||||
"""
|
"""
|
||||||
if not self.blocks:
|
if not self.blocks:
|
||||||
return
|
return
|
||||||
@@ -211,26 +249,23 @@ class Memory(BaseModel, validate_assignment=True):
|
|||||||
node = node.setdefault(part, {})
|
node = node.setdefault(part, {})
|
||||||
node[parts[-1]] = block
|
node[parts[-1]] = block
|
||||||
|
|
||||||
s.write("\n\n<memory_filesystem>\nmemory/\n")
|
s.write("\n\n<memory_filesystem>\n")
|
||||||
|
|
||||||
def _render_tree(node: dict, indent: int = 1):
|
def _render_tree(node: dict, prefix: str = ""):
|
||||||
prefix = " " * indent
|
|
||||||
# Sort: directories first, then files
|
# Sort: directories first, then files
|
||||||
dirs = sorted(k for k, v in node.items() if isinstance(v, dict))
|
dirs = sorted(k for k, v in node.items() if isinstance(v, dict))
|
||||||
files = sorted(k for k, v in node.items() if not isinstance(v, dict))
|
files = sorted(k for k, v in node.items() if not isinstance(v, dict))
|
||||||
|
entries = [(d, True) for d in dirs] + [(f, False) for f in files]
|
||||||
|
|
||||||
for d in dirs:
|
for i, (name, is_dir) in enumerate(entries):
|
||||||
s.write(f"{prefix}{d}/\n")
|
is_last = i == len(entries) - 1
|
||||||
_render_tree(node[d], indent + 1)
|
connector = "└── " if is_last else "├── "
|
||||||
|
if is_dir:
|
||||||
for f in files:
|
s.write(f"{prefix}{connector}{name}/\n")
|
||||||
block = node[f]
|
extension = " " if is_last else "│ "
|
||||||
chars = len(block.value or "")
|
_render_tree(node[name], prefix + extension)
|
||||||
desc = block.description or ""
|
else:
|
||||||
line = f"{prefix}{f}.md ({chars} chars)"
|
s.write(f"{prefix}{connector}{name}.md\n")
|
||||||
if desc:
|
|
||||||
line += f" - {desc}"
|
|
||||||
s.write(f"{line}\n")
|
|
||||||
|
|
||||||
_render_tree(tree)
|
_render_tree(tree)
|
||||||
s.write("</memory_filesystem>")
|
s.write("</memory_filesystem>")
|
||||||
@@ -353,15 +388,15 @@ class Memory(BaseModel, validate_assignment=True):
|
|||||||
|
|
||||||
# Memory blocks (not for react/workflow). Always include wrapper for preview/tests.
|
# Memory blocks (not for react/workflow). Always include wrapper for preview/tests.
|
||||||
if not is_react:
|
if not is_react:
|
||||||
if is_line_numbered:
|
if self.git_enabled:
|
||||||
|
# Git-enabled: filesystem tree + file-style block rendering
|
||||||
|
self._render_memory_filesystem(s)
|
||||||
|
self._render_memory_blocks_git(s)
|
||||||
|
elif is_line_numbered:
|
||||||
self._render_memory_blocks_line_numbered(s)
|
self._render_memory_blocks_line_numbered(s)
|
||||||
else:
|
else:
|
||||||
self._render_memory_blocks_standard(s)
|
self._render_memory_blocks_standard(s)
|
||||||
|
|
||||||
# For git-memory-enabled agents, render a filesystem tree of all blocks
|
|
||||||
if self.git_enabled:
|
|
||||||
self._render_memory_filesystem(s)
|
|
||||||
|
|
||||||
if tool_usage_rules is not None:
|
if tool_usage_rules is not None:
|
||||||
desc = getattr(tool_usage_rules, "description", None) or ""
|
desc = getattr(tool_usage_rules, "description", None) or ""
|
||||||
val = getattr(tool_usage_rules, "value", None) or ""
|
val = getattr(tool_usage_rules, "value", None) or ""
|
||||||
|
|||||||
@@ -498,10 +498,10 @@ async def _sync_after_push(actor_id: str, agent_id: str) -> None:
|
|||||||
|
|
||||||
synced = 0
|
synced = 0
|
||||||
for file_path, content in files.items():
|
for file_path, content in files.items():
|
||||||
if not file_path.startswith("memory/") or not file_path.endswith(".md"):
|
if not file_path.endswith(".md"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
label = file_path[len("memory/") : -3]
|
label = file_path[:-3]
|
||||||
expected_labels.add(label)
|
expected_labels.add(label)
|
||||||
|
|
||||||
# Parse frontmatter to extract metadata alongside value
|
# Parse frontmatter to extract metadata alongside value
|
||||||
@@ -524,12 +524,12 @@ async def _sync_after_push(actor_id: str, agent_id: str) -> None:
|
|||||||
logger.exception("Failed to sync block %s to PostgreSQL (agent=%s)", label, agent_id)
|
logger.exception("Failed to sync block %s to PostgreSQL (agent=%s)", label, agent_id)
|
||||||
|
|
||||||
if synced == 0:
|
if synced == 0:
|
||||||
logger.warning("No memory/*.md files found in repo HEAD during post-push sync (agent=%s)", agent_id)
|
logger.warning("No *.md files found in repo HEAD during post-push sync (agent=%s)", agent_id)
|
||||||
else:
|
else:
|
||||||
# Detach blocks that were removed in git.
|
# Detach blocks that were removed in git.
|
||||||
#
|
#
|
||||||
# We treat git as the source of truth for which blocks are attached to
|
# We treat git as the source of truth for which blocks are attached to
|
||||||
# this agent. If a memory/*.md file disappears from HEAD, detach the
|
# this agent. If a *.md file disappears from HEAD, detach the
|
||||||
# corresponding block from the agent in Postgres.
|
# corresponding block from the agent in Postgres.
|
||||||
try:
|
try:
|
||||||
existing_blocks = await _server_instance.agent_manager.list_agent_blocks_async(
|
existing_blocks = await _server_instance.agent_manager.list_agent_blocks_async(
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ class GitEnabledBlockManager(BlockManager):
|
|||||||
# Check which blocks are missing from repo
|
# Check which blocks are missing from repo
|
||||||
missing_blocks = []
|
missing_blocks = []
|
||||||
for block in blocks:
|
for block in blocks:
|
||||||
expected_path = f"memory/{block.label}.md"
|
expected_path = f"{block.label}.md"
|
||||||
if expected_path not in repo_files:
|
if expected_path not in repo_files:
|
||||||
missing_blocks.append(block)
|
missing_blocks.append(block)
|
||||||
|
|
||||||
@@ -552,7 +552,7 @@ class GitEnabledBlockManager(BlockManager):
|
|||||||
if self.memory_repo_manager is None:
|
if self.memory_repo_manager is None:
|
||||||
raise ValueError("Memory repo manager not configured")
|
raise ValueError("Memory repo manager not configured")
|
||||||
|
|
||||||
path = f"memory/{label}.md" if label else None
|
path = f"{label}.md" if label else None
|
||||||
return await self.memory_repo_manager.get_history_async(
|
return await self.memory_repo_manager.get_history_async(
|
||||||
agent_id=agent_id,
|
agent_id=agent_id,
|
||||||
actor=actor,
|
actor=actor,
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ from letta.utils import enforce_types
|
|||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
# File paths within the memory repository
|
# File paths within the memory repository (blocks stored at repo root as {label}.md)
|
||||||
MEMORY_DIR = "memory"
|
|
||||||
|
|
||||||
# Default local storage path
|
# Default local storage path
|
||||||
DEFAULT_LOCAL_PATH = os.path.expanduser("~/.letta/memfs")
|
DEFAULT_LOCAL_PATH = os.path.expanduser("~/.letta/memfs")
|
||||||
@@ -88,7 +87,7 @@ class MemfsClient:
|
|||||||
initial_files = {}
|
initial_files = {}
|
||||||
|
|
||||||
for block in initial_blocks:
|
for block in initial_blocks:
|
||||||
file_path = f"{MEMORY_DIR}/{block.label}.md"
|
file_path = f"{block.label}.md"
|
||||||
initial_files[file_path] = serialize_block(
|
initial_files[file_path] = serialize_block(
|
||||||
value=block.value or "",
|
value=block.value or "",
|
||||||
description=block.description,
|
description=block.description,
|
||||||
@@ -137,8 +136,8 @@ class MemfsClient:
|
|||||||
# Convert block files to PydanticBlock (metadata is in frontmatter)
|
# Convert block files to PydanticBlock (metadata is in frontmatter)
|
||||||
blocks = []
|
blocks = []
|
||||||
for file_path, content in files.items():
|
for file_path, content in files.items():
|
||||||
if file_path.startswith(f"{MEMORY_DIR}/") and file_path.endswith(".md"):
|
if file_path.endswith(".md"):
|
||||||
label = file_path[len(f"{MEMORY_DIR}/") : -3]
|
label = file_path[:-3]
|
||||||
|
|
||||||
parsed = parse_block_markdown(content)
|
parsed = parse_block_markdown(content)
|
||||||
|
|
||||||
@@ -235,7 +234,7 @@ class MemfsClient:
|
|||||||
|
|
||||||
await self._ensure_repo_exists(agent_id, actor)
|
await self._ensure_repo_exists(agent_id, actor)
|
||||||
|
|
||||||
file_path = f"{MEMORY_DIR}/{label}.md"
|
file_path = f"{label}.md"
|
||||||
file_content = serialize_block(
|
file_content = serialize_block(
|
||||||
value=value,
|
value=value,
|
||||||
description=description,
|
description=description,
|
||||||
@@ -289,7 +288,7 @@ class MemfsClient:
|
|||||||
|
|
||||||
changes = [
|
changes = [
|
||||||
FileChange(
|
FileChange(
|
||||||
path=f"{MEMORY_DIR}/{block.label}.md",
|
path=f"{block.label}.md",
|
||||||
content=file_content,
|
content=file_content,
|
||||||
change_type="add",
|
change_type="add",
|
||||||
),
|
),
|
||||||
@@ -333,7 +332,7 @@ class MemfsClient:
|
|||||||
|
|
||||||
changes = [
|
changes = [
|
||||||
FileChange(
|
FileChange(
|
||||||
path=f"{MEMORY_DIR}/{label}.md",
|
path=f"{label}.md",
|
||||||
content=None,
|
content=None,
|
||||||
change_type="delete",
|
change_type="delete",
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user