claude-mpm 4.7.5__py3-none-any.whl → 4.7.7__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.
@@ -0,0 +1,183 @@
1
+ """
2
+ Kuzu-Memory Response Learning Hook
3
+ ===================================
4
+
5
+ Captures assistant responses and extracts learnings to store in kuzu-memory.
6
+ This completes the bidirectional enrichment cycle:
7
+ - KuzuMemoryHook enriches prompts with memories (READ)
8
+ - KuzuResponseHook stores learnings from responses (WRITE)
9
+
10
+ WHY: To automatically capture and persist important information from agent
11
+ responses, enabling continuous learning across conversations.
12
+
13
+ DESIGN DECISIONS:
14
+ - Priority 80 to run late after main processing
15
+ - Reuses KuzuMemoryHook's storage methods for consistency
16
+ - Graceful degradation if kuzu-memory is not available
17
+ - Extracts structured learnings using patterns and AI
18
+ """
19
+
20
+ import logging
21
+ from typing import Any, Optional
22
+
23
+ from claude_mpm.hooks.base_hook import (
24
+ HookContext,
25
+ HookResult,
26
+ PostDelegationHook,
27
+ )
28
+ from claude_mpm.hooks.kuzu_memory_hook import get_kuzu_memory_hook
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class KuzuResponseHook(PostDelegationHook):
34
+ """
35
+ Hook that captures agent responses and stores learnings in kuzu-memory.
36
+
37
+ This hook:
38
+ 1. Processes agent responses after delegation completes
39
+ 2. Extracts important learnings and information
40
+ 3. Stores memories in kuzu-memory for future retrieval
41
+ 4. Tags memories for better categorization
42
+ """
43
+
44
+ def __init__(self):
45
+ """Initialize the kuzu-memory response learning hook."""
46
+ super().__init__(name="kuzu_response_learner", priority=80)
47
+
48
+ # Reuse the kuzu-memory hook instance for storage
49
+ self.kuzu_hook = get_kuzu_memory_hook()
50
+ self.enabled = self.kuzu_hook.enabled
51
+
52
+ if not self.enabled:
53
+ logger.info(
54
+ "Kuzu-memory response hook disabled (kuzu-memory not available)"
55
+ )
56
+ else:
57
+ logger.info("Kuzu-memory response learning hook enabled")
58
+
59
+ def validate(self, context: HookContext) -> bool:
60
+ """
61
+ Validate if hook should process this context.
62
+
63
+ Args:
64
+ context: Hook context to validate
65
+
66
+ Returns:
67
+ True if hook should execute
68
+ """
69
+ if not self.enabled:
70
+ return False
71
+
72
+ # Check base validation (enabled, correct hook type, has result)
73
+ if not super().validate(context):
74
+ return False
75
+
76
+ # Must have result data to extract learnings from
77
+ result_data = context.data.get("result")
78
+ if not result_data:
79
+ return False
80
+
81
+ return True
82
+
83
+ def execute(self, context: HookContext) -> HookResult:
84
+ """
85
+ Extract and store learnings from agent responses.
86
+
87
+ Args:
88
+ context: Hook context containing response data
89
+
90
+ Returns:
91
+ HookResult with success status and metadata
92
+ """
93
+ if not self.enabled:
94
+ return HookResult(success=True, data=context.data, modified=False)
95
+
96
+ try:
97
+ # Extract response content from various possible formats
98
+ result_data = context.data.get("result", {})
99
+ response_content = self._extract_response_content(result_data)
100
+
101
+ if not response_content:
102
+ logger.debug("No response content found for learning extraction")
103
+ return HookResult(success=True, data=context.data, modified=False)
104
+
105
+ # Extract and store learnings
106
+ count = self.kuzu_hook.extract_and_store_learnings(response_content)
107
+
108
+ if count > 0:
109
+ logger.info(f"Stored {count} learnings from agent response")
110
+ return HookResult(
111
+ success=True,
112
+ data=context.data,
113
+ modified=False,
114
+ metadata={"learnings_stored": count, "memory_backend": "kuzu"},
115
+ )
116
+
117
+ return HookResult(success=True, data=context.data, modified=False)
118
+
119
+ except Exception as e:
120
+ logger.error(f"Error in kuzu response hook: {e}")
121
+ # Don't fail the operation if learning extraction fails
122
+ return HookResult(
123
+ success=True,
124
+ data=context.data,
125
+ modified=False,
126
+ error=f"Learning extraction failed: {e}",
127
+ )
128
+
129
+ def _extract_response_content(self, result_data: Any) -> Optional[str]:
130
+ """
131
+ Extract response content from various result formats.
132
+
133
+ Args:
134
+ result_data: Result data in various possible formats
135
+
136
+ Returns:
137
+ Extracted response content as string, or None
138
+ """
139
+ if not result_data:
140
+ return None
141
+
142
+ # Handle dict format
143
+ if isinstance(result_data, dict):
144
+ # Try common response fields
145
+ for field in ["content", "text", "response", "output", "message"]:
146
+ if field in result_data:
147
+ content = result_data[field]
148
+ if isinstance(content, str):
149
+ return content
150
+ if isinstance(content, dict):
151
+ # Recursively extract from nested dict
152
+ return self._extract_response_content(content)
153
+
154
+ # If dict has no recognizable fields, try converting to string
155
+ return str(result_data)
156
+
157
+ # Handle string format
158
+ if isinstance(result_data, str):
159
+ return result_data
160
+
161
+ # Handle list format (concatenate items)
162
+ if isinstance(result_data, list):
163
+ items = []
164
+ for item in result_data:
165
+ extracted = self._extract_response_content(item)
166
+ if extracted:
167
+ items.append(extracted)
168
+ return "\n\n".join(items) if items else None
169
+
170
+ # Fallback to string conversion
171
+ return str(result_data) if result_data else None
172
+
173
+
174
+ # Create a singleton instance
175
+ _kuzu_response_hook = None
176
+
177
+
178
+ def get_kuzu_response_hook() -> KuzuResponseHook:
179
+ """Get the singleton kuzu-memory response hook instance."""
180
+ global _kuzu_response_hook
181
+ if _kuzu_response_hook is None:
182
+ _kuzu_response_hook = KuzuResponseHook()
183
+ return _kuzu_response_hook
@@ -495,7 +495,7 @@ class MCPServicesCheck(BaseDiagnosticCheck):
495
495
  base_cmd = config["mcp_command"]
