claude-mpm 3.7.8__py3-none-any.whl → 3.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_PM.md +0 -106
- claude_mpm/agents/INSTRUCTIONS.md +0 -96
- claude_mpm/agents/MEMORY.md +88 -0
- claude_mpm/agents/WORKFLOW.md +86 -0
- claude_mpm/agents/templates/code_analyzer.json +2 -2
- claude_mpm/agents/templates/data_engineer.json +1 -1
- claude_mpm/agents/templates/documentation.json +1 -1
- claude_mpm/agents/templates/engineer.json +1 -1
- claude_mpm/agents/templates/ops.json +1 -1
- claude_mpm/agents/templates/qa.json +1 -1
- claude_mpm/agents/templates/research.json +1 -1
- claude_mpm/agents/templates/security.json +1 -1
- claude_mpm/agents/templates/ticketing.json +2 -7
- claude_mpm/agents/templates/version_control.json +1 -1
- claude_mpm/agents/templates/web_qa.json +2 -2
- claude_mpm/agents/templates/web_ui.json +2 -2
- claude_mpm/cli/__init__.py +2 -2
- claude_mpm/cli/commands/__init__.py +2 -1
- claude_mpm/cli/commands/tickets.py +596 -19
- claude_mpm/cli/parser.py +217 -5
- claude_mpm/config/__init__.py +30 -39
- claude_mpm/config/socketio_config.py +8 -5
- claude_mpm/constants.py +13 -0
- claude_mpm/core/__init__.py +8 -18
- claude_mpm/core/cache.py +596 -0
- claude_mpm/core/claude_runner.py +166 -622
- claude_mpm/core/config.py +5 -1
- claude_mpm/core/constants.py +339 -0
- claude_mpm/core/container.py +461 -22
- claude_mpm/core/exceptions.py +392 -0
- claude_mpm/core/framework_loader.py +208 -94
- claude_mpm/core/interactive_session.py +432 -0
- claude_mpm/core/interfaces.py +424 -0
- claude_mpm/core/lazy.py +467 -0
- claude_mpm/core/logging_config.py +444 -0
- claude_mpm/core/oneshot_session.py +465 -0
- claude_mpm/core/optimized_agent_loader.py +485 -0
- claude_mpm/core/optimized_startup.py +490 -0
- claude_mpm/core/service_registry.py +52 -26
- claude_mpm/core/socketio_pool.py +162 -5
- claude_mpm/core/types.py +292 -0
- claude_mpm/core/typing_utils.py +477 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +213 -99
- claude_mpm/init.py +2 -1
- claude_mpm/services/__init__.py +78 -14
- claude_mpm/services/agent/__init__.py +24 -0
- claude_mpm/services/agent/deployment.py +2548 -0
- claude_mpm/services/agent/management.py +598 -0
- claude_mpm/services/agent/registry.py +813 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +587 -268
- claude_mpm/services/agents/memory/agent_memory_manager.py +156 -1
- claude_mpm/services/async_session_logger.py +8 -3
- claude_mpm/services/communication/__init__.py +21 -0
- claude_mpm/services/communication/socketio.py +1933 -0
- claude_mpm/services/communication/websocket.py +479 -0
- claude_mpm/services/core/__init__.py +123 -0
- claude_mpm/services/core/base.py +247 -0
- claude_mpm/services/core/interfaces.py +951 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +23 -23
- claude_mpm/services/framework_claude_md_generator.py +3 -2
- claude_mpm/services/health_monitor.py +4 -3
- claude_mpm/services/hook_service.py +64 -4
- claude_mpm/services/infrastructure/__init__.py +21 -0
- claude_mpm/services/infrastructure/logging.py +202 -0
- claude_mpm/services/infrastructure/monitoring.py +893 -0
- claude_mpm/services/memory/indexed_memory.py +648 -0
- claude_mpm/services/project/__init__.py +21 -0
- claude_mpm/services/project/analyzer.py +864 -0
- claude_mpm/services/project/registry.py +608 -0
- claude_mpm/services/project_analyzer.py +95 -2
- claude_mpm/services/recovery_manager.py +15 -9
- claude_mpm/services/socketio/__init__.py +25 -0
- claude_mpm/services/socketio/handlers/__init__.py +25 -0
- claude_mpm/services/socketio/handlers/base.py +121 -0
- claude_mpm/services/socketio/handlers/connection.py +198 -0
- claude_mpm/services/socketio/handlers/file.py +213 -0
- claude_mpm/services/socketio/handlers/git.py +723 -0
- claude_mpm/services/socketio/handlers/memory.py +27 -0
- claude_mpm/services/socketio/handlers/project.py +25 -0
- claude_mpm/services/socketio/handlers/registry.py +145 -0
- claude_mpm/services/socketio_client_manager.py +12 -7
- claude_mpm/services/socketio_server.py +156 -30
- claude_mpm/services/ticket_manager.py +170 -7
- claude_mpm/utils/error_handler.py +1 -1
- claude_mpm/validation/agent_validator.py +27 -14
- claude_mpm/validation/frontmatter_validator.py +231 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/METADATA +58 -21
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/RECORD +93 -53
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/WHEEL +0 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/top_level.txt +0 -0
| @@ -0,0 +1,648 @@ | |
| 1 | 
            +
            #!/usr/bin/env python3
         | 
| 2 | 
            +
            """Indexed memory service for high-performance memory queries.
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            This module provides optimized memory querying with:
         | 
| 5 | 
            +
            - Inverted index for fast text searches
         | 
| 6 | 
            +
            - B-tree indexing for sorted queries
         | 
| 7 | 
            +
            - Memory-mapped files for large datasets
         | 
| 8 | 
            +
            - Incremental index updates
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            WHY indexed memory:
         | 
| 11 | 
            +
            - Reduces query time from O(n) to O(log n) or O(1)
         | 
| 12 | 
            +
            - Supports 10k+ memory entries with <100ms query time
         | 
| 13 | 
            +
            - Enables complex queries (AND, OR, NOT operations)
         | 
| 14 | 
            +
            - Provides ranked results by relevance
         | 
| 15 | 
            +
            """
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            import bisect
         | 
