claude-mpm 4.7.4__py3-none-any.whl → 4.7.6__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,263 @@
1
+ """
2
+ Kuzu-Memory Pre-Delegation Enrichment Hook
3
+ ==========================================
4
+
5
+ Enriches agent delegation context with relevant memories from kuzu-memory
6
+ before the agent receives the task. This is the READ side of bidirectional
7
+ enrichment.
8
+
9
+ WHY: Agents need access to relevant historical knowledge when performing tasks.
10
+ This hook retrieves memories from kuzu-memory and injects them into the
11
+ delegation context.
12
+
13
+ DESIGN DECISIONS:
14
+ - Priority 10 to run early, before other context modifications
15
+ - Reuses KuzuMemoryHook's retrieval methods for consistency
16
+ - Injects memories as a dedicated section in agent context
17
+ - Falls back gracefully if kuzu-memory is not available
18
+ """
19
+
20
+ import logging
21
+ from typing import Any, Dict, Optional
22
+
23
+ from claude_mpm.hooks.base_hook import HookContext, HookResult, PreDelegationHook
24
+ from claude_mpm.hooks.kuzu_memory_hook import get_kuzu_memory_hook
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class KuzuEnrichmentHook(PreDelegationHook):
30
+ """
31
+ Hook that enriches agent delegation context with kuzu-memory.
32
+
33
+ This hook:
34
+ 1. Extracts the task/prompt from delegation context
35
+ 2. Retrieves relevant memories from kuzu-memory
36
+ 3. Injects memories into agent context
37
+ 4. Formats memories for optimal agent understanding
38
+ """
39
+
40
+ def __init__(self):
41
+ """Initialize the kuzu-memory enrichment hook."""
42
+ super().__init__(name="kuzu_memory_enrichment", priority=10)
43
+
44
+ # Reuse the kuzu-memory hook instance for retrieval
45
+ self.kuzu_hook = get_kuzu_memory_hook()
46
+ self.enabled = self.kuzu_hook.enabled
47
+
48
+ if not self.enabled:
49
+ logger.info(
50
+ "Kuzu-memory enrichment hook disabled (kuzu-memory not available)"
51
+ )
52
+ else:
53
+ logger.info("Kuzu-memory enrichment hook enabled")
54
+
55
+ def validate(self, context: HookContext) -> bool:
56
+ """
57
+ Validate if hook should process this context.
58
+
59
+ Args:
60
+ context: Hook context to validate
61
+
62
+ Returns:
63
+ True if hook should execute
64
+ """
65
+ if not self.enabled:
66
+ return False
67
+
68
+ # Check base validation (enabled, correct hook type, has agent)
69
+ if not super().validate(context):
70
+ return False
71
+
72
+ # Must have agent and context data
73
+ if not context.data.get("agent"):
74
+ return False
75
+
76
+ return True
77
+
78
+ def execute(self, context: HookContext) -> HookResult:
79
+ """
80
+ Enrich delegation context with relevant memories.
81
+
82
+ Args:
83
+ context: Hook context containing delegation data
84
+
85
+ Returns:
86
+ HookResult with enriched context
87
+ """
88
+ if not self.enabled:
89
+ return HookResult(success=True, data=context.data, modified=False)
90
+
91
+ try:
92
+ # Extract query for memory retrieval
93
+ query = self._extract_query_from_context(context.data)
94
+
95
+ if not query:
96
+ logger.debug("No query extracted from context for memory retrieval")
97
+ return HookResult(success=True, data=context.data, modified=False)
98
+
99
+ # Retrieve relevant memories
100
+ memories = self.kuzu_hook._retrieve_memories(query)
101
+
102
+ if not memories:
103
+ logger.debug("No relevant memories found")
104
+ return HookResult(success=True, data=context.data, modified=False)
105
+
106
+ # Enrich context with memories
107
+ enriched_data = self._enrich_delegation_context(
108
+ context.data, memories, context.data.get("agent", "Agent")
109
+ )
110
+
111
+ logger.info(
112
+ f"Enriched delegation context with {len(memories)} memories for {context.data.get('agent')}"
113
+ )
114
+
115
+ return HookResult(
116
+ success=True,
117
+ data=enriched_data,
118
+ modified=True,
119
+ metadata={
120
+ "memories_added": len(memories),
121
+ "memory_source": "kuzu",
122
+ "agent": context.data.get("agent"),
123
+ },
124
+ )
125
+
126
+ except Exception as e:
127
+ logger.error(f"Error in kuzu enrichment hook: {e}")
128
+ # Don't fail the delegation if memory enrichment fails
129
+ return HookResult(
130
+ success=True,
131
+ data=context.data,
132
+ modified=False,
133
+ error=f"Memory enrichment failed: {e}",
134
+ )
135
+
136
+ def _extract_query_from_context(self, data: Dict[str, Any]) -> Optional[str]:
137
+ """
138
+ Extract query text for memory retrieval.
139
+
140
+ Args:
141
+ data: Delegation context data
142
+
143
+ Returns:
144
+ Query string or None
145
+ """
146
+ # Try various context fields
147
+ delegation_context = data.get("context", {})
148
+
149
+ # Handle string context
150
+ if isinstance(delegation_context, str):
151
+ return delegation_context
152
+
153
+ # Handle dict context
154
+ if isinstance(delegation_context, dict):
155
+ # Try common fields
156
+ for field in ["prompt", "task", "query", "user_request", "description"]:
157
+ if field in delegation_context:
158
+ value = delegation_context[field]
159
+ if isinstance(value, str):
160
+ return value
161
+
162
+ # If no specific field, join all string values
163
+ text_parts = [
164
+ str(v) for v in delegation_context.values() if isinstance(v, str)
165
+ ]
166
+ if text_parts:
167
+ return " ".join(text_parts)
168
+
169
+ # Fallback: try to get task or instruction directly
170
+ if "task" in data and isinstance(data["task"], str):
171
+ return data["task"]
172
+
173
+ if "instruction" in data and isinstance(data["instruction"], str):
174
+ return data["instruction"]
175
+
176
+ return None
177
+
178
+ def _enrich_delegation_context(
179
+ self, original_data: Dict[str, Any], memories: list, agent_name: str
180
+ ) -> Dict[str, Any]:
181
+ """
182
+ Enrich delegation context with memories.
183
+
184
+ Args:
185
+ original_data: Original delegation data
186
+ memories: Retrieved memories
187
+ agent_name: Name of the agent
188
+
189
+ Returns:
190
+ Enriched delegation data
191
+ """
192
+ # Format memories
193
+ memory_section = self._format_memory_section(memories, agent_name)
194
+
195
+ # Create enriched data
196
+ enriched_data = original_data.copy()
197
+
198
+ # Get existing context
199
+ delegation_context = enriched_data.get("context", {})
200
+ if isinstance(delegation_context, str):
201
+ delegation_context = {"prompt": delegation_context}
202
+
203
+ # Add memory section
204
+ if isinstance(delegation_context, dict):
205
+ # Prepend memory section to context
206
+ delegation_context["kuzu_memories"] = memory_section
207
+
208
+ # If there's a main prompt/task, prepend memory note
209
+ for field in ["prompt", "task", "instruction"]:
210
+ if field in delegation_context and isinstance(
211
+ delegation_context[field], str
212
+ ):
213
+ delegation_context[field] = (
214
+ f"{memory_section}\n\n{delegation_context[field]}"
215
+ )
216
+ break
217
+ else:
218
+ # If context is not dict, create new dict with memory
219
+ delegation_context = {
220
+ "kuzu_memories": memory_section,
221
+ "original_context": delegation_context,
222
+ }
223
+
224
+ enriched_data["context"] = delegation_context
225
+ enriched_data["_kuzu_enriched"] = True
226
+
227
+ return enriched_data
228
+
229
+ def _format_memory_section(self, memories: list, agent_name: str) -> str:
230
+ """
231
+ Format memories into a readable section.
232
+
233
+ Args:
234
+ memories: List of memory dictionaries
235
+ agent_name: Name of the agent
236
+
237
+ Returns:
238
+ Formatted memory section
239
+ """
240
+ memory_text = self.kuzu_hook._format_memories(memories)
241
+
242
+ return f"""
243
+ === RELEVANT KNOWLEDGE FROM KUZU MEMORY ===
244
+ {agent_name}, you have access to these relevant memories from the knowledge graph:
245
+
246
+ {memory_text}
247
+
248
+ INSTRUCTIONS: Review these memories before proceeding. Apply learned patterns and avoid known mistakes.
249
+ Use this knowledge to provide more informed and contextual responses.
250
+ ===========================================
251
+ """
252
+
253
+
254
+ # Create a singleton instance
255
+ _kuzu_enrichment_hook = None
256
+
257
+
258
+ def get_kuzu_enrichment_hook() -> KuzuEnrichmentHook:
259
+ """Get the singleton kuzu-memory enrichment hook instance."""
260
+ global _kuzu_enrichment_hook
261
+ if _kuzu_enrichment_hook is None:
262
+ _kuzu_enrichment_hook = KuzuEnrichmentHook()
263
+ return _kuzu_enrichment_hook
@@ -157,9 +157,13 @@ class KuzuMemoryHook(SubmitHook):
157
157
  List of relevant memory dictionaries