496
496
  if len(base_cmd) > 0 and base_cmd[0] == config["package"]:
497
497
  # Simple case where first command is the package name
498
- mcp_command = ["pipx", "run", config["package"]] + base_cmd[1:]
498
+ mcp_command = ["pipx", "run", config["package"], *base_cmd[1:]]
499
499
  else:
500
500
  # Complex case - just try running the package with mcp arg
501
501
  mcp_command = ["pipx", "run", config["package"], "mcp"]
@@ -509,7 +509,7 @@ class MCPServicesCheck(BaseDiagnosticCheck):
509
509
  base_cmd = config["mcp_command"]
510
510
  if service_name == "kuzu-memory":
511
511
  # Special case for kuzu-memory with args
512
- mcp_command = ["pipx", "run", base_cmd[0]] + base_cmd[1:]
512
+ mcp_command = ["pipx", "run", base_cmd[0], *base_cmd[1:]]
513
513
  else:
514
514
  mcp_command = ["pipx", "run", *base_cmd]
515
515
  else:
@@ -550,7 +550,7 @@ class MCPServiceVerifier:
550
550
 
551
551
  try:
552
552
  # Build test command - add --help to test without side effects
553
- test_cmd = [command] + args[:2] if args else [command] # Include base args
553
+ test_cmd = [command, *args[:2]] if args else [command] # Include base args
554
554
  test_cmd.append("--help")
555
555
 