| 18 | 
            +
            import hashlib
         | 
| 19 | 
            +
            import json
         | 
| 20 | 
            +
            import mmap
         | 
| 21 | 
            +
            import os
         | 
| 22 | 
            +
            import pickle
         | 
| 23 | 
            +
            import re
         | 
| 24 | 
            +
            import time
         | 
| 25 | 
            +
            from collections import defaultdict, Counter
         | 
| 26 | 
            +
            from dataclasses import dataclass, field
         | 
| 27 | 
            +
            from datetime import datetime, timedelta
         | 
| 28 | 
            +
            from pathlib import Path
         | 
| 29 | 
            +
            from typing import Any, Dict, List, Optional, Set, Tuple, Union
         | 
| 30 | 
            +
             | 
| 31 | 
            +
            from ...core.logger import get_logger
         | 
| 32 | 
            +
            from ...core.cache import FileSystemCache, get_file_cache
         | 
| 33 | 
            +
             | 
| 34 | 
            +
             | 
| 35 | 
            +
            @dataclass
         | 
| 36 | 
            +
            class MemoryEntry:
         | 
| 37 | 
            +
                """Single memory entry with metadata."""
         | 
| 38 | 
            +
                id: str
         | 
| 39 | 
            +
                agent_id: str
         | 
| 40 | 
            +
                content: str
         | 
| 41 | 
            +
                category: str
         | 
| 42 | 
            +
                timestamp: datetime
         | 
| 43 | 
            +
                tags: List[str] = field(default_factory=list)
         | 
| 44 | 
            +
                metadata: Dict[str, Any] = field(default_factory=dict)
         | 
| 45 | 
            +
                relevance_score: float = 0.0
         | 
| 46 | 
            +
             | 
| 47 | 
            +
             | 
| 48 | 
            +
            @dataclass
         | 
| 49 | 
            +
            class QueryResult:
         | 
| 50 | 
            +
                """Result from a memory query."""
         | 
| 51 | 
            +
                entries: List[MemoryEntry]
         | 
| 52 | 
            +
                total_count: int
         | 
| 53 | 
            +
                query_time: float
         | 
| 54 | 
            +
                index_used: str
         | 
| 55 | 
            +
                cache_hit: bool = False
         | 
| 56 | 
            +
             | 
| 57 | 
            +
             | 
| 58 | 
            +
            class InvertedIndex:
         | 
| 59 | 
            +
                """Inverted index for fast text searches.
         | 
| 60 | 
            +
                
         | 
| 61 | 
            +
                WHY inverted index:
         | 
| 62 | 
            +
                - Maps words to document IDs for O(1) lookups
         | 
| 63 | 
            +
                - Supports boolean queries (AND, OR, NOT)
         | 
| 64 | 
            +
                - Enables relevance scoring with TF-IDF
         | 
| 65 | 
            +
                - Efficient for full-text search
         | 
| 66 | 
            +
                """
         | 
| 67 | 
            +
                
         | 
| 68 | 
            +
                def __init__(self):
         | 
| 69 | 
            +
                    # Word -> Set of memory IDs
         | 
| 70 | 
            +
                    self.index: Dict[str, Set[str]] = defaultdict(set)
         | 
| 71 | 
            +
                    # Memory ID -> word frequencies
         | 
| 72 | 
            +
                    self.doc_freqs: Dict[str, Counter] = {}
         | 
| 73 | 
            +
                    # Total documents
         | 
| 74 | 
            +
                    self.doc_count = 0
         | 
| 75 | 
            +
                    
         | 
| 76 | 
            +
                    self.logger = get_logger("inverted_index")
         | 
| 77 | 
            +
                
         | 
| 78 | 
            +
                def add_entry(self, entry_id: str, text: str):
         | 
| 79 | 
            +
                    """Add entry to inverted index."""
         | 
| 80 | 
            +
                    # Tokenize text
         | 
| 81 | 
            +
                    words = self._tokenize(text)
         | 
| 82 | 
            +
                    
         | 
| 83 | 
            +
                    # Update word frequencies
         | 
| 84 | 
            +
                    self.doc_freqs[entry_id] = Counter(words)
         | 
| 85 | 
            +
                    
         | 
| 86 | 
            +
                    # Update inverted index
         | 
| 87 | 
            +
                    for word in set(words):
         | 
| 88 | 
            +
                        self.index[word].add(entry_id)
         | 
| 89 | 
            +
                    
         | 
| 90 | 
            +
                    self.doc_count += 1
         | 
| 91 | 
            +
                
         | 
| 92 | 
            +
                def remove_entry(self, entry_id: str):
         | 
| 93 | 
            +
                    """Remove entry from index."""
         | 
| 94 | 
            +
                    if entry_id not in self.doc_freqs:
         | 
| 95 | 
            +
                        return
         | 
| 96 | 
            +
                    
         | 
| 97 | 
            +
                    # Remove from inverted index
         | 
| 98 | 
            +
                    words = self.doc_freqs[entry_id].keys()
         | 
| 99 | 
            +
                    for word in words:
         | 
| 100 | 
            +
                        self.index[word].discard(entry_id)
         | 
| 101 | 
            +
                        if not self.index[word]:
         | 
| 102 | 
            +
                            del self.index[word]
         | 
| 103 | 
            +
                    
         | 
| 104 | 
            +
                    # Remove document frequency
         | 
| 105 | 
            +
                    del self.doc_freqs[entry_id]
         | 
| 106 | 
            +
                    self.doc_count -= 1
         | 
| 107 | 
            +
                
         | 
| 108 | 
            +
                def search(
         | 
| 109 | 
            +
                    self,
         | 
| 110 | 
            +
                    query: str,
         | 
| 111 | 
            +
                    operator: str = 'AND'
         | 
| 112 | 
            +
                ) -> Set[str]:
         | 
