mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-31 03:12:16 +00:00
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:
co-authored by
Claude Opus 4.6
parent
4f87d3d07a
commit
7abd65da26
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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}",
|
||||
)
|
||||
@@ -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}",
|
||||
)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user