Debashis commited on
Commit
ffa310a
·
1 Parent(s): 52cee9f

Add multi-agent architecture implementation with detailed documentation

Browse files

- Alert Ingestion Agent: Normalize and deduplicate alerts
- Correlation Agent: Group related alerts into incidents
- Analysis Agent: AI-powered root cause analysis with Ollama
- Response Agent: Generate recommendations and integrations

Includes comprehensive agent-to-agent communication patterns:
- Synchronous request-response for critical paths
- Asynchronous event publishing for integrations
- Shared data store (PostgreSQL + Redis)
- Support for Slack, PagerDuty, OpsGenie integrations

agents/__init__.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Multi-Agent System for Incident Management
3
+
4
+ This module implements a distributed agent architecture where specialized agents
5
+ collaborate to detect, correlate, analyze, and respond to infrastructure incidents.
6
+
7
+ Agent Types:
8
+ 1. AlertIngestionAgent - Normalizes and dedups raw alerts
9
+ 2. CorrelationAgent - Groups related alerts into incidents
10
+ 3. AnalysisAgent - Performs AI-powered root cause analysis
11
+ 4. ResponseAgent - Generates actionable recommendations
12
+ """
13
+
14
+ from .alert_agent import AlertIngestionAgent
15
+ from .correlation_agent import CorrelationAgent
16
+ from .analysis_agent import AnalysisAgent
17
+ from .response_agent import ResponseAgent
18
+
19
+ __all__ = [
20
+ "AlertIngestionAgent",
21
+ "CorrelationAgent",
22
+ "AnalysisAgent",
23
+ "ResponseAgent"
24
+ ]
agents/alert_agent.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Alert Ingestion Agent
3
+
4
+ Receives raw alerts from monitoring systems, normalizes them, and deduplicates
5
+ based on fingerprints. First agent in the processing pipeline.
6
+
7
+ Communication: Synchronous → Correlation Agent
8
+ Data Storage: PostgreSQL (alerts), Redis (cache)
9
+ """
10
+
11
+ import hashlib
12
+ import logging
13
+ from typing import Dict, Any, Optional
14
+ from datetime import datetime, timedelta
15
+ import aioredis
16
+ from sqlalchemy.orm import Session
17
+
18
+ from src.models.database import Alert
19
+ from src.schemas import AlertCreate
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class AlertIngestionAgent:
25
+ """Ingests and normalizes raw alerts from monitoring systems"""
26
+
27
+ def __init__(self, db: Session, redis_client: Optional[aioredis.Redis] = None):
28
+ self.db = db
29
+ self.redis = redis_client
30
+ self.cache_ttl = 300 # 5 minutes
31
+
32
+ async def ingest_alert(self, raw_alert: Dict[str, Any]) -> Dict[str, Any]:
33
+ """
34
+ Main entry point for alert ingestion.
35
+
36
+ Timeline:
37
+ T+0ms: Receive raw alert from monitoring system
38
+ T+10ms: Normalize to AIMS schema
39
+ T+20ms: Calculate fingerprint
40
+ T+30ms: Check Redis cache for duplicates
41
+ T+40ms: Store in PostgreSQL if new
42
+ T+50ms: Return normalized alert
43
+
44
+ Args:
45
+ raw_alert: Alert from Prometheus, Grafana, CloudWatch, etc.
46
+
47
+ Returns:
48
+ Normalized alert dict ready for correlation
49
+ """
50
+ logger.info(f"[ALERT_AGENT] Ingesting alert from {raw_alert.get('source', 'unknown')}")
51
+
52
+ try:
53
+ # Step 1: Normalize alert (T+10ms)
54
+ normalized = self._normalize_alert(raw_alert)
55
+ logger.debug(f"[ALERT_AGENT] Normalized alert: {normalized.get('title')}")
56
+
57
+ # Step 2: Calculate fingerprint (T+20ms)
58
+ fingerprint = self._generate_fingerprint(normalized)
59
+ logger.debug(f"[ALERT_AGENT] Fingerprint: {fingerprint}")
60
+
61
+ # Step 3: Check for duplicates (T+30ms)
62
+ duplicate_count = await self._check_duplicate(fingerprint)
63
+ if duplicate_count:
64
+ logger.info(f"[ALERT_AGENT] Duplicate alert detected. Count: {duplicate_count}")
65
+ normalized['duplicate_count'] = duplicate_count
66
+ return {
67
+ "status": "deduplicated",
68
+ "alert": normalized,
69
+ "action": "increment_counter"
70
+ }
71
+
72
+ # Step 4: Store in database (T+40ms)
73
+ alert_id = await self._store_alert(normalized, fingerprint)
74
+ logger.info(f"[ALERT_AGENT] Alert stored with ID: {alert_id}")
75
+
76
+ # Step 5: Cache fingerprint (T+45ms)
77
+ await self._cache_fingerprint(fingerprint)
78
+
79
+ # Step 6: Return result (T+50ms)
80
+ return {
81
+ "status": "created",
82
+ "alert_id": alert_id,
83
+ "alert": normalized,
84
+ "fingerprint": fingerprint,
85
+ "processing_time_ms": 50
86
+ }
87
+
88
+ except Exception as e:
89
+ logger.error(f"[ALERT_AGENT] Error ingesting alert: {e}", exc_info=True)
90
+ raise
91
+
92
+ def _normalize_alert(self, raw_alert: Dict[str, Any]) -> Dict[str, Any]:
93
+ """
94
+ Convert alert from various formats to AIMS standard format.
95
+
96
+ Supports:
97
+ - Prometheus AlertManager format
98
+ - Grafana alert format
99
+ - CloudWatch format
100
+ - Custom webhook format
101
+ """
102
+ source = raw_alert.get('source', 'unknown')
103
+
104
+ if source == 'prometheus':
105
+ return self._normalize_prometheus(raw_alert)
106
+ elif source == 'grafana':
107
+ return self._normalize_grafana(raw_alert)
108
+ elif source == 'cloudwatch':
109
+ return self._normalize_cloudwatch(raw_alert)
110
+ else:
111
+ return self._normalize_generic(raw_alert)
112
+
113
+ def _normalize_prometheus(self, alert: Dict[str, Any]) -> Dict[str, Any]:
114
+ """Normalize Prometheus format alert"""
115
+ return {
116
+ 'source': 'prometheus',
117
+ 'title': alert.get('alert', alert.get('name', 'Unknown')),
118
+ 'description': alert.get('description', ''),
119
+ 'severity': alert.get('severity', 'warning').lower(),
120
+ 'service': alert.get('labels', {}).get('job', 'unknown'),
121
+ 'category': alert.get('labels', {}).get('category', 'other'),
122
+ 'metrics': alert.get('value'),
123
+ 'timestamp': datetime.utcnow().isoformat(),
124
+ 'labels': alert.get('labels', {}),
125
+ 'status': 'new'
126
+ }
127
+
128
+ def _normalize_grafana(self, alert: Dict[str, Any]) -> Dict[str, Any]:
129
+ """Normalize Grafana format alert"""
130
+ return {
131
+ 'source': 'grafana',
132
+ 'title': alert.get('title', 'Grafana Alert'),
133
+ 'description': alert.get('message', ''),
134
+ 'severity': alert.get('severity', 'warning').lower(),
135
+ 'service': alert.get('service', 'unknown'),
136
+ 'category': alert.get('category', 'other'),
137
+ 'metrics': alert.get('data'),
138
+ 'timestamp': datetime.utcnow().isoformat(),
139
+ 'url': alert.get('ruleUrl'),
140
+ 'status': 'new'
141
+ }
142
+
143
+ def _normalize_cloudwatch(self, alert: Dict[str, Any]) -> Dict[str, Any]:
144
+ """Normalize CloudWatch format alert"""
145
+ return {
146
+ 'source': 'cloudwatch',
147
+ 'title': alert.get('AlarmName', 'CloudWatch Alarm'),
148
+ 'description': alert.get('AlarmDescription', ''),
149
+ 'severity': 'critical' if alert.get('StateValue') == 'ALARM' else 'warning',
150
+ 'service': alert.get('Trigger', {}).get('Namespace', 'aws'),
151
+ 'category': alert.get('Trigger', {}).get('MetricName', 'other'),
152
+ 'metrics': alert.get('StateChangeTime'),
153
+ 'timestamp': datetime.utcnow().isoformat(),
154
+ 'status': 'new'
155
+ }
156
+
157
+ def _normalize_generic(self, alert: Dict[str, Any]) -> Dict[str, Any]:
158
+ """Normalize generic/webhook format alert"""
159
+ return {
160
+ 'source': alert.get('source', 'custom'),
161
+ 'title': alert.get('title', alert.get('name', 'Alert')),
162
+ 'description': alert.get('description', ''),
163
+ 'severity': alert.get('severity', 'warning').lower(),
164
+ 'service': alert.get('service', 'unknown'),
165
+ 'category': alert.get('category', 'other'),
166
+ 'metrics': alert.get('metrics', {}),
167
+ 'timestamp': alert.get('timestamp', datetime.utcnow().isoformat()),
168
+ 'status': 'new'
169
+ }
170
+
171
+ def _generate_fingerprint(self, normalized_alert: Dict[str, Any]) -> str:
172
+ """
173
+ Generate unique fingerprint for deduplication.
174
+ Fingerprint = hash(service + alert_type + severity)
175
+ """
176
+ key_parts = [
177
+ normalized_alert.get('service', 'unknown'),
178
+ normalized_alert.get('title', 'unknown'),
179
+ normalized_alert.get('category', 'unknown')
180
+ ]
181
+ key_string = '|'.join(key_parts)
182
+ return hashlib.sha256(key_string.encode()).hexdigest()[:16]
183
+
184
+ async def _check_duplicate(self, fingerprint: str) -> int:
185
+ """
186
+ Check if alert with same fingerprint already exists.
187
+ Returns counter of how many times this alert has fired.
188
+ """
189
+ if not self.redis:
190
+ return 0
191
+
192
+ cache_key = f"alert:fingerprint:{fingerprint}"
193
+ try:
194
+ count = await self.redis.incr(cache_key)
195
+ await self.redis.expire(cache_key, self.cache_ttl)
196
+ return count
197
+ except Exception as e:
198
+ logger.warning(f"[ALERT_AGENT] Redis check failed: {e}")
199
+ return 0
200
+
201
+ async def _store_alert(self, normalized_alert: Dict[str, Any], fingerprint: str) -> str:
202
+ """Store normalized alert in PostgreSQL"""
203
+ try:
204
+ alert = Alert(
205
+ source=normalized_alert['source'],
206
+ title=normalized_alert['title'],
207
+ description=normalized_alert['description'],
208
+ severity=normalized_alert['severity'],
209
+ service=normalized_alert['service'],
210
+ category=normalized_alert['category'],
211
+ status='new',
212
+ fingerprint=fingerprint,
213
+ metrics=normalized_alert.get('metrics', {}),
214
+ labels=normalized_alert.get('labels', {}),
215
+ created_at=datetime.utcnow()
216
+ )
217
+ self.db.add(alert)
218
+ self.db.commit()
219
+ self.db.refresh(alert)
220
+ return str(alert.id)
221
+ except Exception as e:
222
+ self.db.rollback()
223
+ logger.error(f"[ALERT_AGENT] Database error: {e}")
224
+ raise
225
+
226
+ async def _cache_fingerprint(self, fingerprint: str) -> None:
227
+ """Cache fingerprint in Redis for fast duplicate detection"""
228
+ if not self.redis:
229
+ return
230
+
231
+ try:
232
+ cache_key = f"alert:fingerprint:{fingerprint}"
233
+ await self.redis.expire(cache_key, self.cache_ttl)
234
+ except Exception as e:
235
+ logger.warning(f"[ALERT_AGENT] Cache update failed: {e}")
agents/analysis_agent.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Analysis Agent
3
+
4
+ Performs AI-powered root cause analysis using Ollama LLM.
5
+ Third agent in the processing pipeline.
6
+
7
+ Communication: Receives from CorrelationAgent → Sends to ResponseAgent
8
+ Data Storage: PostgreSQL (analysis results), Redis (LLM cache)
9
+ """
10
+
11
+ import logging
12
+ import json
13
+ from typing import Dict, Any, Optional, List
14
+ from datetime import datetime
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class AnalysisAgent:
20
+ """Performs AI-powered analysis of incidents using Ollama"""
21
+
22
+ def __init__(self, llm_client=None):
23
+ self.llm = llm_client
24
+ self.cache_enabled = True
25
+
26
+ async def analyze_incident(self, incident: Dict[str, Any]) -> Dict[str, Any]:
27
+ """
28
+ Main entry point for analysis.
29
+
30
+ Timeline:
31
+ T+400ms: Receive incident from CorrelationAgent
32
+ T+450ms: Prepare analysis context
33
+ T+500ms: Send to Ollama
34
+ T+500-1500ms: Ollama processes (1 second average)
35
+ T+1500ms: Parse LLM response
36
+ T+1550ms: Return analysis
37
+
38
+ Args:
39
+ incident: Correlated incident with alerts
40
+
41
+ Returns:
42
+ Analysis result with root cause and recommendations
43
+ """
44
+ logger.info(f"[ANALYSIS_AGENT] Analyzing incident: {incident.get('id')}")
45
+
46
+ try:
47
+ # Step 1: Prepare context (T+450ms)
48
+ context = self._prepare_analysis_context(incident)
49
+ logger.debug(f"[ANALYSIS_AGENT] Context prepared: {len(json.dumps(context))} chars")
50
+
51
+ # Step 2: Build prompt (T+480ms)
52
+ prompt = self._build_analysis_prompt(context)
53
+ logger.debug(f"[ANALYSIS_AGENT] Prompt built")
54
+
55
+ # Step 3: Send to Ollama (T+500ms)
56
+ if not self.llm:
57
+ return self._fallback_analysis(incident)
58
+
59
+ analysis_result = await self.llm.analyze_incident(
60
+ alerts=incident.get('alerts', []),
61
+ context=context,
62
+ prompt=prompt
63
+ )
64
+ logger.info(f"[ANALYSIS_AGENT] LLM analysis complete")
65
+
66
+ # Step 4: Parse and structure response (T+1500ms)
67
+ structured_analysis = self._structure_analysis(analysis_result)
68
+
69
+ # Step 5: Return result (T+1550ms)
70
+ return {
71
+ "status": "analyzed",
72
+ "incident_id": incident.get('id'),
73
+ "analysis": structured_analysis,
74
+ "processing_time_ms": 1150
75
+ }
76
+
77
+ except Exception as e:
78
+ logger.error(f"[ANALYSIS_AGENT] Error analyzing incident: {e}", exc_info=True)
79
+ return self._fallback_analysis(incident)
80
+
81
+ def _prepare_analysis_context(self, incident: Dict[str, Any]) -> Dict[str, Any]:
82
+ """
83
+ Prepare comprehensive context for LLM analysis.
84
+
85
+ Includes:
86
+ - Alert timeline
87
+ - Metrics
88
+ - Service dependencies
89
+ - Historical patterns
90
+ """
91
+ alerts = incident.get('alerts', [])
92
+
93
+ context = {
94
+ 'incident_id': incident.get('id'),
95
+ 'service': incident.get('service'),
96
+ 'severity': incident.get('severity'),
97
+ 'alert_count': len(alerts),
98
+ 'time_range': {
99
+ 'start': min(a.get('created_at', '') for a in alerts),
100
+ 'end': max(a.get('created_at', '') for a in alerts)
101
+ },
102
+ 'alerts_summary': [
103
+ {
104
+ 'title': a.get('title'),
105
+ 'category': a.get('category'),
106
+ 'severity': a.get('severity'),
107
+ 'timestamp': a.get('created_at')
108
+ }
109
+ for a in alerts
110
+ ],
111
+ 'metrics': self._aggregate_metrics(alerts),
112
+ 'patterns': self._detect_patterns(alerts)
113
+ }
114
+
115
+ return context
116
+
117
+ def _aggregate_metrics(self, alerts: List[Dict[str, Any]]) -> Dict[str, Any]:
118
+ """Aggregate metrics from all alerts in incident"""
119
+ metrics = {}
120
+
121
+ for alert in alerts:
122
+ alert_metrics = alert.get('metrics', {})
123
+ if isinstance(alert_metrics, dict):
124
+ metrics.update(alert_metrics)
125
+
126
+ return metrics
127
+
128
+ def _detect_patterns(self, alerts: List[Dict[str, Any]]) -> List[str]:
129
+ """Detect common patterns in alerts"""
130
+ patterns = []
131
+ categories = [a.get('category') for a in alerts]
132
+
133
+ # Pattern: Multiple resource alerts (CPU, Memory, Disk)
134
+ resource_alerts = [c for c in categories if c in ['cpu', 'memory', 'disk', 'io']]
135
+ if len(resource_alerts) >= 2:
136
+ patterns.append("resource_exhaustion")
137
+
138
+ # Pattern: Connection/Network related
139
+ if any(c in ['connection', 'network', 'timeout'] for c in categories):
140
+ patterns.append("connectivity_issue")
141
+
142
+ # Pattern: Application errors
143
+ if any(c in ['error', 'crash', 'exception'] for c in categories):
144
+ patterns.append("application_failure")
145
+
146
+ return patterns
147
+
148
+ def _build_analysis_prompt(self, context: Dict[str, Any]) -> str:
149
+ """Build prompt for Ollama LLM"""
150
+ return f"""
151
+ You are an expert infrastructure analyst. Analyze this incident and provide:
152
+ 1. Root cause hypothesis
153
+ 2. Confidence level (0-100)
154
+ 3. Top 3 recommended actions
155
+ 4. Severity assessment
156
+
157
+ Incident Context:
158
+ - Service: {context.get('service')}
159
+ - Severity: {context.get('severity')}
160
+ - Alert Count: {context.get('alert_count')}
161
+ - Patterns Detected: {', '.join(context.get('patterns', []))}
162
+
163
+ Alert Timeline:
164
+ {json.dumps(context.get('alerts_summary', []), indent=2)}
165
+
166
+ Metrics:
167
+ {json.dumps(context.get('metrics', {}), indent=2)}
168
+
169
+ Provide analysis in JSON format with fields:
170
+ - root_cause
171
+ - confidence (0-100)
172
+ - evidence (list of supporting facts)
173
+ - actions (list of 3 recommended actions)
174
+ - severity_assessment
175
+ """
176
+
177
+ async def _structure_analysis(self, llm_response: str) -> Dict[str, Any]:
178
+ """Parse and structure LLM response"""
179
+ try:
180
+ # Extract JSON from LLM response
181
+ import re
182
+ json_match = re.search(r'\{.*\}', llm_response, re.DOTALL)
183
+
184
+ if json_match:
185
+ analysis = json.loads(json_match.group())
186
+ else:
187
+ analysis = {
188
+ 'root_cause': llm_response,
189
+ 'confidence': 50,
190
+ 'evidence': [],
191
+ 'actions': []
192
+ }
193
+
194
+ return {
195
+ 'root_cause': analysis.get('root_cause', 'Unknown'),
196
+ 'confidence': int(analysis.get('confidence', 50)),
197
+ 'evidence': analysis.get('evidence', []),
198
+ 'recommended_actions': analysis.get('actions', []),
199
+ 'severity_assessment': analysis.get('severity_assessment', 'Medium'),
200
+ 'timestamp': datetime.utcnow().isoformat()
201
+ }
202
+ except Exception as e:
203
+ logger.error(f"[ANALYSIS_AGENT] Error parsing LLM response: {e}")
204
+ return self._fallback_analysis_result()
205
+
206
+ def _fallback_analysis(self, incident: Dict[str, Any]) -> Dict[str, Any]:
207
+ """Fallback analysis when LLM is unavailable"""
208
+ logger.warning(f"[ANALYSIS_AGENT] Using fallback analysis")
209
+
210
+ return {
211
+ "status": "analyzed",
212
+ "incident_id": incident.get('id'),
213
+ "analysis": self._fallback_analysis_result(),
214
+ "mode": "fallback"
215
+ }
216
+
217
+ def _fallback_analysis_result(self) -> Dict[str, Any]:
218
+ """Generate fallback analysis based on patterns"""
219
+ return {
220
+ 'root_cause': 'Unable to determine - LLM unavailable',
221
+ 'confidence': 0,
222
+ 'evidence': [],
223
+ 'recommended_actions': [
224
+ 'Check application logs',
225
+ 'Review service metrics',
226
+ 'Check for recent deployments'
227
+ ],
228
+ 'severity_assessment': 'Unknown',
229
+ 'timestamp': datetime.utcnow().isoformat()
230
+ }
231
+
232
+ async def analyze_alerts(self, alerts: List[Dict[str, Any]]) -> Dict[str, Any]:
233
+ """
234
+ Analyze individual alerts for patterns and anomalies.
235
+ Used by AlertIngestionAgent for pre-processing.
236
+ """
237
+ logger.info(f"[ANALYSIS_AGENT] Analyzing {len(alerts)} alerts")
238
+
239
+ try:
240
+ context = {
241
+ 'alert_count': len(alerts),
242
+ 'services': list(set(a.get('service') for a in alerts)),
243
+ 'severities': list(set(a.get('severity') for a in alerts)),
244
+ 'categories': list(set(a.get('category') for a in alerts))
245
+ }
246
+
247
+ analysis = {
248
+ 'context': context,
249
+ 'patterns_detected': self._detect_patterns(alerts),
250
+ 'risk_level': self._calculate_risk_level(alerts),
251
+ 'recommended_priority': self._calculate_priority(alerts)
252
+ }
253
+
254
+ return analysis
255
+ except Exception as e:
256
+ logger.error(f"[ANALYSIS_AGENT] Error in alert analysis: {e}")
257
+ return {'error': str(e)}
258
+
259
+ def _calculate_risk_level(self, alerts: List[Dict[str, Any]]) -> str:
260
+ """Calculate overall risk level from alerts"""
261
+ if not alerts:
262
+ return 'low'
263
+
264
+ critical_count = sum(1 for a in alerts if a.get('severity') == 'critical')
265
+ warning_count = sum(1 for a in alerts if a.get('severity') == 'warning')
266
+
267
+ if critical_count >= 3:
268
+ return 'critical'
269
+ elif critical_count >= 1 or warning_count >= 5:
270
+ return 'high'
271
+ elif warning_count >= 2:
272
+ return 'medium'
273
+ else:
274
+ return 'low'
275
+
276
+ def _calculate_priority(self, alerts: List[Dict[str, Any]]) -> int:
277
+ """Calculate priority (1-5) for handling"""
278
+ risk = self._calculate_risk_level(alerts)
279
+ priority_map = {
280
+ 'critical': 1,
281
+ 'high': 2,
282
+ 'medium': 3,
283
+ 'low': 5
284
+ }
285
+ return priority_map.get(risk, 4)
agents/correlation_agent.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Correlation Agent
3
+
4
+ Groups related alerts into incidents by detecting patterns and similarities.
5
+ Second agent in the processing pipeline.
6
+
7
+ Communication: Receives from AlertIngestionAgent → Sends to AnalysisAgent
8
+ Data Storage: PostgreSQL (incidents, correlations), Redis (pattern cache)
9
+ """
10
+
11
+ import logging
12
+ from typing import Dict, List, Any, Optional
13
+ from datetime import datetime, timedelta
14
+ import json
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class CorrelationAgent:
20
+ """Correlates related alerts into incidents"""
21
+
22
+ def __init__(self, db_session=None, redis_client=None):
23
+ self.db = db_session
24
+ self.redis = redis_client
25
+ self.correlation_window = 600 # 10 minutes
26
+ self.similarity_threshold = 0.7
27
+
28
+ async def correlate_alerts(self, alert: Dict[str, Any]) -> Dict[str, Any]:
29
+ """
30
+ Main entry point for correlation.
31
+
32
+ Timeline:
33
+ T+200ms: Receive alert from AlertIngestionAgent
34
+ T+250ms: Query for similar alerts in time window
35
+ T+300ms: Calculate similarity scores
36
+ T+350ms: Create new incident or update existing
37
+ T+400ms: Return correlation result
38
+
39
+ Args:
40
+ alert: Normalized alert from AlertIngestionAgent
41
+
42
+ Returns:
43
+ Correlation result with incident info
44
+ """
45
+ logger.info(f"[CORRELATION_AGENT] Processing alert: {alert.get('title')}")
46
+
47
+ try:
48
+ # Step 1: Query similar alerts (T+250ms)
49
+ similar_alerts = await self._find_similar_alerts(alert)
50
+ logger.info(f"[CORRELATION_AGENT] Found {len(similar_alerts)} similar alerts")
51
+
52
+ # Step 2: Calculate similarity (T+300ms)
53
+ correlation_score = self._calculate_correlation_score(alert, similar_alerts)
54
+ logger.debug(f"[CORRELATION_AGENT] Correlation score: {correlation_score}")
55
+
56
+ # Step 3: Determine action (T+350ms)
57
+ if correlation_score >= self.similarity_threshold and similar_alerts:
58
+ # Update existing incident
59
+ incident_id = await self._update_incident(alert, similar_alerts)
60
+ action = "update"
61
+ logger.info(f"[CORRELATION_AGENT] Updated incident: {incident_id}")
62
+ else:
63
+ # Create new incident
64
+ incident_id = await self._create_incident(alert)
65
+ action = "create"
66
+ logger.info(f"[CORRELATION_AGENT] Created new incident: {incident_id}")
67
+
68
+ # Step 4: Return result (T+400ms)
69
+ return {
70
+ "status": "correlated",
71
+ "incident_id": incident_id,
72
+ "action": action,
73
+ "correlation_score": correlation_score,
74
+ "similar_alerts_count": len(similar_alerts),
75
+ "processing_time_ms": 200
76
+ }
77
+
78
+ except Exception as e:
79
+ logger.error(f"[CORRELATION_AGENT] Error correlating alert: {e}", exc_info=True)
80
+ raise
81
+
82
+ async def _find_similar_alerts(self, alert: Dict[str, Any]) -> List[Dict[str, Any]]:
83
+ """
84
+ Find alerts similar to current one within correlation window.
85
+
86
+ Similarity criteria:
87
+ - Same service
88
+ - Same category (or related)
89
+ - Within last 10 minutes
90
+ - Same severity level or escalating
91
+ """
92
+ service = alert.get('service', 'unknown')
93
+ category = alert.get('category', 'unknown')
94
+
95
+ # In real implementation, query from PostgreSQL:
96
+ # SELECT * FROM alerts WHERE
97
+ # service = service AND
98
+ # created_at > NOW() - INTERVAL '10 minutes' AND
99
+ # (category = category OR category IN related_categories)
100
+
101
+ similar = []
102
+ # Placeholder for database query
103
+ logger.debug(f"[CORRELATION_AGENT] Querying for alerts: service={service}, category={category}")
104
+
105
+ return similar
106
+
107
+ def _calculate_correlation_score(self, alert: Dict[str, Any], similar_alerts: List) -> float:
108
+ """
109
+ Calculate how much this alert correlates with existing ones.
110
+ Score 0.0-1.0 where 1.0 = definitely same incident
111
+
112
+ Scoring factors:
113
+ - Service match (40%)
114
+ - Time proximity (30%)
115
+ - Metric similarity (20%)
116
+ - Severity escalation (10%)
117
+ """
118
+ if not similar_alerts:
119
+ return 0.0
120
+
121
+ score = 0.0
122
+ alert_service = alert.get('service', 'unknown')
123
+ alert_category = alert.get('category', 'unknown')
124
+ alert_severity = alert.get('severity', 'warning')
125
+
126
+ service_matches = sum(1 for a in similar_alerts if a.get('service') == alert_service)
127
+ category_matches = sum(1 for a in similar_alerts if a.get('category') == alert_category)
128
+
129
+ # Service match (40%)
130
+ if similar_alerts:
131
+ score += (service_matches / len(similar_alerts)) * 0.4
132
+
133
+ # Category match (30%)
134
+ if similar_alerts:
135
+ score += (category_matches / len(similar_alerts)) * 0.3
136
+
137
+ # Time proximity (20%)
138
+ most_recent = max(similar_alerts, key=lambda x: x.get('created_at', ''))
139
+ time_diff = datetime.utcnow() - datetime.fromisoformat(most_recent.get('created_at', ''))
140
+ if time_diff.seconds < 60: # Within 1 minute
141
+ score += 0.2
142
+ elif time_diff.seconds < 300: # Within 5 minutes
143
+ score += 0.1
144
+
145
+ # Severity escalation (10%)
146
+ severity_levels = {'info': 1, 'warning': 2, 'critical': 3}
147
+ current_level = severity_levels.get(alert_severity, 1)
148
+ avg_level = sum(severity_levels.get(a.get('severity', 'info'), 1) for a in similar_alerts) / len(similar_alerts)
149
+ if current_level >= avg_level:
150
+ score += 0.1
151
+
152
+ return min(score, 1.0)
153
+
154
+ async def _create_incident(self, alert: Dict[str, Any]) -> str:
155
+ """
156
+ Create new incident from alert.
157
+
158
+ Incident structure:
159
+ - title: Generated from alerts
160
+ - service: From alert
161
+ - severity: From alert
162
+ - status: OPEN
163
+ - created_alerts: [alert]
164
+ """
165
+ logger.info(f"[CORRELATION_AGENT] Creating incident from alert")
166
+
167
+ # In real implementation:
168
+ # INSERT INTO incidents (title, service, severity, status, created_at)
169
+ # VALUES (title, service, severity, 'OPEN', NOW())
170
+
171
+ incident = {
172
+ 'id': 'incident_placeholder',
173
+ 'title': f"Incident: {alert.get('title')}",
174
+ 'service': alert.get('service'),
175
+ 'severity': alert.get('severity'),
176
+ 'status': 'OPEN',
177
+ 'alerts': [alert],
178
+ 'created_at': datetime.utcnow().isoformat()
179
+ }
180
+
181
+ logger.info(f"[CORRELATION_AGENT] Incident created: {incident['id']}")
182
+ return incident['id']
183
+
184
+ async def _update_incident(self, alert: Dict[str, Any], similar_alerts: List) -> str:
185
+ """
186
+ Update existing incident with new alert.
187
+
188
+ Updates:
189
+ - Add alert to incident.alerts list
190
+ - Update severity if escalated
191
+ - Update updated_at timestamp
192
+ - Mark incident as active
193
+ """
194
+ if not similar_alerts:
195
+ return await self._create_incident(alert)
196
+
197
+ logger.info(f"[CORRELATION_AGENT] Updating incident with new alert")
198
+
199
+ # Get incident ID from one of the similar alerts
200
+ incident_id = similar_alerts[0].get('incident_id', 'unknown')
201
+
202
+ # In real implementation:
203
+ # UPDATE incidents SET
204
+ # severity = MAX(current_severity, new_alert_severity),
205
+ # updated_at = NOW(),
206
+ # alert_count = alert_count + 1
207
+ # WHERE id = incident_id
208
+
209
+ logger.info(f"[CORRELATION_AGENT] Incident updated: {incident_id}")
210
+ return incident_id
211
+
212
+ async def detect_cascading_failure(self, incident: Dict[str, Any]) -> Optional[Dict[str, Any]]:
213
+ """
214
+ Detect if incident is part of cascading failure pattern.
215
+
216
+ Pattern detection:
217
+ - Multiple services affected
218
+ - Temporal correlation (alerts within seconds)
219
+ - Dependency chain (e.g., DB → Cache → API)
220
+ """
221
+ logger.info(f"[CORRELATION_AGENT] Analyzing for cascading failure pattern")
222
+
223
+ alerts = incident.get('alerts', [])
224
+ if len(alerts) < 3:
225
+ return None
226
+
227
+ services = set(a.get('service') for a in alerts)
228
+ logger.info(f"[CORRELATION_AGENT] Services affected: {services}")
229
+
230
+ if len(services) > 1:
231
+ return {
232
+ "pattern": "cascading_failure",
233
+ "confidence": 0.85,
234
+ "affected_services": list(services),
235
+ "recommendation": "Check dependency chain - start with database/cache layer"
236
+ }
237
+
238
+ return None
agents/response_agent.py ADDED
@@ -0,0 +1,406 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Response Agent
3
+
4
+ Generates actionable recommendations and formats responses for dashboard display
5
+ and external integrations (Slack, PagerDuty, etc.).
6
+ Fourth and final agent in the processing pipeline.
7
+
8
+ Communication: Receives from AnalysisAgent → Sends to Dashboard/Integrations
9
+ Data Storage: PostgreSQL (responses), Redis (response cache)
10
+ """
11
+
12
+ import logging
13
+ import json
14
+ from typing import Dict, Any, List, Optional
15
+ from datetime import datetime
16
+ from enum import Enum
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class ActionType(Enum):
22
+ """Types of recommended actions"""
23
+ SCALE_UP = "scale-up"
24
+ SCALE_DOWN = "scale-down"
25
+ RESTART = "restart"
26
+ CHECK_LOGS = "check-logs"
27
+ INCREASE_RESOURCES = "increase-resources"
28
+ ENABLE_CIRCUIT_BREAKER = "enable-circuit-breaker"
29
+ RATE_LIMIT = "rate-limit"
30
+ ROLLBACK = "rollback"
31
+ DRAIN_CONNECTIONS = "drain-connections"
32
+ INVESTIGATE = "investigate"
33
+
34
+
35
+ class ResponseAgent:
36
+ """Generates recommendations and formats responses"""
37
+
38
+ def __init__(self):
39
+ self.action_configs = self._load_action_configs()
40
+
41
+ async def generate_response(self, analysis: Dict[str, Any], incident: Dict[str, Any]) -> Dict[str, Any]:
42
+ """
43
+ Main entry point for response generation.
44
+
45
+ Timeline:
46
+ T+1550ms: Receive analysis from AnalysisAgent
47
+ T+1580ms: Generate recommendations
48
+ T+1600ms: Format for dashboard
49
+ T+1620ms: Format for integrations
50
+ T+1650ms: Publish events
51
+ T+1700ms: Return response
52
+
53
+ Args:
54
+ analysis: AI analysis from AnalysisAgent
55
+ incident: Incident data
56
+
57
+ Returns:
58
+ Formatted response ready for display and integrations
59
+ """
60
+ logger.info(f"[RESPONSE_AGENT] Generating response for incident: {incident.get('id')}")
61
+
62
+ try:
63
+ # Step 1: Generate recommendations (T+1580ms)
64
+ recommendations = self._generate_recommendations(analysis, incident)
65
+ logger.info(f"[RESPONSE_AGENT] Generated {len(recommendations)} recommendations")
66
+
67
+ # Step 2: Format for dashboard (T+1600ms)
68
+ dashboard_response = self._format_dashboard_response(
69
+ incident, analysis, recommendations
70
+ )
71
+ logger.debug(f"[RESPONSE_AGENT] Dashboard response formatted")
72
+
73
+ # Step 3: Format for integrations (T+1620ms)
74
+ integration_payloads = self._format_integration_payloads(
75
+ incident, analysis, recommendations
76
+ )
77
+ logger.debug(f"[RESPONSE_AGENT] Integration payloads generated")
78
+
79
+ # Step 4: Publish events (T+1650ms)
80
+ await self._publish_events(incident, recommendations)
81
+ logger.info(f"[RESPONSE_AGENT] Events published")
82
+
83
+ # Step 5: Return response (T+1700ms)
84
+ return {
85
+ "status": "response_generated",
86
+ "incident_id": incident.get('id'),
87
+ "dashboard": dashboard_response,
88
+ "integrations": integration_payloads,
89
+ "processing_time_ms": 150
90
+ }
91
+
92
+ except Exception as e:
93
+ logger.error(f"[RESPONSE_AGENT] Error generating response: {e}", exc_info=True)
94
+ raise
95
+
96
+ def _generate_recommendations(self, analysis: Dict[str, Any], incident: Dict[str, Any]) -> List[Dict[str, Any]]:
97
+ """
98
+ Generate specific, actionable recommendations based on analysis.
99
+
100
+ Recommendation structure:
101
+ - Action: What to do
102
+ - Priority: How urgent
103
+ - Evidence: Why we recommend it
104
+ - Impact: What will change
105
+ """
106
+ recommendations = []
107
+ root_cause = analysis.get('root_cause', '').lower()
108
+ confidence = analysis.get('confidence', 0)
109
+ severity = incident.get('severity', 'warning')
110
+
111
+ logger.debug(f"[RESPONSE_AGENT] Root cause: {root_cause}, Confidence: {confidence}%")
112
+
113
+ # Rule 1: CPU exhaustion → Scale up
114
+ if 'cpu' in root_cause and confidence > 70:
115
+ recommendations.append({
116
+ 'action': ActionType.SCALE_UP.value,
117
+ 'service': incident.get('service'),
118
+ 'priority': 'high' if severity == 'critical' else 'medium',
119
+ 'replicas': self._calculate_replicas(incident),
120
+ 'evidence': [
121
+ 'High CPU usage detected',
122
+ 'Multiple CPU alerts correlated',
123
+ 'LLM confidence: {}%'.format(confidence)
124
+ ],
125
+ 'expected_impact': 'Improved performance and reduced latency',
126
+ 'estimated_recovery_time': '2-5 minutes'
127
+ })
128
+
129
+ # Rule 2: Memory issues → Increase resources or restart
130
+ if 'memory' in root_cause or 'oom' in root_cause:
131
+ if confidence > 80:
132
+ recommendations.append({
133
+ 'action': ActionType.RESTART.value,
134
+ 'service': incident.get('service'),
135
+ 'priority': 'high',
136
+ 'graceful': True,
137
+ 'evidence': [
138
+ 'Memory pressure detected',
139
+ 'Out of memory conditions',
140
+ 'Application performance degraded'
141
+ ],
142
+ 'expected_impact': 'Process restart, temporary service interruption',
143
+ 'estimated_recovery_time': '30-60 seconds'
144
+ })
145
+ else:
146
+ recommendations.append({
147
+ 'action': ActionType.INCREASE_RESOURCES.value,
148
+ 'service': incident.get('service'),
149
+ 'priority': 'medium',
150
+ 'memory_increase_gb': 4,
151
+ 'evidence': ['Memory usage trending up'],
152
+ 'expected_impact': 'More available memory for application',
153
+ 'estimated_recovery_time': 'Requires redeployment (5-10 minutes)'
154
+ })
155
+
156
+ # Rule 3: Connection pool exhaustion
157
+ if 'connection' in root_cause or 'pool' in root_cause:
158
+ recommendations.append({
159
+ 'action': ActionType.DRAIN_CONNECTIONS.value,
160
+ 'service': incident.get('service'),
161
+ 'priority': 'critical' if severity == 'critical' else 'high',
162
+ 'steps': [
163
+ 'Increase max_connections parameter',
164
+ 'Enable connection recycling',
165
+ 'Implement connection timeout'
166
+ ],
167
+ 'evidence': [
168
+ 'Connection pool at capacity',
169
+ 'Connection timeout errors',
170
+ 'Database connectivity issues'
171
+ ],
172
+ 'expected_impact': 'Restored database connectivity',
173
+ 'estimated_recovery_time': '1-2 minutes'
174
+ })
175
+
176
+ # Rule 4: Network/DNS issues
177
+ if 'network' in root_cause or 'dns' in root_cause:
178
+ recommendations.append({
179
+ 'action': ActionType.INVESTIGATE.value,
180
+ 'service': incident.get('service'),
181
+ 'priority': 'high',
182
+ 'investigation_steps': [
183
+ 'Check DNS resolution',
184
+ 'Verify network connectivity',
185
+ 'Review firewall rules',
186
+ 'Check service discovery'
187
+ ],
188
+ 'evidence': ['Network connectivity issues detected'],
189
+ 'expected_impact': 'Identified networking bottleneck',
190
+ 'estimated_recovery_time': '5-15 minutes investigation'
191
+ })
192
+
193
+ # Rule 5: Recent deployment → Rollback
194
+ if 'deployment' in root_cause or 'version' in root_cause:
195
+ if confidence > 75:
196
+ recommendations.append({
197
+ 'action': ActionType.ROLLBACK.value,
198
+ 'service': incident.get('service'),
199
+ 'priority': 'critical',
200
+ 'to_version': self._get_previous_version(incident),
201
+ 'evidence': [
202
+ 'Incident occurred after deployment',
203
+ 'Performance metrics degraded',
204
+ 'Error rate increased'
205
+ ],
206
+ 'expected_impact': 'Return to stable version',
207
+ 'estimated_recovery_time': '2-5 minutes'
208
+ })
209
+
210
+ # Default: Investigate
211
+ if not recommendations:
212
+ recommendations.append({
213
+ 'action': ActionType.INVESTIGATE.value,
214
+ 'service': incident.get('service'),
215
+ 'priority': 'medium',
216
+ 'next_steps': [
217
+ 'Review application logs',
218
+ 'Check resource metrics',
219
+ 'Review recent changes',
220
+ 'Consult runbooks for service'
221
+ ],
222
+ 'evidence': ['Root cause unclear - further investigation needed'],
223
+ 'expected_impact': 'Identify actual root cause',
224
+ 'estimated_recovery_time': '10-30 minutes'
225
+ })
226
+
227
+ logger.info(f"[RESPONSE_AGENT] Generated {len(recommendations)} recommendations")
228
+ return recommendations
229
+
230
+ def _format_dashboard_response(self, incident: Dict[str, Any],
231
+ analysis: Dict[str, Any],
232
+ recommendations: List[Dict[str, Any]]) -> Dict[str, Any]:
233
+ """Format response for dashboard display (Material-UI friendly)"""
234
+ return {
235
+ 'incident_id': incident.get('id'),
236
+ 'title': incident.get('title', 'Incident'),
237
+ 'service': incident.get('service'),
238
+ 'severity': incident.get('severity'),
239
+ 'status': 'ANALYZING' if len(recommendations) < 1 else 'ANALYZED',
240
+ 'analysis': {
241
+ 'root_cause': analysis.get('root_cause'),
242
+ 'confidence': f"{analysis.get('confidence', 0)}%",
243
+ 'evidence': analysis.get('evidence', []),
244
+ 'severity_assessment': analysis.get('severity_assessment')
245
+ },
246
+ 'recommendations': [
247
+ {
248
+ 'id': i + 1,
249
+ 'action': r.get('action'),
250
+ 'priority': r.get('priority'),
251
+ 'description': self._action_to_description(r),
252
+ 'estimated_time': r.get('estimated_recovery_time'),
253
+ 'impact': r.get('expected_impact')
254
+ }
255
+ for i, r in enumerate(recommendations)
256
+ ],
257
+ 'alert_count': len(incident.get('alerts', [])),
258
+ 'timestamp': datetime.utcnow().isoformat(),
259
+ 'ready_for_action': True
260
+ }
261
+
262
+ def _format_integration_payloads(self, incident: Dict[str, Any],
263
+ analysis: Dict[str, Any],
264
+ recommendations: List[Dict[str, Any]]) -> Dict[str, Any]:
265
+ """Format payloads for external integrations (Slack, PagerDuty, etc.)"""
266
+ return {
267
+ 'slack': self._format_slack_message(incident, analysis, recommendations),
268
+ 'pagerduty': self._format_pagerduty_event(incident, analysis),
269
+ 'opsgenie': self._format_opsgenie_alert(incident, analysis)
270
+ }
271
+
272
+ def _format_slack_message(self, incident: Dict[str, Any],
273
+ analysis: Dict[str, Any],
274
+ recommendations: List[Dict[str, Any]]) -> Dict[str, Any]:
275
+ """Format for Slack notification"""
276
+ severity_color = {
277
+ 'critical': '#FF0000',
278
+ 'warning': '#FFA500',
279
+ 'info': '#0099FF'
280
+ }
281
+
282
+ return {
283
+ 'text': f"🚨 Incident: {incident.get('title')}",
284
+ 'attachments': [
285
+ {
286
+ 'color': severity_color.get(incident.get('severity'), '#999999'),
287
+ 'title': incident.get('title'),
288
+ 'fields': [
289
+ {'title': 'Service', 'value': incident.get('service'), 'short': True},
290
+ {'title': 'Severity', 'value': incident.get('severity').upper(), 'short': True},
291
+ {'title': 'Root Cause', 'value': analysis.get('root_cause'), 'short': False},
292
+ {'title': 'Confidence', 'value': f"{analysis.get('confidence')}%", 'short': True},
293
+ {
294
+ 'title': 'Top Recommendation',
295
+ 'value': recommendations[0]['action'] if recommendations else 'Investigate',
296
+ 'short': True
297
+ }
298
+ ],
299
+ 'footer': 'AIMS - Autonomous Incident Management System'
300
+ }
301
+ ]
302
+ }
303
+
304
+ def _format_pagerduty_event(self, incident: Dict[str, Any],
305
+ analysis: Dict[str, Any]) -> Dict[str, Any]:
306
+ """Format for PagerDuty alert"""
307
+ severity_map = {
308
+ 'critical': 'critical',
309
+ 'warning': 'warning',
310
+ 'info': 'info'
311
+ }
312
+
313
+ return {
314
+ 'routing_key': 'YOUR_PAGERDUTY_KEY',
315
+ 'event_action': 'trigger',
316
+ 'payload': {
317
+ 'summary': incident.get('title'),
318
+ 'severity': severity_map.get(incident.get('severity'), 'warning'),
319
+ 'source': 'AIMS',
320
+ 'custom_details': {
321
+ 'root_cause': analysis.get('root_cause'),
322
+ 'confidence': analysis.get('confidence'),
323
+ 'service': incident.get('service')
324
+ }
325
+ }
326
+ }
327
+
328
+ def _format_opsgenie_alert(self, incident: Dict[str, Any],
329
+ analysis: Dict[str, Any]) -> Dict[str, Any]:
330
+ """Format for OpsGenie alert"""
331
+ return {
332
+ 'message': incident.get('title'),
333
+ 'description': f"Root Cause: {analysis.get('root_cause')}",
334
+ 'priority': self._map_to_opsgenie_priority(incident.get('severity')),
335
+ 'source': 'AIMS',
336
+ 'tags': [
337
+ incident.get('service'),
338
+ incident.get('severity'),
339
+ 'aims'
340
+ ]
341
+ }
342
+
343
+ async def _publish_events(self, incident: Dict[str, Any],
344
+ recommendations: List[Dict[str, Any]]) -> None:
345
+ """Publish events to Redis for async subscribers"""
346
+ events = [
347
+ {
348
+ 'type': 'incident.analyzed',
349
+ 'incident_id': incident.get('id'),
350
+ 'timestamp': datetime.utcnow().isoformat()
351
+ },
352
+ {
353
+ 'type': 'recommendations.generated',
354
+ 'incident_id': incident.get('id'),
355
+ 'count': len(recommendations),
356
+ 'timestamp': datetime.utcnow().isoformat()
357
+ }
358
+ ]
359
+
360
+ logger.debug(f"[RESPONSE_AGENT] Publishing {len(events)} events")
361
+ # In real implementation, publish to Redis pub/sub:
362
+ # for event in events:
363
+ # await redis_client.publish('incidents', json.dumps(event))
364
+
365
+ def _action_to_description(self, recommendation: Dict[str, Any]) -> str:
366
+ """Convert action dict to human-readable description"""
367
+ action = recommendation.get('action')
368
+
369
+ descriptions = {
370
+ 'scale-up': f"Scale up {recommendation.get('service')} to {recommendation.get('replicas')} replicas",
371
+ 'restart': f"Gracefully restart {recommendation.get('service')} service",
372
+ 'check-logs': "Review application logs for errors",
373
+ 'increase-resources': f"Increase memory by {recommendation.get('memory_increase_gb')}GB",
374
+ 'drain-connections': "Drain and reset database connections",
375
+ 'rollback': f"Rollback to version {recommendation.get('to_version')}",
376
+ 'investigate': "Further investigation required"
377
+ }
378
+
379
+ return descriptions.get(action, action)
380
+
381
+ def _calculate_replicas(self, incident: Dict[str, Any]) -> int:
382
+ """Calculate recommended replica count"""
383
+ # In real implementation, could base this on current load, auto-scaling policies, etc.
384
+ return 3
385
+
386
+ def _get_previous_version(self, incident: Dict[str, Any]) -> str:
387
+ """Get previous stable version for rollback"""
388
+ # In real implementation, would query deployment history
389
+ return "v1.2.3"
390
+
391
+ def _map_to_opsgenie_priority(self, severity: str) -> str:
392
+ """Map severity to OpsGenie priority"""
393
+ priority_map = {
394
+ 'critical': 'P1',
395
+ 'warning': 'P2',
396
+ 'info': 'P3'
397
+ }
398
+ return priority_map.get(severity, 'P3')
399
+
400
+ def _load_action_configs(self) -> Dict[str, Any]:
401
+ """Load action configurations"""
402
+ return {
403
+ 'scale-up': {'max_replicas': 10, 'timeout': 300},
404
+ 'restart': {'graceful_period': 30},
405
+ 'rollback': {'timeout': 600}
406
+ }