| 113 | 
            +
                    """Search index for matching entries.
         | 
| 114 | 
            +
                    
         | 
| 115 | 
            +
                    Args:
         | 
| 116 | 
            +
                        query: Search query
         | 
| 117 | 
            +
                        operator: Boolean operator (AND, OR, NOT)
         | 
| 118 | 
            +
                    
         | 
| 119 | 
            +
                    Returns:
         | 
| 120 | 
            +
                        Set of matching entry IDs
         | 
| 121 | 
            +
                    """
         | 
| 122 | 
            +
                    words = self._tokenize(query)
         | 
| 123 | 
            +
                    if not words:
         | 
| 124 | 
            +
                        return set()
         | 
| 125 | 
            +
                    
         | 
| 126 | 
            +
                    # Get entry sets for each word
         | 
| 127 | 
            +
                    entry_sets = [self.index.get(word, set()) for word in words]
         | 
| 128 | 
            +
                    
         | 
| 129 | 
            +
                    if not entry_sets:
         | 
| 130 | 
            +
                        return set()
         | 
| 131 | 
            +
                    
         | 
| 132 | 
            +
                    # Apply boolean operator
         | 
| 133 | 
            +
                    if operator == 'AND':
         | 
| 134 | 
            +
                        result = entry_sets[0]
         | 
| 135 | 
            +
                        for s in entry_sets[1:]:
         | 
| 136 | 
            +
                            result = result.intersection(s)
         | 
| 137 | 
            +
                    elif operator == 'OR':
         | 
| 138 | 
            +
                        result = set()
         | 
| 139 | 
            +
                        for s in entry_sets:
         | 
| 140 | 
            +
                            result = result.union(s)
         | 
| 141 | 
            +
                    elif operator == 'NOT':
         | 
| 142 | 
            +
                        # Return entries that don't contain any query words
         | 
| 143 | 
            +
                        all_entries = set(self.doc_freqs.keys())
         | 
| 144 | 
            +
                        excluded = set()
         | 
| 145 | 
            +
                        for s in entry_sets:
         | 
| 146 | 
            +
                            excluded = excluded.union(s)
         | 
| 147 | 
            +
                        result = all_entries - excluded
         | 
| 148 | 
            +
                    else:
         | 
| 149 | 
            +
                        result = entry_sets[0]
         | 
| 150 | 
            +
                    
         | 
| 151 | 
            +
                    return result
         | 
| 152 | 
            +
                
         | 
| 153 | 
            +
                def calculate_relevance(
         | 
| 154 | 
            +
                    self,
         | 
| 155 | 
            +
                    entry_id: str,
         | 
| 156 | 
            +
                    query: str
         | 
| 157 | 
            +
                ) -> float:
         | 
| 158 | 
            +
                    """Calculate TF-IDF relevance score.
         | 
| 159 | 
            +
                    
         | 
| 160 | 
            +
                    Args:
         | 
| 161 | 
            +
                        entry_id: Memory entry ID
         | 
| 162 | 
            +
                        query: Search query
         | 
| 163 | 
            +
                    
         | 
| 164 | 
            +
                    Returns:
         | 
| 165 | 
            +
                        Relevance score (0.0 to 1.0)
         | 
| 166 | 
            +
                    """
         | 
| 167 | 
            +
                    if entry_id not in self.doc_freqs:
         | 
| 168 | 
            +
                        return 0.0
         | 
| 169 | 
            +
                    
         | 
| 170 | 
            +
                    query_words = self._tokenize(query)
         | 
| 171 | 
            +
                    if not query_words:
         | 
| 172 | 
            +
                        return 0.0
         | 
| 173 | 
            +
                    
         | 
| 174 | 
            +
                    score = 0.0
         | 
| 175 | 
            +
                    doc_freq = self.doc_freqs[entry_id]
         | 
| 176 | 
            +
                    
         | 
| 177 | 
            +
                    for word in query_words:
         | 
| 178 | 
            +
                        if word not in doc_freq:
         | 
| 179 | 
            +
                            continue
         | 
| 180 | 
            +
                        
         | 
| 181 | 
            +
                        # Term frequency
         | 
| 182 | 
            +
                        tf = doc_freq[word] / sum(doc_freq.values())
         | 
| 183 | 
            +
                        
         | 
| 184 | 
            +
                        # Inverse document frequency
         | 
| 185 | 
            +
                        if word in self.index:
         | 
| 186 | 
            +
                            idf = 1.0 + (self.doc_count / len(self.index[word]))
         | 
| 187 | 
            +
                        else:
         | 
| 188 | 
            +
                            idf = 1.0
         | 
| 189 | 
            +
                        
         | 
| 190 | 
            +
                        score += tf * idf
         | 
| 191 | 
            +
                    
         | 
| 192 | 
            +
                    # Normalize score
         | 
| 193 | 
            +
                    return min(1.0, score / len(query_words))
         | 
| 194 | 
            +
                
         | 
| 195 | 
            +
                def _tokenize(self, text: str) -> List[str]:
         | 
| 196 | 
            +
                    """Tokenize text into words."""
         | 
| 197 | 
            +
                    # Convert to lowercase and split on non-alphanumeric
         | 
| 198 | 
            +
                    text = text.lower()
         | 
| 199 | 
            +
                    words = re.findall(r'\b[a-z0-9]+\b', text)
         | 
| 200 | 
            +
                    
         | 
| 201 | 
            +
                    # Remove stop words (simplified list)
         | 
| 202 | 
            +
                    stop_words = {'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for'}
         | 
| 203 | 
            +
                    return [w for w in words if w not in stop_words and len(w) > 2]
         | 
| 204 | 
            +
                
         | 
| 205 | 
            +
                def save(self, path: Path):
         | 
| 206 | 
            +
                    """Persist index to disk."""
         | 
