devsquad 3.6.0__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.
- devsquad-3.6.0.dist-info/METADATA +944 -0
- devsquad-3.6.0.dist-info/RECORD +95 -0
- devsquad-3.6.0.dist-info/WHEEL +5 -0
- devsquad-3.6.0.dist-info/entry_points.txt +2 -0
- devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
- devsquad-3.6.0.dist-info/top_level.txt +2 -0
- scripts/__init__.py +0 -0
- scripts/ai_semantic_matcher.py +512 -0
- scripts/alert_manager.py +505 -0
- scripts/api/__init__.py +43 -0
- scripts/api/models.py +386 -0
- scripts/api/routes/__init__.py +20 -0
- scripts/api/routes/dispatch.py +348 -0
- scripts/api/routes/lifecycle.py +330 -0
- scripts/api/routes/metrics_gates.py +347 -0
- scripts/api_server.py +318 -0
- scripts/auth.py +451 -0
- scripts/cli/__init__.py +1 -0
- scripts/cli/cli_visual.py +642 -0
- scripts/cli.py +1094 -0
- scripts/collaboration/__init__.py +212 -0
- scripts/collaboration/_version.py +1 -0
- scripts/collaboration/agent_briefing.py +656 -0
- scripts/collaboration/ai_semantic_matcher.py +260 -0
- scripts/collaboration/anchor_checker.py +281 -0
- scripts/collaboration/anti_rationalization.py +470 -0
- scripts/collaboration/async_integration_example.py +255 -0
- scripts/collaboration/batch_scheduler.py +149 -0
- scripts/collaboration/checkpoint_manager.py +561 -0
- scripts/collaboration/ci_feedback_adapter.py +351 -0
- scripts/collaboration/code_map_generator.py +247 -0
- scripts/collaboration/concern_pack_loader.py +352 -0
- scripts/collaboration/confidence_score.py +496 -0
- scripts/collaboration/config_loader.py +188 -0
- scripts/collaboration/consensus.py +244 -0
- scripts/collaboration/context_compressor.py +533 -0
- scripts/collaboration/coordinator.py +668 -0
- scripts/collaboration/dispatcher.py +1636 -0
- scripts/collaboration/dual_layer_context.py +128 -0
- scripts/collaboration/enhanced_worker.py +539 -0
- scripts/collaboration/feature_usage_tracker.py +206 -0
- scripts/collaboration/five_axis_consensus.py +334 -0
- scripts/collaboration/input_validator.py +401 -0
- scripts/collaboration/integration_example.py +287 -0
- scripts/collaboration/intent_workflow_mapper.py +350 -0
- scripts/collaboration/language_parsers.py +269 -0
- scripts/collaboration/lifecycle_protocol.py +1446 -0
- scripts/collaboration/llm_backend.py +453 -0
- scripts/collaboration/llm_cache.py +448 -0
- scripts/collaboration/llm_cache_async.py +347 -0
- scripts/collaboration/llm_retry.py +387 -0
- scripts/collaboration/llm_retry_async.py +389 -0
- scripts/collaboration/mce_adapter.py +597 -0
- scripts/collaboration/memory_bridge.py +1607 -0
- scripts/collaboration/models.py +537 -0
- scripts/collaboration/null_providers.py +297 -0
- scripts/collaboration/operation_classifier.py +289 -0
- scripts/collaboration/output_slicer.py +225 -0
- scripts/collaboration/performance_monitor.py +462 -0
- scripts/collaboration/permission_guard.py +865 -0
- scripts/collaboration/prompt_assembler.py +756 -0
- scripts/collaboration/prompt_variant_generator.py +483 -0
- scripts/collaboration/protocols.py +267 -0
- scripts/collaboration/report_formatter.py +352 -0
- scripts/collaboration/retrospective.py +279 -0
- scripts/collaboration/role_matcher.py +92 -0
- scripts/collaboration/role_template_market.py +352 -0
- scripts/collaboration/rule_collector.py +678 -0
- scripts/collaboration/scratchpad.py +346 -0
- scripts/collaboration/skill_registry.py +151 -0
- scripts/collaboration/skillifier.py +878 -0
- scripts/collaboration/standardized_role_template.py +317 -0
- scripts/collaboration/task_completion_checker.py +237 -0
- scripts/collaboration/test_quality_guard.py +695 -0
- scripts/collaboration/unified_gate_engine.py +598 -0
- scripts/collaboration/usage_tracker.py +309 -0
- scripts/collaboration/user_friendly_error.py +176 -0
- scripts/collaboration/verification_gate.py +312 -0
- scripts/collaboration/warmup_manager.py +635 -0
- scripts/collaboration/worker.py +513 -0
- scripts/collaboration/workflow_engine.py +684 -0
- scripts/dashboard.py +1088 -0
- scripts/generate_benchmark_report.py +786 -0
- scripts/history_manager.py +604 -0
- scripts/mcp_server.py +289 -0
- skills/__init__.py +32 -0
- skills/dispatch/handler.py +52 -0
- skills/intent/handler.py +59 -0
- skills/registry.py +67 -0
- skills/retrospective/__init__.py +0 -0
- skills/retrospective/handler.py +125 -0
- skills/review/handler.py +356 -0
- skills/security/handler.py +454 -0
- skills/test/__init__.py +0 -0
- skills/test/handler.py +78 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
MCEAdapter — CarryMem (formerly MCE) Integration Adapter
|
|
5
|
+
|
|
6
|
+
Bridges DevSquad's MemoryBridge with CarryMem, the portable AI memory layer.
|
|
7
|
+
CarryMem is an EXTERNAL project — never modify its source code.
|
|
8
|
+
|
|
9
|
+
Design Principles:
|
|
10
|
+
- CarryMem as optional dependency — auto-degrade on import failure
|
|
11
|
+
- All calls wrapped in try/except — zero intrusion, no impact on main flow
|
|
12
|
+
- Lazy initialization — no impact on cold start speed
|
|
13
|
+
- Type mapping between DevSquad MemoryType and CarryMem memory types
|
|
14
|
+
|
|
15
|
+
CarryMem API Reference (v0.8+):
|
|
16
|
+
- CarryMem() — main entry point (replaces old MemoryClassificationEngineFacade)
|
|
17
|
+
- classify_message(text, context) → dict with entries
|
|
18
|
+
- classify_and_remember(text, context) → dict with entries + stored keys
|
|
19
|
+
- recall_memories(query, filters, limit) → list of dicts
|
|
20
|
+
- forget_memory(memory_id) → bool
|
|
21
|
+
- get_stats() → dict
|
|
22
|
+
- whoami() → dict (user identity profile)
|
|
23
|
+
- check_conflicts() → list of conflicts
|
|
24
|
+
- check_quality(min_score) → list of low-quality memories
|
|
25
|
+
|
|
26
|
+
Memory Type Mapping (DevSquad ↔ CarryMem):
|
|
27
|
+
DevSquad MemoryType | CarryMem Type
|
|
28
|
+
--------------------+------------------
|
|
29
|
+
KNOWLEDGE | fact_declaration
|
|
30
|
+
EPISODIC | task_pattern
|
|
31
|
+
SEMANTIC | sentiment_marker
|
|
32
|
+
FEEDBACK | correction
|
|
33
|
+
PATTERN | task_pattern
|
|
34
|
+
ANALYSIS | decision
|
|
35
|
+
CORRECTION | correction
|
|
36
|
+
(no mapping) | user_preference
|
|
37
|
+
(no mapping) | relationship
|
|
38
|
+
|
|
39
|
+
Example Usage:
|
|
40
|
+
adapter = MCEAdapter(enable=True)
|
|
41
|
+
if adapter.is_available:
|
|
42
|
+
result = adapter.classify("User successfully logged in")
|
|
43
|
+
print(result) # MCEResult(memory_type='decision', confidence=0.92, ...)
|
|
44
|
+
else:
|
|
45
|
+
print("CarryMem unavailable, using default classification")
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
import uuid
|
|
49
|
+
from dataclasses import dataclass, field
|
|
50
|
+
from typing import Any, Dict, List, Optional
|
|
51
|
+
import re as _re
|
|
52
|
+
import unicodedata
|
|
53
|
+
import logging
|
|
54
|
+
import threading
|
|
55
|
+
|
|
56
|
+
logger = logging.getLogger(__name__)
|
|
57
|
+
|
|
58
|
+
CARRYMEM_TO_DEVOPSQUAD = {
|
|
59
|
+
"user_preference": "knowledge",
|
|
60
|
+
"correction": "correction",
|
|
61
|
+
"fact_declaration": "knowledge",
|
|
62
|
+
"decision": "analysis",
|
|
63
|
+
"relationship": "knowledge",
|
|
64
|
+
"task_pattern": "pattern",
|
|
65
|
+
"sentiment_marker": "semantic",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
DEVSQUAD_TO_CARRYMEM = {
|
|
69
|
+
"knowledge": "fact_declaration",
|
|
70
|
+
"episodic": "task_pattern",
|
|
71
|
+
"semantic": "sentiment_marker",
|
|
72
|
+
"feedback": "correction",
|
|
73
|
+
"pattern": "task_pattern",
|
|
74
|
+
"analysis": "decision",
|
|
75
|
+
"correction": "correction",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
RULE_TYPES = frozenset({"forbid", "avoid", "always", "prefer"})
|
|
79
|
+
|
|
80
|
+
_MAX_RULE_TEXT_LENGTH = 500
|
|
81
|
+
_USER_ID_BLOCKED_CHARS = _re.compile(r'[<>\'";&|`$\\]')
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class MCEResult:
|
|
85
|
+
memory_type: str = ""
|
|
86
|
+
confidence: float = 0.0
|
|
87
|
+
tier: str = "tier2"
|
|
88
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
89
|
+
|
|
90
|
+
def to_dict(self) -> Dict:
|
|
91
|
+
return {
|
|
92
|
+
"type": self.memory_type,
|
|
93
|
+
"confidence": round(self.confidence, 4),
|
|
94
|
+
"tier": self.tier,
|
|
95
|
+
"metadata": self.metadata,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class MCEStatus:
|
|
101
|
+
available: bool = False
|
|
102
|
+
version: str = ""
|
|
103
|
+
init_error: Optional[str] = None
|
|
104
|
+
classify_count: int = 0
|
|
105
|
+
classify_fail_count: int = 0
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class MCEAdapter:
|
|
109
|
+
"""
|
|
110
|
+
CarryMem Integration Adapter
|
|
111
|
+
|
|
112
|
+
Wraps CarryMem (memory_classification_engine.CarryMem) providing
|
|
113
|
+
unified classify / store / retrieve interfaces.
|
|
114
|
+
Gracefully degrades when CarryMem is not installed.
|
|
115
|
+
|
|
116
|
+
Thread Safety: All public methods are thread-safe (internal RLock protection)
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
_instance = None
|
|
120
|
+
_singleton_lock = threading.Lock()
|
|
121
|
+
|
|
122
|
+
def __init__(self, enable: bool = False):
|
|
123
|
+
self._lock = threading.RLock()
|
|
124
|
+
self._status = MCEStatus()
|
|
125
|
+
self._carrymem = None
|
|
126
|
+
self._adapter_type = "none"
|
|
127
|
+
|
|
128
|
+
if enable:
|
|
129
|
+
self._try_init()
|
|
130
|
+
|
|
131
|
+
def _try_init(self):
|
|
132
|
+
with self._lock:
|
|
133
|
+
try:
|
|
134
|
+
from memory_classification_engine.integration.devsquad import DevSquadAdapter
|
|
135
|
+
self._carrymem = DevSquadAdapter()
|
|
136
|
+
self._status.available = True
|
|
137
|
+
self._adapter_type = "devsquad_adapter"
|
|
138
|
+
|
|
139
|
+
version = getattr(
|
|
140
|
+
__import__('memory_classification_engine'),
|
|
141
|
+
'__version__',
|
|
142
|
+
'unknown'
|
|
143
|
+
)
|
|
144
|
+
self._status.version = str(version)
|
|
145
|
+
|
|
146
|
+
except ImportError:
|
|
147
|
+
try:
|
|
148
|
+
from memory_classification_engine import CarryMem
|
|
149
|
+
self._carrymem = CarryMem()
|
|
150
|
+
self._status.available = True
|
|
151
|
+
self._adapter_type = "carrymem_legacy"
|
|
152
|
+
|
|
153
|
+
version = getattr(
|
|
154
|
+
__import__('memory_classification_engine'),
|
|
155
|
+
'__version__',
|
|
156
|
+
'unknown'
|
|
157
|
+
)
|
|
158
|
+
self._status.version = str(version)
|
|
159
|
+
|
|
160
|
+
except ImportError as e:
|
|
161
|
+
self._status.available = False
|
|
162
|
+
self._status.init_error = f"CarryMem not installed: {e}"
|
|
163
|
+
self._adapter_type = "none"
|
|
164
|
+
except (AttributeError, ModuleNotFoundError) as e:
|
|
165
|
+
self._status.available = False
|
|
166
|
+
self._status.init_error = f"CarryMem module error: {e}"
|
|
167
|
+
self._adapter_type = "none"
|
|
168
|
+
except Exception as e:
|
|
169
|
+
import logging
|
|
170
|
+
logging.getLogger(__name__).warning(
|
|
171
|
+
f"Unexpected error initializing CarryMem: {type(e).__name__}: {e}"
|
|
172
|
+
)
|
|
173
|
+
self._status.available = False
|
|
174
|
+
self._status.init_error = f"{type(e).__name__}: {e}"
|
|
175
|
+
self._adapter_type = "none"
|
|
176
|
+
except Exception as e:
|
|
177
|
+
import logging
|
|
178
|
+
logging.getLogger(__name__).warning(
|
|
179
|
+
f"Unexpected error in MCEAdapter init: {type(e).__name__}: {e}"
|
|
180
|
+
)
|
|
181
|
+
self._status.available = False
|
|
182
|
+
self._status.init_error = f"{type(e).__name__}: {e}"
|
|
183
|
+
self._adapter_type = "none"
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def is_available(self) -> bool:
|
|
187
|
+
return self._status.available
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def status(self) -> MCEStatus:
|
|
191
|
+
with self._lock:
|
|
192
|
+
return MCEStatus(
|
|
193
|
+
available=self._status.available,
|
|
194
|
+
version=self._status.version,
|
|
195
|
+
init_error=self._status.init_error,
|
|
196
|
+
classify_count=self._status.classify_count,
|
|
197
|
+
classify_fail_count=self._status.classify_fail_count,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def classify(self, text: str,
|
|
201
|
+
context: Optional[Dict] = None,
|
|
202
|
+
timeout_ms: int = 500) -> Optional[MCEResult]:
|
|
203
|
+
with self._lock:
|
|
204
|
+
if not self.is_available or not self._carrymem:
|
|
205
|
+
self._status.classify_fail_count += 1
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
import time as _time
|
|
210
|
+
start = _time.time()
|
|
211
|
+
raw_result = self._carrymem.classify_message(text, context)
|
|
212
|
+
elapsed_ms = (_time.time() - start) * 1000
|
|
213
|
+
|
|
214
|
+
if elapsed_ms > timeout_ms:
|
|
215
|
+
self._status.classify_fail_count += 1
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
result = self._normalize_result(raw_result)
|
|
219
|
+
self._status.classify_count += 1
|
|
220
|
+
return result
|
|
221
|
+
|
|
222
|
+
except Exception as e:
|
|
223
|
+
logger.debug("MCE classify failed: %s", e)
|
|
224
|
+
self._status.classify_fail_count += 1
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
def classify_batch(self,
|
|
228
|
+
texts: List[str],
|
|
229
|
+
context: Optional[Dict] = None) -> List[Optional[MCEResult]]:
|
|
230
|
+
return [self.classify(t, context) for t in texts]
|
|
231
|
+
|
|
232
|
+
def store_memory(self, memory_data: Dict) -> bool:
|
|
233
|
+
with self._lock:
|
|
234
|
+
if not self.is_available or not self._carrymem:
|
|
235
|
+
return False
|
|
236
|
+
try:
|
|
237
|
+
message = memory_data.get("content", memory_data.get("message", ""))
|
|
238
|
+
context = memory_data.get("context")
|
|
239
|
+
if not message:
|
|
240
|
+
return False
|
|
241
|
+
result = self._carrymem.classify_and_remember(message, context=context)
|
|
242
|
+
return result.get("stored", False)
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.debug("MCE store_memory failed: %s", e)
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
def retrieve_memories(self,
|
|
248
|
+
query: str,
|
|
249
|
+
tier: str = "tier2",
|
|
250
|
+
limit: int = 20,
|
|
251
|
+
memory_type: Optional[str] = None) -> List[Dict]:
|
|
252
|
+
with self._lock:
|
|
253
|
+
if not self.is_available or not self._carrymem:
|
|
254
|
+
return []
|
|
255
|
+
try:
|
|
256
|
+
filters = {}
|
|
257
|
+
if memory_type:
|
|
258
|
+
carrymem_type = DEVSQUAD_TO_CARRYMEM.get(memory_type, memory_type)
|
|
259
|
+
filters["type"] = carrymem_type
|
|
260
|
+
results = self._carrymem.recall_memories(
|
|
261
|
+
query=query, filters=filters or None, limit=limit,
|
|
262
|
+
)
|
|
263
|
+
return results if isinstance(results, list) else []
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.debug("MCE retrieve_memories failed: %s", e)
|
|
266
|
+
return []
|
|
267
|
+
|
|
268
|
+
def whoami(self) -> Optional[Dict]:
|
|
269
|
+
with self._lock:
|
|
270
|
+
if not self.is_available or not self._carrymem:
|
|
271
|
+
return None
|
|
272
|
+
try:
|
|
273
|
+
return self._carrymem.whoami()
|
|
274
|
+
except Exception:
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
def check_conflicts(self) -> List[Dict]:
|
|
278
|
+
with self._lock:
|
|
279
|
+
if not self.is_available or not self._carrymem:
|
|
280
|
+
return []
|
|
281
|
+
try:
|
|
282
|
+
return self._carrymem.check_conflicts()
|
|
283
|
+
except Exception:
|
|
284
|
+
return []
|
|
285
|
+
|
|
286
|
+
def shutdown(self):
|
|
287
|
+
with self._lock:
|
|
288
|
+
if self._carrymem and hasattr(self._carrymem, 'close'):
|
|
289
|
+
try:
|
|
290
|
+
self._carrymem.close()
|
|
291
|
+
except Exception:
|
|
292
|
+
pass
|
|
293
|
+
self._carrymem = None
|
|
294
|
+
self._status.available = False
|
|
295
|
+
|
|
296
|
+
def force_reinit(self):
|
|
297
|
+
self.shutdown()
|
|
298
|
+
self._try_init()
|
|
299
|
+
|
|
300
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
301
|
+
"""Return memory statistics from CarryMem."""
|
|
302
|
+
if not self.is_available or not self._carrymem:
|
|
303
|
+
return {
|
|
304
|
+
"total_users": 0,
|
|
305
|
+
"total_rules": 0,
|
|
306
|
+
"available": False,
|
|
307
|
+
"adapter_type": self._adapter_type,
|
|
308
|
+
}
|
|
309
|
+
with self._lock:
|
|
310
|
+
try:
|
|
311
|
+
if hasattr(self._carrymem, 'get_stats'):
|
|
312
|
+
return self._carrymem.get_stats()
|
|
313
|
+
except Exception:
|
|
314
|
+
pass
|
|
315
|
+
return {
|
|
316
|
+
"total_users": 0,
|
|
317
|
+
"total_rules": 0,
|
|
318
|
+
"available": self.is_available,
|
|
319
|
+
"adapter_type": self._adapter_type,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
@staticmethod
|
|
323
|
+
def _sanitize_user_id(user_id: str) -> str:
|
|
324
|
+
"""Sanitize user_id to prevent injection attacks.
|
|
325
|
+
|
|
326
|
+
CarryMem trusts caller-provided user_id without authentication.
|
|
327
|
+
DevSquad must ensure user_id is safe before passing it.
|
|
328
|
+
"""
|
|
329
|
+
if not user_id:
|
|
330
|
+
return "default"
|
|
331
|
+
normalized = unicodedata.normalize('NFKC', str(user_id))
|
|
332
|
+
sanitized = _USER_ID_BLOCKED_CHARS.sub('_', normalized)
|
|
333
|
+
while '../' in sanitized or '..\\' in sanitized:
|
|
334
|
+
sanitized = sanitized.replace('../', '_').replace('..\\', '_')
|
|
335
|
+
sanitized = sanitized.replace('/', '_').replace('\\', '_')
|
|
336
|
+
sanitized = sanitized.strip()[:128]
|
|
337
|
+
return sanitized or "default"
|
|
338
|
+
|
|
339
|
+
def match_rules(self, task_description: str, user_id: str,
|
|
340
|
+
role: Optional[str] = None, max_rules: int = 5) -> List[Dict[str, Any]]:
|
|
341
|
+
"""Match rules based on task description and role.
|
|
342
|
+
|
|
343
|
+
Calls DevSquadAdapter.match_rules() when available (CarryMem v0.2.8+).
|
|
344
|
+
Falls back to CarryMem legacy API, then keyword-based matching.
|
|
345
|
+
"""
|
|
346
|
+
safe_user_id = self._sanitize_user_id(user_id)
|
|
347
|
+
|
|
348
|
+
if not self.is_available or not self._carrymem:
|
|
349
|
+
return self._keyword_fallback_match(task_description, safe_user_id, role, max_rules)
|
|
350
|
+
|
|
351
|
+
with self._lock:
|
|
352
|
+
try:
|
|
353
|
+
if hasattr(self._carrymem, 'match_rules'):
|
|
354
|
+
result = self._carrymem.match_rules(
|
|
355
|
+
task_description=task_description,
|
|
356
|
+
user_id=safe_user_id,
|
|
357
|
+
role=role,
|
|
358
|
+
max_rules=max_rules
|
|
359
|
+
)
|
|
360
|
+
else:
|
|
361
|
+
return self._keyword_fallback_match(task_description, safe_user_id, role, max_rules)
|
|
362
|
+
if isinstance(result, list):
|
|
363
|
+
return self._normalize_matched_rules(result)
|
|
364
|
+
return []
|
|
365
|
+
except Exception as e:
|
|
366
|
+
logger.warning("CarryMem match_rules failed: %s", e)
|
|
367
|
+
return self._keyword_fallback_match(task_description, safe_user_id, role, max_rules)
|
|
368
|
+
|
|
369
|
+
def format_rules_as_prompt(self, rules: List[Dict[str, Any]]) -> str:
|
|
370
|
+
"""Format matched rules as injectable prompt text.
|
|
371
|
+
|
|
372
|
+
Calls DevSquadAdapter.format_rules_as_prompt() when available (CarryMem v0.2.8+).
|
|
373
|
+
Falls back to simple formatting when CarryMem is unavailable.
|
|
374
|
+
"""
|
|
375
|
+
if not rules:
|
|
376
|
+
return ""
|
|
377
|
+
|
|
378
|
+
for rule in rules:
|
|
379
|
+
for key in ("action", "trigger"):
|
|
380
|
+
text = rule.get(key, "")
|
|
381
|
+
if isinstance(text, str) and len(text) > _MAX_RULE_TEXT_LENGTH:
|
|
382
|
+
rule[key] = text[:_MAX_RULE_TEXT_LENGTH]
|
|
383
|
+
|
|
384
|
+
if not self.is_available or not self._carrymem:
|
|
385
|
+
return self._format_rules_fallback(rules)
|
|
386
|
+
|
|
387
|
+
with self._lock:
|
|
388
|
+
try:
|
|
389
|
+
if hasattr(self._carrymem, 'format_rules_as_prompt'):
|
|
390
|
+
return self._carrymem.format_rules_as_prompt(rules)
|
|
391
|
+
return self._format_rules_fallback(rules)
|
|
392
|
+
except Exception as e:
|
|
393
|
+
logger.warning("CarryMem format_rules_as_prompt failed: %s", e)
|
|
394
|
+
return self._format_rules_fallback(rules)
|
|
395
|
+
|
|
396
|
+
def add_rule(self, trigger: str, action: str, rule_type: str = "always",
|
|
397
|
+
confidence: float = 0.8) -> Optional[Dict[str, Any]]:
|
|
398
|
+
"""Add a rule to CarryMem or local storage.
|
|
399
|
+
|
|
400
|
+
Uses DevSquadAdapter.add_rule() when available (CarryMem v0.2.8+).
|
|
401
|
+
Falls back to local storage when CarryMem is unavailable.
|
|
402
|
+
"""
|
|
403
|
+
if rule_type not in ("always", "avoid", "prefer", "forbid"):
|
|
404
|
+
rule_type = "always"
|
|
405
|
+
|
|
406
|
+
if self.is_available and self._carrymem:
|
|
407
|
+
with self._lock:
|
|
408
|
+
try:
|
|
409
|
+
if hasattr(self._carrymem, 'add_rule'):
|
|
410
|
+
return self._carrymem.add_rule(
|
|
411
|
+
trigger=trigger, action=action,
|
|
412
|
+
rule_type=rule_type, confidence=confidence,
|
|
413
|
+
)
|
|
414
|
+
except Exception as e:
|
|
415
|
+
logger.warning("CarryMem add_rule failed: %s", e)
|
|
416
|
+
|
|
417
|
+
logger.info("CarryMem add_rule unavailable, storing locally")
|
|
418
|
+
try:
|
|
419
|
+
from scripts.collaboration.rule_collector import RuleData, LocalRuleStorage
|
|
420
|
+
storage = LocalRuleStorage()
|
|
421
|
+
rule = RuleData(
|
|
422
|
+
trigger=trigger, action=action, type=rule_type,
|
|
423
|
+
confidence=confidence, source="mce_adapter_fallback",
|
|
424
|
+
)
|
|
425
|
+
result = storage.store(rule)
|
|
426
|
+
if result.success:
|
|
427
|
+
return {"rule_id": result.rule_id, "storage": "local_fallback",
|
|
428
|
+
"type": rule_type, "success": True}
|
|
429
|
+
except Exception as e:
|
|
430
|
+
logger.error("Local fallback store failed: %s", e)
|
|
431
|
+
return {"rule_id": f"RULE-LOCAL-{uuid.uuid4().hex[:12]}",
|
|
432
|
+
"storage": "local_fallback", "type": rule_type, "success": False}
|
|
433
|
+
|
|
434
|
+
def _keyword_fallback_match(self, task_description: str, user_id: str,
|
|
435
|
+
role: Optional[str] = None,
|
|
436
|
+
max_rules: int = 5) -> List[Dict[str, Any]]:
|
|
437
|
+
"""Keyword-based rule matching fallback when CarryMem is unavailable."""
|
|
438
|
+
try:
|
|
439
|
+
all_rules = self._get_all_rules_raw(user_id, role)
|
|
440
|
+
except Exception:
|
|
441
|
+
return []
|
|
442
|
+
|
|
443
|
+
if not all_rules:
|
|
444
|
+
return []
|
|
445
|
+
|
|
446
|
+
desc_lower = task_description.lower()
|
|
447
|
+
desc_words = set(desc_lower.split())
|
|
448
|
+
|
|
449
|
+
scored = []
|
|
450
|
+
for rule in all_rules:
|
|
451
|
+
trigger = rule.get("trigger", "").lower()
|
|
452
|
+
action = rule.get("action", "").lower()
|
|
453
|
+
trigger_words = set(trigger.split())
|
|
454
|
+
overlap = len(desc_words & trigger_words)
|
|
455
|
+
if trigger and trigger in desc_lower:
|
|
456
|
+
overlap += 2
|
|
457
|
+
if overlap > 0:
|
|
458
|
+
rule_copy = dict(rule)
|
|
459
|
+
rule_copy["relevance_score"] = min(1.0, overlap / max(len(trigger_words), 1))
|
|
460
|
+
scored.append(rule_copy)
|
|
461
|
+
|
|
462
|
+
scored.sort(key=lambda r: r.get("relevance_score", 0.0), reverse=True)
|
|
463
|
+
return scored[:max_rules]
|
|
464
|
+
|
|
465
|
+
def _get_all_rules_raw(self, user_id: str, role: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
466
|
+
"""Retrieve all rules as raw dicts for keyword fallback matching."""
|
|
467
|
+
if not self.is_available or not self._carrymem:
|
|
468
|
+
return []
|
|
469
|
+
|
|
470
|
+
with self._lock:
|
|
471
|
+
try:
|
|
472
|
+
context = {}
|
|
473
|
+
if role:
|
|
474
|
+
context["role"] = role
|
|
475
|
+
rules_str = self._carrymem.get_rules(user_id=user_id, context=context)
|
|
476
|
+
if not isinstance(rules_str, list):
|
|
477
|
+
return []
|
|
478
|
+
return [self._parse_rule_string(r) for r in rules_str if isinstance(r, str)]
|
|
479
|
+
except Exception:
|
|
480
|
+
return []
|
|
481
|
+
|
|
482
|
+
@staticmethod
|
|
483
|
+
def _parse_rule_string(rule_str: str) -> Dict[str, Any]:
|
|
484
|
+
"""Parse a prefixed rule string into a structured dict.
|
|
485
|
+
|
|
486
|
+
Format: "[TYPE] Description text (override)"
|
|
487
|
+
"""
|
|
488
|
+
result: Dict[str, Any] = {
|
|
489
|
+
"rule_type": "always",
|
|
490
|
+
"trigger": "",
|
|
491
|
+
"action": rule_str,
|
|
492
|
+
"relevance_score": 0.0,
|
|
493
|
+
"rule_id": "",
|
|
494
|
+
"override": False,
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
stripped = rule_str.strip()
|
|
498
|
+
type_match = _re.match(r'^\[(FORBID|AVOID|ALWAYS)\]\s*', stripped, _re.IGNORECASE)
|
|
499
|
+
if type_match:
|
|
500
|
+
type_str = type_match.group(1).lower()
|
|
501
|
+
if type_str in RULE_TYPES:
|
|
502
|
+
result["rule_type"] = type_str
|
|
503
|
+
stripped = stripped[type_match.end():]
|
|
504
|
+
|
|
505
|
+
if stripped.endswith("(override)"):
|
|
506
|
+
result["override"] = True
|
|
507
|
+
stripped = stripped[:-len("(override)")].strip()
|
|
508
|
+
|
|
509
|
+
result["action"] = stripped
|
|
510
|
+
result["trigger"] = stripped.lower()
|
|
511
|
+
return result
|
|
512
|
+
|
|
513
|
+
@staticmethod
|
|
514
|
+
def _format_rules_fallback(rules: List[Dict[str, Any]]) -> str:
|
|
515
|
+
"""Simple rule formatting fallback when CarryMem is unavailable."""
|
|
516
|
+
if not rules:
|
|
517
|
+
return ""
|
|
518
|
+
|
|
519
|
+
parts = ["=== Applicable Rules ==="]
|
|
520
|
+
for rule in rules:
|
|
521
|
+
rule_type = rule.get("rule_type", "always").upper()
|
|
522
|
+
action = rule.get("action", "")
|
|
523
|
+
override = " (non-overridable)" if rule.get("override") else ""
|
|
524
|
+
parts.append(f"[{rule_type}] {action}{override}")
|
|
525
|
+
parts.append("")
|
|
526
|
+
return "\n".join(parts)
|
|
527
|
+
|
|
528
|
+
@staticmethod
|
|
529
|
+
def _normalize_matched_rules(rules: List[Any]) -> List[Dict[str, Any]]:
|
|
530
|
+
"""Normalize CarryMem rule objects into standard dict format."""
|
|
531
|
+
normalized = []
|
|
532
|
+
for rule in rules:
|
|
533
|
+
if isinstance(rule, dict):
|
|
534
|
+
rule_type = rule.get("rule_type", rule.get("type", "always"))
|
|
535
|
+
if rule_type not in RULE_TYPES:
|
|
536
|
+
rule_type = "always"
|
|
537
|
+
normalized.append({
|
|
538
|
+
"rule_type": rule_type,
|
|
539
|
+
"trigger": rule.get("trigger", ""),
|
|
540
|
+
"action": rule.get("action", rule.get("description", "")),
|
|
541
|
+
"relevance_score": float(rule.get("relevance_score", rule.get("score", 0.0))),
|
|
542
|
+
"rule_id": str(rule.get("rule_id", rule.get("id", ""))),
|
|
543
|
+
"override": bool(rule.get("override", False)),
|
|
544
|
+
})
|
|
545
|
+
return normalized
|
|
546
|
+
|
|
547
|
+
@staticmethod
|
|
548
|
+
def _normalize_result(raw: Any) -> MCEResult:
|
|
549
|
+
if raw is None:
|
|
550
|
+
return MCEResult()
|
|
551
|
+
|
|
552
|
+
if isinstance(raw, dict):
|
|
553
|
+
entries = raw.get("entries", [])
|
|
554
|
+
if entries:
|
|
555
|
+
first = entries[0] if isinstance(entries[0], dict) else {}
|
|
556
|
+
mt = first.get("type", first.get("memory_type", "general"))
|
|
557
|
+
carrymem_type = str(mt)
|
|
558
|
+
devsqu_type = CARRYMEM_TO_DEVOPSQUAD.get(carrymem_type, carrymem_type)
|
|
559
|
+
conf = first.get("confidence", 0.0)
|
|
560
|
+
if isinstance(conf, (int, float)):
|
|
561
|
+
conf = min(max(float(conf), 0.0), 1.0)
|
|
562
|
+
else:
|
|
563
|
+
try:
|
|
564
|
+
conf = float(str(conf))
|
|
565
|
+
except (ValueError, TypeError):
|
|
566
|
+
conf = 0.5
|
|
567
|
+
tier = first.get("tier", 2)
|
|
568
|
+
tier_str = f"tier{tier}" if isinstance(tier, int) else str(tier)
|
|
569
|
+
meta = {k: v for k, v in first.items()
|
|
570
|
+
if k not in ('type', 'memory_type', 'confidence', 'tier')}
|
|
571
|
+
meta["carrymem_type"] = carrymem_type
|
|
572
|
+
return MCEResult(
|
|
573
|
+
memory_type=devsqu_type,
|
|
574
|
+
confidence=conf,
|
|
575
|
+
tier=tier_str,
|
|
576
|
+
metadata=meta,
|
|
577
|
+
)
|
|
578
|
+
should_remember = raw.get("should_remember", False)
|
|
579
|
+
return MCEResult(
|
|
580
|
+
memory_type="general",
|
|
581
|
+
confidence=0.5,
|
|
582
|
+
metadata={"should_remember": should_remember},
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
if isinstance(raw, str):
|
|
586
|
+
devsqu_type = CARRYMEM_TO_DEVOPSQUAD.get(raw, raw)
|
|
587
|
+
return MCEResult(memory_type=devsqu_type)
|
|
588
|
+
|
|
589
|
+
return MCEResult(metadata={"raw": str(raw)[:200]})
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def get_global_mce_adapter(enable: bool = False) -> MCEAdapter:
|
|
593
|
+
if MCEAdapter._instance is None:
|
|
594
|
+
with MCEAdapter._singleton_lock:
|
|
595
|
+
if MCEAdapter._instance is None:
|
|
596
|
+
MCEAdapter._instance = MCEAdapter(enable=enable)
|
|
597
|
+
return MCEAdapter._instance
|