126 lines
3.9 KiB
Python
126 lines
3.9 KiB
Python
# services/memory/service.py
|
|
"""Memory service — persistent memory storage and retrieval."""
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import List, Optional, Dict, Any
|
|
import os
|
|
|
|
from .memory import MemoryManager
|
|
from .memory_vector import MemoryVectorStore
|
|
|
|
|
|
@dataclass
|
|
class Memory:
|
|
"""A stored memory."""
|
|
id: str
|
|
text: str
|
|
timestamp: int
|
|
session_id: Optional[str] = None
|
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass
|
|
class MemorySearchResult:
|
|
"""Result of memory search."""
|
|
memories: List[Memory]
|
|
query: str
|
|
total: int
|
|
|
|
|
|
class MemoryService:
|
|
"""
|
|
Memory storage and retrieval service.
|
|
|
|
Usage:
|
|
service = MemoryService()
|
|
await service.remember("User prefers dark mode")
|
|
results = await service.recall("preferences")
|
|
"""
|
|
|
|
def __init__(self, data_dir: str = "data"):
|
|
self.manager = MemoryManager(data_dir)
|
|
self.vector_store = MemoryVectorStore(data_dir) if os.path.exists(
|
|
os.path.join(data_dir, "memory_vectors")
|
|
) else None
|
|
|
|
@staticmethod
|
|
def _to_memory(entry: Dict[str, Any], metadata: Optional[Dict[str, Any]] = None) -> Memory:
|
|
return Memory(
|
|
id=entry.get("id", ""),
|
|
text=entry.get("text", ""),
|
|
timestamp=entry.get("timestamp", 0),
|
|
session_id=entry.get("session_id"),
|
|
metadata=metadata or {},
|
|
)
|
|
|
|
async def remember(self, text: str, session_id: Optional[str] = None) -> Memory:
|
|
"""
|
|
Store a new memory.
|
|
|
|
Args:
|
|
text: Memory content
|
|
session_id: Optional session association
|
|
|
|
Returns:
|
|
Created Memory object
|
|
"""
|
|
entry = self.manager.add_entry(text)
|
|
if session_id:
|
|
entry["session_id"] = session_id
|
|
|
|
memories = self.manager.load_all()
|
|
memories.append(entry)
|
|
self.manager.save(memories)
|
|
|
|
# Also add to vector store if available
|
|
if self.vector_store and self.vector_store.healthy:
|
|
self.vector_store.add(entry["id"], entry["text"])
|
|
|
|
return self._to_memory(entry)
|
|
|
|
async def recall(self, query: str, top_k: int = 5) -> MemorySearchResult:
|
|
"""
|
|
Search memories.
|
|
|
|
Args:
|
|
query: Search query
|
|
top_k: Max results
|
|
|
|
Returns:
|
|
MemorySearchResult with matching memories
|
|
"""
|
|
# Try vector search first
|
|
all_memories = self.manager.load_all()
|
|
by_id = {m.get("id"): m for m in all_memories}
|
|
if self.vector_store and self.vector_store.healthy:
|
|
results = self.vector_store.search(query, k=top_k)
|
|
found = []
|
|
for result in results:
|
|
entry = by_id.get(result.get("memory_id"))
|
|
if entry:
|
|
found.append(self._to_memory(entry, metadata={"score": result.get("score")}))
|
|
if found:
|
|
return MemorySearchResult(memories=found, query=query, total=len(found))
|
|
|
|
# Fallback to keyword search
|
|
results = self.manager.get_relevant_memories(query, all_memories, max_items=top_k)
|
|
memories = [self._to_memory(m) for m in results]
|
|
return MemorySearchResult(memories=memories, query=query, total=len(memories))
|
|
|
|
def get_all(self, limit: int = 100) -> List[Memory]:
|
|
"""Get all memories."""
|
|
memories = self.manager.load_all()[:limit]
|
|
return [self._to_memory(m) for m in memories]
|
|
|
|
def delete(self, memory_id: str) -> bool:
|
|
"""Delete a memory by ID."""
|
|
memories = self.manager.load_all()
|
|
remaining = [m for m in memories if m.get("id") != memory_id]
|
|
if len(remaining) == len(memories):
|
|
return False
|
|
|
|
self.manager.save(remaining)
|
|
if self.vector_store and self.vector_store.healthy:
|
|
self.vector_store.remove(memory_id)
|
|
return True
|