| 207 | 
            +
                    data = {
         | 
| 208 | 
            +
                        'index': dict(self.index),
         | 
| 209 | 
            +
                        'doc_freqs': dict(self.doc_freqs),
         | 
| 210 | 
            +
                        'doc_count': self.doc_count
         | 
| 211 | 
            +
                    }
         | 
| 212 | 
            +
                    with open(path, 'wb') as f:
         | 
| 213 | 
            +
                        pickle.dump(data, f)
         | 
| 214 | 
            +
                
         | 
| 215 | 
            +
                def load(self, path: Path):
         | 
| 216 | 
            +
                    """Load index from disk."""
         | 
| 217 | 
            +
                    if not path.exists():
         | 
| 218 | 
            +
                        return
         | 
| 219 | 
            +
                    
         | 
| 220 | 
            +
                    with open(path, 'rb') as f:
         | 
| 221 | 
            +
                        data = pickle.load(f)
         | 
| 222 | 
            +
                    
         | 
| 223 | 
            +
                    self.index = defaultdict(set, {k: set(v) for k, v in data['index'].items()})
         | 
| 224 | 
            +
                    self.doc_freqs = data['doc_freqs']
         | 
| 225 | 
            +
                    self.doc_count = data['doc_count']
         | 
| 226 | 
            +
             | 
| 227 | 
            +
             | 
| 228 | 
            +
            class BTreeIndex:
         | 
| 229 | 
            +
                """B-tree index for sorted queries.
         | 
| 230 | 
            +
                
         | 
| 231 | 
            +
                WHY B-tree index:
         | 
| 232 | 
            +
                - Maintains sorted order for range queries
         | 
| 233 | 
            +
                - O(log n) search, insert, delete
         | 
| 234 | 
            +
                - Efficient for timestamp-based queries
         | 
| 235 | 
            +
                - Supports pagination
         | 
| 236 | 
            +
                """
         | 
| 237 | 
            +
                
         | 
| 238 | 
            +
                def __init__(self, key_func=None):
         | 
| 239 | 
            +
                    # Sorted list of (key, entry_id) tuples
         | 
| 240 | 
            +
                    self.index: List[Tuple[Any, str]] = []
         | 
| 241 | 
            +
                    self.key_func = key_func or (lambda x: x)
         | 
| 242 | 
            +
                    self.logger = get_logger("btree_index")
         | 
| 243 | 
            +
                
         | 
| 244 | 
            +
                def add_entry(self, entry_id: str, key: Any):
         | 
| 245 | 
            +
                    """Add entry to B-tree index."""
         | 
| 246 | 
            +
                    bisect.insort(self.index, (self.key_func(key), entry_id))
         | 
| 247 | 
            +
                
         | 
| 248 | 
            +
                def remove_entry(self, entry_id: str):
         | 
| 249 | 
            +
                    """Remove entry from index."""
         | 
| 250 | 
            +
                    self.index = [(k, id) for k, id in self.index if id != entry_id]
         | 
| 251 | 
            +
                
         | 
| 252 | 
            +
                def range_search(
         | 
| 253 | 
            +
                    self,
         | 
| 254 | 
            +
                    min_key: Any = None,
         | 
| 255 | 
            +
                    max_key: Any = None,
         | 
| 256 | 
            +
                    limit: int = None
         | 
| 257 | 
            +
                ) -> List[str]:
         | 
| 258 | 
            +
                    """Search for entries in key range.
         | 
| 259 | 
            +
                    
         | 
| 260 | 
            +
                    Args:
         | 
| 261 | 
            +
                        min_key: Minimum key value (inclusive)
         | 
| 262 | 
            +
                        max_key: Maximum key value (inclusive)
         | 
| 263 | 
            +
                        limit: Maximum results to return
         | 
| 264 | 
            +
                    
         | 
| 265 | 
            +
                    Returns:
         | 
| 266 | 
            +
                        List of matching entry IDs
         | 
| 267 | 
            +
                    """
         | 
| 268 | 
            +
                    # Find range boundaries
         | 
| 269 | 
            +
                    if min_key is not None:
         | 
| 270 | 
            +
                        min_key = self.key_func(min_key)
         | 
| 271 | 
            +
                        start = bisect.bisect_left(self.index, (min_key, ''))
         | 
| 272 | 
            +
                    else:
         | 
| 273 | 
            +
                        start = 0
         | 
| 274 | 
            +
                    
         | 
| 275 | 
            +
                    if max_key is not None:
         | 
| 276 | 
            +
                        max_key = self.key_func(max_key)
         | 
| 277 | 
            +
                        end = bisect.bisect_right(self.index, (max_key, '\xff'))
         | 
| 278 | 
            +
                    else:
         | 
| 279 | 
            +
                        end = len(self.index)
         | 
| 280 | 
            +
                    
         | 
| 281 | 
            +
                    # Extract entry IDs
         | 
| 282 | 
            +
                    results = [entry_id for _, entry_id in self.index[start:end]]
         | 
| 283 | 
            +
                    
         | 
| 284 | 
            +
                    if limit:
         | 
| 285 | 
            +
                        results = results[:limit]
         | 
| 286 | 
            +
                    
         | 
| 287 | 
            +
                    return results
         | 
| 288 | 
            +
                
         | 
| 289 | 
            +
                def get_recent(self, n: int = 10) -> List[str]:
         | 
| 290 | 
            +
                    """Get n most recent entries."""
         | 
| 291 | 
            +
                    return [entry_id for _, entry_id in self.index[-n:]]
         | 
| 292 | 
            +
                
         | 
| 293 | 
            +
                def get_oldest(self, n: int = 10) -> List[str]:
         | 
| 294 | 
            +
                    """Get n oldest entries."""
         | 
| 295 | 
            +
                    return [entry_id for _, entry_id in self.index[:n]]
         | 
| 296 | 
            +
             | 
| 297 | 
            +
             | 
| 298 | 
            +
            class IndexedMemoryService:
         | 
