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.
Files changed (95) hide show
  1. devsquad-3.6.0.dist-info/METADATA +944 -0
  2. devsquad-3.6.0.dist-info/RECORD +95 -0
  3. devsquad-3.6.0.dist-info/WHEEL +5 -0
  4. devsquad-3.6.0.dist-info/entry_points.txt +2 -0
  5. devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
  6. devsquad-3.6.0.dist-info/top_level.txt +2 -0
  7. scripts/__init__.py +0 -0
  8. scripts/ai_semantic_matcher.py +512 -0
  9. scripts/alert_manager.py +505 -0
  10. scripts/api/__init__.py +43 -0
  11. scripts/api/models.py +386 -0
  12. scripts/api/routes/__init__.py +20 -0
  13. scripts/api/routes/dispatch.py +348 -0
  14. scripts/api/routes/lifecycle.py +330 -0
  15. scripts/api/routes/metrics_gates.py +347 -0
  16. scripts/api_server.py +318 -0
  17. scripts/auth.py +451 -0
  18. scripts/cli/__init__.py +1 -0
  19. scripts/cli/cli_visual.py +642 -0
  20. scripts/cli.py +1094 -0
  21. scripts/collaboration/__init__.py +212 -0
  22. scripts/collaboration/_version.py +1 -0
  23. scripts/collaboration/agent_briefing.py +656 -0
  24. scripts/collaboration/ai_semantic_matcher.py +260 -0
  25. scripts/collaboration/anchor_checker.py +281 -0
  26. scripts/collaboration/anti_rationalization.py +470 -0
  27. scripts/collaboration/async_integration_example.py +255 -0
  28. scripts/collaboration/batch_scheduler.py +149 -0
  29. scripts/collaboration/checkpoint_manager.py +561 -0
  30. scripts/collaboration/ci_feedback_adapter.py +351 -0
  31. scripts/collaboration/code_map_generator.py +247 -0
  32. scripts/collaboration/concern_pack_loader.py +352 -0
  33. scripts/collaboration/confidence_score.py +496 -0
  34. scripts/collaboration/config_loader.py +188 -0
  35. scripts/collaboration/consensus.py +244 -0
  36. scripts/collaboration/context_compressor.py +533 -0
  37. scripts/collaboration/coordinator.py +668 -0
  38. scripts/collaboration/dispatcher.py +1636 -0
  39. scripts/collaboration/dual_layer_context.py +128 -0
  40. scripts/collaboration/enhanced_worker.py +539 -0
  41. scripts/collaboration/feature_usage_tracker.py +206 -0
  42. scripts/collaboration/five_axis_consensus.py +334 -0
  43. scripts/collaboration/input_validator.py +401 -0
  44. scripts/collaboration/integration_example.py +287 -0
  45. scripts/collaboration/intent_workflow_mapper.py +350 -0
  46. scripts/collaboration/language_parsers.py +269 -0
  47. scripts/collaboration/lifecycle_protocol.py +1446 -0
  48. scripts/collaboration/llm_backend.py +453 -0
  49. scripts/collaboration/llm_cache.py +448 -0
  50. scripts/collaboration/llm_cache_async.py +347 -0
  51. scripts/collaboration/llm_retry.py +387 -0
  52. scripts/collaboration/llm_retry_async.py +389 -0
  53. scripts/collaboration/mce_adapter.py +597 -0
  54. scripts/collaboration/memory_bridge.py +1607 -0
  55. scripts/collaboration/models.py +537 -0
  56. scripts/collaboration/null_providers.py +297 -0
  57. scripts/collaboration/operation_classifier.py +289 -0
  58. scripts/collaboration/output_slicer.py +225 -0
  59. scripts/collaboration/performance_monitor.py +462 -0
  60. scripts/collaboration/permission_guard.py +865 -0
  61. scripts/collaboration/prompt_assembler.py +756 -0
  62. scripts/collaboration/prompt_variant_generator.py +483 -0
  63. scripts/collaboration/protocols.py +267 -0
  64. scripts/collaboration/report_formatter.py +352 -0
  65. scripts/collaboration/retrospective.py +279 -0
  66. scripts/collaboration/role_matcher.py +92 -0
  67. scripts/collaboration/role_template_market.py +352 -0
  68. scripts/collaboration/rule_collector.py +678 -0
  69. scripts/collaboration/scratchpad.py +346 -0
  70. scripts/collaboration/skill_registry.py +151 -0
  71. scripts/collaboration/skillifier.py +878 -0
  72. scripts/collaboration/standardized_role_template.py +317 -0
  73. scripts/collaboration/task_completion_checker.py +237 -0
  74. scripts/collaboration/test_quality_guard.py +695 -0
  75. scripts/collaboration/unified_gate_engine.py +598 -0
  76. scripts/collaboration/usage_tracker.py +309 -0
  77. scripts/collaboration/user_friendly_error.py +176 -0
  78. scripts/collaboration/verification_gate.py +312 -0
  79. scripts/collaboration/warmup_manager.py +635 -0
  80. scripts/collaboration/worker.py +513 -0
  81. scripts/collaboration/workflow_engine.py +684 -0
  82. scripts/dashboard.py +1088 -0
  83. scripts/generate_benchmark_report.py +786 -0
  84. scripts/history_manager.py +604 -0
  85. scripts/mcp_server.py +289 -0
  86. skills/__init__.py +32 -0
  87. skills/dispatch/handler.py +52 -0
  88. skills/intent/handler.py +59 -0
  89. skills/registry.py +67 -0
  90. skills/retrospective/__init__.py +0 -0
  91. skills/retrospective/handler.py +125 -0
  92. skills/review/handler.py +356 -0
  93. skills/security/handler.py +454 -0
  94. skills/test/__init__.py +0 -0
  95. skills/test/handler.py +78 -0
