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,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)
|