| 299 | 
            +
                """High-performance memory service with multiple indexes.
         | 
| 300 | 
            +
                
         | 
| 301 | 
            +
                WHY this design:
         | 
| 302 | 
            +
                - Multiple specialized indexes for different query types
         | 
| 303 | 
            +
                - LRU cache for frequent queries
         | 
| 304 | 
            +
                - Incremental index updates for efficiency
         | 
| 305 | 
            +
                - Memory-mapped files for large datasets
         | 
| 306 | 
            +
                
         | 
| 307 | 
            +
                Example:
         | 
| 308 | 
            +
                    memory = IndexedMemoryService()
         | 
| 309 | 
            +
                    
         | 
| 310 | 
            +
                    # Add memory entry
         | 
| 311 | 
            +
                    memory.add_memory(
         | 
| 312 | 
            +
                        agent_id='engineer',
         | 
| 313 | 
            +
                        content='Use dependency injection for testability',
         | 
| 314 | 
            +
                        category='pattern'
         | 
| 315 | 
            +
                    )
         | 
| 316 | 
            +
                    
         | 
| 317 | 
            +
                    # Fast text search
         | 
| 318 | 
            +
                    results = memory.search('dependency injection')
         | 
| 319 | 
            +
                    
         | 
| 320 | 
            +
                    # Range query by timestamp
         | 
| 321 | 
            +
                    recent = memory.get_recent_memories(hours=24)
         | 
| 322 | 
            +
                """
         | 
| 323 | 
            +
                
         | 
| 324 | 
            +
                def __init__(
         | 
| 325 | 
            +
                    self,
         | 
| 326 | 
            +
                    data_dir: Optional[Path] = None,
         | 
| 327 | 
            +
                    cache_size_mb: int = 50,
         | 
| 328 | 
            +
                    enable_mmap: bool = False
         | 
| 329 | 
            +
                ):
         | 
| 330 | 
            +
                    """Initialize indexed memory service.
         | 
| 331 | 
            +
                    
         | 
| 332 | 
            +
                    Args:
         | 
| 333 | 
            +
                        data_dir: Directory for persisting indexes
         | 
| 334 | 
            +
                        cache_size_mb: Cache size for query results
         | 
| 335 | 
            +
                        enable_mmap: Use memory-mapped files for large datasets
         | 
| 336 | 
            +
                    """
         | 
| 337 | 
            +
                    self.data_dir = data_dir or Path.home() / ".claude-mpm" / "memory"
         | 
| 338 | 
            +
                    self.data_dir.mkdir(parents=True, exist_ok=True)
         | 
| 339 | 
            +
                    
         | 
| 340 | 
            +
                    self.enable_mmap = enable_mmap
         | 
| 341 | 
            +
                    
         | 
| 342 | 
            +
                    # Memory storage
         | 
| 343 | 
            +
                    self.memories: Dict[str, MemoryEntry] = {}
         | 
| 344 | 
            +
                    
         | 
| 345 | 
            +
                    # Indexes
         | 
| 346 | 
            +
                    self.text_index = InvertedIndex()
         | 
| 347 | 
            +
                    self.time_index = BTreeIndex(key_func=lambda dt: dt.timestamp())
         | 
| 348 | 
            +
                    self.agent_index: Dict[str, Set[str]] = defaultdict(set)
         | 
| 349 | 
            +
                    self.category_index: Dict[str, Set[str]] = defaultdict(set)
         | 
| 350 | 
            +
                    self.tag_index: Dict[str, Set[str]] = defaultdict(set)
         | 
| 351 | 
            +
                    
         | 
| 352 | 
            +
                    # Query cache
         | 
| 353 | 
            +
                    self.cache = get_file_cache(max_size_mb=cache_size_mb, default_ttl=300)
         | 
| 354 | 
            +
                    
         | 
| 355 | 
            +
                    # Logger
         | 
| 356 | 
            +
                    self.logger = get_logger("indexed_memory")
         | 
| 357 | 
            +
                    
         | 
| 358 | 
            +
                    # Load existing data
         | 
| 359 | 
            +
                    self._load_indexes()
         | 
| 360 | 
            +
                
         | 
| 361 | 
            +
                def add_memory(
         | 
| 362 | 
            +
                    self,
         | 
| 363 | 
            +
                    agent_id: str,
         | 
| 364 | 
            +
                    content: str,
         | 
| 365 | 
            +
                    category: str = 'general',
         | 
| 366 | 
            +
                    tags: List[str] = None,
         | 
| 367 | 
            +
                    metadata: Dict[str, Any] = None
         | 
| 368 | 
            +
                ) -> str:
         | 
| 369 | 
            +
                    """Add a new memory entry.
         | 
| 370 | 
            +
                    
         | 
| 371 | 
            +
                    Args:
         | 
| 372 | 
            +
                        agent_id: Agent that owns this memory
         | 
| 373 | 
            +
                        content: Memory content
         | 
| 374 | 
            +
                        category: Memory category
         | 
| 375 | 
            +
                        tags: Optional tags
         | 
| 376 | 
            +
                        metadata: Optional metadata
         | 
| 377 | 
            +
                    
         | 
| 378 | 
            +
                    Returns:
         | 
| 379 | 
            +
                        Memory entry ID
         | 
| 380 | 
            +
                    """
         | 
| 381 | 
            +
                    # Generate ID
         | 
| 382 | 
            +
                    entry_id = self._generate_id(agent_id, content)
         | 
| 383 | 
            +
                    
         | 
| 384 | 
            +
                    # Create entry
         | 
| 385 | 
            +
                    entry = MemoryEntry(
         | 
| 386 | 
            +
                        id=entry_id,
         | 
| 387 | 
            +
                        agent_id=agent_id,
         | 
| 388 | 
            +
                        content=content,
         | 
| 389 | 
            +
                        category=category,
         | 
| 390 | 
            +
                        timestamp=datetime.now(),
         | 
| 391 | 
            +
                        tags=tags or [],
         | 
| 392 | 
            +
                        metadata=metadata or {}
         | 
| 393 | 
            +
                    )
         | 
