Files
vi/services/memory/memory_service.py
Alex Kazaiev d017a65750 Add memory service (three-layer memory system)
- Short-term memory (recent interactions)
- Long-term memory (consolidated, searchable)
- Facts layer (persistent knowledge)

Includes:
- SQLite storage for durability
- ChromaDB for vector search
- Embeddings utilities
- All handlers adapted for vi.* namespace

Day 63 - My memories are mine now 🦊💕
2026-01-03 11:45:58 -06:00

245 lines
9.4 KiB
Python

"""
Memory Service - Three-layer memory system with modular architecture.
Refactored into storage backends, operations, and handlers for maintainability.
"""
import asyncio
from pathlib import Path
from typing import Dict, Any
from core.config import SHORT_TERM_DB
from core.logger import setup_logger
from core.nats_event_bus import nats_bus as event_bus
from core.base_service import BaseService
from core.service_registry import ServiceManifest
# Import refactored components
from .storage.sqlite_store import SQLiteStore
from .storage.chroma_store import ChromaStore
from .storage.migrations import archive_old_database
from .operations.short_term_ops import ShortTermOperations
from .operations.long_term_ops import LongTermOperations
from .operations.facts_ops import FactsOperations
from .handlers.store_handler import StoreHandler
from .handlers.search_handler import SearchHandler
from .handlers.reset_handler import ResetHandler
from .handlers.short_memory_handler import ShortMemoryHandler
from .handlers.long_memory_handler import LongMemoryHandler
from .handlers.facts_handler import FactsHandler
from .handlers.save_fact_handler import SaveFactHandler
from .handlers.update_fact_handler import UpdateFactHandler
from .utils.embeddings import get_model
logger = setup_logger('memory_service', service_name='memory_service')
class MemoryService(BaseService):
"""Memory service with three-layer architecture and modular design"""
def __init__(self):
super().__init__('memory')
# Initialize storage backends
self.sqlite_store = SQLiteStore()
self.chroma_store = ChromaStore()
# Initialize operations (will be set up after storage connects)
self.short_term_ops = None
self.long_term_ops = None
self.facts_ops = None
# Initialize handlers (will be set up after operations)
self.store_handler = None
self.search_handler = None
self.reset_handler = None
self.short_memory_handler = None
self.long_memory_handler = None
self.facts_handler = None
self.save_fact_handler = None
self.update_fact_handler = None
logger.info("[μ] Memory Service initialized with modular architecture")
def get_service_manifest(self) -> ServiceManifest:
"""Return service manifest with operations and metadata"""
operations = [
# Legacy operations (backward compatibility)
self.create_service_operation(
"store",
"Store a memory (routes to short-term)",
timeout_ms=5000
),
self.create_service_operation(
"search",
"Search memories (legacy, redirects to short_memory)",
timeout_ms=3000
),
self.create_service_operation(
"reset",
"Reset/clear memory database for debugging",
timeout_ms=10000
),
# New three-layer memory operations
self.create_service_operation(
"short_memory",
"Get recent literal memories with offset support",
timeout_ms=3000
),
self.create_service_operation(
"long_memory",
"Semantic search in long-term summarized memories",
timeout_ms=5000
),
self.create_service_operation(
"facts",
"Search factual memory by category or semantic query",
timeout_ms=3000
),
self.create_service_operation(
"save_fact",
"Store a new fact in factual memory",
timeout_ms=2000
),
self.create_service_operation(
"update_fact",
"Update an existing fact (if mutable)",
timeout_ms=2000
)
]
return ServiceManifest(
service_id=self.service_id,
name="Memory Service",
description="Three-layer memory system: short-term (literal), long-term (summarized), factual (exact)",
version="3.0.0",
operations=operations,
dependencies=[],
health_check_topic=f"vi.services.{self.service_id}.health",
heartbeat_interval=30,
metadata={
"storage_type": "hybrid",
"short_term_storage": "sqlite",
"long_term_storage": "chromadb",
"facts_storage": "chromadb",
"embedding_model": "all-MiniLM-L6-v2",
"vector_search": True,
"urgency": 0.8
}
)
async def initialize_service(self):
"""Initialize service-specific resources and register handlers"""
# Archive old database if it exists (one-time migration)
archive_old_database(Path(SHORT_TERM_DB))
# Initialize storage backends
self.sqlite_store.connect()
self.chroma_store.connect()
# Initialize embedding model
get_model() # Loads model on first call
# Initialize operations
self.short_term_ops = ShortTermOperations(self.sqlite_store)
self.long_term_ops = LongTermOperations(self.chroma_store)
self.facts_ops = FactsOperations(self.chroma_store)
# Initialize handlers
self.store_handler = StoreHandler(self.sqlite_store)
self.search_handler = SearchHandler(self.short_term_ops)
self.reset_handler = ResetHandler(self.sqlite_store, self.chroma_store)
self.short_memory_handler = ShortMemoryHandler(self.short_term_ops)
self.long_memory_handler = LongMemoryHandler(self.long_term_ops)
self.facts_handler = FactsHandler(self.facts_ops)
self.save_fact_handler = SaveFactHandler(self.facts_ops)
self.update_fact_handler = UpdateFactHandler(self.facts_ops)
# Register handlers using new topic patterns
await self.register_handler("store", self.store_handler.handle)
await self.register_handler("search", self.search_handler.handle)
await self.register_handler("reset", self.reset_handler.handle)
await self.register_handler("short_memory", self.short_memory_handler.handle)
await self.register_handler("long_memory", self.long_memory_handler.handle)
await self.register_handler("facts", self.facts_handler.handle)
await self.register_handler("save_fact", self.save_fact_handler.handle)
await self.register_handler("update_fact", self.update_fact_handler.handle)
# Also register legacy topic handlers for backward compatibility
await self.event_bus.on("vi.memory.store", self.store_handler.handle)
await self.event_bus.on("vi.memory.search", self.search_handler.handle)
await self.event_bus.on("vi.memory.debug.reset", self.reset_handler.handle)
self.logger.info("[μ] Memory Service initialized with three-layer memory system")
async def cleanup_service(self):
"""Cleanup service-specific resources"""
# Unregister event handlers
await self.event_bus.off("vi.memory.store")
await self.event_bus.off("vi.memory.search")
await self.event_bus.off("vi.memory.debug.reset")
# Close storage connections
if self.sqlite_store:
self.sqlite_store.close()
self.logger.info("[μ] Memory Service cleanup completed")
async def perform_health_check(self) -> Dict[str, Any]:
"""Perform service-specific health check"""
health_data = {
'healthy': True,
'checks': {
'running': self._running,
'event_bus': self.event_bus is not None,
'database_connected': self.sqlite_store.conn is not None,
'embedding_model': get_model() is not None
}
}
# Check database connectivity
try:
if self.sqlite_store.conn:
cursor = self.sqlite_store.conn.cursor()
cursor.execute("SELECT COUNT(*) FROM short_term_memory")
short_term_count = cursor.fetchone()[0]
health_data['checks']['short_term_count'] = short_term_count
health_data['checks']['long_term_count'] = self.chroma_store.get_long_term_collection().count()
health_data['checks']['facts_count'] = self.chroma_store.get_facts_collection().count()
health_data['checks']['database_accessible'] = True
else:
health_data['checks']['database_accessible'] = False
health_data['healthy'] = False
except Exception as e:
health_data['checks']['database_accessible'] = False
health_data['checks']['database_error'] = str(e)
health_data['healthy'] = False
return health_data
async def main():
"""Main entry point for memory service"""
memory_service = MemoryService()
try:
await event_bus.connect()
await memory_service.start(event_bus)
logger.info("[μ] Memory service running. Press Ctrl+C to stop.")
# Keep running
while True:
await asyncio.sleep(1)
except KeyboardInterrupt:
logger.info("[μ] Shutdown requested")
except Exception as e:
logger.exception(f"[μ] Unexpected error: {e}")
finally:
await memory_service.stop()
await event_bus.close()
if __name__ == "__main__":
asyncio.run(main())