diff options
| author | batsumaru <> | 2026-09-15 15:13:34 +0900 |
|---|---|---|
| committer | batsumaru <> | 2026-09-15 15:13:34 +0900 |
| commit | 1a67c5edb6fa6165e99cc25ed380303ed817af09 (patch) | |
| tree | d2eab77e7275a559bfd639fd80b15321b5e514f3 /personal_agent/memory | |
| parent | 5629cabbb38c7d91e66a419ba7cb3a93a9a988a1 (diff) | |
Cleaned up project tree
Diffstat (limited to 'personal_agent/memory')
| -rw-r--r-- | personal_agent/memory/__init__.py | 0 | ||||
| -rw-r--r-- | personal_agent/memory/models.py | 101 | ||||
| -rw-r--r-- | personal_agent/memory/store.py | 99 |
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), + ) |
