Spaces:
Runtime error
Runtime error
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 +24 -0
- agents/alert_agent.py +235 -0
- agents/analysis_agent.py +285 -0
- agents/correlation_agent.py +238 -0
- agents/response_agent.py +406 -0
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 |
+
}
|