""" Memory System Configuration. Provides Pydantic settings for the Agent Memory System, including storage backends, capacity limits, and consolidation policies. """ import threading from functools import lru_cache from typing import Any from pydantic import Field, field_validator, model_validator from pydantic_settings import BaseSettings class MemorySettings(BaseSettings): """ Configuration for the Agent Memory System. All settings can be overridden via environment variables with the MEM_ prefix. """ # Working Memory Settings working_memory_backend: str = Field( default="redis", description="Backend for working memory: 'redis' or 'memory'", ) working_memory_default_ttl_seconds: int = Field( default=3600, ge=60, le=86400, description="Default TTL for working memory items (1 hour default)", ) working_memory_max_items_per_session: int = Field( default=1000, ge=100, le=100000, description="Maximum items per session in working memory", ) working_memory_max_value_size_bytes: int = Field( default=1048576, # 1MB ge=1024, le=104857600, # 100MB description="Maximum size of a single value in working memory", ) working_memory_checkpoint_enabled: bool = Field( default=True, description="Enable checkpointing for working memory recovery", ) # Redis Settings (for working memory) redis_url: str = Field( default="redis://localhost:6379/0", description="Redis connection URL", ) redis_prefix: str = Field( default="mem", description="Redis key prefix for memory items", ) redis_connection_timeout_seconds: int = Field( default=5, ge=1, le=60, description="Redis connection timeout", ) # Episodic Memory Settings episodic_max_episodes_per_project: int = Field( default=10000, ge=100, le=1000000, description="Maximum episodes to retain per project", ) episodic_default_importance: float = Field( default=0.5, ge=0.0, le=1.0, description="Default importance score for new episodes", ) episodic_retention_days: int = Field( default=365, ge=7, le=3650, description="Days to retain episodes before archival", ) # Semantic Memory Settings semantic_max_facts_per_project: int = Field( default=50000, ge=1000, le=10000000, description="Maximum facts to retain per project", ) semantic_confidence_decay_days: int = Field( default=90, ge=7, le=365, description="Days until confidence decays by 50%", ) semantic_min_confidence: float = Field( default=0.1, ge=0.0, le=1.0, description="Minimum confidence before fact is pruned", ) # Procedural Memory Settings procedural_max_procedures_per_project: int = Field( default=1000, ge=10, le=100000, description="Maximum procedures per project", ) procedural_min_success_rate: float = Field( default=0.3, ge=0.0, le=1.0, description="Minimum success rate before procedure is pruned", ) procedural_min_uses_before_suggest: int = Field( default=3, ge=1, le=100, description="Minimum uses before procedure is suggested", ) # Embedding Settings embedding_model: str = Field( default="text-embedding-3-small", description="Model to use for embeddings", ) embedding_dimensions: int = Field( default=1536, ge=256, le=4096, description="Embedding vector dimensions", ) embedding_batch_size: int = Field( default=100, ge=1, le=1000, description="Batch size for embedding generation", ) embedding_cache_enabled: bool = Field( default=True, description="Enable caching of embeddings", ) # Retrieval Settings retrieval_default_limit: int = Field( default=10, ge=1, le=100, description="Default limit for retrieval queries", ) retrieval_max_limit: int = Field( default=100, ge=10, le=1000, description="Maximum limit for retrieval queries", ) retrieval_min_similarity: float = Field( default=0.5, ge=0.0, le=1.0, description="Minimum similarity score for retrieval", ) # Consolidation Settings consolidation_enabled: bool = Field( default=True, description="Enable automatic memory consolidation", ) consolidation_batch_size: int = Field( default=100, ge=10, le=1000, description="Batch size for consolidation jobs", ) consolidation_schedule_cron: str = Field( default="0 3 * * *", description="Cron expression for nightly consolidation (3 AM)", ) consolidation_working_to_episodic_delay_minutes: int = Field( default=30, ge=5, le=1440, description="Minutes after session end before consolidating to episodic", ) # Pruning Settings pruning_enabled: bool = Field( default=True, description="Enable automatic memory pruning", ) pruning_min_age_days: int = Field( default=7, ge=1, le=365, description="Minimum age before memory can be pruned", ) pruning_importance_threshold: float = Field( default=0.2, ge=0.0, le=1.0, description="Importance threshold below which memory can be pruned", ) # Caching Settings cache_enabled: bool = Field( default=True, description="Enable caching for memory retrieval", ) cache_ttl_seconds: int = Field( default=300, ge=10, le=3600, description="Cache TTL for retrieval results", ) cache_max_items: int = Field( default=10000, ge=100, le=1000000, description="Maximum items in memory cache", ) # Performance Settings max_retrieval_time_ms: int = Field( default=100, ge=10, le=5000, description="Target maximum retrieval time in milliseconds", ) parallel_retrieval: bool = Field( default=True, description="Enable parallel retrieval from multiple memory types", ) max_parallel_retrievals: int = Field( default=4, ge=1, le=10, description="Maximum concurrent retrieval operations", ) @field_validator("working_memory_backend") @classmethod def validate_backend(cls, v: str) -> str: """Validate working memory backend.""" valid_backends = {"redis", "memory"} if v not in valid_backends: raise ValueError(f"backend must be one of: {valid_backends}") return v @field_validator("embedding_model") @classmethod def validate_embedding_model(cls, v: str) -> str: """Validate embedding model name.""" valid_models = { "text-embedding-3-small", "text-embedding-3-large", "text-embedding-ada-002", } if v not in valid_models: raise ValueError(f"embedding_model must be one of: {valid_models}") return v @model_validator(mode="after") def validate_limits(self) -> "MemorySettings": """Validate that limits are consistent.""" if self.retrieval_default_limit > self.retrieval_max_limit: raise ValueError( f"retrieval_default_limit ({self.retrieval_default_limit}) " f"cannot exceed retrieval_max_limit ({self.retrieval_max_limit})" ) return self def get_working_memory_config(self) -> dict[str, Any]: """Get working memory configuration as a dictionary.""" return { "backend": self.working_memory_backend, "default_ttl_seconds": self.working_memory_default_ttl_seconds, "max_items_per_session": self.working_memory_max_items_per_session, "max_value_size_bytes": self.working_memory_max_value_size_bytes, "checkpoint_enabled": self.working_memory_checkpoint_enabled, } def get_redis_config(self) -> dict[str, Any]: """Get Redis configuration as a dictionary.""" return { "url": self.redis_url, "prefix": self.redis_prefix, "connection_timeout_seconds": self.redis_connection_timeout_seconds, } def get_embedding_config(self) -> dict[str, Any]: """Get embedding configuration as a dictionary.""" return { "model": self.embedding_model, "dimensions": self.embedding_dimensions, "batch_size": self.embedding_batch_size, "cache_enabled": self.embedding_cache_enabled, } def get_consolidation_config(self) -> dict[str, Any]: """Get consolidation configuration as a dictionary.""" return { "enabled": self.consolidation_enabled, "batch_size": self.consolidation_batch_size, "schedule_cron": self.consolidation_schedule_cron, "working_to_episodic_delay_minutes": ( self.consolidation_working_to_episodic_delay_minutes ), } def to_dict(self) -> dict[str, Any]: """Convert settings to dictionary for logging/debugging.""" return { "working_memory": self.get_working_memory_config(), "redis": self.get_redis_config(), "episodic": { "max_episodes_per_project": self.episodic_max_episodes_per_project, "default_importance": self.episodic_default_importance, "retention_days": self.episodic_retention_days, }, "semantic": { "max_facts_per_project": self.semantic_max_facts_per_project, "confidence_decay_days": self.semantic_confidence_decay_days, "min_confidence": self.semantic_min_confidence, }, "procedural": { "max_procedures_per_project": self.procedural_max_procedures_per_project, "min_success_rate": self.procedural_min_success_rate, "min_uses_before_suggest": self.procedural_min_uses_before_suggest, }, "embedding": self.get_embedding_config(), "retrieval": { "default_limit": self.retrieval_default_limit, "max_limit": self.retrieval_max_limit, "min_similarity": self.retrieval_min_similarity, }, "consolidation": self.get_consolidation_config(), "pruning": { "enabled": self.pruning_enabled, "min_age_days": self.pruning_min_age_days, "importance_threshold": self.pruning_importance_threshold, }, "cache": { "enabled": self.cache_enabled, "ttl_seconds": self.cache_ttl_seconds, "max_items": self.cache_max_items, }, "performance": { "max_retrieval_time_ms": self.max_retrieval_time_ms, "parallel_retrieval": self.parallel_retrieval, "max_parallel_retrievals": self.max_parallel_retrievals, }, } model_config = { "env_prefix": "MEM_", "env_file": ".env", "env_file_encoding": "utf-8", "case_sensitive": False, "extra": "ignore", } # Thread-safe singleton pattern _settings: MemorySettings | None = None _settings_lock = threading.Lock() def get_memory_settings() -> MemorySettings: """ Get the global MemorySettings instance. Thread-safe with double-checked locking pattern. Returns: MemorySettings instance """ global _settings if _settings is None: with _settings_lock: if _settings is None: _settings = MemorySettings() return _settings def reset_memory_settings() -> None: """ Reset the global settings instance. Primarily used for testing. """ global _settings with _settings_lock: _settings = None @lru_cache(maxsize=1) def get_default_settings() -> MemorySettings: """ Get default settings (cached). Use this for read-only access to defaults. For mutable access, use get_memory_settings(). """ return MemorySettings()