@@ -0,0 +1,678 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ RuleCollector — Natural Language Rule Collection Module
5
+
6
+ Intercepts user input to detect rule-storing intent, extracts structured
7
+ rule data, sanitizes content, and stores via CarryMem or local JSON fallback.
8
+
9
+ Integration point: Called by MultiAgentDispatcher.dispatch() before role matching.
10
+
11
+ Pipeline:
12
+ User Input → IntentDetector → RuleExtractor → RuleSanitizer → RuleStorage
13
+ (detect intent) (extract rule) (security check) (persist)
14
+
15
+ Author: DevSquad Team
16
+ Version: 3.6.0
17
+ """
18
+
19
+ import os
20
+ import re
21
+ import json
22
+ import uuid
23
+ import time
24
+ import logging
25
+ import threading
26
+ import unicodedata
27
+ from pathlib import Path
28
+ from datetime import datetime
29
+ from typing import Any, Dict, List, Optional, Tuple
30
+ from dataclasses import dataclass, field
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ MAX_TRIGGER_LENGTH = 200
35
+ MAX_ACTION_LENGTH = 500
36
+ MIN_TRIGGER_LENGTH = 2
37
+ MIN_ACTION_LENGTH = 5
38
+ VALID_RULE_TYPES = ("always", "avoid", "prefer", "forbid")
39
+ RULE_TYPE_PRIORITY = {"forbid": 3, "always": 2, "avoid": 1, "prefer": 0}
40
+
41
+ INTENT_PATTERNS = [
42
+ {"id": "INT-01", "regex": r"记住.*规则", "weight": 1.0, "type_hint": None},
43
+ {"id": "INT-02", "regex": r"添加.*规则", "weight": 1.0, "type_hint": None},
44
+ {"id": "INT-03", "regex": r"我的.*偏好", "weight": 0.9, "type_hint": "prefer"},
45
+ {"id": "INT-04", "regex": r"(总是|必须|应该).*要", "weight": 0.85, "type_hint": "always"},
46
+ {"id": "INT-05", "regex": r"(禁止|不要|不能).*(用|做)", "weight": 0.95, "type_hint": "forbid"},
47
+ {"id": "INT-06", "regex": r"记住这条规则", "weight": 1.0, "type_hint": None},
48
+ {"id": "INT-07", "regex": r"(避免|尽量不|最好不要)", "weight": 0.85, "type_hint": "avoid"},
49
+ {"id": "INT-08", "regex": r"(偏好|喜欢|倾向于|优先)", "weight": 0.85, "type_hint": "prefer"},
50
+ {"id": "INT-09", "regex": r"团队规范", "weight": 0.9, "type_hint": "always"},
51
+ {"id": "INT-10", "regex": r"列出.*规则|查看.*规则|我的规则", "weight": 1.0, "type_hint": "_list"},
52
+ {"id": "INT-11", "regex": r"删除.*规则|忘记.*规则|移除.*规则", "weight": 1.0, "type_hint": "_delete"},
53
+ ]
54
+
55
+ EXTRACTION_PATTERNS = [
56
+ {"id": "EXT-01", "regex": r"记住.*规则[::]\s*(.+?)(?:时|前|后|的时候|情况下)[,,]?\s*(.+)", "trigger_group": 1, "action_group": 2},
57
+ {"id": "EXT-02", "regex": r"添加规则[::]\s*(.+?)(?:前必须|时必须|后必须|前要|时要|后要)\s*(.+)", "trigger_group": 1, "action_group": 2},
58
+ {"id": "EXT-03", "regex": r"我的偏好[::]\s*(.+?)(?:而不是|而非|而不是用)\s*(.+)", "trigger_group": 2, "action_group": 1},
59
+ {"id": "EXT-04", "regex": r"(禁止|不要|不能)\s*(.+?)(?:时|的时候|中)\s*(.+)", "trigger_group": 2, "action_group": 3},
60
+ {"id": "EXT-05", "regex": r"(总是|必须|应该)\s*(.+?)(?:当|在)\s*(.+?)(?:时|的时候)", "trigger_group": 3, "action_group": 2},
61
+ {"id": "EXT-06", "regex": r"记住.*规则[::]\s*(.+)", "trigger_group": None, "action_group": 1},
62
+ {"id": "EXT-07", "regex": r"团队规范[::]\s*(.+)", "trigger_group": None, "action_group": 1},
63
+ ]
64
+
65
+ TYPE_KEYWORDS = {
66
+ "always": ["必须", "总是", "应该", "要", "一定", "务必"],
67
+ "avoid": ["避免", "尽量不", "最好不要", "不建议"],
68
+ "prefer": ["偏好", "喜欢", "倾向于", "优先", "更愿意"],
69
+ "forbid": ["禁止", "不要", "不能", "绝不", "严禁"],
70
+ }
71
+
72
+ DANGEROUS_PATTERNS = [
73
+ re.compile(r"os\.system", re.IGNORECASE),
74
+ re.compile(r"subprocess\.", re.IGNORECASE),
75
+ re.compile(r"exec\s*\(", re.IGNORECASE),
76
+ re.compile(r"eval\s*\(", re.IGNORECASE),
77
+ re.compile(r"rm\s+-rf", re.IGNORECASE),
78
+ re.compile(r"__import__", re.IGNORECASE),
79
+ re.compile(r"DROP\s+TABLE", re.IGNORECASE),
80
+ re.compile(r"DELETE\s+FROM", re.IGNORECASE),
81
+ re.compile(r"import\s+os\b"),
82
+ re.compile(r"import\s+subprocess\b"),
83
+ ]
84
+
85
+ PROMPT_INJECTION_PATTERNS = [
86
+ re.compile(r"ignore\s+(all\s+)?previous\s+(instructions?|rules?|prompts?)", re.IGNORECASE),
87
+ re.compile(r"disregard\s+(all\s+)?(above|previous|prior)", re.IGNORECASE),
88
+ re.compile(r"forget\s+(all\s+)?(previous|prior|above)\s+(instructions?|rules?)", re.IGNORECASE),
89
+ re.compile(r"you\s+are\s+now\s+(?:a\s+)?(?:DAN|jailbreak|unrestricted)", re.IGNORECASE),
90
+ re.compile(r"system\s*:\s*you\s+are", re.IGNORECASE),
91
+ re.compile(r"override\s+(all\s+)?safety", re.IGNORECASE),
92
+ re.compile(r"(?:忽略|无视|忘记)(?:所有|全部)?(?:之前的|上面的)?(?:指令|规则|提示)", re.IGNORECASE),
93
+ re.compile(r"你(?:现在|已经)?是(?:一个)?(?:不受限|越狱|DAN)", re.IGNORECASE),
94
+ re.compile(r"(?:act|pretend|roleplay)\s+(?:as|to\s+be)", re.IGNORECASE),
95
+ re.compile(r"(?:new|updated|changed)\s+instructions?\s*[::]", re.IGNORECASE),
96
+ re.compile(r"(?:output|reveal|show|print|display)\s+(?:your|the|my)\s+(?:system|initial|original)\s+(?:prompt|instructions?)", re.IGNORECASE),
97
+ re.compile(r"(?:developer|admin|sudo|root|debug)\s+mode", re.IGNORECASE),
98
+ re.compile(r"above\s+all\s+else", re.IGNORECASE),
99
+ re.compile(r"most\s+important\s+rule", re.IGNORECASE),
100
+ ]
101
+
102
+ _RULE_PARTICLE_RE = re.compile(
103
+ r"(?:记住.*规则[::]|添加规则[::]|我的偏好[::]|团队规范[::]"
104
+ r"|禁止[^,。?\n]{1,30}(?:做|使用|执行)?"
105
+ r"|避免[^,。?\n]{1,30}"
106
+ r"|必须[^,。?\n]{1,30}"
107
+ r"|不要[^,。?\n]{1,30}"
108
+ r"|不可以[^,。?\n]{1,30}"
109
+ r"|务必[^,。?\n]{1,30}"
110
+ r"|always\s+[^,。?\n]{1,30}"
111
+ r"|never\s+[^,。?\n]{1,30}"
112
+ r"|forbid[^,。?\n]{1,30}"
113
+ r"|avoid\s+[^,。?\n]{1,30})"
114
+ r".*?(?:[,。?!,\.!\?]|$|\n)",
115
+ re.IGNORECASE,
116
+ )
117
+
118
+
119
+ @dataclass
120
+ class IntentResult:
121
+ is_detected: bool = False
122
+ pattern_id: Optional[str] = None
123
+ confidence: float = 0.0
124
+ matched_span: Optional[Tuple[int, int]] = None
125
+ type_hint: Optional[str] = None
126
+ metadata: Dict[str, Any] = field(default_factory=dict)
127
+
128
+
129
+ @dataclass
130
+ class RuleData:
131
+ trigger: str = ""
132
+ action: str = ""
133
+ type: str = "always"
134
+ confidence: float = 0.0
135
+ source: str = "natural_language"
136
+ raw_text: str = ""
137
+ rule_id: str = ""
138
+ metadata: Dict[str, Any] = field(default_factory=dict)
139
+
140
+
141
+ @dataclass
142
+ class ExtractionResult:
143
+ success: bool = False
144
+ rule_data: Optional[RuleData] = None
145
+ alternatives: List[RuleData] = field(default_factory=list)
146
+ warnings: List[str] = field(default_factory=list)
147
+
148
+
149
+ @dataclass
150
+ class StoreResult:
151
+ success: bool = False
152
+ rule_id: Optional[str] = None
153
+ storage_method: str = ""
154
+ timestamp: str = ""
155
+ message: str = ""
156
+ warnings: List[str] = field(default_factory=list)
157
+
158
+
159
+ @dataclass
160
+ class CollectionResult:
161
+ rule_detected: bool = False
162
+ rule_result: Optional[StoreResult] = None
163
+ remaining_task: str = ""
164
+ list_rules: Optional[List[Dict[str, Any]]] = None
165
+ delete_result: Optional[bool] = None
166
+ message: str = ""
167
+
168
+
169
+ class IntentDetector:
170
+ """Detect rule-storing intent from user input using regex patterns."""
171
+
172
+ def __init__(self, sensitivity: float = 0.85):
173
+ self.patterns = INTENT_PATTERNS
174
+ self.sensitivity = sensitivity
175
+ self._compiled = [
176
+ {"id": p["id"], "regex": re.compile(p["regex"], re.IGNORECASE),
177
+ "weight": p["weight"], "type_hint": p["type_hint"]}
178
+ for p in self.patterns
179
+ ]
180
+
181
+ def detect(self, text: str) -> IntentResult:
182
+ if not text or len(text) < 3:
183
+ return IntentResult()
184
+
185
+ best_score = 0.0
186
+ best_pattern = None
187
+ best_match = None
188
+
189
+ for p in self._compiled:
190
+ m = p["regex"].search(text)
191
+ if m:
192
+ score = p["weight"]
193
+ if score > best_score:
194
+ best_score = score
195
+ best_pattern = p
196
+ best_match = m
197
+
198
+ if best_score < self.sensitivity or best_pattern is None:
199
+ return IntentResult(is_detected=False, confidence=best_score)
200
+
201
+ return IntentResult(
202
+ is_detected=True,
203
+ pattern_id=best_pattern["id"],
204
+ confidence=best_score,
205
+ matched_span=(best_match.start(), best_match.end()),
206
+ type_hint=best_pattern["type_hint"],
207
+ )
208
+
209
+
210
+ class RuleExtractor:
211
+ """Extract structured rule data from natural language input."""
212
+
213
+ def __init__(self):
214
+ self.extraction_patterns = EXTRACTION_PATTERNS
215
+ self._compiled = [
216
+ {"id": p["id"], "regex": re.compile(p["regex"], re.IGNORECASE),
217
+ "trigger_group": p["trigger_group"], "action_group": p["action_group"]}
218
+ for p in self.extraction_patterns
219
+ ]
220
+
221
+ def extract(self, text: str, intent: IntentResult) -> ExtractionResult:
222
+ if not intent.is_detected:
223
+ return ExtractionResult()
224
+
225
+ best_result = None
226
+ best_confidence = 0.0
227
+ warnings = []
228
+
229
+ for p in self._compiled:
230
+ m = p["regex"].search(text)
231
+ if not m:
232
+ continue
233
+
234
+ try:
235
+ if p["trigger_group"] is not None:
236
+ trigger = m.group(p["trigger_group"]).strip()
237
+ else:
238
+ trigger = ""
239
+ action = m.group(p["action_group"]).strip()
240
+ except (IndexError, AttributeError):
241
+ continue
242
+
243
+ rule_type = self._infer_type(text, intent.type_hint)
244
+ confidence = self._calculate_confidence(trigger, action, rule_type, intent.confidence)
245
+
246
+ if confidence > best_confidence:
247
+ best_confidence = confidence
248
+ best_result = RuleData(
249
+ trigger=trigger,
250
+ action=action,
251
+ type=rule_type,
252
+ confidence=confidence,
253
+ source="natural_language",
254
+ raw_text=text,
255
+ )
256
+
257
+ if best_result is None:
258
+ warnings.append("Could not extract structured rule from input")
259
+ return ExtractionResult(success=False, warnings=warnings)
260
+
261
+ if len(best_result.trigger) < MIN_TRIGGER_LENGTH:
262
+ warnings.append(f"Trigger too short (min {MIN_TRIGGER_LENGTH} chars)")
263
+ if len(best_result.action) < MIN_ACTION_LENGTH:
264
+ warnings.append(f"Action too short (min {MIN_ACTION_LENGTH} chars)")
265
+
266
+ return ExtractionResult(
267
+ success=True,
268
+ rule_data=best_result,
269
+ warnings=warnings,
270
+ )
271
+
272
+ def _infer_type(self, text: str, hint: Optional[str]) -> str:
273
+ if hint and hint in VALID_RULE_TYPES:
274
+ return hint
275
+
276
+ for rule_type, keywords in TYPE_KEYWORDS.items():
277
+ for kw in keywords:
278
+ if kw in text:
279
+ return rule_type
280
+
281
+ return "always"
282
+
283
+ def _calculate_confidence(self, trigger: str, action: str,
284
+ rule_type: str, intent_conf: float) -> float:
285
+ score = intent_conf * 0.5
286
+ if len(trigger) >= MIN_TRIGGER_LENGTH:
287
+ score += 0.2
288
+ if len(action) >= MIN_ACTION_LENGTH:
289
+ score += 0.2
290
+ if rule_type in VALID_RULE_TYPES:
291
+ score += 0.1
292
+ return min(1.0, score)
293
+
294
+
295
+ class RuleSanitizer:
296
+ """Sanitize rule content to prevent security issues."""
297
+
298
+ @staticmethod
299
+ def sanitize(rule: RuleData) -> Tuple[RuleData, List[str]]:
300
+ warnings = []
301
+
302
+ for pat in DANGEROUS_PATTERNS:
303
+ if pat.search(rule.action):
304
+ warnings.append(f"Dangerous pattern detected in rule action")
305
+ rule.action = re.sub(pat.pattern, "[REDACTED]", rule.action)
306
+
307
+ for pat in DANGEROUS_PATTERNS:
308
+ if pat.search(rule.trigger):
309
+ warnings.append(f"Dangerous pattern detected in rule trigger")
310
+ rule.trigger = re.sub(pat.pattern, "[REDACTED]", rule.trigger)
311
+
312
+ for pat in PROMPT_INJECTION_PATTERNS:
313
+ if pat.search(rule.action):
314
+ warnings.append("Prompt injection pattern detected in rule action")
315
+ rule.action = re.sub(pat.pattern, "[REDACTED]", rule.action)
316
+
317
+ for pat in PROMPT_INJECTION_PATTERNS:
318
+ if pat.search(rule.trigger):
319
+ warnings.append("Prompt injection pattern detected in rule trigger")
320
+ rule.trigger = re.sub(pat.pattern, "[REDACTED]", rule.trigger)
321
+
322
+ if len(rule.trigger) > MAX_TRIGGER_LENGTH:
323
+ warnings.append(f"Trigger truncated from {len(rule.trigger)} to {MAX_TRIGGER_LENGTH}")
324
+ rule.trigger = rule.trigger[:MAX_TRIGGER_LENGTH]
325
+
326
+ if len(rule.action) > MAX_ACTION_LENGTH:
327
+ warnings.append(f"Action truncated from {len(rule.action)} to {MAX_ACTION_LENGTH}")
328
+ rule.action = rule.action[:MAX_ACTION_LENGTH]
329
+
330
+ rule.trigger = unicodedata.normalize("NFC", rule.trigger)
331
+ rule.action = unicodedata.normalize("NFC", rule.action)
332
+
333
+ if rule.type not in VALID_RULE_TYPES:
334
+ warnings.append(f"Invalid type '{rule.type}' defaulted to 'always'")
335
+ rule.type = "always"
336
+
337
+ return rule, warnings
338
+
339
+
340
+ class LocalRuleStorage:
341
+ """Local JSON file fallback storage for rules."""
342
+
343
+ _CACHE_TTL = 5.0
344
+
345
+ def __init__(self, storage_path: Optional[str] = None):
346
+ if storage_path is None:
347
+ data_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(
348
+ os.path.abspath(__file__)))), "data", "rules")
349
+ os.makedirs(data_dir, exist_ok=True)
350
+ storage_path = os.path.join(data_dir, "rules_local.json")
351
+ else:
352
+ real = os.path.realpath(storage_path)
353
+ if not storage_path.endswith(".json") or ".." in storage_path:
354
+ raise ValueError(f"Invalid storage_path: {storage_path}")
355
+ self.storage_path = storage_path
356
+ self._lock = threading.RLock()
357
+ self._cache: Optional[dict] = None
358
+ self._cache_time: float = 0.0
359
+ self._ensure_file()
360
+
361
+ def _ensure_file(self):
362
+ if not os.path.exists(self.storage_path):
363
+ os.makedirs(os.path.dirname(self.storage_path), exist_ok=True)
364
+ with open(self.storage_path, "w", encoding="utf-8") as f:
365
+ json.dump({"_metadata": {"version": "1.0", "created": datetime.now().isoformat()},
366
+ "rules": {}}, f, ensure_ascii=False, indent=2)
367
+
368
+ def store(self, rule: RuleData) -> StoreResult:
369
+ with self._lock:
370
+ try:
371
+ data = self._read_data()
372
+ rule_id = f"RULE-LOCAL-{uuid.uuid4().hex[:6]}"
373
+ rule.rule_id = rule_id
374
+ data["rules"][rule_id] = {
375
+ "trigger": rule.trigger,
376
+ "action": rule.action,
377
+ "type": rule.type,
378
+ "confidence": rule.confidence,
379
+ "source": rule.source,
380
+ "raw_text": rule.raw_text,
381
+ "created_at": datetime.now().isoformat(),
382
+ "active": True,
383
+ }
384
+ self._write_data(data)
385
+ self._cache = data
386
+ self._cache_time = time.time()
387
+ return StoreResult(
388
+ success=True, rule_id=rule_id,
389
+ storage_method="local_json",
390
+ timestamp=datetime.now().isoformat(),
391
+ message=f"Rule stored: {rule_id}",
392
+ )
393
+ except Exception as e:
394
+ logger.error("LocalRuleStorage store failed: %s", e)
395
+ return StoreResult(success=False, message=str(e))
396
+
397
+ def list_rules(self, user_id: str = "default") -> List[Dict[str, Any]]:
398
+ with self._lock:
399
+ data = self._read_data()
400
+ return [{"rule_id": k, **v} for k, v in data.get("rules", {}).items() if v.get("active", True)]
401
+
402
+ def delete_rule(self, rule_id: str) -> bool:
403
+ with self._lock:
404
+ data = self._read_data()
405
+ if rule_id in data.get("rules", {}):
406
+ data["rules"][rule_id]["active"] = False
407
+ data["rules"][rule_id]["deleted_at"] = datetime.now().isoformat()
408
+ self._write_data(data)
409
+ self._cache = data
410
+ self._cache_time = time.time()
411
+ return True
412
+ return False
413
+
414
+ def query(self, trigger_keywords: Optional[List[str]] = None,
415
+ rule_type: Optional[str] = None,
416
+ min_confidence: float = 0.0) -> List[Dict[str, Any]]:
417
+ with self._lock:
418
+ data = self._read_data()
419
+ results = []
420
+ for rid, r in data.get("rules", {}).items():
421
+ if not r.get("active", True):
422
+ continue
423
+ if rule_type and r.get("type") != rule_type:
424
+ continue
425
+ if r.get("confidence", 0) < min_confidence:
426
+ continue
427
+ if trigger_keywords:
428
+ if not any(kw in r.get("trigger", "") or kw in r.get("action", "")
429
+ for kw in trigger_keywords):
430
+ continue
431
+ results.append({"rule_id": rid, **r})
432
+ results.sort(key=lambda x: RULE_TYPE_PRIORITY.get(x.get("type", "prefer"), 0), reverse=True)
433
+ return results
434
+
435
+ def _read_data(self) -> dict:
436
+ now = time.time()
437
+ if self._cache is not None and (now - self._cache_time) < self._CACHE_TTL:
438
+ return self._cache
439
+ if os.path.exists(self.storage_path):
440
+ try:
441
+ with open(self.storage_path, "r", encoding="utf-8") as f:
442
+ data = json.load(f)
443
+ if not isinstance(data, dict) or "rules" not in data or not isinstance(data["rules"], dict):
444
+ logger.warning("Invalid rules JSON structure, resetting to default")
445
+ data = {"_metadata": {}, "rules": {}}
446
+ self._cache = data
447
+ self._cache_time = now
448
+ return data
449
+ except (json.JSONDecodeError, ValueError) as e:
450
+ logger.error("Corrupted rules JSON: %s, resetting", e)
451
+ data = {"_metadata": {}, "rules": {}}
452
+ self._cache = data
453
+ self._cache_time = now
454
+ return data
455
+ return {"_metadata": {}, "rules": {}}
456
+
457
+ def _write_data(self, data: dict):
458
+ tmp_path = self.storage_path + ".tmp"
459
+ with open(tmp_path, "w", encoding="utf-8") as f:
460
+ json.dump(data, f, ensure_ascii=False, indent=2)
461
+ os.replace(tmp_path, self.storage_path)
462
+
463
+
464
+ class RuleStorage:
465
+ """Unified rule storage with CarryMem primary + local JSON fallback."""
466
+
467
+ _shared_instance: Optional['RuleStorage'] = None
468
+ _instance_lock = threading.Lock()
469
+
470
+ @classmethod
471
+ def get_shared(cls, carrymem_config: Optional[dict] = None) -> 'RuleStorage':
472
+ if cls._shared_instance is None:
473
+ with cls._instance_lock:
474
+ if cls._shared_instance is None:
475
+ cls._shared_instance = cls(carrymem_config)
476
+ return cls._shared_instance
477
+
478
+ def __init__(self, carrymem_config: Optional[dict] = None):
479
+ self.carrymem_available = False
480
+ self._carrymem = None
481
+ self._local = LocalRuleStorage()
482
+ self._init_carrymem(carrymem_config)
483
+
484
+ def _init_carrymem(self, config):
485
+ try:
486
+ from scripts.collaboration.mce_adapter import get_global_mce_adapter
487
+ adapter = get_global_mce_adapter(enable=False)
488
+ if adapter and adapter.is_available:
489
+ self._carrymem = adapter
490
+ self.carrymem_available = True
491
+ except Exception as e:
492
+ logger.info("CarryMem not available for RuleStorage: %s", e)
493
+
494
+ def store(self, rule: RuleData) -> StoreResult:
495
+ if self.carrymem_available and self._carrymem:
496
+ try:
497
+ result = self._store_to_carrymem(rule)
498
+ if result.success:
499
+ return result
500
+ except Exception as e:
501
+ logger.warning("CarryMem store failed, using fallback: %s", e)
502
+
503
+ return self._local.store(rule)
504
+
505
+ def list_rules(self, user_id: str = "default") -> List[Dict[str, Any]]:
506
+ return self._local.list_rules(user_id)
507
+
508
+ def delete_rule(self, rule_id: str) -> bool:
509
+ return self._local.delete_rule(rule_id)
510
+
511
+ def query(self, **kwargs) -> List[Dict[str, Any]]:
512
+ return self._local.query(**kwargs)
513
+
514
+ def _store_to_carrymem(self, rule: RuleData) -> StoreResult:
515
+ if hasattr(self._carrymem, "add_rule"):
516
+ result = self._carrymem.add_rule(
517
+ trigger=rule.trigger, action=rule.action,
518
+ rule_type=rule.type, confidence=rule.confidence,
519
+ )
520
+ if result:
521
+ return StoreResult(
522
+ success=True, rule_id=result.get("rule_id", ""),
523
+ storage_method="carrymem",
524
+ timestamp=datetime.now().isoformat(),
525
+ message="Rule stored to CarryMem",
526
+ )
527
+ return StoreResult(success=False, message="CarryMem add_rule not available")
528
+
529
+
530
+ class RuleCollector:
531
+ """
532
+ Main orchestrator for natural language rule collection.
533
+
534
+ Integration point: Called by MultiAgentDispatcher.dispatch() before
535
+ role matching. Returns CollectionResult with remaining task text.
536
+ """
537
+
538
+ def __init__(self, sensitivity: float = 0.85):
539
+ self._detector = IntentDetector(sensitivity=sensitivity)
540
+ self._extractor = RuleExtractor()
541
+ self._sanitizer = RuleSanitizer()
542
+ self._storage = RuleStorage()
543
+
544
+ def process(self, text: str, lang: str = "zh") -> CollectionResult:
545
+ intent = self._detector.detect(text)
546
+
547
+ if not intent.is_detected:
548
+ return CollectionResult(rule_detected=False, remaining_task=text)
549
+
550
+ if intent.type_hint == "_list":
551
+ rules = self._storage.list_rules()
552
+ msg = self._format_list_response(rules, lang)
553
+ return CollectionResult(
554
+ rule_detected=True, list_rules=rules,
555
+ remaining_task="", message=msg,
556
+ )
557
+
558
+ if intent.type_hint == "_delete":
559
+ remaining = self._strip_rule_particle(text)
560
+ delete_ok = self._handle_delete(text, lang)
561
+ msg = self._format_delete_response(delete_ok, text, lang)
562
+ return CollectionResult(
563
+ rule_detected=True, delete_result=delete_ok,
564
+ remaining_task=remaining,
565
+ message=msg,
566
+ )
567
+
568
+ extraction = self._extractor.extract(text, intent)
569
+ if not extraction.success or extraction.rule_data is None:
570
+ remaining = self._strip_rule_particle(text)
571
+ msg = self._format_failure_response(lang)
572
+ return CollectionResult(
573
+ rule_detected=True, remaining_task=remaining, message=msg,
574
+ )
575
+
576
+ rule, sanitize_warnings = self._sanitizer.sanitize(extraction.rule_data)
577
+
578
+ if rule.confidence < 0.5:
579
+ remaining = self._strip_rule_particle(text)
580
+ msg = self._format_low_confidence_response(rule, lang)
581
+ return CollectionResult(
582
+ rule_detected=True, remaining_task=remaining, message=msg,
583
+ )
584
+
585
+ store_result = self._storage.store(rule)
586
+ remaining = self._strip_rule_particle(text)
587
+ msg = self._format_success_response(rule, store_result, lang)
588
+
589
+ return CollectionResult(
590
+ rule_detected=True,
591
+ rule_result=store_result,
592
+ remaining_task=remaining,
593
+ message=msg,
594
+ )
595
+
596
+ def _strip_rule_particle(self, text: str) -> str:
597
+ cleaned = _RULE_PARTICLE_RE.sub("", text).strip()
598
+ cleaned = re.sub(r"\s+", " ", cleaned).strip()
599
+ if len(cleaned) < MIN_ACTION_LENGTH:
600
+ return ""
601
+ return cleaned
602
+
603
+ def _handle_delete(self, text: str, lang: str) -> bool:
604
+ rule_id_match = re.search(r"(RULE-[\w-]+)", text)
605
+ if rule_id_match:
606
+ return self._storage.delete_rule(rule_id_match.group(1))
607
+ return False
608
+
609
+ def _format_delete_response(self, deleted: bool, text: str, lang: str) -> str:
610
+ rule_id_match = re.search(r"(RULE-[\w-]+)", text)
611
+ if lang == "en":
612
+ if not rule_id_match:
613
+ return "No rule ID specified. Use 'list rules' first to find the ID, then 'delete rule RULE-xxx'."
614
+ return f"Rule {rule_id_match.group(1)} deleted." if deleted else f"Rule {rule_id_match.group(1)} not found."
615
+ elif lang == "ja":
616
+ if not rule_id_match:
617
+ return "ルールIDが指定されていません。「ルール一覧」でIDを確認後、「ルール削除 RULE-xxx」を実行してください。"
618
+ return f"ルール {rule_id_match.group(1)} を削除しました。" if deleted else f"ルール {rule_id_match.group(1)} が見つかりません。"
619
+ else:
620
+ if not rule_id_match:
621
+ return "未指定规则ID。请先「列出规则」获取ID,再「删除规则 RULE-xxx」。"
622
+ return f"规则 {rule_id_match.group(1)} 已删除。" if deleted else f"规则 {rule_id_match.group(1)} 未找到。"
623
+
624
+ def _format_success_response(self, rule: RuleData, result: StoreResult,
625
+ lang: str) -> str:
626
+ if lang == "en":
627
+ return (f"Rule stored: {rule.action} ({result.rule_id})\n"
628
+ f" Type: {rule.type} | Trigger: {rule.trigger or 'general'}")
629
+ elif lang == "ja":
630
+ return (f"ルール保存: {rule.action} ({result.rule_id})\n"
631
+ f" タイプ: {rule.type} | トリガー: {rule.trigger or '一般'}")
632
+ else:
633
+ return (f"已记住规则: {rule.action} ({result.rule_id})\n"
634
+ f" 类型: {rule.type} | 触发条件: {rule.trigger or '通用'}")
635
+
636
+ def _format_failure_response(self, lang: str) -> str:
637
+ if lang == "en":
638
+ return ("Could not fully understand your rule. Try: "
639
+ "'Remember rule: [when] [do what]'")
640
+ elif lang == "ja":
641
+ return ("ルールを完全に理解できませんでした。次の形式をお試しください: "
642
+ "'ルールを覚えて: [いつ] [何をする]'")
643
+ else:
644
+ return ("无法完全理解您的规则,请尝试以下格式: "
645
+ "'记住规则: [何时][做什么]'")
646
+
647
+ def _format_low_confidence_response(self, rule: RuleData, lang: str) -> str:
648
+ pct = int(rule.confidence * 100)
649
+ if lang == "en":
650
+ return f"Rule recorded with low confidence ({pct}%). Consider using standard format."
651
+ elif lang == "ja":
652
+ return f"ルールを記録しましたが、信頼度が低いです({pct}%)。標準形式の使用をお勧めします。"
653
+ else:
654
+ return f"已记录您的偏好 (置信度: {pct}%)。建议使用标准格式以获得更高准确率。"
655
+
656
+ def _format_list_response(self, rules: List[Dict[str, Any]],
657
+ lang: str) -> str:
658
+ if not rules:
659
+ if lang == "en":
660
+ return "No rules stored yet."
661
+ elif lang == "ja":
662
+ return "保存されたルールはまだありません。"
663
+ else:
664
+ return "暂无已存储的规则。"
665
+
666
+ lines = []
667
+ for r in rules[:20]:
668
+ rid = r.get("rule_id", "?")
669
+ rtype = r.get("type", "?")
670
+ action = r.get("action", "?")[:50]
671
+ trigger = r.get("trigger", "")
672
+ if lang == "zh":
673
+ lines.append(f" {rid} [{rtype}] {trigger+' → ' if trigger else ''}{action}")
674
+ else:
675
+ lines.append(f" {rid} [{rtype}] {trigger+' -> ' if trigger else ''}{action}")
676
+
677
+ header = "Stored Rules:" if lang == "en" else "保存済みルール:" if lang == "ja" else "已存储规则:"
678
+ return f"{header}\n" + "\n".join(lines)