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,878 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Skillifier - Automatic Skill Generation System
|
|
5
|
+
|
|
6
|
+
Analyzes successful execution histories, extracts reusable patterns,
|
|
7
|
+
generates Skill proposals with quality validation, and publishes to SkillRegistry.
|
|
8
|
+
|
|
9
|
+
Core workflow:
|
|
10
|
+
ExecutionRecord → Pattern Extraction → Generalization → SkillProposal → Validation → Publish
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
import json
|
|
15
|
+
import uuid
|
|
16
|
+
import hashlib
|
|
17
|
+
import threading
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from datetime import datetime, timedelta
|
|
20
|
+
from enum import Enum
|
|
21
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from .permission_guard import ActionType as PGActionType
|
|
25
|
+
except ImportError:
|
|
26
|
+
class PGActionType(Enum):
|
|
27
|
+
FILE_READ = "file_read"
|
|
28
|
+
FILE_CREATE = "file_create"
|
|
29
|
+
FILE_MODIFY = "file_modify"
|
|
30
|
+
FILE_DELETE = "file_delete"
|
|
31
|
+
SHELL_EXECUTE = "shell_execute"
|
|
32
|
+
NETWORK_REQUEST = "network_request"
|
|
33
|
+
GIT_OPERATION = "git_operation"
|
|
34
|
+
ENVIRONMENT = "environment"
|
|
35
|
+
PROCESS_SPAWN = "process_spawn"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ProposalStatus(Enum):
|
|
39
|
+
DRAFT = "draft"
|
|
40
|
+
REVIEWING = "reviewing"
|
|
41
|
+
APPROVED = "approved"
|
|
42
|
+
PUBLISHED = "published"
|
|
43
|
+
REJECTED = "rejected"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SkillCategory(Enum):
|
|
47
|
+
CODE_GENERATION = "code-generation"
|
|
48
|
+
CODE_REVIEW = "code-review"
|
|
49
|
+
TESTING = "testing"
|
|
50
|
+
DEPLOYMENT = "deployment"
|
|
51
|
+
REFACTORING = "refactoring"
|
|
52
|
+
DOCUMENTATION = "documentation"
|
|
53
|
+
ANALYSIS = "analysis"
|
|
54
|
+
INTEGRATION = "integration"
|
|
55
|
+
SECURITY = "security"
|
|
56
|
+
PERFORMANCE = "performance"
|
|
57
|
+
AUTO_GENERATED = "auto-generated"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ============================================================
|
|
61
|
+
# Data Models - Execution Layer
|
|
62
|
+
# ============================================================
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class ExecutionStep:
|
|
66
|
+
step_order: int
|
|
67
|
+
action_type: PGActionType
|
|
68
|
+
target: str
|
|
69
|
+
description: str
|
|
70
|
+
outcome: str = "success"
|
|
71
|
+
duration_ms: int = 0
|
|
72
|
+
input_data: Optional[str] = None
|
|
73
|
+
output_data: Optional[str] = None
|
|
74
|
+
|
|
75
|
+
def to_dict(self) -> Dict:
|
|
76
|
+
return {
|
|
77
|
+
"step_order": self.step_order,
|
|
78
|
+
"action_type": self.action_type.value,
|
|
79
|
+
"target": self.target,
|
|
80
|
+
"description": self.description,
|
|
81
|
+
"outcome": self.outcome,
|
|
82
|
+
"duration_ms": self.duration_ms,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def from_dict(cls, d: Dict) -> "ExecutionStep":
|
|
87
|
+
return cls(
|
|
88
|
+
step_order=d.get("step_order", 0),
|
|
89
|
+
action_type=PGActionType(d.get("action_type", "file_read")),
|
|
90
|
+
target=d.get("target", ""),
|
|
91
|
+
description=d.get("description", ""),
|
|
92
|
+
outcome=d.get("outcome", "success"),
|
|
93
|
+
duration_ms=d.get("duration_ms", 0),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class ExecutionRecord:
|
|
99
|
+
record_id: str = field(default_factory=lambda: f"er-{uuid.uuid4().hex[:12]}")
|
|
100
|
+
task_description: str = ""
|
|
101
|
+
start_time: datetime = field(default_factory=datetime.now)
|
|
102
|
+
end_time: Optional[datetime] = None
|
|
103
|
+
duration_seconds: float = 0.0
|
|
104
|
+
success: bool = True
|
|
105
|
+
worker_id: str = ""
|
|
106
|
+
role_id: str = ""
|
|
107
|
+
steps: List[ExecutionStep] = field(default_factory=list)
|
|
108
|
+
artifacts: List[str] = field(default_factory=list)
|
|
109
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
110
|
+
|
|
111
|
+
def finalize(self):
|
|
112
|
+
if self.end_time is None:
|
|
113
|
+
self.end_time = datetime.now()
|
|
114
|
+
if self.duration_seconds == 0.0 and self.start_time:
|
|
115
|
+
delta = self.end_time - self.start_time
|
|
116
|
+
self.duration_seconds = max(0.0, delta.total_seconds())
|
|
117
|
+
|
|
118
|
+
def to_dict(self) -> Dict:
|
|
119
|
+
return {
|
|
120
|
+
"record_id": self.record_id,
|
|
121
|
+
"task_description": self.task_description,
|
|
122
|
+
"success": self.success,
|
|
123
|
+
"worker_id": self.worker_id,
|
|
124
|
+
"role_id": self.role_id,
|
|
125
|
+
"step_count": len(self.steps),
|
|
126
|
+
"duration_seconds": round(self.duration_seconds, 2),
|
|
127
|
+
"artifacts": self.artifacts,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def from_dict(cls, d: Dict) -> "ExecutionRecord":
|
|
132
|
+
return cls(
|
|
133
|
+
record_id=d.get("record_id", f"er-{uuid.uuid4().hex[:12]}"),
|
|
134
|
+
task_description=d.get("task_description", ""),
|
|
135
|
+
success=d.get("success", True),
|
|
136
|
+
worker_id=d.get("worker_id", ""),
|
|
137
|
+
role_id=d.get("role_id", ""),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ============================================================
|
|
142
|
+
# Data Models - Pattern Layer
|
|
143
|
+
# ============================================================
|
|
144
|
+
|
|
145
|
+
@dataclass
|
|
146
|
+
class PatternStep:
|
|
147
|
+
action_type: PGActionType
|
|
148
|
+
target_pattern: str
|
|
149
|
+
description_template: str
|
|
150
|
+
is_required: bool = True
|
|
151
|
+
estimated_risk: float = 0.0
|
|
152
|
+
|
|
153
|
+
def to_dict(self) -> Dict:
|
|
154
|
+
return {
|
|
155
|
+
"action_type": self.action_type.value,
|
|
156
|
+
"target_pattern": self.target_pattern,
|
|
157
|
+
"description_template": self.description_template,
|
|
158
|
+
"is_required": self.is_required,
|
|
159
|
+
"estimated_risk": self.estimated_risk,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class SuccessPattern:
|
|
165
|
+
pattern_id: str = field(default_factory=lambda: f"sp-{uuid.uuid4().hex[:10]}")
|
|
166
|
+
name: str = ""
|
|
167
|
+
description: str = ""
|
|
168
|
+
source_records: List[str] = field(default_factory=list)
|
|
169
|
+
steps_template: List[PatternStep] = field(default_factory=list)
|
|
170
|
+
trigger_keywords: List[str] = field(default_factory=list)
|
|
171
|
+
applicable_roles: List[str] = field(default_factory=list)
|
|
172
|
+
frequency: int = 1
|
|
173
|
+
confidence: float = 0.5
|
|
174
|
+
avg_success_rate: float = 1.0
|
|
175
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
176
|
+
|
|
177
|
+
def to_dict(self) -> Dict:
|
|
178
|
+
return {
|
|
179
|
+
"pattern_id": self.pattern_id,
|
|
180
|
+
"name": self.name,
|
|
181
|
+
"description": self.description,
|
|
182
|
+
"frequency": self.frequency,
|
|
183
|
+
"confidence": round(self.confidence, 3),
|
|
184
|
+
"avg_success_rate": round(self.avg_success_rate, 3),
|
|
185
|
+
"step_count": len(self.steps_template),
|
|
186
|
+
"trigger_keywords": self.trigger_keywords,
|
|
187
|
+
"source_record_count": len(self.source_records),
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ============================================================
|
|
192
|
+
# Data Models - Skill Proposal Layer
|
|
193
|
+
# ============================================================
|
|
194
|
+
|
|
195
|
+
@dataclass
|
|
196
|
+
class SkillStepDef:
|
|
197
|
+
step_number: int
|
|
198
|
+
action_type: PGActionType
|
|
199
|
+
target_pattern: str
|
|
200
|
+
description: str
|
|
201
|
+
is_required: bool = True
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@dataclass
|
|
205
|
+
class ValidationResult:
|
|
206
|
+
score: float = 0.0
|
|
207
|
+
completeness: float = 0.0
|
|
208
|
+
specificity: float = 0.0
|
|
209
|
+
repeatability: float = 0.0
|
|
210
|
+
safety: float = 0.0
|
|
211
|
+
issues: List[str] = field(default_factory=list)
|
|
212
|
+
suggestions: List[str] = field(default_factory=list)
|
|
213
|
+
|
|
214
|
+
def grade(self) -> str:
|
|
215
|
+
if self.score >= 85:
|
|
216
|
+
return "A"
|
|
217
|
+
elif self.score >= 70:
|
|
218
|
+
return "B"
|
|
219
|
+
elif self.score >= 55:
|
|
220
|
+
return "C"
|
|
221
|
+
else:
|
|
222
|
+
return "D"
|
|
223
|
+
|
|
224
|
+
def to_dict(self) -> Dict:
|
|
225
|
+
return {
|
|
226
|
+
"score": round(self.score, 1),
|
|
227
|
+
"grade": self.grade(),
|
|
228
|
+
"completeness": round(self.completeness, 1),
|
|
229
|
+
"specificity": round(self.specificity, 1),
|
|
230
|
+
"repeatability": round(self.repeatability, 1),
|
|
231
|
+
"safety": round(self.safety, 1),
|
|
232
|
+
"issues": self.issues,
|
|
233
|
+
"suggestions": self.suggestions,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@dataclass
|
|
238
|
+
class SkillProposal:
|
|
239
|
+
proposal_id: str = field(default_factory=lambda: f"prop-{uuid.uuid4().hex[:10]}")
|
|
240
|
+
name: str = ""
|
|
241
|
+
slug: str = ""
|
|
242
|
+
version: str = "1.0.0"
|
|
243
|
+
description: str = ""
|
|
244
|
+
category: str = "auto-generated"
|
|
245
|
+
trigger_conditions: List[str] = field(default_factory=list)
|
|
246
|
+
steps: List[SkillStepDef] = field(default_factory=list)
|
|
247
|
+
required_roles: List[str] = field(default_factory=list)
|
|
248
|
+
input_schema: Dict[str, Any] = field(default_factory=dict)
|
|
249
|
+
output_schema: Dict[str, Any] = field(default_factory=dict)
|
|
250
|
+
acceptance_criteria: List[str] = field(default_factory=list)
|
|
251
|
+
source_pattern: Optional[str] = None
|
|
252
|
+
quality_score: float = 0.0
|
|
253
|
+
validation_result: Optional[ValidationResult] = None
|
|
254
|
+
status: ProposalStatus = ProposalStatus.DRAFT
|
|
255
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
256
|
+
approved_by: Optional[str] = None
|
|
257
|
+
published_at: Optional[datetime] = None
|
|
258
|
+
|
|
259
|
+
def _generate_slug(self):
|
|
260
|
+
slug = re.sub(r'[^a-zA-Z0-9\s-]', '', self.name.lower())
|
|
261
|
+
slug = re.sub(r'\s+', '-', slug.strip())
|
|
262
|
+
return slug or "unnamed-skill"
|
|
263
|
+
|
|
264
|
+
def to_dict(self) -> Dict:
|
|
265
|
+
d = {
|
|
266
|
+
"proposal_id": self.proposal_id,
|
|
267
|
+
"name": self.name,
|
|
268
|
+
"slug": self.slug,
|
|
269
|
+
"version": self.version,
|
|
270
|
+
"description": self.description,
|
|
271
|
+
"category": self.category,
|
|
272
|
+
"status": self.status.value,
|
|
273
|
+
"step_count": len(self.steps),
|
|
274
|
+
"quality_score": round(self.quality_score, 1),
|
|
275
|
+
"created_at": self.created_at.isoformat(),
|
|
276
|
+
}
|
|
277
|
+
if self.validation_result:
|
|
278
|
+
d["validation"] = self.validation_result.to_dict()
|
|
279
|
+
if self.published_at:
|
|
280
|
+
d["published_at"] = self.published_at.isoformat()
|
|
281
|
+
return d
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ============================================================
|
|
285
|
+
# Skillifier Core Engine
|
|
286
|
+
# ============================================================
|
|
287
|
+
|
|
288
|
+
class Skillifier:
|
|
289
|
+
"""Automatic skill generation from execution history analysis"""
|
|
290
|
+
|
|
291
|
+
DEFAULT_MIN_OCCURRENCES = 2
|
|
292
|
+
DEFAULT_MIN_CONFIDENCE = 0.6
|
|
293
|
+
|
|
294
|
+
CATEGORY_KEYWORDS = {
|
|
295
|
+
SkillCategory.CODE_GENERATION: ["create", "generate", "init", "setup", "build",
|
|
296
|
+
"implement", "develop", "write", "new"],
|
|
297
|
+
SkillCategory.CODE_REVIEW: ["review", "audit", "inspect", "check", "analyze-code",
|
|
298
|
+
"lint", "quality"],
|
|
299
|
+
SkillCategory.TESTING: ["test", "spec", "verify", "assert", "coverage", "pytest",
|
|
300
|
+
"unittest", "e2e"],
|
|
301
|
+
SkillCategory.DEPLOYMENT: ["deploy", "release", "ship", "publish", "ci/cd",
|
|
302
|
+
"docker", "kubernetes", "production"],
|
|
303
|
+
SkillCategory.REFACTORING: ["refactor", "cleanup", "optimize", "restructure",
|
|
304
|
+
"simplify", "improve"],
|
|
305
|
+
SkillCategory.DOCUMENTATION: ["document", "readme", "api-doc", "comment",
|
|
306
|
+
"wiki", "guide", "manual"],
|
|
307
|
+
SkillCategory.ANALYSIS: ["analyze", "diagnose", "investigate", "profile",
|
|
308
|
+
"benchmark", "measure"],
|
|
309
|
+
SkillCategory.INTEGRATION: ["integrate", "connect", "configure", "setup-env",
|
|
310
|
+
"pipeline", "workflow"],
|
|
311
|
+
SkillCategory.SECURITY: ["security", "vulnerability", "auth", "permission",
|
|
312
|
+
"encrypt", "scan"],
|
|
313
|
+
SkillCategory.PERFORMANCE: ["performance", "speed", "cache", "optimize-fast",
|
|
314
|
+
"latency", "throughput"],
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
def __init__(self,
|
|
318
|
+
min_pattern_occurrences: int = DEFAULT_MIN_OCCURRENCES,
|
|
319
|
+
min_confidence: float = DEFAULT_MIN_CONFIDENCE,
|
|
320
|
+
auto_analyze: bool = True):
|
|
321
|
+
self.min_occurrences = min_pattern_occurrences
|
|
322
|
+
self.min_confidence = min_confidence
|
|
323
|
+
self.auto_analyze = auto_analyze
|
|
324
|
+
self._records: List[ExecutionRecord] = []
|
|
325
|
+
self._patterns: List[SuccessPattern] = []
|
|
326
|
+
self._proposals: Dict[str, SkillProposal] = {}
|
|
327
|
+
self._lock = threading.RLock()
|
|
328
|
+
|
|
329
|
+
# ================================================================
|
|
330
|
+
# Record Management
|
|
331
|
+
# ================================================================
|
|
332
|
+
|
|
333
|
+
def record_execution(self, record: ExecutionRecord) -> None:
|
|
334
|
+
with self._lock:
|
|
335
|
+
record.finalize()
|
|
336
|
+
self._records.append(record)
|
|
337
|
+
|
|
338
|
+
def get_records(self, since: datetime = None, until: datetime = None,
|
|
339
|
+
success_only: bool = True) -> List[ExecutionRecord]:
|
|
340
|
+
results = self._records
|
|
341
|
+
if since:
|
|
342
|
+
results = [r for r in results if r.start_time >= since]
|
|
343
|
+
if until:
|
|
344
|
+
results = [r for r in results if r.start_time <= until]
|
|
345
|
+
if success_only:
|
|
346
|
+
results = [r for r in results if r.success]
|
|
347
|
+
return list(results)
|
|
348
|
+
|
|
349
|
+
# ================================================================
|
|
350
|
+
# Pattern Extraction
|
|
351
|
+
# ================================================================
|
|
352
|
+
|
|
353
|
+
def analyze_history(self, since: datetime = None,
|
|
354
|
+
until: datetime = None) -> List[SuccessPattern]:
|
|
355
|
+
with self._lock:
|
|
356
|
+
records = self.get_records(since=since, until=until, success_only=True)
|
|
357
|
+
if len(records) < self.min_occurrences:
|
|
358
|
+
return []
|
|
359
|
+
|
|
360
|
+
clusters = self._cluster_sequences(records)
|
|
361
|
+
patterns = []
|
|
362
|
+
for cluster_records in clusters.values():
|
|
363
|
+
if len(cluster_records) < self.min_occurrences:
|
|
364
|
+
continue
|
|
365
|
+
pattern = self._build_pattern_from_cluster(cluster_records)
|
|
366
|
+
if pattern.confidence >= self.min_confidence:
|
|
367
|
+
patterns.append(pattern)
|
|
368
|
+
existing_ids = {p.pattern_id for p in self._patterns}
|
|
369
|
+
if pattern.pattern_id not in existing_ids:
|
|
370
|
+
self._patterns.append(pattern)
|
|
371
|
+
|
|
372
|
+
patterns.sort(key=lambda p: p.confidence, reverse=True)
|
|
373
|
+
return patterns
|
|
374
|
+
|
|
375
|
+
def _cluster_sequences(self, records: List[ExecutionRecord]) -> Dict[int, List[ExecutionRecord]]:
|
|
376
|
+
clusters: Dict[int, List[ExecutionRecord]] = {}
|
|
377
|
+
cluster_id = 0
|
|
378
|
+
|
|
379
|
+
for record in records:
|
|
380
|
+
if len(record.steps) == 0:
|
|
381
|
+
continue
|
|
382
|
+
best_cluster = -1
|
|
383
|
+
best_similarity = -1.0
|
|
384
|
+
|
|
385
|
+
for cid, members in clusters.items():
|
|
386
|
+
rep = members[0]
|
|
387
|
+
sim = self._sequence_similarity(record.steps, rep.steps)
|
|
388
|
+
if sim > best_similarity and sim > 0.45:
|
|
389
|
+
best_similarity = sim
|
|
390
|
+
best_cluster = cid
|
|
391
|
+
|
|
392
|
+
if best_cluster >= 0:
|
|
393
|
+
clusters[best_cluster].append(record)
|
|
394
|
+
else:
|
|
395
|
+
clusters[cluster_id] = [record]
|
|
396
|
+
cluster_id += 1
|
|
397
|
+
|
|
398
|
+
return clusters
|
|
399
|
+
|
|
400
|
+
def _sequence_similarity(self, seq_a: List[ExecutionStep],
|
|
401
|
+
seq_b: List[ExecutionStep]) -> float:
|
|
402
|
+
if not seq_a or not seq_b:
|
|
403
|
+
return 0.0
|
|
404
|
+
len_a, len_b = len(seq_a), len(seq_b)
|
|
405
|
+
if abs(len_a - len_b) > max(len_a, len_b) * 0.7:
|
|
406
|
+
return 0.0
|
|
407
|
+
|
|
408
|
+
n = min(len_a, len_b)
|
|
409
|
+
total_sim = 0.0
|
|
410
|
+
match_count = 0
|
|
411
|
+
for i in range(n):
|
|
412
|
+
sim = self._step_similarity(seq_a[i], seq_b[i])
|
|
413
|
+
total_sim += sim
|
|
414
|
+
if sim > 0.3:
|
|
415
|
+
match_count += 1
|
|
416
|
+
|
|
417
|
+
base_score = total_sim / max(n, 1)
|
|
418
|
+
length_penalty = 1.0 - abs(len_a - len_b) / max(len_a, len_b, 1)
|
|
419
|
+
match_ratio = match_count / max(n, 1)
|
|
420
|
+
return base_score * 0.5 + length_penalty * 0.25 + match_ratio * 0.25
|
|
421
|
+
|
|
422
|
+
def _step_similarity(self, a: ExecutionStep, b: ExecutionStep) -> float:
|
|
423
|
+
if a.action_type != b.action_type:
|
|
424
|
+
return 0.0
|
|
425
|
+
score = 0.4
|
|
426
|
+
if self._extension_match(a.target, b.target):
|
|
427
|
+
score += 0.3
|
|
428
|
+
elif self._directory_match(a.target, b.target):
|
|
429
|
+
score += 0.15
|
|
430
|
+
word_overlap = self._word_overlap(a.description, b.description)
|
|
431
|
+
score += 0.2 * word_overlap
|
|
432
|
+
if a.outcome == b.outcome == "success":
|
|
433
|
+
score += 0.1
|
|
434
|
+
return min(1.0, score)
|
|
435
|
+
|
|
436
|
+
def _extension_match(self, target_a: str, target_b: str) -> bool:
|
|
437
|
+
ext_a = self._get_extension(target_a)
|
|
438
|
+
ext_b = self._get_extension(target_b)
|
|
439
|
+
return ext_a and ext_b and ext_a == ext_b
|
|
440
|
+
|
|
441
|
+
def _directory_match(self, target_a: str, target_b: str) -> bool:
|
|
442
|
+
parts_a = target_a.replace("\\", "/").rstrip("/").split("/")
|
|
443
|
+
parts_b = target_b.replace("\\", "/").rstrip("/").split("/")
|
|
444
|
+
if len(parts_a) < 2 or len(parts_b) < 2:
|
|
445
|
+
return False
|
|
446
|
+
return parts_a[:-1] == parts_b[:-1]
|
|
447
|
+
|
|
448
|
+
@staticmethod
|
|
449
|
+
def _get_extension(path: str) -> str:
|
|
450
|
+
if "." in path:
|
|
451
|
+
return path.rsplit(".", 1)[-1].lower()
|
|
452
|
+
return ""
|
|
453
|
+
|
|
454
|
+
@staticmethod
|
|
455
|
+
def _word_overlap(text_a: str, text_b: str) -> float:
|
|
456
|
+
words_a = set(re.findall(r'\w{2,}', text_a.lower()))
|
|
457
|
+
words_b = set(re.findall(r'\w{2,}', text_b.lower()))
|
|
458
|
+
if not words_a or not words_b:
|
|
459
|
+
return 0.0
|
|
460
|
+
intersection = words_a & words_b
|
|
461
|
+
union = words_a | words_b
|
|
462
|
+
return len(intersection) / len(union)
|
|
463
|
+
|
|
464
|
+
def _build_pattern_from_cluster(self, records: List[ExecutionRecord]) -> SuccessPattern:
|
|
465
|
+
rep = records[0]
|
|
466
|
+
all_steps = [r.steps for r in records if r.steps]
|
|
467
|
+
if not all_steps:
|
|
468
|
+
return SuccessPattern(name="empty-pattern")
|
|
469
|
+
|
|
470
|
+
min_len = min(len(s) for s in all_steps)
|
|
471
|
+
template_steps = []
|
|
472
|
+
for i in range(min_len):
|
|
473
|
+
step_samples = [s[i] for s in all_steps if i < len(s)]
|
|
474
|
+
if step_samples:
|
|
475
|
+
ps = self._generalize_step(step_samples)
|
|
476
|
+
template_steps.append(ps)
|
|
477
|
+
|
|
478
|
+
keywords = self._extract_trigger_keywords(rep.task_description, rep.steps)
|
|
479
|
+
roles = list(set(r.role_id for r in records if r.role_id))
|
|
480
|
+
success_rate = sum(1 for r in records if r.success) / max(len(records), 1)
|
|
481
|
+
confidence = self._calculate_confidence(records, template_steps)
|
|
482
|
+
|
|
483
|
+
name = self._generate_pattern_name(rep.task_description, template_steps)
|
|
484
|
+
|
|
485
|
+
return SuccessPattern(
|
|
486
|
+
name=name,
|
|
487
|
+
description=f"Auto-extracted pattern from {len(records)} executions",
|
|
488
|
+
source_records=[r.record_id for r in records],
|
|
489
|
+
steps_template=template_steps,
|
|
490
|
+
trigger_keywords=keywords,
|
|
491
|
+
applicable_roles=roles,
|
|
492
|
+
frequency=len(records),
|
|
493
|
+
confidence=confidence,
|
|
494
|
+
avg_success_rate=success_rate,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
def _generalize_step(self, step_samples: List[ExecutionStep]) -> PatternStep:
|
|
498
|
+
sample = step_samples[0]
|
|
499
|
+
targets = [s.target for s in step_samples]
|
|
500
|
+
generalized_target = self._generalize_target(targets)
|
|
501
|
+
descriptions = [s.description for s in step_samples]
|
|
502
|
+
desc_template = self._generalize_description(descriptions)
|
|
503
|
+
|
|
504
|
+
risk_scores = []
|
|
505
|
+
has_error = any(s.outcome != "success" for s in step_samples)
|
|
506
|
+
avg_duration = sum(s.duration_ms for s in step_samples) / max(len(step_samples), 1)
|
|
507
|
+
|
|
508
|
+
return PatternStep(
|
|
509
|
+
action_type=sample.action_type,
|
|
510
|
+
target_pattern=generalized_target,
|
|
511
|
+
description_template=desc_template,
|
|
512
|
+
is_required=not has_error,
|
|
513
|
+
estimated_risk=min(1.0, avg_duration / 5000.0),
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
def _generalize_target(self, targets: List[str]) -> str:
|
|
517
|
+
if not targets:
|
|
518
|
+
return "*"
|
|
519
|
+
extensions = set(self._get_extension(t) for t in targets)
|
|
520
|
+
directories = set("/".join(t.replace("\\", "/").rstrip("/").split("/")[:-1])
|
|
521
|
+
for t in targets if "/" in t or "\\" in t)
|
|
522
|
+
|
|
523
|
+
if len(extensions) == 1 and list(extensions)[0]:
|
|
524
|
+
ext = list(extensions)[0]
|
|
525
|
+
if len(directories) == 1:
|
|
526
|
+
dir_part = list(directories)[0]
|
|
527
|
+
return f"{dir_part}/*.{ext}"
|
|
528
|
+
return f"*.{ext}"
|
|
529
|
+
if len(directories) == 1:
|
|
530
|
+
return f"{list(directories)[0]}/*"
|
|
531
|
+
return "*"
|
|
532
|
+
|
|
533
|
+
def _generalize_description(self, descriptions: List[str]) -> str:
|
|
534
|
+
if not descriptions:
|
|
535
|
+
return ""
|
|
536
|
+
common_words = []
|
|
537
|
+
if descriptions:
|
|
538
|
+
word_counts: Dict[str, int] = {}
|
|
539
|
+
for desc in descriptions:
|
|
540
|
+
for w in re.findall(r'\w{3,}', desc.lower()):
|
|
541
|
+
word_counts[w] = word_counts.get(w, 0) + 1
|
|
542
|
+
threshold = max(len(descriptions) * 0.6, 1)
|
|
543
|
+
common_words = [w for w, c in sorted(word_counts.items(), key=lambda x: -x[1])
|
|
544
|
+
if c >= threshold][:8]
|
|
545
|
+
return " ".join(common_words) if common_words else descriptions[0]
|
|
546
|
+
|
|
547
|
+
def _extract_trigger_keywords(self, task_desc: str,
|
|
548
|
+
steps: List[ExecutionStep]) -> List[str]:
|
|
549
|
+
keywords = set()
|
|
550
|
+
text = (task_desc + " " + " ".join(s.description for s in steps)).lower()
|
|
551
|
+
important = re.findall(r'\b\w{4,}\b', text)
|
|
552
|
+
stop_words = {"that", "with", "from", "this", "they", "have", "been",
|
|
553
|
+
"were", "their", "will", "would", "could", "should", "into"}
|
|
554
|
+
for w in important:
|
|
555
|
+
if w not in stop_words and len(w) >= 4:
|
|
556
|
+
keywords.add(w)
|
|
557
|
+
return sorted(list(keywords))[:10]
|
|
558
|
+
|
|
559
|
+
def _calculate_confidence(self, records: List[ExecutionRecord],
|
|
560
|
+
steps: List[PatternStep]) -> float:
|
|
561
|
+
freq_factor = min(1.0, len(records) / 10.0)
|
|
562
|
+
success_factor = sum(1 for r in records if r.success) / max(len(records), 1)
|
|
563
|
+
consistency = 1.0
|
|
564
|
+
if len(records) >= 2:
|
|
565
|
+
lengths = [len(r.steps) for r in records]
|
|
566
|
+
avg_len = sum(lengths) / len(lengths)
|
|
567
|
+
variance = sum((l - avg_len) ** 2 for l in lengths) / len(lengths)
|
|
568
|
+
consistency = max(0.3, 1.0 - variance / (avg_len ** 2 + 1))
|
|
569
|
+
step_quality = sum(1.0 - ps.estimated_risk for ps in steps) / max(len(steps), 1)
|
|
570
|
+
return (freq_factor * 0.3 + success_factor * 0.3 +
|
|
571
|
+
consistency * 0.2 + step_quality * 0.2)
|
|
572
|
+
|
|
573
|
+
def _generate_pattern_name(self, task_desc: str,
|
|
574
|
+
steps: List[PatternStep]) -> str:
|
|
575
|
+
if task_desc:
|
|
576
|
+
words = re.findall(r'[A-Za-z][A-Za-z0-9 ]{2,20}', task_desc)
|
|
577
|
+
if words:
|
|
578
|
+
return words[0].strip().title() + " Pattern"
|
|
579
|
+
if steps:
|
|
580
|
+
action_names = {s.action_type.value.replace("_", " ").title() for s in steps}
|
|
581
|
+
return " & ".join(sorted(action_names)[:3]) + " Pattern"
|
|
582
|
+
return "Unnamed Pattern"
|
|
583
|
+
|
|
584
|
+
# ================================================================
|
|
585
|
+
# Skill Generation
|
|
586
|
+
# ================================================================
|
|
587
|
+
|
|
588
|
+
def generate_skill(self, pattern: SuccessPattern) -> SkillProposal:
|
|
589
|
+
steps = [
|
|
590
|
+
SkillStepDef(
|
|
591
|
+
step_number=i + 1,
|
|
592
|
+
action_type=ps.action_type,
|
|
593
|
+
target_pattern=ps.target_pattern,
|
|
594
|
+
description=ps.description_template or ps.action_type.value,
|
|
595
|
+
is_required=ps.is_required,
|
|
596
|
+
)
|
|
597
|
+
for i, ps in enumerate(pattern.steps_template)
|
|
598
|
+
]
|
|
599
|
+
|
|
600
|
+
category = self._classify_category(pattern)
|
|
601
|
+
slug = self._make_slug(pattern.name)
|
|
602
|
+
desc = self._generate_description(pattern, steps)
|
|
603
|
+
input_schema = self._infer_input_schema(steps)
|
|
604
|
+
output_schema = self._infer_output_schema(pattern)
|
|
605
|
+
acceptance = self._infer_acceptance_criteria(pattern)
|
|
606
|
+
|
|
607
|
+
proposal = SkillProposal(
|
|
608
|
+
name=pattern.name.replace(" Pattern", "").strip(),
|
|
609
|
+
slug=slug,
|
|
610
|
+
description=desc,
|
|
611
|
+
category=category.value,
|
|
612
|
+
trigger_conditions=list(pattern.trigger_keywords),
|
|
613
|
+
steps=steps,
|
|
614
|
+
required_roles=list(pattern.applicable_roles),
|
|
615
|
+
input_schema=input_schema,
|
|
616
|
+
output_schema=output_schema,
|
|
617
|
+
acceptance_criteria=acceptance,
|
|
618
|
+
source_pattern=pattern.pattern_id,
|
|
619
|
+
quality_score=pattern.confidence * 100,
|
|
620
|
+
status=ProposalStatus.DRAFT,
|
|
621
|
+
)
|
|
622
|
+
proposal.slug = proposal._generate_slug()
|
|
623
|
+
|
|
624
|
+
validation = self.validate_skill(proposal)
|
|
625
|
+
proposal.validation_result = validation
|
|
626
|
+
proposal.quality_score = validation.score
|
|
627
|
+
self._proposals[proposal.proposal_id] = proposal
|
|
628
|
+
return proposal
|
|
629
|
+
|
|
630
|
+
def _classify_category(self, pattern: SuccessPattern) -> SkillCategory:
|
|
631
|
+
text = (pattern.name + " " +
|
|
632
|
+
" ".join(pattern.trigger_keywords) +
|
|
633
|
+
" ".join(ps.description_template for ps in pattern.steps_template)).lower()
|
|
634
|
+
best_cat = SkillCategory.AUTO_GENERATED
|
|
635
|
+
best_score = 0
|
|
636
|
+
for cat, keywords in self.CATEGORY_KEYWORDS.items():
|
|
637
|
+
score = sum(1 for kw in keywords if kw in text)
|
|
638
|
+
if score > best_score:
|
|
639
|
+
best_score = score
|
|
640
|
+
best_cat = cat
|
|
641
|
+
return best_cat
|
|
642
|
+
|
|
643
|
+
def _make_slug(self, name: str) -> str:
|
|
644
|
+
slug = re.sub(r'[^a-z0-9\s-]', '', name.lower())
|
|
645
|
+
slug = re.sub(r'\s+', '-', slug.strip())
|
|
646
|
+
return slug or "auto-skill"
|
|
647
|
+
|
|
648
|
+
def _generate_description(self, pattern: SuccessPattern,
|
|
649
|
+
steps: List[SkillStepDef]) -> str:
|
|
650
|
+
action_types = [s.action_type.value.replace("_", " ") for s in steps]
|
|
651
|
+
unique_actions = list(dict.fromkeys(action_types))
|
|
652
|
+
return (f"Auto-generated skill: {' → '.join(unique_actions[:5])}. "
|
|
653
|
+
f"Based on {pattern.frequency} successful executions. "
|
|
654
|
+
f"Confidence: {pattern.confidence:.0%}.")
|
|
655
|
+
|
|
656
|
+
def _infer_input_schema(self, steps: List[SkillStepDef]) -> Dict[str, Any]:
|
|
657
|
+
schema: Dict[str, Any] = {}
|
|
658
|
+
has_wildcard = any("*" in s.target_pattern for s in steps)
|
|
659
|
+
if has_wildcard:
|
|
660
|
+
schema["target_path"] = {"type": "string", "description": "Target file/directory path"}
|
|
661
|
+
has_shell = any(s.action_type == PGActionType.SHELL_EXECUTE for s in steps)
|
|
662
|
+
if has_shell:
|
|
663
|
+
schema["command_args"] = {"type": "string", "description": "Command arguments"}
|
|
664
|
+
return schema
|
|
665
|
+
|
|
666
|
+
def _infer_output_schema(self, pattern: SuccessPattern) -> Dict[str, Any]:
|
|
667
|
+
create_steps = [ps for ps in pattern.steps_template
|
|
668
|
+
if ps.action_type in (PGActionType.FILE_CREATE,
|
|
669
|
+
PGActionType.FILE_MODIFY)]
|
|
670
|
+
if create_steps:
|
|
671
|
+
return {
|
|
672
|
+
"created_files": {"type": "array", "description": "Files created/modified"},
|
|
673
|
+
"artifacts": {"type": "array", "description": "Output artifacts"},
|
|
674
|
+
}
|
|
675
|
+
return {}
|
|
676
|
+
|
|
677
|
+
def _infer_acceptance_criteria(self, pattern: SuccessPattern) -> List[str]:
|
|
678
|
+
criteria = []
|
|
679
|
+
if pattern.avg_success_rate >= 0.9:
|
|
680
|
+
criteria.append(f"Historical success rate >= {pattern.avg_success_rate:.0%}")
|
|
681
|
+
if pattern.frequency >= 3:
|
|
682
|
+
criteria.append(f"Verified across {pattern.frequency}+ executions")
|
|
683
|
+
if pattern.steps_template:
|
|
684
|
+
criteria.append(f"All {len(pattern.steps_template)} steps completed successfully")
|
|
685
|
+
return criteria
|
|
686
|
+
|
|
687
|
+
# ================================================================
|
|
688
|
+
# Quality Validation (5-Dimension Scoring)
|
|
689
|
+
# ================================================================
|
|
690
|
+
|
|
691
|
+
def validate_skill(self, proposal: SkillProposal) -> ValidationResult:
|
|
692
|
+
completeness = self._score_completeness(proposal)
|
|
693
|
+
specificity = self._score_specificity(proposal)
|
|
694
|
+
repeatability = self._score_repeatability(proposal)
|
|
695
|
+
safety = self._score_safety(proposal)
|
|
696
|
+
practicality = self._score_practicality(proposal)
|
|
697
|
+
|
|
698
|
+
score = (completeness * 0.25 + specificity * 0.20 +
|
|
699
|
+
repeatability * 0.20 + safety * 0.20 + practicality * 0.15)
|
|
700
|
+
|
|
701
|
+
issues = []
|
|
702
|
+
suggestions = []
|
|
703
|
+
|
|
704
|
+
if completeness < 60:
|
|
705
|
+
issues.append("步骤定义不完整,缺少关键信息")
|
|
706
|
+
if len(proposal.steps) == 0:
|
|
707
|
+
issues.append("无任何步骤定义")
|
|
708
|
+
if len(proposal.steps) > 20:
|
|
709
|
+
issues.append(f"步骤过多({len(proposal.steps)}步),建议拆分")
|
|
710
|
+
if safety < 50:
|
|
711
|
+
issues.append("包含高风险操作")
|
|
712
|
+
if len(proposal.trigger_conditions) < 3:
|
|
713
|
+
issues.append("触发条件过于宽泛")
|
|
714
|
+
suggestions.append("增加更多触发关键词以提高特异性")
|
|
715
|
+
if repeatability < 50:
|
|
716
|
+
suggestions.append("需要更多成功执行记录来提高可重复性")
|
|
717
|
+
if completeness < 80:
|
|
718
|
+
suggestions.append("补充输入输出Schema和验收标准")
|
|
719
|
+
|
|
720
|
+
result = ValidationResult(
|
|
721
|
+
score=round(score, 1),
|
|
722
|
+
completeness=round(completeness, 1),
|
|
723
|
+
specificity=round(specificity, 1),
|
|
724
|
+
repeatability=round(repeatability, 1),
|
|
725
|
+
safety=round(safety, 1),
|
|
726
|
+
issues=issues,
|
|
727
|
+
suggestions=suggestions,
|
|
728
|
+
)
|
|
729
|
+
proposal.validation_result = result
|
|
730
|
+
return result
|
|
731
|
+
|
|
732
|
+
def _score_completeness(self, p: SkillProposal) -> float:
|
|
733
|
+
score = 100.0
|
|
734
|
+
if not p.name:
|
|
735
|
+
score -= 30
|
|
736
|
+
if not p.description:
|
|
737
|
+
score -= 15
|
|
738
|
+
if not p.steps:
|
|
739
|
+
score -= 40
|
|
740
|
+
if not p.input_schema:
|
|
741
|
+
score -= 10
|
|
742
|
+
if not p.output_schema:
|
|
743
|
+
score -= 5
|
|
744
|
+
if not p.acceptance_criteria:
|
|
745
|
+
score -= 10
|
|
746
|
+
for s in p.steps:
|
|
747
|
+
if not s.description or s.description == s.action_type.value:
|
|
748
|
+
score -= 3
|
|
749
|
+
return max(0.0, score)
|
|
750
|
+
|
|
751
|
+
def _score_specificity(self, p: SkillProposal) -> float:
|
|
752
|
+
score = 80.0
|
|
753
|
+
kw_count = len(p.trigger_conditions)
|
|
754
|
+
score += min(20, kw_count * 3)
|
|
755
|
+
generic_patterns = sum(1 for s in p.steps if s.target_pattern == "*")
|
|
756
|
+
score -= generic_patterns * 10
|
|
757
|
+
return max(0.0, min(100.0, score))
|
|
758
|
+
|
|
759
|
+
def _score_repeatability(self, p: SkillProposal) -> float:
|
|
760
|
+
score = 60.0
|
|
761
|
+
if p.source_pattern:
|
|
762
|
+
pattern = next((pat for pat in self._patterns
|
|
763
|
+
if pat.pattern_id == p.source_pattern), None)
|
|
764
|
+
if pattern:
|
|
765
|
+
score += min(30, pattern.frequency * 5)
|
|
766
|
+
score += pattern.avg_success_rate * 10
|
|
767
|
+
return max(0.0, min(100.0, score))
|
|
768
|
+
|
|
769
|
+
def _score_safety(self, p: SkillProposal) -> float:
|
|
770
|
+
score = 100.0
|
|
771
|
+
high_risk_types = {PGActionType.FILE_DELETE, PGActionType.SHELL_EXECUTE,
|
|
772
|
+
PGActionType.PROCESS_SPAWN}
|
|
773
|
+
for s in p.steps:
|
|
774
|
+
if s.action_type in high_risk_types:
|
|
775
|
+
score -= 15
|
|
776
|
+
if "*" in s.target_pattern and s.action_type in high_risk_types:
|
|
777
|
+
score -= 10
|
|
778
|
+
return max(0.0, score)
|
|
779
|
+
|
|
780
|
+
def _score_practicality(self, p: SkillProposal) -> float:
|
|
781
|
+
score = 70.0
|
|
782
|
+
n_steps = len(p.steps)
|
|
783
|
+
if 3 <= n_steps <= 15:
|
|
784
|
+
score += 20
|
|
785
|
+
elif n_steps < 3:
|
|
786
|
+
score -= 15
|
|
787
|
+
elif n_steps <= 20:
|
|
788
|
+
score += 5
|
|
789
|
+
else:
|
|
790
|
+
score -= 20
|
|
791
|
+
return max(0.0, min(100.0, score))
|
|
792
|
+
|
|
793
|
+
# ================================================================
|
|
794
|
+
# Publishing & Discovery
|
|
795
|
+
# ================================================================
|
|
796
|
+
|
|
797
|
+
def approve_and_publish(self, proposal_id: str,
|
|
798
|
+
approver: str = "system") -> bool:
|
|
799
|
+
with self._lock:
|
|
800
|
+
proposal = self._proposals.get(proposal_id)
|
|
801
|
+
if not proposal:
|
|
802
|
+
return False
|
|
803
|
+
if proposal.status == ProposalStatus.PUBLISHED:
|
|
804
|
+
return True
|
|
805
|
+
proposal.status = ProposalStatus.PUBLISHED
|
|
806
|
+
proposal.approved_by = approver
|
|
807
|
+
proposal.published_at = datetime.now()
|
|
808
|
+
return True
|
|
809
|
+
|
|
810
|
+
def suggest_skills_for_task(self, task_description: str) -> List[SkillProposal]:
|
|
811
|
+
task_lower = task_description.lower()
|
|
812
|
+
task_words = set(re.findall(r'\w{3,}', task_lower))
|
|
813
|
+
scored = []
|
|
814
|
+
for prop in self._proposals.values():
|
|
815
|
+
if prop.status not in (ProposalStatus.APPROVED, ProposalStatus.PUBLISHED):
|
|
816
|
+
continue
|
|
817
|
+
score = 0.0
|
|
818
|
+
for tc in prop.trigger_conditions:
|
|
819
|
+
if tc.lower() in task_lower:
|
|
820
|
+
score += 2.0
|
|
821
|
+
for tw in task_words:
|
|
822
|
+
if tw in tc.lower():
|
|
823
|
+
score += 0.5
|
|
824
|
+
if prop.quality_score >= 70:
|
|
825
|
+
score += 1.0
|
|
826
|
+
if score > 0:
|
|
827
|
+
scored.append((score, prop))
|
|
828
|
+
|
|
829
|
+
scored.sort(key=lambda x: -x[0])
|
|
830
|
+
return [p for _, p in scored[:10]]
|
|
831
|
+
|
|
832
|
+
# ================================================================
|
|
833
|
+
# Query & Export
|
|
834
|
+
# ================================================================
|
|
835
|
+
|
|
836
|
+
def get_pattern_library(self) -> List[SuccessPattern]:
|
|
837
|
+
return list(self._patterns)
|
|
838
|
+
|
|
839
|
+
def get_proposals(self, status=None) -> List[SkillProposal]:
|
|
840
|
+
props = list(self._proposals.values())
|
|
841
|
+
if status:
|
|
842
|
+
props = [p for p in props if p.status == status]
|
|
843
|
+
return props
|
|
844
|
+
|
|
845
|
+
def export_patterns(self) -> str:
|
|
846
|
+
return json.dumps([p.to_dict() for p in self._patterns],
|
|
847
|
+
indent=2, ensure_ascii=False, default=str)
|
|
848
|
+
|
|
849
|
+
def export_state(self) -> Dict:
|
|
850
|
+
with self._lock:
|
|
851
|
+
return {
|
|
852
|
+
"records_count": len(self._records),
|
|
853
|
+
"patterns_count": len(self._patterns),
|
|
854
|
+
"proposals_count": len(self._proposals),
|
|
855
|
+
"patterns": [p.to_dict() for p in self._patterns],
|
|
856
|
+
"proposal_ids": list(self._proposals.keys()),
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
860
|
+
with self._lock:
|
|
861
|
+
published = sum(1 for p in self._proposals.values()
|
|
862
|
+
if p.status == ProposalStatus.PUBLISHED)
|
|
863
|
+
avg_confidence = 0.0
|
|
864
|
+
if self._patterns:
|
|
865
|
+
avg_confidence = sum(p.confidence for p in self._patterns) / len(self._patterns)
|
|
866
|
+
avg_quality = 0.0
|
|
867
|
+
validated = [p for p in self._proposals.values() if p.validation_result]
|
|
868
|
+
if validated:
|
|
869
|
+
avg_quality = sum(p.quality_score for p in validated) / len(validated)
|
|
870
|
+
return {
|
|
871
|
+
"total_records": len(self._records),
|
|
872
|
+
"successful_records": sum(1 for r in self._records if r.success),
|
|
873
|
+
"total_patterns": len(self._patterns),
|
|
874
|
+
"total_proposals": len(self._proposals),
|
|
875
|
+
"published_skills": published,
|
|
876
|
+
"avg_pattern_confidence": round(avg_confidence, 3),
|
|
877
|
+
"avg_quality_score": round(avg_quality, 1),
|
|
878
|
+
}
|