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
scripts/alert_manager.py
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
DevSquad Alert Manager
|
|
5
|
+
|
|
6
|
+
Notification system for critical events and alerts.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Multiple notification channels (Slack, Email, Console)
|
|
10
|
+
- Alert severity levels (INFO, WARNING, ERROR, CRITICAL)
|
|
11
|
+
- Rate limiting to prevent alert spam
|
|
12
|
+
- Alert history and deduplication
|
|
13
|
+
- Configurable via config/alerts.yaml
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
from scripts.alert_manager import AlertManager, AlertSeverity
|
|
17
|
+
|
|
18
|
+
alerts = AlertManager()
|
|
19
|
+
alerts.send_alert(
|
|
20
|
+
severity=AlertSeverity.ERROR,
|
|
21
|
+
title="Gate Check Failed",
|
|
22
|
+
message="Build gate failed for project X",
|
|
23
|
+
channel="slack"
|
|
24
|
+
)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
import smtplib
|
|
29
|
+
import sys
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from datetime import datetime, timedelta
|
|
32
|
+
from email.mime.text import MIMEText
|
|
33
|
+
from email.mime.multipart import MIMEMultipart
|
|
34
|
+
from enum import Enum
|
|
35
|
+
from typing import Any, Dict, List, Optional
|
|
36
|
+
|
|
37
|
+
import os
|
|
38
|
+
import yaml
|
|
39
|
+
|
|
40
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AlertSeverity(Enum):
|
|
46
|
+
"""Alert severity levels."""
|
|
47
|
+
INFO = "info"
|
|
48
|
+
WARNING = "warning"
|
|
49
|
+
ERROR = "error"
|
|
50
|
+
CRITICAL = "critical"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AlertChannel(Enum):
|
|
54
|
+
"""Notification channels."""
|
|
55
|
+
CONSOLE = "console"
|
|
56
|
+
SLACK = "slack"
|
|
57
|
+
EMAIL = "email"
|
|
58
|
+
WEBHOOK = "webhook"
|
|
59
|
+
ALL = "all"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class Alert:
|
|
64
|
+
"""Alert data structure."""
|
|
65
|
+
id: str
|
|
66
|
+
severity: AlertSeverity
|
|
67
|
+
title: str
|
|
68
|
+
message: str
|
|
69
|
+
source: str
|
|
70
|
+
timestamp: datetime
|
|
71
|
+
channel: AlertChannel
|
|
72
|
+
acknowledged: bool = False
|
|
73
|
+
resolved_at: Optional[datetime] = None
|
|
74
|
+
|
|
75
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
76
|
+
return {
|
|
77
|
+
"id": self.id,
|
|
78
|
+
"severity": self.severity.value,
|
|
79
|
+
"title": self.title,
|
|
80
|
+
"message": self.message,
|
|
81
|
+
"source": self.source,
|
|
82
|
+
"timestamp": self.timestamp.isoformat(),
|
|
83
|
+
"channel": self.channel.value,
|
|
84
|
+
"acknowledged": self.acknowledged,
|
|
85
|
+
"resolved_at": self.resolved_at.isoformat() if self.resolved_at else None
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class AlertManager:
|
|
90
|
+
"""
|
|
91
|
+
Centralized alert management system.
|
|
92
|
+
|
|
93
|
+
Handles sending notifications through multiple channels
|
|
94
|
+
with rate limiting and deduplication.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, config_path: Optional[str] = None):
|
|
98
|
+
"""
|
|
99
|
+
Initialize AlertManager.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
config_path: Path to alerts configuration file.
|
|
103
|
+
Defaults to config/alerts.yaml
|
|
104
|
+
"""
|
|
105
|
+
self.config_path = config_path or os.path.join(
|
|
106
|
+
os.path.dirname(__file__),
|
|
107
|
+
"..",
|
|
108
|
+
"config",
|
|
109
|
+
"alerts.yaml"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
self.config = self._load_config()
|
|
113
|
+
self.alert_history: List[Alert] = []
|
|
114
|
+
self.recent_alerts: Dict[str, datetime] = {} # For deduplication
|
|
115
|
+
|
|
116
|
+
# Rate limiting settings
|
|
117
|
+
self.rate_limit_window = self.config.get("rate_limit", {}).get("window_seconds", 60)
|
|
118
|
+
self.max_alerts_per_window = self.config.get("rate_limit", {}).get("max_alerts", 10)
|
|
119
|
+
self._alert_count_in_window = 0
|
|
120
|
+
self._window_start = datetime.now()
|
|
121
|
+
|
|
122
|
+
logger.info(f"AlertManager initialized")
|
|
123
|
+
|
|
124
|
+
def _load_config(self) -> Dict[str, Any]:
|
|
125
|
+
"""Load alert configuration."""
|
|
126
|
+
try:
|
|
127
|
+
if os.path.exists(self.config_path):
|
|
128
|
+
with open(self.config_path, "r", encoding="utf-8") as f:
|
|
129
|
+
return yaml.safe_load(f) or {}
|
|
130
|
+
return {}
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.error(f"Failed to load alert config: {e}")
|
|
133
|
+
return {}
|
|
134
|
+
|
|
135
|
+
def _generate_alert_id(self) -> str:
|
|
136
|
+
"""Generate unique alert ID."""
|
|
137
|
+
import hashlib
|
|
138
|
+
timestamp = datetime.now().isoformat()
|
|
139
|
+
return hashlib.md5(timestamp.encode()).hexdigest()[:12]
|
|
140
|
+
|
|
141
|
+
def _check_rate_limit(self) -> bool:
|
|
142
|
+
"""
|
|
143
|
+
Check if we're within rate limits.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
True if allowed to send, False if rate limited
|
|
147
|
+
"""
|
|
148
|
+
now = datetime.now()
|
|
149
|
+
|
|
150
|
+
# Reset window if expired
|
|
151
|
+
if now - self._window_start > timedelta(seconds=self.rate_limit_window):
|
|
152
|
+
self._window_start = now
|
|
153
|
+
self._alert_count_in_window = 0
|
|
154
|
+
|
|
155
|
+
if self._alert_count_in_window >= self.max_alerts_per_window:
|
|
156
|
+
logger.warning("Rate limit exceeded for alerts")
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
self._alert_count_in_window += 1
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
def _deduplicate(self, title: str, message: str, window_minutes: int = 5) -> bool:
|
|
163
|
+
"""
|
|
164
|
+
Check for duplicate alerts within time window.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
title: Alert title
|
|
168
|
+
message: Alert message
|
|
169
|
+
window_minutes: Deduplication window in minutes
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
True if duplicate (should skip), False if unique
|
|
173
|
+
"""
|
|
174
|
+
key = f"{title}:{message[:50]}"
|
|
175
|
+
now = datetime.now()
|
|
176
|
+
|
|
177
|
+
if key in self.recent_alerts:
|
|
178
|
+
last_sent = self.recent_alerts[key]
|
|
179
|
+
if now - last_sent < timedelta(minutes=window_minutes):
|
|
180
|
+
logger.debug(f"Duplicate alert suppressed: {title}")
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
self.recent_alerts[key] = now
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
def send_alert(
|
|
187
|
+
self,
|
|
188
|
+
severity: AlertSeverity,
|
|
189
|
+
title: str,
|
|
190
|
+
message: str,
|
|
191
|
+
source: str = "devsquad",
|
|
192
|
+
channel: Optional[str] = None,
|
|
193
|
+
deduplicate: bool = True
|
|
194
|
+
) -> Optional[Alert]:
|
|
195
|
+
"""
|
|
196
|
+
Send an alert notification.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
severity: Alert severity level
|
|
200
|
+
title: Short alert title
|
|
201
|
+
message: Detailed alert message
|
|
202
|
+
source: Alert source identifier
|
|
203
|
+
channel: Specific channel or None for default
|
|
204
|
+
deduplicate: Whether to check for duplicates
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Alert object if sent, None if suppressed
|
|
208
|
+
"""
|
|
209
|
+
# Check rate limit
|
|
210
|
+
if not self._check_rate_limit():
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
# Check for duplicates
|
|
214
|
+
if deduplicate and self._deduplicate(title, message):
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
# Determine target channels
|
|
218
|
+
target_channel = AlertChannel(channel) if channel else self._get_default_channel(severity)
|
|
219
|
+
|
|
220
|
+
# Create alert object
|
|
221
|
+
alert = Alert(
|
|
222
|
+
id=self._generate_alert_id(),
|
|
223
|
+
severity=severity,
|
|
224
|
+
title=title,
|
|
225
|
+
message=message,
|
|
226
|
+
source=source,
|
|
227
|
+
timestamp=datetime.now(),
|
|
228
|
+
channel=target_channel
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Send to appropriate channel(s)
|
|
232
|
+
try:
|
|
233
|
+
if target_channel == AlertChannel.ALL:
|
|
234
|
+
self._send_to_console(alert)
|
|
235
|
+
self._send_to_slack(alert)
|
|
236
|
+
self._send_to_email(alert)
|
|
237
|
+
elif target_channel == AlertChannel.SLACK:
|
|
238
|
+
self._send_to_slack(alert)
|
|
239
|
+
self._send_to_console(alert) # Also log to console
|
|
240
|
+
elif target_channel == AlertChannel.EMAIL:
|
|
241
|
+
self._send_to_email(alert)
|
|
242
|
+
self._send_to_console(alert)
|
|
243
|
+
else:
|
|
244
|
+
self._send_to_console(alert)
|
|
245
|
+
|
|
246
|
+
# Store in history
|
|
247
|
+
self.alert_history.append(alert)
|
|
248
|
+
|
|
249
|
+
# Trim history if too large (keep last 1000)
|
|
250
|
+
if len(self.alert_history) > 1000:
|
|
251
|
+
self.alert_history = self.alert_history[-1000:]
|
|
252
|
+
|
|
253
|
+
logger.info(f"Alert sent: [{severity.value.upper()}] {title}")
|
|
254
|
+
return alert
|
|
255
|
+
|
|
256
|
+
except Exception as e:
|
|
257
|
+
logger.error(f"Failed to send alert: {e}")
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
def _get_default_channel(self, severity: AlertSeverity) -> AlertChannel:
|
|
261
|
+
"""Get default channel based on severity."""
|
|
262
|
+
defaults = self.config.get("defaults", {})
|
|
263
|
+
channel_map = defaults.get("channels_by_severity", {
|
|
264
|
+
"critical": ["slack", "email"],
|
|
265
|
+
"error": ["slack", "console"],
|
|
266
|
+
"warning": ["console"],
|
|
267
|
+
"info": ["console"]
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
channels = channel_map.get(severity.value, ["console"])
|
|
271
|
+
if len(channels) > 1:
|
|
272
|
+
return AlertChannel.ALL
|
|
273
|
+
return AlertChannel(channels[0])
|
|
274
|
+
|
|
275
|
+
def _send_to_console(self, alert: Alert):
|
|
276
|
+
"""Send alert to console/log."""
|
|
277
|
+
emoji_map = {
|
|
278
|
+
AlertSeverity.INFO: "ā¹ļø",
|
|
279
|
+
AlertSeverity.WARNING: "ā ļø",
|
|
280
|
+
AlertSeverity.ERROR: "ā",
|
|
281
|
+
AlertSeverity.CRITICAL: "šØ"
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
emoji = emoji_map.get(alert.severity, "š¢")
|
|
285
|
+
|
|
286
|
+
log_method = {
|
|
287
|
+
AlertSeverity.INFO: logger.info,
|
|
288
|
+
AlertSeverity.WARNING: logger.warning,
|
|
289
|
+
AlertSeverity.ERROR: logger.error,
|
|
290
|
+
AlertSeverity.CRITICAL: logger.critical
|
|
291
|
+
}.get(alert.severity, logger.info)
|
|
292
|
+
|
|
293
|
+
log_method(
|
|
294
|
+
f"{emoji} [{alert.severity.value.upper()}] {alert.title}\n"
|
|
295
|
+
f" Source: {alert.source}\n"
|
|
296
|
+
f" Message: {alert.message}\n"
|
|
297
|
+
f" ID: {alert.id}"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def _send_to_slack(self, alert: Alert):
|
|
301
|
+
"""Send alert to Slack webhook."""
|
|
302
|
+
slack_config = self.config.get("channels", {}).get("slack", {})
|
|
303
|
+
webhook_url = slack_config.get("webhook_url")
|
|
304
|
+
|
|
305
|
+
if not webhook_url:
|
|
306
|
+
logger.debug("Slack webhook URL not configured")
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
import urllib.request
|
|
311
|
+
import json
|
|
312
|
+
|
|
313
|
+
color_map = {
|
|
314
|
+
AlertSeverity.INFO: "#36a64f",
|
|
315
|
+
AlertSeverity.WARNING: "#ff9800",
|
|
316
|
+
AlertSeverity.ERROR: "#f44336",
|
|
317
|
+
AlertSeverity.CRITICAL: "#9c27b0"
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
payload = {
|
|
321
|
+
"attachments": [{
|
|
322
|
+
"color": color_map.get(alert.severity, "#808080"),
|
|
323
|
+
"title": f"[{alert.severity.value.upper()}] {alert.title}",
|
|
324
|
+
"text": alert.message,
|
|
325
|
+
"fields": [
|
|
326
|
+
{"title": "Source", "value": alert.source, "short": True},
|
|
327
|
+
{"title": "Severity", "value": alert.severity.value.upper(), "short": True},
|
|
328
|
+
{"title": "Alert ID", "value": alert.id, "short": True},
|
|
329
|
+
{"title": "Time", "value": alert.timestamp.strftime("%Y-%m-%d %H:%M:%S"), "short": True}
|
|
330
|
+
],
|
|
331
|
+
"footer": "DevSquad Alert System",
|
|
332
|
+
"ts": int(alert.timestamp.timestamp())
|
|
333
|
+
}]
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
data = json.dumps(payload).encode('utf-8')
|
|
337
|
+
req = urllib.request.Request(
|
|
338
|
+
webhook_url,
|
|
339
|
+
data=data,
|
|
340
|
+
headers={'Content-Type': 'application/json'}
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
with urllib.request.urlopen(req, timeout=10) as response:
|
|
344
|
+
if response.status == 200:
|
|
345
|
+
logger.info(f"Slack alert sent successfully: {alert.id}")
|
|
346
|
+
|
|
347
|
+
except ImportError:
|
|
348
|
+
logger.warning("urllib not available for Slack integration")
|
|
349
|
+
except Exception as e:
|
|
350
|
+
logger.error(f"Failed to send Slack alert: {e}")
|
|
351
|
+
|
|
352
|
+
def _send_to_email(self, alert: Alert):
|
|
353
|
+
"""Send alert via email."""
|
|
354
|
+
email_config = self.config.get("channels", {}).get("email", {})
|
|
355
|
+
|
|
356
|
+
smtp_server = email_config.get("smtp_server")
|
|
357
|
+
smtp_port = email_config.get("smtp_port", 587)
|
|
358
|
+
sender = email_config.get("sender")
|
|
359
|
+
recipients = email_config.get("recipients", [])
|
|
360
|
+
username = email_config.get("username")
|
|
361
|
+
password = email_config.get("password")
|
|
362
|
+
|
|
363
|
+
if not all([smtp_server, sender, recipients]):
|
|
364
|
+
logger.debug("Email configuration incomplete")
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
msg = MIMEMultipart()
|
|
369
|
+
msg['From'] = sender
|
|
370
|
+
msg['To'] = ', '.join(recipients)
|
|
371
|
+
msg['Subject'] = f"[DevSquad-{alert.severity.value.upper()}] {alert.title}"
|
|
372
|
+
|
|
373
|
+
body = f"""
|
|
374
|
+
DevSquad Alert Notification
|
|
375
|
+
============================
|
|
376
|
+
|
|
377
|
+
Severity: {alert.severity.value.upper()}
|
|
378
|
+
Title: {alert.title}
|
|
379
|
+
Source: {alert.source}
|
|
380
|
+
Time: {alert.timestamp.strftime("%Y-%m-%d %H:%M:%S")}
|
|
381
|
+
Alert ID: {alert.id}
|
|
382
|
+
|
|
383
|
+
Message:
|
|
384
|
+
{alert.message}
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
This is an automated message from DevSquad Alert System.
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
msg.attach(MIMEText(body, 'plain'))
|
|
391
|
+
|
|
392
|
+
with smtplib.SMTP(smtp_server, smtp_port) as server:
|
|
393
|
+
server.starttls()
|
|
394
|
+
if username and password:
|
|
395
|
+
server.login(username, password)
|
|
396
|
+
server.sendmail(sender, recipients, msg.as_string())
|
|
397
|
+
|
|
398
|
+
logger.info(f"Email alert sent successfully: {alert.id}")
|
|
399
|
+
|
|
400
|
+
except Exception as e:
|
|
401
|
+
logger.error(f"Failed to send email alert: {e}")
|
|
402
|
+
|
|
403
|
+
def get_alert_history(
|
|
404
|
+
self,
|
|
405
|
+
severity: Optional[AlertSeverity] = None,
|
|
406
|
+
hours: int = 24,
|
|
407
|
+
limit: int = 50
|
|
408
|
+
) -> List[Dict]:
|
|
409
|
+
"""
|
|
410
|
+
Get alert history.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
severity: Filter by severity (optional)
|
|
414
|
+
hours: Look back period in hours
|
|
415
|
+
limit: Maximum number of alerts to return
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
List of alert dictionaries
|
|
419
|
+
"""
|
|
420
|
+
cutoff = datetime.now() - timedelta(hours=hours)
|
|
421
|
+
|
|
422
|
+
filtered = [
|
|
423
|
+
alert.to_dict()
|
|
424
|
+
for alert in self.alert_history
|
|
425
|
+
if alert.timestamp >= cutoff
|
|
426
|
+
and (severity is None or alert.severity == severity)
|
|
427
|
+
]
|
|
428
|
+
|
|
429
|
+
# Sort by timestamp descending
|
|
430
|
+
filtered.sort(key=lambda x: x["timestamp"], reverse=True)
|
|
431
|
+
|
|
432
|
+
return filtered[:limit]
|
|
433
|
+
|
|
434
|
+
def get_alert_stats(self, hours: int = 24) -> Dict[str, Any]:
|
|
435
|
+
"""
|
|
436
|
+
Get alert statistics.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
hours: Statistics period in hours
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
Dictionary with alert statistics
|
|
443
|
+
"""
|
|
444
|
+
cutoff = datetime.now() - timedelta(hours=hours)
|
|
445
|
+
recent_alerts = [a for a in self.alert_history if a.timestamp >= cutoff]
|
|
446
|
+
|
|
447
|
+
stats = {
|
|
448
|
+
"total": len(recent_alerts),
|
|
449
|
+
"by_severity": {},
|
|
450
|
+
"by_source": {},
|
|
451
|
+
"period_hours": hours
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
for alert in recent_alerts:
|
|
455
|
+
sev = alert.severity.value
|
|
456
|
+
stats["by_severity"][sev] = stats["by_severity"].get(sev, 0) + 1
|
|
457
|
+
|
|
458
|
+
src = alert.source
|
|
459
|
+
stats["by_source"][src] = stats["by_source"].get(src, 0) + 1
|
|
460
|
+
|
|
461
|
+
return stats
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# Convenience functions for quick alerts
|
|
465
|
+
def alert_info(title: str, message: str, **kwargs) -> Optional[Alert]:
|
|
466
|
+
"""Send INFO level alert."""
|
|
467
|
+
mgr = AlertManager()
|
|
468
|
+
return mgr.send_alert(AlertSeverity.INFO, title, message, **kwargs)
|
|
469
|
+
|
|
470
|
+
def alert_warning(title: str, message: str, **kwargs) -> Optional[Alert]:
|
|
471
|
+
"""Send WARNING level alert."""
|
|
472
|
+
mgr = AlertManager()
|
|
473
|
+
return mgr.send_alert(AlertSeverity.WARNING, title, message, **kwargs)
|
|
474
|
+
|
|
475
|
+
def alert_error(title: str, message: str, **kwargs) -> Optional[Alert]:
|
|
476
|
+
"""Send ERROR level alert."""
|
|
477
|
+
mgr = AlertManager()
|
|
478
|
+
return mgr.send_alert(AlertSeverity.ERROR, title, message, **kwargs)
|
|
479
|
+
|
|
480
|
+
def alert_critical(title: str, message: str, **kwargs) -> Optional[Alert]:
|
|
481
|
+
"""Send CRITICAL level alert."""
|
|
482
|
+
mgr = AlertManager()
|
|
483
|
+
return mgr.send_alert(AlertSeverity.CRITICAL, title, message, **kwargs)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
if __name__ == "__main__":
|
|
487
|
+
# Demo: Send test alerts
|
|
488
|
+
print("\nš DevSquad Alert Manager Demo\n")
|
|
489
|
+
print("=" * 50)
|
|
490
|
+
|
|
491
|
+
alerts = AlertManager()
|
|
492
|
+
|
|
493
|
+
# Test different severity levels
|
|
494
|
+
alerts.send_alert(AlertSeverity.INFO, "Test Info", "This is an info message")
|
|
495
|
+
alerts.send_alert(AlertSeverity.WARNING, "Test Warning", "This is a warning")
|
|
496
|
+
alerts.send_alert(AlertSeverity.ERROR, "Test Error", "This is an error message")
|
|
497
|
+
alerts.send_alert(AlertSeverity.CRITICAL, "Test Critical", "This is critical!")
|
|
498
|
+
|
|
499
|
+
# Show statistics
|
|
500
|
+
stats = alerts.get_alert_stats()
|
|
501
|
+
print(f"\nš Alert Statistics:")
|
|
502
|
+
print(f"Total alerts: {stats['total']}")
|
|
503
|
+
print(f"By severity: {stats['by_severity']}")
|
|
504
|
+
|
|
505
|
+
print("\nā
Demo completed!")
|
scripts/api/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# API Package
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
DevSquad REST API Package
|
|
5
|
+
|
|
6
|
+
Provides FastAPI-based REST API for DevSquad multi-agent collaboration system.
|
|
7
|
+
|
|
8
|
+
Modules:
|
|
9
|
+
- models: Pydantic request/response models
|
|
10
|
+
- routes: API endpoint routers (lifecycle, metrics_gates, dispatch)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from scripts.api.models import *
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
# Lifecycle models
|
|
17
|
+
"LifecyclePhase",
|
|
18
|
+
"PhaseStatus",
|
|
19
|
+
"PhaseActionRequest",
|
|
20
|
+
"PhaseActionResult",
|
|
21
|
+
"CommandMapping",
|
|
22
|
+
# Metrics & Gates models
|
|
23
|
+
"GateCheckRequest",
|
|
24
|
+
"GateResult",
|
|
25
|
+
"MetricsSnapshot",
|
|
26
|
+
"HealthCheck",
|
|
27
|
+
# Task Dispatch models (NEW in V3.6.0)
|
|
28
|
+
"TaskDispatchRequest",
|
|
29
|
+
"QuickDispatchRequest",
|
|
30
|
+
"DispatchResponse",
|
|
31
|
+
"WorkerResultItem",
|
|
32
|
+
"IntentMatchInfo",
|
|
33
|
+
"FiveAxisResult",
|
|
34
|
+
"AnchorResult",
|
|
35
|
+
"RoleInfo",
|
|
36
|
+
"RolesListResponse",
|
|
37
|
+
"DispatchHistoryResponse",
|
|
38
|
+
# Common models
|
|
39
|
+
"UserRole",
|
|
40
|
+
"APIError",
|
|
41
|
+
"APISuccess",
|
|
42
|
+
"PaginatedResponse",
|
|
43
|
+
]
|