summaryrefslogtreecommitdiff
path: root/personal_agent/memory
diff options
context:
space:
mode:
authorbatsumaru <>2026-09-15 15:13:34 +0900
committerbatsumaru <>2026-09-15 15:13:34 +0900
commit1a67c5edb6fa6165e99cc25ed380303ed817af09 (patch)
treed2eab77e7275a559bfd639fd80b15321b5e514f3 /personal_agent/memory
parent5629cabbb38c7d91e66a419ba7cb3a93a9a988a1 (diff)
Cleaned up project tree
Diffstat (limited to 'personal_agent/memory')
-rw-r--r--personal_agent/memory/__init__.py0
-rw-r--r--personal_agent/memory/models.py101
-rw-r--r--personal_agent/memory/store.py99
3 files changed, 200 insertions, 0 deletions
diff --git a/personal_agent/memory/__init__.py b/personal_agent/memory/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/personal_agent/memory/__init__.py
diff --git a/personal_agent/memory/models.py b/personal_agent/memory/models.py
new file mode 100644
index 0000000..aab2064
--- /dev/null
+++ b/personal_agent/memory/models.py
@@ -0,0 +1,101 @@
+from dataclasses import dataclass, field
+from datetime import UTC, datetime
+from enum import Enum
+from uuid import UUID, uuid7
+
+
+class MemoryCategory(Enum):
+ SEMANTIC = "semantic"
+ EPISODIC = "episodic"
+
+
+@dataclass
+class LongTermMemory:
+ id: UUID
+ content: str
+ category: MemoryCategory
+ created_at: datetime
+
+ @classmethod
+ def create(cls, content: str, category: MemoryCategory) -> LongTermMemory:
+ return cls(
+ id=uuid7(), content=content, category=category, created_at=datetime.now(UTC)
+ )
+
+
+class TaskStatus(Enum):
+ ACTIVE = "active"
+ PAUSED = "paused"
+ COMPLETED = "completed"
+
+
+@dataclass
+class WorkingMemory:
+ objective: str | None = None
+ facts: list[str] = field(default_factory=list)
+ notes: list[str] = field(default_factory=list)
+ status: TaskStatus | None = None
+
+ def __str__(self) -> str:
+ parts: list[str] = []
+
+ if self.objective:
+ parts.append(f"Objective: {self.objective}")
+
+ if self.status:
+ parts.append(f"Status: {self.status.value}")
+
+ if self.facts:
+ parts.append("Facts:")
+ parts.extend(f"- {fact}" for fact in self.facts)
+
+ if self.notes:
+ parts.append("Notes:")
+ parts.extend(f"- {note}" for note in self.notes)
+
+ return "\n".join(parts) if parts else "(empty)"
+
+ def set_objective(self, objective: str) -> None:
+ self.objective = objective
+
+ def add_fact(self, fact: str) -> None:
+ self.facts.append(fact)
+
+ def remove_fact(self, fact: str) -> None:
+ if fact in self.facts:
+ self.facts.remove(fact)
+
+ def add_note(self, note: str) -> None:
+ self.notes.append(note)
+
+ def remove_note(self, note: str) -> None:
+ if note in self.notes:
+ self.notes.remove(note)
+
+ def set_status(self, status: TaskStatus):
+ self.status = status
+
+ def clear(self) -> None:
+ self.objective = None
+ self.status = None
+ self.facts.clear()
+ self.notes.clear()
+
+ def start_task(self, objective: str) -> None:
+ self.clear()
+ self.objective = objective
+ self.set_status(TaskStatus.ACTIVE)
+
+ def pause_task(self) -> None:
+ if self.status == TaskStatus.ACTIVE:
+ self.set_status(TaskStatus.PAUSED)
+
+ def resume_task(self) -> None:
+ if self.status == TaskStatus.PAUSED:
+ self.set_status(TaskStatus.ACTIVE)
+
+ def complete_task(self) -> None:
+ self.set_status(TaskStatus.COMPLETED)
+
+ def end_task(self) -> None:
+ self.clear()
diff --git a/personal_agent/memory/store.py b/personal_agent/memory/store.py
new file mode 100644
index 0000000..e5fca96
--- /dev/null
+++ b/personal_agent/memory/store.py
@@ -0,0 +1,99 @@
+from datetime import datetime
+from uuid import UUID
+
+from personal_agent.database.database import Database
+from personal_agent.memory.models import LongTermMemory, MemoryCategory
+
+
+class LongTermMemoryStore:
+ def __init__(self, database: Database) -> None:
+ self.database = database
+
+ def add(self, memory: LongTermMemory) -> None:
+ created_at = memory.created_at.isoformat().replace("+00:00", "Z")
+
+ with self.database.connect() as connection:
+ connection.execute(
+ """
+ INSERT INTO memories (id, content, category, created_at)
+ VALUES (?, ?, ?, ?)
+ """,
+ (str(memory.id), memory.content, memory.category.value, created_at),
+ )
+
+ def get(self, memory_id: UUID) -> LongTermMemory | None:
+ with self.database.connect() as connection:
+ row = connection.execute(
+ """
+ SELECT id, content, category, created_at
+ FROM memories
+ WHERE id = ?
+ """,
+ (str(memory_id),),
+ ).fetchone()
+
+ if row is None:
+ return None
+
+ return self._from_row(row)
+
+ def list(self) -> list[LongTermMemory]:
+ with self.database.connect() as connection:
+ rows = connection.execute(
+ """
+ SELECT id, content, category, created_at
+ FROM memories
+ ORDER BY id
+ """
+ ).fetchall()
+
+ return [self._from_row(row) for row in rows]
+
+ def delete(self, memory_id: UUID) -> None:
+ with self.database.connect() as connection:
+ connection.execute(
+ "DELETE FROM memories WHERE id = ?",
+ (str(memory_id),),
+ )
+
+ def search(
+ self,
+ query: str,
+ category: MemoryCategory | None = None,
+ limit: int = 10,
+ ) -> list[LongTermMemory]:
+ if limit <= 0:
+ raise ValueError("Search limit must be positive")
+
+ if not query.strip():
+ raise ValueError("Search query must not be empty")
+
+ sql = """
+ SELECT id, content, category, created_at
+ FROM memories
+ WHERE content LIKE ?
+ """
+ parameters: list[str | int] = [f"%{query}%"]
+
+ if category is not None:
+ sql += " AND category = ?"
+ parameters.append(category.value)
+
+ sql += " ORDER BY id LIMIT ?"
+ parameters.append(limit)
+
+ with self.database.connect() as connection:
+ rows = connection.execute(sql, parameters).fetchall()
+
+ return [self._from_row(row) for row in rows]
+
+ @staticmethod
+ def _from_row(row: tuple[str, str, str, str]) -> LongTermMemory:
+ memory_id, content, category, created_at = row
+
+ return LongTermMemory(
+ id=UUID(memory_id),
+ content=content,
+ category=MemoryCategory(category),
+ created_at=datetime.fromisoformat(created_at),
+ )