| 394 | 
            +
                    
         | 
| 395 | 
            +
                    # Store entry
         | 
| 396 | 
            +
                    self.memories[entry_id] = entry
         | 
| 397 | 
            +
                    
         | 
| 398 | 
            +
                    # Update indexes
         | 
| 399 | 
            +
                    self.text_index.add_entry(entry_id, content)
         | 
| 400 | 
            +
                    self.time_index.add_entry(entry_id, entry.timestamp)
         | 
| 401 | 
            +
                    self.agent_index[agent_id].add(entry_id)
         | 
| 402 | 
            +
                    self.category_index[category].add(entry_id)
         | 
| 403 | 
            +
                    for tag in entry.tags:
         | 
| 404 | 
            +
                        self.tag_index[tag].add(entry_id)
         | 
| 405 | 
            +
                    
         | 
| 406 | 
            +
                    # Invalidate cache
         | 
| 407 | 
            +
                    self.cache.invalidate_pattern("query:*")
         | 
| 408 | 
            +
                    
         | 
| 409 | 
            +
                    self.logger.debug(f"Added memory {entry_id} for agent {agent_id}")
         | 
| 410 | 
            +
                    return entry_id
         | 
| 411 | 
            +
                
         | 
| 412 | 
            +
                def search(
         | 
| 413 | 
            +
                    self,
         | 
| 414 | 
            +
                    query: str,
         | 
| 415 | 
            +
                    agent_id: Optional[str] = None,
         | 
| 416 | 
            +
                    category: Optional[str] = None,
         | 
| 417 | 
            +
                    tags: Optional[List[str]] = None,
         | 
| 418 | 
            +
                    limit: int = 50,
         | 
| 419 | 
            +
                    operator: str = 'AND'
         | 
| 420 | 
            +
                ) -> QueryResult:
         | 
| 421 | 
            +
                    """Search memories with multiple filters.
         | 
| 422 | 
            +
                    
         | 
| 423 | 
            +
                    Args:
         | 
| 424 | 
            +
                        query: Text search query
         | 
| 425 | 
            +
                        agent_id: Filter by agent
         | 
| 426 | 
            +
                        category: Filter by category
         | 
| 427 | 
            +
                        tags: Filter by tags
         | 
| 428 | 
            +
                        limit: Maximum results
         | 
| 429 | 
            +
                        operator: Boolean operator for text search
         | 
| 430 | 
            +
                    
         | 
| 431 | 
            +
                    Returns:
         | 
| 432 | 
            +
                        Query results with metadata
         | 
| 433 | 
            +
                    """
         | 
| 434 | 
            +
                    start_time = time.time()
         | 
| 435 | 
            +
                    
         | 
| 436 | 
            +
                    # Generate cache key
         | 
| 437 | 
            +
                    cache_key = f"query:{hashlib.md5(f'{query}:{agent_id}:{category}:{tags}:{limit}:{operator}'.encode()).hexdigest()}"
         | 
| 438 | 
            +
                    
         | 
| 439 | 
            +
                    # Check cache
         | 
| 440 | 
            +
                    cached = self.cache.get(cache_key)
         | 
| 441 | 
            +
                    if cached:
         | 
| 442 | 
            +
                        return QueryResult(
         | 
| 443 | 
            +
                            entries=cached['entries'],
         | 
| 444 | 
            +
                            total_count=cached['total_count'],
         | 
| 445 | 
            +
                            query_time=0.001,
         | 
| 446 | 
            +
                            index_used='cache',
         | 
| 447 | 
            +
                            cache_hit=True
         | 
| 448 | 
            +
                        )
         | 
| 449 | 
            +
                    
         | 
| 450 | 
            +
                    # Text search
         | 
| 451 | 
            +
                    if query:
         | 
| 452 | 
            +
                        matching_ids = self.text_index.search(query, operator)
         | 
| 453 | 
            +
                    else:
         | 
| 454 | 
            +
                        matching_ids = set(self.memories.keys())
         | 
| 455 | 
            +
                    
         | 
| 456 | 
            +
                    # Apply filters
         | 
| 457 | 
            +
                    if agent_id:
         | 
| 458 | 
            +
                        matching_ids &= self.agent_index.get(agent_id, set())
         | 
| 459 | 
            +
                    
         | 
| 460 | 
            +
                    if category:
         | 
| 461 | 
            +
                        matching_ids &= self.category_index.get(category, set())
         | 
| 462 | 
            +
                    
         | 
| 463 | 
            +
                    if tags:
         | 
| 464 | 
            +
                        for tag in tags:
         | 
| 465 | 
            +
                            matching_ids &= self.tag_index.get(tag, set())
         | 
| 466 | 
            +
                    
         | 
| 467 | 
            +
                    # Get entries and calculate relevance
         | 
| 468 | 
            +
                    entries = []
         | 
| 469 | 
            +
                    for entry_id in matching_ids:
         | 
| 470 | 
            +
                        if entry_id in self.memories:
         | 
| 471 | 
            +
                            entry = self.memories[entry_id]
         | 
| 472 | 
            +
                            if query:
         | 
| 473 | 
            +
                                entry.relevance_score = self.text_index.calculate_relevance(entry_id, query)
         | 
| 474 | 
            +
                            entries.append(entry)
         | 
| 475 | 
            +
                    
         | 
| 476 | 
            +
                    # Sort by relevance and timestamp
         | 
| 477 | 
            +
                    entries.sort(key=lambda e: (-e.relevance_score, -e.timestamp.timestamp()))
         | 
| 478 | 
            +
                    
         | 
| 479 | 
            +
                    # Apply limit
         | 
| 480 | 
            +
                    limited_entries = entries[:limit]
         | 
| 481 | 
            +
                    
         | 
| 482 | 
            +
                    # Cache result
         | 