158
158
  """
159
159
  try:
160
- # Use kuzu-memory recall command
160
+ # Type narrowing: ensure kuzu_memory_cmd is not None before using
161
+ if self.kuzu_memory_cmd is None:
162
+ return []
163
+
164
+ # Use kuzu-memory recall command (v1.2.7+ syntax)
161
165
  result = subprocess.run(
162
- [self.kuzu_memory_cmd, "recall", query, "--format", "json"],
166
+ [self.kuzu_memory_cmd, "memory", "recall", query, "--format", "json"],
163
167
  capture_output=True,
164
168
  text=True,
165
169
  timeout=5,
@@ -168,10 +172,21 @@ class KuzuMemoryHook(SubmitHook):
168
172
  )
169
173
 
170
174
  if result.returncode == 0 and result.stdout:
171
- memories = json.loads(result.stdout)
172
- return memories if isinstance(memories, list) else []
173
-
174
- except (subprocess.TimeoutExpired, json.JSONDecodeError, Exception) as e:
175
+ try:
176
+ # Parse JSON with strict=False to handle control characters
177
+ data = json.loads(result.stdout, strict=False)
178
+ # v1.2.7 returns dict with 'memories' key, not array
179
+ if isinstance(data, dict):
180
+ memories = data.get("memories", [])
181
+ else:
182
+ memories = data if isinstance(data, list) else []
183
+ return memories
184
+ except json.JSONDecodeError as e:
185
+ logger.warning(f"Failed to parse kuzu-memory JSON output: {e}")
186
+ logger.debug(f"Raw output: {result.stdout[:200]}")
187
+ return [] # Graceful fallback
188
+
189
+ except (subprocess.TimeoutExpired, Exception) as e:
175
190
  logger.debug(f"Memory retrieval failed: {e}")
176
191
 
177
192
  return []
@@ -255,12 +270,12 @@ Note: Use the memories above to provide more informed and contextual responses.
255
270
  Returns:
256
271
  True if storage was successful
257
272
  """
258
- if not self.enabled:
273
+ if not self.enabled or self.kuzu_memory_cmd is None:
259
274
  return False
260
275
 
261
276
  try:
262
- # Use kuzu-memory remember command (synchronous)
263
- cmd = [self.kuzu_memory_cmd, "remember", content]
277
+ # Use kuzu-memory store command (v1.2.7+ syntax)
278
+ cmd = [self.kuzu_memory_cmd, "memory", "store", content]
264
279
 
265
280
  # Execute store command in project directory
266
281
  result = subprocess.run(
@@ -273,8 +288,10 @@ Note: Use the memories above to provide more informed and contextual responses.
273
288
  )
274
289
 
275
290
  if result.returncode == 0:
276
- logger.debug(f"Stored memory: {content[:50]}...")
291
+ logger.debug(f"Stored memory in kuzu: {content[:50]}...")
277
292
  return True
293
+ logger.warning(f"Failed to store memory in kuzu: {result.stderr}")
294
+ return False
278
295
 
279
296
  except Exception as e:
280
297
  logger.error(f"Failed to store memory: {e}")
@@ -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