Spaces:
Runtime error
Runtime error
🥚 Initial DigiPal deployment to HuggingFace Spaces🤖 Generated with [Claude Code](https://claude.ai/code)Co-Authored-By: Claude <noreply@anthropic.com>
4399e64
| """ | |
| Enhanced memory management system for DigiPal with emotional values and RAG capabilities. | |
| This module provides comprehensive memory management including: | |
| - Memory caching for frequently accessed pet data | |
| - Emotional memory system with happiness/stress values | |
| - Simple RAG implementation for relevant memory retrieval | |
| - Memory cleanup and optimization | |
| """ | |
| import logging | |
| import json | |
| import time | |
| from datetime import datetime, timedelta | |
| from typing import Dict, List, Optional, Any, Tuple, Set | |
| from dataclasses import dataclass, field | |
| from collections import defaultdict, OrderedDict | |
| import threading | |
| import weakref | |
| import gc | |
| from .models import DigiPal, Interaction | |
| from .enums import LifeStage, InteractionResult | |
| from ..storage.storage_manager import StorageManager | |
| logger = logging.getLogger(__name__) | |
| class EmotionalMemory: | |
| """Represents a memory with emotional context and metadata.""" | |
| id: str | |
| timestamp: datetime | |
| content: str | |
| memory_type: str # 'interaction', 'action', 'event', 'detail' | |
| emotional_value: float # -1.0 (very stressful) to 1.0 (very happy) | |
| importance: float # 0.0 to 1.0, affects retention | |
| tags: Set[str] = field(default_factory=set) | |
| related_attributes: Dict[str, int] = field(default_factory=dict) | |
| access_count: int = 0 | |
| last_accessed: datetime = field(default_factory=datetime.now) | |
| def to_dict(self) -> Dict[str, Any]: | |
| """Convert to dictionary for serialization.""" | |
| return { | |
| 'id': self.id, | |
| 'timestamp': self.timestamp.isoformat(), | |
| 'content': self.content, | |
| 'memory_type': self.memory_type, | |
| 'emotional_value': self.emotional_value, | |
| 'importance': self.importance, | |
| 'tags': list(self.tags), | |
| 'related_attributes': self.related_attributes, | |
| 'access_count': self.access_count, | |
| 'last_accessed': self.last_accessed.isoformat() | |
| } | |
| def from_dict(cls, data: Dict[str, Any]) -> 'EmotionalMemory': | |
| """Create from dictionary.""" | |
| data['timestamp'] = datetime.fromisoformat(data['timestamp']) | |
| data['last_accessed'] = datetime.fromisoformat(data['last_accessed']) | |
| data['tags'] = set(data.get('tags', [])) | |
| return cls(**data) | |
| class MemoryCache: | |
| """LRU cache for frequently accessed pet data with automatic cleanup.""" | |
| def __init__(self, max_size: int = 1000, ttl_seconds: int = 3600): | |
| """ | |
| Initialize memory cache. | |
| Args: | |
| max_size: Maximum number of items to cache | |
| ttl_seconds: Time-to-live for cached items in seconds | |
| """ | |
| self.max_size = max_size | |
| self.ttl_seconds = ttl_seconds | |
| self._cache: OrderedDict = OrderedDict() | |
| self._timestamps: Dict[str, float] = {} | |
| self._lock = threading.RLock() | |
| # Start cleanup thread | |
| self._cleanup_thread = threading.Thread(target=self._cleanup_loop, daemon=True) | |
| self._stop_cleanup = False | |
| self._cleanup_thread.start() | |
| def get(self, key: str) -> Optional[Any]: | |
| """Get item from cache.""" | |
| with self._lock: | |
| current_time = time.time() | |
| # Check if item exists and is not expired | |
| if key in self._cache: | |
| if current_time - self._timestamps[key] < self.ttl_seconds: | |
| # Move to end (most recently used) | |
| self._cache.move_to_end(key) | |
| return self._cache[key] | |
| else: | |
| # Item expired, remove it | |
| del self._cache[key] | |
| del self._timestamps[key] | |
| return None | |
| def put(self, key: str, value: Any) -> None: | |
| """Put item in cache.""" | |
| with self._lock: | |
| current_time = time.time() | |
| # If key exists, update it | |
| if key in self._cache: | |
| self._cache[key] = value | |
| self._timestamps[key] = current_time | |
| self._cache.move_to_end(key) | |
| return | |
| # If cache is full, remove least recently used item | |
| if len(self._cache) >= self.max_size: | |
| oldest_key = next(iter(self._cache)) | |
| del self._cache[oldest_key] | |
| del self._timestamps[oldest_key] | |
| # Add new item | |
| self._cache[key] = value | |
| self._timestamps[key] = current_time | |
| def remove(self, key: str) -> bool: | |
| """Remove item from cache.""" | |
| with self._lock: | |
| if key in self._cache: | |
| del self._cache[key] | |
| del self._timestamps[key] | |
| return True | |
| return False | |
| def clear(self) -> None: | |
| """Clear all cached items.""" | |
| with self._lock: | |
| self._cache.clear() | |
| self._timestamps.clear() | |
| def size(self) -> int: | |
| """Get current cache size.""" | |
| with self._lock: | |
| return len(self._cache) | |
| def _cleanup_loop(self) -> None: | |
| """Background cleanup loop for expired items.""" | |
| while not self._stop_cleanup: | |
| try: | |
| current_time = time.time() | |
| expired_keys = [] | |
| with self._lock: | |
| for key, timestamp in self._timestamps.items(): | |
| if current_time - timestamp >= self.ttl_seconds: | |
| expired_keys.append(key) | |
| for key in expired_keys: | |
| if key in self._cache: | |
| del self._cache[key] | |
| if key in self._timestamps: | |
| del self._timestamps[key] | |
| if expired_keys: | |
| logger.debug(f"Cleaned up {len(expired_keys)} expired cache items") | |
| # Sleep for cleanup interval (1/4 of TTL) | |
| time.sleep(max(60, self.ttl_seconds // 4)) | |
| except Exception as e: | |
| logger.error(f"Error in cache cleanup loop: {e}") | |
| time.sleep(60) | |
| def shutdown(self) -> None: | |
| """Shutdown the cache and cleanup thread.""" | |
| self._stop_cleanup = True | |
| if self._cleanup_thread.is_alive(): | |
| self._cleanup_thread.join(timeout=5) | |
| class SimpleRAG: | |
| """Simple Retrieval-Augmented Generation for memory retrieval.""" | |
| def __init__(self, max_context_memories: int = 5): | |
| """ | |
| Initialize simple RAG system. | |
| Args: | |
| max_context_memories: Maximum memories to include in context | |
| """ | |
| self.max_context_memories = max_context_memories | |
| def retrieve_relevant_memories(self, query: str, memories: List[EmotionalMemory], | |
| current_context: Dict[str, Any]) -> List[EmotionalMemory]: | |
| """ | |
| Retrieve relevant memories for a given query using simple similarity. | |
| Args: | |
| query: User input or context query | |
| memories: Available memories to search | |
| current_context: Current pet state and context | |
| Returns: | |
| List of relevant memories sorted by relevance | |
| """ | |
| if not memories: | |
| return [] | |
| query_lower = query.lower() | |
| scored_memories = [] | |
| for memory in memories: | |
| score = self._calculate_relevance_score(memory, query_lower, current_context) | |
| if score > 0: | |
| scored_memories.append((memory, score)) | |
| # Sort by score (descending) and take top memories | |
| scored_memories.sort(key=lambda x: x[1], reverse=True) | |
| return [memory for memory, score in scored_memories[:self.max_context_memories]] | |
| def _calculate_relevance_score(self, memory: EmotionalMemory, query_lower: str, | |
| context: Dict[str, Any]) -> float: | |
| """Calculate relevance score for a memory.""" | |
| score = 0.0 | |
| # Text similarity (simple keyword matching) | |
| memory_content_lower = memory.content.lower() | |
| query_words = set(query_lower.split()) | |
| memory_words = set(memory_content_lower.split()) | |
| # Keyword overlap | |
| common_words = query_words.intersection(memory_words) | |
| if common_words: | |
| score += len(common_words) / len(query_words) * 0.4 | |
| # Tag matching | |
| query_tags = self._extract_tags_from_query(query_lower) | |
| tag_overlap = query_tags.intersection(memory.tags) | |
| if tag_overlap: | |
| score += len(tag_overlap) / max(len(query_tags), 1) * 0.3 | |
| # Recency boost (more recent memories are more relevant) | |
| hours_ago = (datetime.now() - memory.timestamp).total_seconds() / 3600 | |
| recency_score = max(0, 1 - (hours_ago / 168)) # Decay over a week | |
| score += recency_score * 0.2 | |
| # Importance boost | |
| score += memory.importance * 0.1 | |
| # Emotional relevance (memories with strong emotions are more memorable) | |
| emotional_strength = abs(memory.emotional_value) | |
| score += emotional_strength * 0.1 | |
| # Access frequency (frequently accessed memories are more relevant) | |
| access_boost = min(memory.access_count / 10, 1.0) * 0.1 | |
| score += access_boost | |
| return score | |
| def _extract_tags_from_query(self, query_lower: str) -> Set[str]: | |
| """Extract potential tags from query text.""" | |
| # Simple tag extraction based on common patterns | |
| tags = set() | |
| # Action-based tags | |
| if any(word in query_lower for word in ['eat', 'food', 'hungry', 'feed']): | |
| tags.add('eating') | |
| if any(word in query_lower for word in ['sleep', 'rest', 'tired', 'nap']): | |
| tags.add('sleeping') | |
| if any(word in query_lower for word in ['train', 'exercise', 'workout']): | |
| tags.add('training') | |
| if any(word in query_lower for word in ['play', 'fun', 'game']): | |
| tags.add('playing') | |
| if any(word in query_lower for word in ['good', 'praise', 'well done']): | |
| tags.add('praise') | |
| if any(word in query_lower for word in ['bad', 'scold', 'no']): | |
| tags.add('discipline') | |
| # Emotional tags | |
| if any(word in query_lower for word in ['happy', 'joy', 'excited']): | |
| tags.add('positive') | |
| if any(word in query_lower for word in ['sad', 'upset', 'angry']): | |
| tags.add('negative') | |
| return tags | |
| class EnhancedMemoryManager: | |
| """Enhanced memory manager with emotional values, caching, and RAG capabilities.""" | |
| def __init__(self, storage_manager: StorageManager, cache_size: int = 1000, | |
| max_memories_per_pet: int = 500): | |
| """ | |
| Initialize enhanced memory manager. | |
| Args: | |
| storage_manager: Storage manager for persistence | |
| cache_size: Size of memory cache | |
| max_memories_per_pet: Maximum memories to keep per pet | |
| """ | |
| self.storage_manager = storage_manager | |
| self.max_memories_per_pet = max_memories_per_pet | |
| # Memory cache for frequently accessed data | |
| self.memory_cache = MemoryCache(max_size=cache_size) | |
| # Pet memories storage (pet_id -> List[EmotionalMemory]) | |
| self.pet_memories: Dict[str, List[EmotionalMemory]] = defaultdict(list) | |
| # RAG system for memory retrieval | |
| self.rag_system = SimpleRAG() | |
| # Memory statistics | |
| self.memory_stats = defaultdict(lambda: { | |
| 'total_memories': 0, | |
| 'happy_memories': 0, | |
| 'stressful_memories': 0, | |
| 'neutral_memories': 0, | |
| 'last_cleanup': datetime.now() | |
| }) | |
| # Background cleanup | |
| self._cleanup_thread = None | |
| self._stop_cleanup = False | |
| logger.info("Enhanced memory manager initialized") | |
| def add_memory(self, pet_id: str, content: str, memory_type: str, | |
| emotional_value: float = 0.0, importance: float = 0.5, | |
| tags: Optional[Set[str]] = None, | |
| related_attributes: Optional[Dict[str, int]] = None) -> str: | |
| """ | |
| Add a new memory for a pet. | |
| Args: | |
| pet_id: Pet identifier | |
| content: Memory content | |
| memory_type: Type of memory ('interaction', 'action', 'event', 'detail') | |
| emotional_value: Emotional value (-1.0 to 1.0) | |
| importance: Importance level (0.0 to 1.0) | |
| tags: Optional tags for categorization | |
| related_attributes: Optional attribute changes related to this memory | |
| Returns: | |
| Memory ID | |
| """ | |
| memory_id = f"{pet_id}_{int(time.time() * 1000)}" | |
| memory = EmotionalMemory( | |
| id=memory_id, | |
| timestamp=datetime.now(), | |
| content=content, | |
| memory_type=memory_type, | |
| emotional_value=max(-1.0, min(1.0, emotional_value)), | |
| importance=max(0.0, min(1.0, importance)), | |
| tags=tags or set(), | |
| related_attributes=related_attributes or {} | |
| ) | |
| # Add to pet memories | |
| self.pet_memories[pet_id].append(memory) | |
| # Update statistics | |
| self._update_memory_stats(pet_id, memory) | |
| # Manage memory size | |
| self._manage_memory_size(pet_id) | |
| # Cache the memory | |
| self.memory_cache.put(f"memory_{memory_id}", memory) | |
| logger.debug(f"Added memory {memory_id} for pet {pet_id}: {content[:50]}...") | |
| return memory_id | |
| def add_interaction_memory(self, pet: DigiPal, interaction: Interaction) -> str: | |
| """ | |
| Add memory from an interaction with emotional context. | |
| Args: | |
| pet: DigiPal instance | |
| interaction: Interaction to convert to memory | |
| Returns: | |
| Memory ID | |
| """ | |
| # Calculate emotional value based on interaction | |
| emotional_value = self._calculate_emotional_value(interaction, pet) | |
| # Calculate importance based on success and attribute changes | |
| importance = self._calculate_importance(interaction) | |
| # Extract tags from interaction | |
| tags = self._extract_interaction_tags(interaction) | |
| # Create memory content | |
| content = f"User said: '{interaction.user_input}' - I responded: '{interaction.pet_response}'" | |
| return self.add_memory( | |
| pet_id=pet.id, | |
| content=content, | |
| memory_type='interaction', | |
| emotional_value=emotional_value, | |
| importance=importance, | |
| tags=tags, | |
| related_attributes=interaction.attribute_changes | |
| ) | |
| def add_action_memory(self, pet_id: str, action: str, result: str, | |
| attribute_changes: Dict[str, int]) -> str: | |
| """ | |
| Add memory from a care action. | |
| Args: | |
| pet_id: Pet identifier | |
| action: Action performed | |
| result: Result of the action | |
| attribute_changes: Attribute changes from action | |
| Returns: | |
| Memory ID | |
| """ | |
| # Calculate emotional value based on attribute changes | |
| emotional_value = 0.0 | |
| if 'happiness' in attribute_changes: | |
| emotional_value += attribute_changes['happiness'] / 100.0 | |
| if 'energy' in attribute_changes: | |
| emotional_value += attribute_changes['energy'] / 200.0 | |
| # Clamp emotional value | |
| emotional_value = max(-1.0, min(1.0, emotional_value)) | |
| # Calculate importance based on magnitude of changes | |
| importance = min(1.0, sum(abs(v) for v in attribute_changes.values()) / 100.0) | |
| # Extract tags | |
| tags = {action, 'action'} | |
| if emotional_value > 0.3: | |
| tags.add('positive') | |
| elif emotional_value < -0.3: | |
| tags.add('negative') | |
| content = f"Action: {action} - Result: {result}" | |
| return self.add_memory( | |
| pet_id=pet_id, | |
| content=content, | |
| memory_type='action', | |
| emotional_value=emotional_value, | |
| importance=importance, | |
| tags=tags, | |
| related_attributes=attribute_changes | |
| ) | |
| def add_life_event_memory(self, pet_id: str, event: str, emotional_impact: float = 0.0) -> str: | |
| """ | |
| Add memory for significant life events (evolution, achievements, etc.). | |
| Args: | |
| pet_id: Pet identifier | |
| event: Event description | |
| emotional_impact: Emotional impact of the event | |
| Returns: | |
| Memory ID | |
| """ | |
| return self.add_memory( | |
| pet_id=pet_id, | |
| content=event, | |
| memory_type='event', | |
| emotional_value=emotional_impact, | |
| importance=0.9, # Life events are usually important | |
| tags={'life_event', 'milestone'} | |
| ) | |
| def get_relevant_memories(self, pet_id: str, query: str, | |
| current_context: Optional[Dict[str, Any]] = None) -> List[EmotionalMemory]: | |
| """ | |
| Get relevant memories for a query using RAG. | |
| Args: | |
| pet_id: Pet identifier | |
| query: Query text | |
| current_context: Current pet state context | |
| Returns: | |
| List of relevant memories | |
| """ | |
| memories = self.pet_memories.get(pet_id, []) | |
| if not memories: | |
| return [] | |
| context = current_context or {} | |
| relevant_memories = self.rag_system.retrieve_relevant_memories(query, memories, context) | |
| # Update access counts for retrieved memories | |
| for memory in relevant_memories: | |
| memory.access_count += 1 | |
| memory.last_accessed = datetime.now() | |
| return relevant_memories | |
| def get_memory_context_for_llm(self, pet_id: str, query: str, | |
| current_context: Optional[Dict[str, Any]] = None) -> str: | |
| """ | |
| Get formatted memory context for LLM input. | |
| Args: | |
| pet_id: Pet identifier | |
| query: Current query | |
| current_context: Current pet state | |
| Returns: | |
| Formatted memory context string | |
| """ | |
| relevant_memories = self.get_relevant_memories(pet_id, query, current_context) | |
| if not relevant_memories: | |
| return "" | |
| context_parts = ["Recent relevant memories:"] | |
| for memory in relevant_memories: | |
| # Format memory with emotional context | |
| emotional_indicator = "" | |
| if memory.emotional_value > 0.3: | |
| emotional_indicator = " (happy memory)" | |
| elif memory.emotional_value < -0.3: | |
| emotional_indicator = " (stressful memory)" | |
| time_ago = self._format_time_ago(memory.timestamp) | |
| context_parts.append(f"- {time_ago}: {memory.content}{emotional_indicator}") | |
| return "\n".join(context_parts) | |
| def get_emotional_state_summary(self, pet_id: str) -> Dict[str, Any]: | |
| """ | |
| Get emotional state summary based on recent memories. | |
| Args: | |
| pet_id: Pet identifier | |
| Returns: | |
| Emotional state summary | |
| """ | |
| memories = self.pet_memories.get(pet_id, []) | |
| if not memories: | |
| return {'overall_mood': 'neutral', 'recent_trend': 'stable'} | |
| # Analyze recent memories (last 24 hours) | |
| recent_cutoff = datetime.now() - timedelta(hours=24) | |
| recent_memories = [m for m in memories if m.timestamp > recent_cutoff] | |
| if not recent_memories: | |
| recent_memories = memories[-10:] # Use last 10 if no recent ones | |
| # Calculate emotional metrics | |
| total_emotional_value = sum(m.emotional_value for m in recent_memories) | |
| avg_emotional_value = total_emotional_value / len(recent_memories) | |
| positive_memories = sum(1 for m in recent_memories if m.emotional_value > 0.1) | |
| negative_memories = sum(1 for m in recent_memories if m.emotional_value < -0.1) | |
| # Determine overall mood | |
| if avg_emotional_value > 0.3: | |
| overall_mood = 'very_happy' | |
| elif avg_emotional_value > 0.1: | |
| overall_mood = 'happy' | |
| elif avg_emotional_value < -0.3: | |
| overall_mood = 'stressed' | |
| elif avg_emotional_value < -0.1: | |
| overall_mood = 'unhappy' | |
| else: | |
| overall_mood = 'neutral' | |
| # Determine trend | |
| if len(recent_memories) >= 5: | |
| first_half = recent_memories[:len(recent_memories)//2] | |
| second_half = recent_memories[len(recent_memories)//2:] | |
| first_avg = sum(m.emotional_value for m in first_half) / len(first_half) | |
| second_avg = sum(m.emotional_value for m in second_half) / len(second_half) | |
| if second_avg - first_avg > 0.2: | |
| trend = 'improving' | |
| elif first_avg - second_avg > 0.2: | |
| trend = 'declining' | |
| else: | |
| trend = 'stable' | |
| else: | |
| trend = 'stable' | |
| return { | |
| 'overall_mood': overall_mood, | |
| 'recent_trend': trend, | |
| 'avg_emotional_value': avg_emotional_value, | |
| 'positive_memories': positive_memories, | |
| 'negative_memories': negative_memories, | |
| 'total_recent_memories': len(recent_memories) | |
| } | |
| def cleanup_old_memories(self, pet_id: str, max_age_days: int = 30) -> int: | |
| """ | |
| Clean up old memories while preserving important ones. | |
| Args: | |
| pet_id: Pet identifier | |
| max_age_days: Maximum age for memories in days | |
| Returns: | |
| Number of memories cleaned up | |
| """ | |
| memories = self.pet_memories.get(pet_id, []) | |
| if not memories: | |
| return 0 | |
| cutoff_date = datetime.now() - timedelta(days=max_age_days) | |
| # Separate memories into keep and remove lists | |
| keep_memories = [] | |
| removed_count = 0 | |
| for memory in memories: | |
| # Always keep important memories or recent ones | |
| if (memory.importance > 0.7 or | |
| memory.timestamp > cutoff_date or | |
| abs(memory.emotional_value) > 0.5): | |
| keep_memories.append(memory) | |
| else: | |
| removed_count += 1 | |
| # Update memories list | |
| self.pet_memories[pet_id] = keep_memories | |
| # Update statistics | |
| self.memory_stats[pet_id]['last_cleanup'] = datetime.now() | |
| if removed_count > 0: | |
| logger.info(f"Cleaned up {removed_count} old memories for pet {pet_id}") | |
| return removed_count | |
| def get_memory_statistics(self, pet_id: str) -> Dict[str, Any]: | |
| """Get memory statistics for a pet.""" | |
| memories = self.pet_memories.get(pet_id, []) | |
| stats = self.memory_stats[pet_id].copy() | |
| # Update current counts | |
| stats['total_memories'] = len(memories) | |
| stats['happy_memories'] = sum(1 for m in memories if m.emotional_value > 0.1) | |
| stats['stressful_memories'] = sum(1 for m in memories if m.emotional_value < -0.1) | |
| stats['neutral_memories'] = stats['total_memories'] - stats['happy_memories'] - stats['stressful_memories'] | |
| # Memory type breakdown | |
| type_counts = defaultdict(int) | |
| for memory in memories: | |
| type_counts[memory.memory_type] += 1 | |
| stats['memory_types'] = dict(type_counts) | |
| # Recent activity | |
| recent_cutoff = datetime.now() - timedelta(hours=24) | |
| stats['recent_memories'] = sum(1 for m in memories if m.timestamp > recent_cutoff) | |
| return stats | |
| def _calculate_emotional_value(self, interaction: Interaction, pet: DigiPal) -> float: | |
| """Calculate emotional value for an interaction.""" | |
| emotional_value = 0.0 | |
| # Base emotional value from success/failure | |
| if interaction.success: | |
| emotional_value += 0.2 | |
| else: | |
| emotional_value -= 0.3 | |
| # Adjust based on interaction result | |
| if interaction.result == InteractionResult.SUCCESS: | |
| emotional_value += 0.1 | |
| elif interaction.result == InteractionResult.FAILURE: | |
| emotional_value -= 0.2 | |
| elif interaction.result == InteractionResult.STAGE_INAPPROPRIATE: | |
| emotional_value -= 0.1 | |
| # Adjust based on command type | |
| command = interaction.interpreted_command.lower() | |
| if command in ['good', 'praise']: | |
| emotional_value += 0.4 | |
| elif command in ['bad', 'scold']: | |
| emotional_value -= 0.3 | |
| elif command in ['play', 'fun']: | |
| emotional_value += 0.2 | |
| elif command in ['eat', 'food'] and pet.energy < 50: | |
| emotional_value += 0.3 # Food when hungry is very positive | |
| # Adjust based on attribute changes | |
| if 'happiness' in interaction.attribute_changes: | |
| emotional_value += interaction.attribute_changes['happiness'] / 100.0 | |
| return max(-1.0, min(1.0, emotional_value)) | |
| def _calculate_importance(self, interaction: Interaction) -> float: | |
| """Calculate importance level for an interaction.""" | |
| importance = 0.5 # Base importance | |
| # Increase importance for successful interactions | |
| if interaction.success: | |
| importance += 0.2 | |
| # Increase importance based on attribute changes | |
| total_change = sum(abs(v) for v in interaction.attribute_changes.values()) | |
| importance += min(0.3, total_change / 100.0) | |
| # Special commands are more important | |
| special_commands = ['evolution', 'death', 'birth', 'milestone'] | |
| if any(cmd in interaction.interpreted_command.lower() for cmd in special_commands): | |
| importance += 0.3 | |
| return max(0.0, min(1.0, importance)) | |
| def _extract_interaction_tags(self, interaction: Interaction) -> Set[str]: | |
| """Extract tags from an interaction.""" | |
| tags = {'interaction'} | |
| command = interaction.interpreted_command.lower() | |
| # Command-based tags | |
| if command in ['eat', 'food', 'feed']: | |
| tags.add('eating') | |
| elif command in ['sleep', 'rest']: | |
| tags.add('sleeping') | |
| elif command in ['train', 'exercise']: | |
| tags.add('training') | |
| elif command in ['play', 'fun']: | |
| tags.add('playing') | |
| elif command in ['good', 'praise']: | |
| tags.add('praise') | |
| elif command in ['bad', 'scold']: | |
| tags.add('discipline') | |
| # Success/failure tags | |
| if interaction.success: | |
| tags.add('successful') | |
| else: | |
| tags.add('failed') | |
| return tags | |
| def _update_memory_stats(self, pet_id: str, memory: EmotionalMemory) -> None: | |
| """Update memory statistics.""" | |
| stats = self.memory_stats[pet_id] | |
| stats['total_memories'] += 1 | |
| if memory.emotional_value > 0.1: | |
| stats['happy_memories'] += 1 | |
| elif memory.emotional_value < -0.1: | |
| stats['stressful_memories'] += 1 | |
| else: | |
| stats['neutral_memories'] += 1 | |
| def _manage_memory_size(self, pet_id: str) -> None: | |
| """Manage memory size to prevent unlimited growth.""" | |
| memories = self.pet_memories[pet_id] | |
| if len(memories) > self.max_memories_per_pet: | |
| # Sort by importance and recency, keep the most important/recent | |
| memories.sort(key=lambda m: (m.importance, m.timestamp.timestamp()), reverse=True) | |
| # Keep top memories | |
| self.pet_memories[pet_id] = memories[:self.max_memories_per_pet] | |
| removed_count = len(memories) - self.max_memories_per_pet | |
| logger.debug(f"Removed {removed_count} old memories for pet {pet_id}") | |
| def _format_time_ago(self, timestamp: datetime) -> str: | |
| """Format timestamp as 'time ago' string.""" | |
| delta = datetime.now() - timestamp | |
| if delta.days > 0: | |
| return f"{delta.days} day{'s' if delta.days != 1 else ''} ago" | |
| elif delta.seconds > 3600: | |
| hours = delta.seconds // 3600 | |
| return f"{hours} hour{'s' if hours != 1 else ''} ago" | |
| elif delta.seconds > 60: | |
| minutes = delta.seconds // 60 | |
| return f"{minutes} minute{'s' if minutes != 1 else ''} ago" | |
| else: | |
| return "just now" | |
| def start_background_cleanup(self) -> None: | |
| """Start background cleanup thread.""" | |
| if self._cleanup_thread and self._cleanup_thread.is_alive(): | |
| return | |
| self._stop_cleanup = False | |
| self._cleanup_thread = threading.Thread(target=self._background_cleanup_loop, daemon=True) | |
| self._cleanup_thread.start() | |
| logger.info("Started background memory cleanup") | |
| def stop_background_cleanup(self) -> None: | |
| """Stop background cleanup thread.""" | |
| self._stop_cleanup = True | |
| if self._cleanup_thread: | |
| self._cleanup_thread.join(timeout=5) | |
| def _background_cleanup_loop(self) -> None: | |
| """Background cleanup loop.""" | |
| while not self._stop_cleanup: | |
| try: | |
| # Clean up old memories for all pets | |
| for pet_id in list(self.pet_memories.keys()): | |
| self.cleanup_old_memories(pet_id) | |
| # Force garbage collection | |
| gc.collect() | |
| # Sleep for 1 hour | |
| time.sleep(3600) | |
| except Exception as e: | |
| logger.error(f"Error in background memory cleanup: {e}") | |
| time.sleep(300) # Sleep 5 minutes on error | |
| def shutdown(self) -> None: | |
| """Shutdown the memory manager.""" | |
| logger.info("Shutting down enhanced memory manager") | |
| # Stop background cleanup | |
| self.stop_background_cleanup() | |
| # Shutdown cache | |
| self.memory_cache.shutdown() | |
| # Clear memories to free memory | |
| self.pet_memories.clear() | |
| self.memory_stats.clear() | |
| logger.info("Enhanced memory manager shutdown complete") |