| 483 | 
            +
                    cache_data = {
         | 
| 484 | 
            +
                        'entries': limited_entries,
         | 
| 485 | 
            +
                        'total_count': len(entries)
         | 
| 486 | 
            +
                    }
         | 
| 487 | 
            +
                    self.cache.put(cache_key, cache_data, ttl=300)
         | 
| 488 | 
            +
                    
         | 
| 489 | 
            +
                    # Return result
         | 
| 490 | 
            +
                    return QueryResult(
         | 
| 491 | 
            +
                        entries=limited_entries,
         | 
| 492 | 
            +
                        total_count=len(entries),
         | 
| 493 | 
            +
                        query_time=time.time() - start_time,
         | 
| 494 | 
            +
                        index_used='text_index' if query else 'full_scan'
         | 
| 495 | 
            +
                    )
         | 
| 496 | 
            +
                
         | 
| 497 | 
            +
                def get_recent_memories(
         | 
| 498 | 
            +
                    self,
         | 
| 499 | 
            +
                    hours: Optional[int] = None,
         | 
| 500 | 
            +
                    days: Optional[int] = None,
         | 
| 501 | 
            +
                    limit: int = 50
         | 
| 502 | 
            +
                ) -> QueryResult:
         | 
| 503 | 
            +
                    """Get recent memories within time range.
         | 
| 504 | 
            +
                    
         | 
| 505 | 
            +
                    Args:
         | 
| 506 | 
            +
                        hours: Hours to look back
         | 
| 507 | 
            +
                        days: Days to look back
         | 
| 508 | 
            +
                        limit: Maximum results
         | 
| 509 | 
            +
                    
         | 
| 510 | 
            +
                    Returns:
         | 
| 511 | 
            +
                        Recent memories
         | 
| 512 | 
            +
                    """
         | 
| 513 | 
            +
                    start_time = time.time()
         | 
| 514 | 
            +
                    
         | 
| 515 | 
            +
                    # Calculate time range
         | 
| 516 | 
            +
                    now = datetime.now()
         | 
| 517 | 
            +
                    if hours:
         | 
| 518 | 
            +
                        min_time = now - timedelta(hours=hours)
         | 
| 519 | 
            +
                    elif days:
         | 
| 520 | 
            +
                        min_time = now - timedelta(days=days)
         | 
| 521 | 
            +
                    else:
         | 
| 522 | 
            +
                        min_time = None
         | 
| 523 | 
            +
                    
         | 
| 524 | 
            +
                    # Use time index for range search
         | 
| 525 | 
            +
                    entry_ids = self.time_index.range_search(
         | 
| 526 | 
            +
                        min_key=min_time,
         | 
| 527 | 
            +
                        max_key=now,
         | 
| 528 | 
            +
                        limit=limit
         | 
| 529 | 
            +
                    )
         | 
| 530 | 
            +
                    
         | 
| 531 | 
            +
                    # Get entries
         | 
| 532 | 
            +
                    entries = [self.memories[id] for id in entry_ids if id in self.memories]
         | 
| 533 | 
            +
                    
         | 
| 534 | 
            +
                    return QueryResult(
         | 
| 535 | 
            +
                        entries=entries,
         | 
| 536 | 
            +
                        total_count=len(entries),
         | 
| 537 | 
            +
                        query_time=time.time() - start_time,
         | 
| 538 | 
            +
                        index_used='time_index'
         | 
| 539 | 
            +
                    )
         | 
| 540 | 
            +
                
         | 
| 541 | 
            +
                def get_agent_memories(
         | 
| 542 | 
            +
                    self,
         | 
| 543 | 
            +
                    agent_id: str,
         | 
| 544 | 
            +
                    limit: int = 50
         | 
| 545 | 
            +
                ) -> QueryResult:
         | 
| 546 | 
            +
                    """Get all memories for a specific agent.
         | 
| 547 | 
            +
                    
         | 
| 548 | 
            +
                    Args:
         | 
| 549 | 
            +
                        agent_id: Agent ID
         | 
| 550 | 
            +
                        limit: Maximum results
         | 
| 551 | 
            +
                    
         | 
| 552 | 
            +
                    Returns:
         | 
| 553 | 
            +
                        Agent's memories
         | 
| 554 | 
            +
                    """
         | 
| 555 | 
            +
                    start_time = time.time()
         | 
| 556 | 
            +
                    
         | 
| 557 | 
            +
                    # Use agent index
         | 
| 558 | 
            +
                    entry_ids = list(self.agent_index.get(agent_id, set()))[:limit]
         | 
| 559 | 
            +
                    
         | 
| 560 | 
            +
                    # Get entries
         | 
| 561 | 
            +
                    entries = [self.memories[id] for id in entry_ids if id in self.memories]
         | 
| 562 | 
            +
                    
         | 
| 563 | 
            +
                    # Sort by timestamp
         | 
| 564 | 
            +
                    entries.sort(key=lambda e: -e.timestamp.timestamp())
         | 
| 565 | 
            +
                    
         | 
| 566 | 
            +
                    return QueryResult(
         | 
| 567 | 
            +
                        entries=entries,
         | 
| 568 | 
            +
                        total_count=len(self.agent_index.get(agent_id, set())),
         | 
| 569 | 
            +
                        query_time=time.time() - start_time,
         | 
| 570 | 
            +
                        index_used='agent_index'
         | 
| 571 | 
            +
                    )
         | 
| 572 | 
            +
                
         | 
| 573 | 
            +
                def _generate_id(self, agent_id: str, content: str) -> str:
         | 
| 574 | 
            +
                    """Generate unique ID for memory entry."""
         | 
| 575 | 
            +
                    timestamp = datetime.now().isoformat()
         | 
| 576 | 
            +
                    hash_input = f"{agent_id}:{content[:100]}:{timestamp}"
         | 
| 577 | 
            +
                    return hashlib.md5(hash_input.encode()).hexdigest()[:12]
         | 
| 578 | 
            +
                
         | 
| 579 | 
            +
                def _save_indexes(self):
         | 
