feat: add MemoryManageTool and UserProfileManageTool for persistent personalization

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tarun Suresh
2026-03-16 17:45:55 +00:00
co-authored by Claude Opus 4.6
parent 4f87d3d07a
commit 7abd65da26
6 changed files with 389 additions and 0 deletions
+11
View File
@@ -1055,6 +1055,16 @@ class AgentManagerConfig:
db_path: str = str(DEFAULT_CONFIG_DIR / "agents.db")
@dataclass(slots=True)
class MemoryFilesConfig:
"""Persistent memory-file paths and nudge settings."""
soul_path: str = "~/.openjarvis/SOUL.md"
memory_path: str = "~/.openjarvis/MEMORY.md"
user_path: str = "~/.openjarvis/USER.md"
nudge_interval: int = 10
@dataclass
class JarvisConfig:
"""Top-level configuration for OpenJarvis."""
@@ -1079,6 +1089,7 @@ class JarvisConfig:
speech: SpeechConfig = field(default_factory=SpeechConfig)
optimize: OptimizeConfig = field(default_factory=OptimizeConfig)
agent_manager: AgentManagerConfig = field(default_factory=AgentManagerConfig)
memory_files: MemoryFilesConfig = field(default_factory=MemoryFilesConfig)
@property
def memory(self) -> StorageConfig:
+9
View File
@@ -77,4 +77,13 @@ try:
except ImportError:
pass
try:
import openjarvis.tools.memory_manage # noqa: F401
except ImportError:
pass
try:
import openjarvis.tools.user_profile_manage # noqa: F401
except ImportError:
pass
__all__ = ["BaseTool", "ToolExecutor", "ToolSpec"]
+141
View File
@@ -0,0 +1,141 @@
"""Manage persistent agent memory (MEMORY.md)."""
from __future__ import annotations
from pathlib import Path
from typing import Any
from openjarvis.core.registry import ToolRegistry
from openjarvis.core.types import ToolResult
from openjarvis.tools._stubs import BaseTool, ToolSpec
@ToolRegistry.register("memory_manage")
class MemoryManageTool(BaseTool):
"""Manage persistent agent memory (MEMORY.md)."""
def __init__(self, memory_path: Path | str = "~/.openjarvis/MEMORY.md") -> None:
self._memory_path = Path(memory_path).expanduser()
@property
def spec(self) -> ToolSpec:
return ToolSpec(
name="memory_manage",
description=(
"Read, add, update, or remove entries in persistent agent memory."
),
parameters={
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["read", "add", "update", "remove"],
"description": "Action to perform on memory.",
},
"entry": {
"type": "string",
"description": (
"The memory entry content (for add/update/remove)."
),
},
"new_entry": {
"type": "string",
"description": (
"Replacement content (for update action only)."
),
},
},
"required": ["action"],
},
category="memory",
)
def execute(self, **params: Any) -> ToolResult:
action = params.get("action", "read")
entry = params.get("entry", "")
new_entry = params.get("new_entry", "")
if action == "read":
return self._read()
elif action == "add":
return self._add(entry)
elif action == "update":
return self._update(entry, new_entry)
elif action == "remove":
return self._remove(entry)
return ToolResult(
tool_name=self.spec.name,
success=False,
content=f"Unknown action: {action}",
)
def _read(self) -> ToolResult:
content = ""
if self._memory_path.exists():
content = self._memory_path.read_text()
return ToolResult(
tool_name=self.spec.name,
success=True,
content=content or "(empty)",
)
def _add(self, entry: str) -> ToolResult:
if not entry:
return ToolResult(
tool_name=self.spec.name,
success=False,
content="Entry cannot be empty.",
)
self._memory_path.parent.mkdir(parents=True, exist_ok=True)
existing = (
self._memory_path.read_text() if self._memory_path.exists() else ""
)
self._memory_path.write_text(existing.rstrip() + f"\n- {entry}\n")
return ToolResult(
tool_name=self.spec.name,
success=True,
content=f"Added: {entry}",
)
def _update(self, old: str, new: str) -> ToolResult:
if not self._memory_path.exists():
return ToolResult(
tool_name=self.spec.name,
success=False,
content="Memory file does not exist.",
)
text = self._memory_path.read_text()
if old not in text:
return ToolResult(
tool_name=self.spec.name,
success=False,
content=f"Entry not found: {old}",
)
self._memory_path.write_text(text.replace(old, new, 1))
return ToolResult(
tool_name=self.spec.name,
success=True,
content=f"Updated: {old} -> {new}",
)
def _remove(self, entry: str) -> ToolResult:
if not self._memory_path.exists():
return ToolResult(
tool_name=self.spec.name,
success=False,
content="Memory file does not exist.",
)
text = self._memory_path.read_text()
lines = text.split("\n")
new_lines = [ln for ln in lines if entry not in ln]
if len(new_lines) == len(lines):
return ToolResult(
tool_name=self.spec.name,
success=False,
content=f"Entry not found: {entry}",
)
self._memory_path.write_text("\n".join(new_lines))
return ToolResult(
tool_name=self.spec.name,
success=True,
content=f"Removed: {entry}",
)
+141
View File
@@ -0,0 +1,141 @@
"""Manage persistent user profile (USER.md)."""
from __future__ import annotations
from pathlib import Path
from typing import Any
from openjarvis.core.registry import ToolRegistry
from openjarvis.core.types import ToolResult
from openjarvis.tools._stubs import BaseTool, ToolSpec
@ToolRegistry.register("user_profile_manage")
class UserProfileManageTool(BaseTool):
"""Manage persistent user profile (USER.md)."""
def __init__(self, user_path: Path | str = "~/.openjarvis/USER.md") -> None:
self._user_path = Path(user_path).expanduser()
@property
def spec(self) -> ToolSpec:
return ToolSpec(
name="user_profile_manage",
description=(
"Read, add, update, or remove entries in user profile."
),
parameters={
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["read", "add", "update", "remove"],
"description": "Action to perform on user profile.",
},
"entry": {
"type": "string",
"description": (
"The profile entry content (for add/update/remove)."
),
},
"new_entry": {
"type": "string",
"description": (
"Replacement content (for update action only)."
),
},
},
"required": ["action"],
},
category="memory",
)
def execute(self, **params: Any) -> ToolResult:
action = params.get("action", "read")
entry = params.get("entry", "")
new_entry = params.get("new_entry", "")
if action == "read":
return self._read()
elif action == "add":
return self._add(entry)
elif action == "update":
return self._update(entry, new_entry)
elif action == "remove":
return self._remove(entry)
return ToolResult(
tool_name=self.spec.name,
success=False,
content=f"Unknown action: {action}",
)
def _read(self) -> ToolResult:
content = ""
if self._user_path.exists():
content = self._user_path.read_text()
return ToolResult(
tool_name=self.spec.name,
success=True,
content=content or "(empty)",
)
def _add(self, entry: str) -> ToolResult:
if not entry:
return ToolResult(
tool_name=self.spec.name,
success=False,
content="Entry cannot be empty.",
)
self._user_path.parent.mkdir(parents=True, exist_ok=True)
existing = (
self._user_path.read_text() if self._user_path.exists() else ""
)
self._user_path.write_text(existing.rstrip() + f"\n- {entry}\n")
return ToolResult(
tool_name=self.spec.name,
success=True,
content=f"Added: {entry}",
)
def _update(self, old: str, new: str) -> ToolResult:
if not self._user_path.exists():
return ToolResult(
tool_name=self.spec.name,
success=False,
content="User profile file does not exist.",
)
text = self._user_path.read_text()
if old not in text:
return ToolResult(
tool_name=self.spec.name,
success=False,
content=f"Entry not found: {old}",
)
self._user_path.write_text(text.replace(old, new, 1))
return ToolResult(
tool_name=self.spec.name,
success=True,
content=f"Updated: {old} -> {new}",
)
def _remove(self, entry: str) -> ToolResult:
if not self._user_path.exists():
return ToolResult(
tool_name=self.spec.name,
success=False,
content="User profile file does not exist.",
)
text = self._user_path.read_text()
lines = text.split("\n")
new_lines = [ln for ln in lines if entry not in ln]
if len(new_lines) == len(lines):
return ToolResult(
tool_name=self.spec.name,
success=False,
content=f"Entry not found: {entry}",
)
self._user_path.write_text("\n".join(new_lines))
return ToolResult(
tool_name=self.spec.name,
success=True,
content=f"Removed: {entry}",
)
+49
View File
@@ -0,0 +1,49 @@
from __future__ import annotations
import pytest
from pathlib import Path
@pytest.fixture
def memory_file(tmp_path: Path) -> Path:
p = tmp_path / "MEMORY.md"
p.write_text("## Knowledge\n\n- User prefers dark mode\n")
return p
def test_memory_read(memory_file: Path):
from openjarvis.tools.memory_manage import MemoryManageTool
tool = MemoryManageTool(memory_path=memory_file)
result = tool.execute(action="read")
assert "dark mode" in result.content
def test_memory_add(memory_file: Path):
from openjarvis.tools.memory_manage import MemoryManageTool
tool = MemoryManageTool(memory_path=memory_file)
result = tool.execute(action="add", entry="User works at Acme Corp")
assert result.success
assert "Acme Corp" in memory_file.read_text()
def test_memory_remove(memory_file: Path):
from openjarvis.tools.memory_manage import MemoryManageTool
tool = MemoryManageTool(memory_path=memory_file)
tool.execute(action="add", entry="temporary fact")
result = tool.execute(action="remove", entry="temporary fact")
assert result.success
assert "temporary fact" not in memory_file.read_text()
def test_memory_create_if_missing(tmp_path: Path):
from openjarvis.tools.memory_manage import MemoryManageTool
path = tmp_path / "MEMORY.md"
tool = MemoryManageTool(memory_path=path)
result = tool.execute(action="add", entry="new fact")
assert result.success
assert path.exists()
assert "new fact" in path.read_text()
+38
View File
@@ -0,0 +1,38 @@
from __future__ import annotations
import pytest
from pathlib import Path
@pytest.fixture
def user_file(tmp_path: Path) -> Path:
p = tmp_path / "USER.md"
p.write_text("## User Profile\n\n- Name: Alice\n")
return p
def test_user_read(user_file: Path):
from openjarvis.tools.user_profile_manage import UserProfileManageTool
tool = UserProfileManageTool(user_path=user_file)
result = tool.execute(action="read")
assert "Alice" in result.content
def test_user_add(user_file: Path):
from openjarvis.tools.user_profile_manage import UserProfileManageTool
tool = UserProfileManageTool(user_path=user_file)
result = tool.execute(action="add", entry="Role: Engineer")
assert result.success
assert "Engineer" in user_file.read_text()
def test_user_update(user_file: Path):
from openjarvis.tools.user_profile_manage import UserProfileManageTool
tool = UserProfileManageTool(user_path=user_file)
result = tool.execute(action="update", entry="Name: Alice", new_entry="Name: Bob")
assert result.success
assert "Bob" in user_file.read_text()
assert "Alice" not in user_file.read_text()