556
556
  result = subprocess.run(
@@ -41,8 +41,8 @@ class MemoryHookService(BaseService, MemoryHookInterface):
41
41
  These hooks ensure memory is properly managed and persisted.
42
42
 
43
43
  DESIGN DECISION: We register hooks for key lifecycle events:
44
- - Before Claude interaction: Load relevant memories
45
- - After Claude interaction: Save new memories
44
+ - Before Claude interaction: Load relevant memories (kuzu-memory + legacy)
45
+ - After Claude interaction: Save new memories (kuzu-memory + legacy)
46
46
  - On error: Ensure memory state is preserved
47
47
  """
48
48
  if not self.hook_service:
@@ -90,11 +90,93 @@ class MemoryHookService(BaseService, MemoryHookInterface):
90
90
  if success2:
91
91
  self.registered_hooks.append("memory_save")
92
92
 
93
- self.logger.debug("Memory hooks registered successfully")
93
+ self.logger.debug("Legacy memory hooks registered successfully")
94
+
95
+ # Register kuzu-memory hooks if available
96
+ self._register_kuzu_memory_hooks()
94
97
 
95
98
  except Exception as e:
96
99
  self.logger.warning(f"Failed to register memory hooks: {e}")
97
100
 
101
+ def _register_kuzu_memory_hooks(self):
102
+ """Register kuzu-memory bidirectional enrichment hooks.
103
+
104
+ WHY: Kuzu-memory provides persistent knowledge graph storage that works
105
+ across conversations. This enables:
106
+ 1. Delegation context enrichment with relevant memories (READ)
107
+ 2. Automatic learning extraction from responses (WRITE)
108
+
109
+ DESIGN DECISION: These hooks are separate from legacy memory hooks to
110
+ allow independent evolution and configuration. Both systems can coexist.
111
+ """
112
+ try:
113
+ # Check if kuzu-memory is enabled in config
114
+ from claude_mpm.core.config import Config
115
+
116
+ config = Config()
117
+ kuzu_config = config.get("memory.kuzu", {})
118
+ if isinstance(kuzu_config, dict):
119
+ kuzu_enabled = kuzu_config.get("enabled", True)
120
+ enrichment_enabled = kuzu_config.get("enrichment", True)
121
+ learning_enabled = kuzu_config.get("learning", True)
122
+ else:
123
+ # Default to enabled if config section doesn't exist
124
+ kuzu_enabled = True
125
+ enrichment_enabled = True
126
+ learning_enabled = True
127
+
128
+ if not kuzu_enabled:
129
+ self.logger.debug("Kuzu-memory disabled in configuration")
130
+ return
131
+
132
+ from claude_mpm.hooks import (
133
+ get_kuzu_enrichment_hook,
134
+ get_kuzu_response_hook,
135
+ )
136
+
137
+ # Get kuzu-memory hooks
138
+ enrichment_hook = get_kuzu_enrichment_hook()
139
+ learning_hook = get_kuzu_response_hook()
140
+
141
+ # Register enrichment hook (PreDelegationHook) if enabled
142
+ if enrichment_hook.enabled and enrichment_enabled:
143
+ success = self.hook_service.register_hook(enrichment_hook)
144
+ if success:
145
+ self.registered_hooks.append("kuzu_memory_enrichment")
146
+ self.logger.info(
147
+ "✅ Kuzu-memory enrichment enabled (prompts → memories)"
148
+ )
149
+ else:
150
+ self.logger.warning(
151
+ "Failed to register kuzu-memory enrichment hook"
152
+ )
153
+ elif not enrichment_enabled:
154
+ self.logger.debug("Kuzu-memory enrichment disabled in configuration")
155
+
156
+ # Register learning hook (PostDelegationHook) if enabled
157
+ if learning_hook.enabled and learning_enabled:
158
+ success = self.hook_service.register_hook(learning_hook)
159
+ if success:
160
+ self.registered_hooks.append("kuzu_response_learner")
161
+ self.logger.info(
162
+ "✅ Kuzu-memory learning enabled (responses → memories)"
163
+ )
164
+ else:
165
+ self.logger.warning("Failed to register kuzu-memory learning hook")
166
+ elif not learning_enabled:
167
+ self.logger.debug("Kuzu-memory learning disabled in configuration")
168
+
169
+ # If neither hook is enabled, kuzu-memory is not available
170
+ if not enrichment_hook.enabled and not learning_hook.enabled:
171
+ self.logger.debug(
172
+ "Kuzu-memory not available. Install with: pipx install kuzu-memory"
173
+ )
174
+
175
+ except ImportError as e:
176
+ self.logger.debug(f"Kuzu-memory hooks not available: {e}")
177
+ except Exception as e:
178
+ self.logger.warning(f"Failed to register kuzu-memory hooks: {e}")
179
+
98
180
  def _load_relevant_memories_hook(self, context):
99
181
  """Hook function to load relevant memories before Claude interaction.
100
182
 
@@ -14,7 +14,7 @@ import logging
14
14
  import subprocess
15
15
  import sys
16
16
  import time
17
- from typing import Dict, List, Optional, Set, Tuple
17
+ from typing import Any, Dict, List, Optional, Set, Tuple
18
18
 
19
19
  from packaging.requirements import InvalidRequirement, Requirement
20
20
 
@@ -72,7 +72,7 @@ class AgentDependencyLoader:
72
72
  Returns:
73
73
  Dictionary mapping agent IDs to their file paths
74
74
  """
75
- deployed_agents = {}
75
+ deployed_agents: Dict[str, Path] = {}
76
76
  claude_agents_dir = Path.cwd() / ".claude" / "agents"
77
77
 
78
78
  if not claude_agents_dir.exists():
@@ -409,7 +409,7 @@ class AgentDependencyLoader:
409
409
  Returns:
410
410
  Analysis results including missing and satisfied dependencies
411
411
  """
412
- results = {
412
+ results: Dict[str, Any] = {
413
413
  "agents": {},
414
414
  "summary": {
415
415
  "total_agents": len(self.deployed_agents),
@@ -422,7 +422,7 @@ class AgentDependencyLoader:
422
422
  }
423
423
 
424
424
  for agent_id, deps in self.agent_dependencies.items():
425
- agent_result = {
425
+ agent_result: Dict[str, Dict[str, List[str]]] = {
426
426
  "python": {"satisfied": [], "missing": [], "outdated": []},
427
427
  "system": {"satisfied": [], "missing": []},
428
428
  }
@@ -476,8 +476,8 @@ class AgentDependencyLoader:
476
476
  """
477
477
  import sys
478
478
 
479
- compatible = []
480
- incompatible = []
479
+ compatible: List[str] = []
480
+ incompatible: List[str] = []
481
481
 
482
482
  for dep in dependencies:
483
483
  try:
@@ -593,20 +593,37 @@ class AgentDependencyLoader:
593
593
  try:
594
594
  cmd = [sys.executable, "-m", "pip", "install"]
595
595
 
596
- # Check for PEP 668 managed environment
596
+ # Check environment and add appropriate flags
597
+ import os
597
598
  import sysconfig
598
599
 
599
- stdlib_path = sysconfig.get_path("stdlib")
600
- marker_file = Path(stdlib_path) / "EXTERNALLY-MANAGED"
601
- parent_marker = marker_file.parent.parent / "EXTERNALLY-MANAGED"
600
+ # Check if in virtualenv
601
+ in_virtualenv = (
602
+ (hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix)
603
+ or (hasattr(sys, "real_prefix"))
604
+ or (os.environ.get("VIRTUAL_ENV") is not None)
605
+ )
602
606
 
603
- if marker_file.exists() or parent_marker.exists():
604
- logger.warning(
605
- "PEP 668 managed environment detected. "
606
- "Installing with --break-system-packages --user flags. "
607
- "Consider using a virtual environment instead."
608
- )
609
- cmd.extend(["--break-system-packages", "--user"])
607
+ if in_virtualenv:
608
+ # In virtualenv - no special flags needed
609
+ logger.debug("Installing in virtualenv (no special flags)")
610
+ else:
611
+ # Check for PEP 668 managed environment
612
+ stdlib_path = sysconfig.get_path("stdlib")
613
+ marker_file = Path(stdlib_path) / "EXTERNALLY-MANAGED"
614
+ parent_marker = marker_file.parent.parent / "EXTERNALLY-MANAGED"
615
+
616
+ if marker_file.exists() or parent_marker.exists():
617
+ logger.warning(
618
+ "PEP 668 managed environment detected. "
619
+ "Installing with --break-system-packages flag. "
620
+ "Consider using a virtual environment instead."
621
+ )
622
+ cmd.append("--break-system-packages")
623
+ else:
624
+ # Normal system Python - use --user
625
+ cmd.append("--user")
626
+ logger.debug("Installing with --user flag")
610
627
 
611
628
  cmd.extend(compatible)
612
629
 
@@ -946,13 +963,16 @@ class AgentDependencyLoader:
946
963
 
947
964
  def check_deployed_agent_dependencies(
948
965
  auto_install: bool = False, verbose: bool = False
949
- ) -> None:
966
+ ) -> int:
950
967
  """
951
968
  Check dependencies for currently deployed agents.
952
969
 
953
970
  Args:
954
971
  auto_install: If True, automatically install missing Python dependencies
955
972
  verbose: If True, enable verbose logging
973
+
974
+ Returns:
975
+ Status code: 0 if all dependencies satisfied, 1 if missing dependencies
956
976
  """
957
977
  if verbose:
958
978
  logging.getLogger().setLevel(logging.DEBUG)
@@ -9,6 +9,7 @@ DESIGN DECISION: We implement exponential backoff for retries and provide
9
9
  multiple installation strategies (pip, conda, source) to maximize success rate.
10
10
  """
11
11
 
12
+ import os
12
13
  import re
13
14
  import subprocess
14
15
  import sys
@@ -80,6 +81,7 @@ class RobustPackageInstaller:
80
81
  self.use_cache = use_cache
81
82
  self.attempts: List[InstallAttempt] = []
82
83
  self.success_cache: Dict[str, bool] = {}
84
+ self.in_virtualenv = self._check_virtualenv()
83
85
  self.is_pep668_managed = self._check_pep668_managed()
84
86
  self.pep668_warning_shown = False
85
87
 
@@ -201,6 +203,35 @@ class RobustPackageInstaller:
201
203
  except Exception as e:
202
204
  return False, f"Unexpected error: {e!s}"
203
205
 
206
+ def _check_virtualenv(self) -> bool:
207
+ """
208
+ Check if running inside a virtual environment.
209
+
210
+ WHY: Virtual environments are already isolated and don't need
211
+ --user or --break-system-packages flags. In fact, using --user
212
+ in a virtualenv causes errors.
213
+
214
+ Returns:
215
+ True if in a virtualenv, False otherwise
216
+ """
217
+ # Multiple ways to detect virtualenv
218
+ return (
219
+ (
220
+ # venv creates sys.base_prefix
221
+ hasattr(sys, "base_prefix")
222
+ and sys.base_prefix != sys.prefix
223
+ )
224
+ or (
225
+ # virtualenv creates sys.real_prefix
226
+ hasattr(sys, "real_prefix")
227
+ )
228
+ or (
229
+ # VIRTUAL_ENV environment variable
230
+ os.environ.get("VIRTUAL_ENV")
231
+ is not None
232
+ )
233
+ )
234
+
204
235
  def _check_pep668_managed(self) -> bool:
205
236
  """
206
237
  Check if Python environment is PEP 668 externally managed.
@@ -211,6 +242,11 @@ class RobustPackageInstaller:
211
242
  Returns:
212
243
  True if PEP 668 managed, False otherwise
213
244
  """
245
+ # If in virtualenv, PEP 668 doesn't apply
246
+ if self.in_virtualenv:
247
+ logger.debug("Running in virtualenv, PEP 668 restrictions don't apply")
248
+ return False
249
+
214
250
  # Check for EXTERNALLY-MANAGED marker file
215
251
  stdlib_path = sysconfig.get_path("stdlib")
216
252
  marker_file = Path(stdlib_path) / "EXTERNALLY-MANAGED"
@@ -240,7 +276,7 @@ class RobustPackageInstaller:
240
276
  "Your Python installation is marked as externally managed (PEP 668).\n"
241
277
  "This typically means you're using a system Python managed by Homebrew, apt, etc.\n"
242
278
  "\n"
243
- "Installing packages with --break-system-packages --user flags...\n"
279
+ "Installing packages with --break-system-packages flag...\n"
244
280
  "\n"
245
281
  "RECOMMENDED: Use a virtual environment instead:\n"
246
282
  " python -m venv .venv\n"
@@ -255,8 +291,10 @@ class RobustPackageInstaller:
255
291
  """
256
292
  Build the installation command for a given strategy.
257
293
 
258
- WHY: PEP 668 support added to handle externally managed Python
259
- environments by adding appropriate flags when needed.
294
+ WHY: Proper environment detection ensures we use the right pip flags:
295
+ - Virtualenv: No special flags needed (already isolated)
296
+ - PEP 668 system: Use --break-system-packages only
297
+ - Normal system: Use --user for user-local install
260
298
 
261
299
  Args:
262
300
  package_spec: Package specification
@@ -267,14 +305,19 @@ class RobustPackageInstaller:
267
305
  """
268
306
  base_cmd = [sys.executable, "-m", "pip", "install"]
269
307
 
270
- # Add PEP 668 bypass flags if needed
271
- if self.is_pep668_managed:
308
+ # Determine appropriate flags based on environment
309
+ if self.in_virtualenv:
310
+ # In virtualenv - no special flags needed
311
+ logger.debug("Installing in virtualenv (no special flags)")
312
+ elif self.is_pep668_managed:
313
+ # System Python with PEP 668 - use --break-system-packages only
272
314
  self._show_pep668_warning()
273
- # Add flags to bypass PEP 668 restrictions
274
- base_cmd.extend(["--break-system-packages", "--user"])
275
- logger.debug(
276
- "Added --break-system-packages --user flags for PEP 668 environment"
277
- )
315
+ base_cmd.append("--break-system-packages")
316
+ logger.debug("Added --break-system-packages flag for PEP 668 environment")
317
+ else:
318
+ # Normal system Python - use --user for user-local install
319
+ base_cmd.append("--user")
320
+ logger.debug("Added --user flag for user-local installation")
278
321
 
279
322
  # Add cache control
280
323
  if not self.use_cache:
@@ -612,11 +655,16 @@ class RobustPackageInstaller:
612
655
  try:
613
656
  cmd = [sys.executable, "-m", "pip", "install"]
614
657
 
615
- # Add PEP 668 bypass flags if needed
616
- if self.is_pep668_managed:
658
+ # Add appropriate flags based on environment
659
+ if self.in_virtualenv:
660
+ logger.debug("Batch install in virtualenv (no special flags)")
661
+ elif self.is_pep668_managed:
617
662
  self._show_pep668_warning()
618
- cmd.extend(["--break-system-packages", "--user"])
619
- logger.debug("Added PEP 668 flags for batch installation")
663
+ cmd.append("--break-system-packages")
664
+ logger.debug("Added --break-system-packages for batch installation")
665
+ else:
666
+ cmd.append("--user")
667
+ logger.debug("Added --user flag for batch installation")
620
668
 
621
669
  cmd.extend(packages)
622
670
 
@@ -654,12 +702,18 @@ class RobustPackageInstaller:
654
702
  lines.append("INSTALLATION REPORT")
655
703
  lines.append("=" * 60)
656
704
 
657
- # Add PEP 668 status
658
- if self.is_pep668_managed:
659
- lines.append("")
705
+ # Add environment status
706
+ lines.append("")
707
+ if self.in_virtualenv:
708
+ lines.append("✓ Environment: Virtual Environment (isolated)")
709
+ lines.append(" No special pip flags needed")
710
+ elif self.is_pep668_managed:
660
711
  lines.append("⚠️ PEP 668 Managed Environment: YES")
661
- lines.append(" Installations used --break-system-packages --user flags")
712
+ lines.append(" Installations used --break-system-packages flag")
662
713
  lines.append(" Consider using a virtual environment for better isolation")
714
+ else:
715
+ lines.append("Environment: System Python")
716
+ lines.append(" Installations used --user flag for user-local install")
663
717
 
664
718
  # Summary
665
719
  total_attempts = len(self.attempts)
@@ -672,7 +726,7 @@ class RobustPackageInstaller:
672
726
  lines.append("")
673
727
 
674
728
  # Details by package
675
- packages = {}
729
+ packages: Dict[str, List[InstallAttempt]] = {}
676
730
  for attempt in self.attempts:
677
731
  if attempt.package not in packages:
678
732
  packages[attempt.package] = []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-mpm
3
- Version: 4.7.5
3
+ Version: 4.7.7
4
4
  Summary: Claude Multi-Agent Project Manager - Orchestrate Claude with agent delegation and ticket tracking
5
5
  Author-email: Bob Matsuoka <bob@matsuoka.com>
6
6
  Maintainer: Claude MPM Team