| 580 | 
            +
                    """Persist all indexes to disk."""
         | 
| 581 | 
            +
                    # Save text index
         | 
| 582 | 
            +
                    self.text_index.save(self.data_dir / "text_index.pkl")
         | 
| 583 | 
            +
                    
         | 
| 584 | 
            +
                    # Save other indexes
         | 
| 585 | 
            +
                    with open(self.data_dir / "indexes.pkl", 'wb') as f:
         | 
| 586 | 
            +
                        pickle.dump({
         | 
| 587 | 
            +
                            'memories': self.memories,
         | 
| 588 | 
            +
                            'time_index': self.time_index.index,
         | 
| 589 | 
            +
                            'agent_index': dict(self.agent_index),
         | 
| 590 | 
            +
                            'category_index': dict(self.category_index),
         | 
| 591 | 
            +
                            'tag_index': dict(self.tag_index)
         | 
| 592 | 
            +
                        }, f)
         | 
| 593 | 
            +
                    
         | 
| 594 | 
            +
                    self.logger.info(f"Saved {len(self.memories)} memories to disk")
         | 
| 595 | 
            +
                
         | 
| 596 | 
            +
                def _load_indexes(self):
         | 
| 597 | 
            +
                    """Load indexes from disk."""
         | 
| 598 | 
            +
                    # Load text index
         | 
| 599 | 
            +
                    text_index_path = self.data_dir / "text_index.pkl"
         | 
| 600 | 
            +
                    if text_index_path.exists():
         | 
| 601 | 
            +
                        self.text_index.load(text_index_path)
         | 
| 602 | 
            +
                    
         | 
| 603 | 
            +
                    # Load other indexes
         | 
| 604 | 
            +
                    indexes_path = self.data_dir / "indexes.pkl"
         | 
| 605 | 
            +
                    if indexes_path.exists():
         | 
| 606 | 
            +
                        with open(indexes_path, 'rb') as f:
         | 
| 607 | 
            +
                            data = pickle.load(f)
         | 
| 608 | 
            +
                        
         | 
| 609 | 
            +
                        self.memories = data.get('memories', {})
         | 
| 610 | 
            +
                        self.time_index.index = data.get('time_index', [])
         | 
| 611 | 
            +
                        self.agent_index = defaultdict(set, {k: set(v) for k, v in data.get('agent_index', {}).items()})
         | 
| 612 | 
            +
                        self.category_index = defaultdict(set, {k: set(v) for k, v in data.get('category_index', {}).items()})
         | 
| 613 | 
            +
                        self.tag_index = defaultdict(set, {k: set(v) for k, v in data.get('tag_index', {}).items()})
         | 
| 614 | 
            +
                        
         | 
| 615 | 
            +
                        self.logger.info(f"Loaded {len(self.memories)} memories from disk")
         | 
| 616 | 
            +
                
         | 
| 617 | 
            +
                def get_stats(self) -> Dict[str, Any]:
         | 
| 618 | 
            +
                    """Get memory service statistics."""
         | 
| 619 | 
            +
                    return {
         | 
| 620 | 
            +
                        'total_memories': len(self.memories),
         | 
| 621 | 
            +
                        'agents': len(self.agent_index),
         | 
| 622 | 
            +
                        'categories': len(self.category_index),
         | 
| 623 | 
            +
                        'tags': len(self.tag_index),
         | 
| 624 | 
            +
                        'index_size': {
         | 
| 625 | 
            +
                            'text': len(self.text_index.index),
         | 
| 626 | 
            +
                            'time': len(self.time_index.index),
         | 
| 627 | 
            +
                            'agent': sum(len(v) for v in self.agent_index.values()),
         | 
| 628 | 
            +
                            'category': sum(len(v) for v in self.category_index.values()),
         | 
| 629 | 
            +
                            'tag': sum(len(v) for v in self.tag_index.values())
         | 
| 630 | 
            +
                        },
         | 
| 631 | 
            +
                        'cache_stats': self.cache.get_stats()
         | 
| 632 | 
            +
                    }
         | 
| 633 | 
            +
                
         | 
| 634 | 
            +
                def cleanup(self):
         | 
| 635 | 
            +
                    """Save indexes and cleanup resources."""
         | 
| 636 | 
            +
                    self._save_indexes()
         | 
| 637 | 
            +
             | 
| 638 | 
            +
             | 
| 639 | 
            +
            # Global memory service instance
         | 
| 640 | 
            +
            _memory_service: Optional[IndexedMemoryService] = None
         | 
| 641 | 
            +
             | 
| 642 | 
            +
             | 
| 643 | 
            +
            def get_indexed_memory() -> IndexedMemoryService:
         | 
| 644 | 
            +
                """Get or create global indexed memory service."""
         | 
| 645 | 
            +
                global _memory_service
         | 
| 646 | 
            +
                if _memory_service is None:
         | 
| 647 | 
            +
                    _memory_service = IndexedMemoryService()
         | 
| 648 | 
            +
                return _memory_service
         | 
| @@ -0,0 +1,21 @@ | |
| 1 | 
            +
            """
         | 
| 2 | 
            +
            Project Management Services Module
         | 
| 3 | 
            +
            ==================================
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            This module contains all project-related services including
         | 
| 6 | 
            +
            project analysis and registry management.
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            Part of TSK-0046: Service Layer Architecture Reorganization
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            Services:
         | 
| 11 | 
            +
            - ProjectAnalyzer: Analyzes project structure and metadata
         | 
| 12 | 
            +
            - ProjectRegistry: Manages project registration and discovery
         | 
| 13 | 
            +
            """
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            from .analyzer import ProjectAnalyzer
         | 
| 16 | 
            +
            from .registry import ProjectRegistry
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            __all__ = [
         | 
| 19 | 
            +
                'ProjectAnalyzer',
         | 
| 20 | 
            +
                'ProjectRegistry',
         | 
| 21 | 
            +
            ]
         |