diff options
| author | batsumaru <> | 2026-09-15 16:23:26 +0900 |
|---|---|---|
| committer | batsumaru <> | 2026-09-15 16:23:26 +0900 |
| commit | 796c8978eaaf080146ba41ec49c75f3c7f3cf404 (patch) | |
| tree | e23495733dc9e9e5534c35731e1030fb2d3f5da9 /personal_agent | |
| parent | 1a67c5edb6fa6165e99cc25ed380303ed817af09 (diff) | |
Wired long-term memory into agent
Diffstat (limited to 'personal_agent')
| -rw-r--r-- | personal_agent/agent.py | 9 | ||||
| -rw-r--r-- | personal_agent/cli.py | 10 | ||||
| -rw-r--r-- | personal_agent/tools/__init__.py | 6 | ||||
| -rw-r--r-- | personal_agent/tools/memory.py | 194 |
4 files changed, 218 insertions, 1 deletions
diff --git a/personal_agent/agent.py b/personal_agent/agent.py index 73cc422..9c456a8 100644 --- a/personal_agent/agent.py +++ b/personal_agent/agent.py @@ -4,7 +4,11 @@ from personal_agent.context import ContextBuilder from personal_agent.identity import AgentIdentity, UserProfile from personal_agent.llm import LLM from personal_agent.memory.models import WorkingMemory +from personal_agent.memory.store import LongTermMemoryStore from personal_agent.tools import ( + LongTermMemoryDeleteTool, + LongTermMemorySaveTool, + LongTermMemorySearchTool, MemoryAddFactTool, MemoryAddNoteTool, MemoryRemoveFactTool, @@ -22,6 +26,7 @@ class Agent: agent_identity: AgentIdentity, user_profile: UserProfile, tools: ToolRegistry, + long_term_memory_store: LongTermMemoryStore, ) -> None: self.llm = llm self.tools = tools @@ -32,12 +37,16 @@ class Agent: ) self.working_memory = WorkingMemory() + self.long_term_memory_store = long_term_memory_store self.tools.register(MemoryAddFactTool(self.working_memory)) self.tools.register(MemoryRemoveFactTool(self.working_memory)) self.tools.register(MemoryRemoveNoteTool(self.working_memory)) self.tools.register(MemoryAddNoteTool(self.working_memory)) self.tools.register(TaskStartTool(self.working_memory)) + self.tools.register(LongTermMemorySaveTool(self.long_term_memory_store)) + self.tools.register(LongTermMemorySearchTool(self.long_term_memory_store)) + self.tools.register(LongTermMemoryDeleteTool(self.long_term_memory_store)) self.messages: list[dict[str, object]] = [] diff --git a/personal_agent/cli.py b/personal_agent/cli.py index 7bd747a..57df7b0 100644 --- a/personal_agent/cli.py +++ b/personal_agent/cli.py @@ -1,6 +1,10 @@ +from pathlib import Path + from personal_agent.agent import Agent +from personal_agent.database.database import Database from personal_agent.identity import load_agent_identity, load_user_profile from personal_agent.llm import LLM +from personal_agent.memory.store import LongTermMemoryStore from personal_agent.tools import ( CalculatorTool, FileSystemListTool, @@ -23,11 +27,17 @@ tools.register(WeatherTool()) tools.register(FileSystemListTool()) tools.register(FileSystemReadTool()) +database = Database(Path("data/agent.db")) +database.migrate() + +long_term_memory_store = LongTermMemoryStore(database) + agent = Agent( llm, agent_identity, user_profile, tools, + long_term_memory_store, ) while True: diff --git a/personal_agent/tools/__init__.py b/personal_agent/tools/__init__.py index 7c18f88..96ac65e 100644 --- a/personal_agent/tools/__init__.py +++ b/personal_agent/tools/__init__.py @@ -3,6 +3,9 @@ from personal_agent.tools.calculator import CalculatorTool from personal_agent.tools.filesystem import FileSystemListTool, FileSystemReadTool from personal_agent.tools.hello import HelloTool from personal_agent.tools.memory import ( + LongTermMemoryDeleteTool, + LongTermMemorySaveTool, + LongTermMemorySearchTool, MemoryAddFactTool, MemoryAddNoteTool, MemoryRemoveFactTool, @@ -16,6 +19,9 @@ __all__ = [ "FileSystemListTool", "FileSystemReadTool", "HelloTool", + "LongTermMemoryDeleteTool", + "LongTermMemorySaveTool", + "LongTermMemorySearchTool", "MemoryAddFactTool", "MemoryAddNoteTool", "MemoryRemoveFactTool", diff --git a/personal_agent/tools/memory.py b/personal_agent/tools/memory.py index 2287d4e..851a69d 100644 --- a/personal_agent/tools/memory.py +++ b/personal_agent/tools/memory.py @@ -1,4 +1,7 @@ -from personal_agent.memory.models import WorkingMemory +from uuid import UUID + +from personal_agent.memory.models import LongTermMemory, MemoryCategory, WorkingMemory +from personal_agent.memory.store import LongTermMemoryStore from personal_agent.tools.base import Tool, ToolError, ToolResult @@ -195,3 +198,192 @@ class MemoryRemoveNoteTool(Tool): self.memory.remove_note(note) return ToolResult(content=f"Removed note: {note}") + + +class LongTermMemorySaveTool(Tool): + def __init__(self, store: LongTermMemoryStore) -> None: + self.store = store + + @property + def name(self) -> str: + return "memory_save" + + @property + def description(self) -> str: + return ( + "Save durable information for use across future conversations. " + "Use semantic for stable facts or preferences, and episodic for " + "meaningful past events." + ) + + @property + def schema(self) -> dict[str, object]: + return { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "The durable information to remember.", + }, + "category": { + "type": "string", + "enum": ["semantic", "episodic"], + "description": ( + "semantic for stable facts/preferences; " + "episodic for meaningful past events." + ), + }, + }, + "required": ["content", "category"], + } + + def execute(self, arguments: dict[str, object]) -> ToolResult: + content = arguments.get("content") + category = arguments.get("category") + + if not isinstance(content, str): + raise ToolError("content must be a string") + + if not content.strip(): + raise ToolError("content must not be empty") + + if not isinstance(category, str): + raise ToolError("category must be a string") + + try: + memory_category = MemoryCategory(category) + except ValueError: + raise ToolError("category must be 'semantic' or 'episodic'") + + memory = LongTermMemory.create(content, memory_category) + self.store.add(memory) + + return ToolResult(content=f"Saved memory: {memory.id}") + + +class LongTermMemorySearchTool(Tool): + def __init__(self, store: LongTermMemoryStore) -> None: + self.store = store + + @property + def name(self) -> str: + return "memory_search" + + @property + def description(self) -> str: + return ( + "Search long-term memory for information relevant to the current " + "conversation. Use semantic for stable facts or preferences, and " + "episodic for meaningful past events." + ) + + @property + def schema(self) -> dict[str, object]: + return { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The information to search for.", + }, + "category": { + "type": "string", + "enum": ["semantic", "episodic"], + "description": "Optionally restrict results to one category.", + }, + "limit": { + "type": "integer", + "minimum": 1, + "description": "Maximum number of memories to return.", + }, + }, + "required": ["query"], + } + + def execute(self, arguments: dict[str, object]) -> ToolResult: + query = arguments.get("query") + category = arguments.get("category") + limit = arguments.get("limit", 10) + + if not isinstance(query, str): + raise ToolError("query must be a string") + + if not query.strip(): + raise ToolError("query must not be empty") + + if category is not None: + if not isinstance(category, str): + raise ToolError("category must be a string") + + try: + memory_category = MemoryCategory(category) + except ValueError: + raise ToolError("category must be 'semantic' or 'episodic'") + else: + memory_category = None + + if not isinstance(limit, int) or isinstance(limit, bool): + raise ToolError("limit must be an integer") + + if limit <= 0: + raise ToolError("limit must be positive") + + memories = self.store.search( + query=query, + category=memory_category, + limit=limit, + ) + + if not memories: + return ToolResult(content="No matching memories found.") + + lines = [ + f"- [{memory.category.value}] {memory.id}: {memory.content}" + for memory in memories + ] + + return ToolResult(content="\n".join(lines)) + + +class LongTermMemoryDeleteTool(Tool): + def __init__(self, store: LongTermMemoryStore) -> None: + self.store = store + + @property + def name(self) -> str: + return "memory_delete" + + @property + def description(self) -> str: + return ( + "Delete a long-term memory by its exact ID. Only delete a memory " + "when its ID is known from a previous memory search or save result." + ) + + @property + def schema(self) -> dict[str, object]: + return { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The exact UUID of the memory to delete.", + }, + }, + "required": ["id"], + } + + def execute(self, arguments: dict[str, object]) -> ToolResult: + memory_id = arguments.get("id") + + if not isinstance(memory_id, str): + raise ToolError("id must be a string") + + try: + parsed_id = UUID(memory_id) + except ValueError: + raise ToolError("id must be a valid UUID") + + self.store.delete(parsed_id) + + return ToolResult(content=f"Deleted memory: {parsed_id}") |
