agenttel 0.3.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. agenttel/__init__.py +13 -0
  2. agenttel/agent/__init__.py +18 -0
  3. agenttel/agent/auth.py +134 -0
  4. agenttel/agent/context.py +86 -0
  5. agenttel/agent/health.py +142 -0
  6. agenttel/agent/identity.py +104 -0
  7. agenttel/agent/incident.py +146 -0
  8. agenttel/agent/mcp_server.py +320 -0
  9. agenttel/agent/remediation.py +167 -0
  10. agenttel/agent/reporting.py +140 -0
  11. agenttel/agentic/__init__.py +5 -0
  12. agenttel/agentic/cost.py +54 -0
  13. agenttel/agentic/guardrail.py +85 -0
  14. agenttel/agentic/orchestration.py +160 -0
  15. agenttel/agentic/quality.py +129 -0
  16. agenttel/agentic/scopes.py +467 -0
  17. agenttel/agentic/tracer.py +161 -0
  18. agenttel/agentic_attributes.py +112 -0
  19. agenttel/anomaly/__init__.py +6 -0
  20. agenttel/anomaly/detector.py +59 -0
  21. agenttel/anomaly/patterns.py +68 -0
  22. agenttel/attributes.py +84 -0
  23. agenttel/baseline/__init__.py +13 -0
  24. agenttel/baseline/composite.py +31 -0
  25. agenttel/baseline/provider.py +42 -0
  26. agenttel/baseline/rolling.py +148 -0
  27. agenttel/causality/__init__.py +5 -0
  28. agenttel/causality/tracker.py +148 -0
  29. agenttel/config.py +197 -0
  30. agenttel/engine.py +161 -0
  31. agenttel/enums.py +198 -0
  32. agenttel/error/__init__.py +5 -0
  33. agenttel/error/classifier.py +71 -0
  34. agenttel/events.py +113 -0
  35. agenttel/fastapi/__init__.py +24 -0
  36. agenttel/fastapi/config.py +63 -0
  37. agenttel/fastapi/decorators.py +91 -0
  38. agenttel/fastapi/middleware.py +114 -0
  39. agenttel/genai/__init__.py +33 -0
  40. agenttel/genai/anthropic.py +145 -0
  41. agenttel/genai/attributes.py +43 -0
  42. agenttel/genai/bedrock.py +125 -0
  43. agenttel/genai/cost.py +91 -0
  44. agenttel/genai/langchain.py +168 -0
  45. agenttel/genai/openai.py +153 -0
  46. agenttel/genai/span_builder.py +127 -0
  47. agenttel/models.py +146 -0
  48. agenttel/processor.py +190 -0
  49. agenttel/slo/__init__.py +5 -0
  50. agenttel/slo/tracker.py +108 -0
  51. agenttel/topology/__init__.py +5 -0
  52. agenttel/topology/registry.py +86 -0
  53. agenttel-0.3.0a1.dist-info/METADATA +228 -0
  54. agenttel-0.3.0a1.dist-info/RECORD +56 -0
  55. agenttel-0.3.0a1.dist-info/WHEEL +4 -0
  56. agenttel-0.3.0a1.dist-info/entry_points.txt +2 -0
agenttel/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """AgentTel Python SDK — AI-native telemetry for Python services."""
2
+
3
+ from agenttel.config import AgentTelConfig
4
+ from agenttel.engine import AgentTelEngine
5
+ from agenttel.processor import AgentTelSpanProcessor
6
+
7
+ __all__ = [
8
+ "AgentTelConfig",
9
+ "AgentTelEngine",
10
+ "AgentTelSpanProcessor",
11
+ ]
12
+
13
+ __version__ = "0.3.0-alpha"
@@ -0,0 +1,18 @@
1
+ """Agent interface for AgentTel (MCP server, health, identity, incident)."""
2
+
3
+ from agenttel.agent.auth import (
4
+ AuthConfig,
5
+ ToolPermission,
6
+ ToolPermissionRegistry as AuthPermissionRegistry,
7
+ )
8
+ from agenttel.agent.health import ServiceHealthAggregator
9
+ from agenttel.agent.identity import AgentIdentity, ToolPermissionRegistry
10
+
11
+ __all__ = [
12
+ "AuthConfig",
13
+ "AuthPermissionRegistry",
14
+ "ServiceHealthAggregator",
15
+ "AgentIdentity",
16
+ "ToolPermission",
17
+ "ToolPermissionRegistry",
18
+ ]
agenttel/agent/auth.py ADDED
@@ -0,0 +1,134 @@
1
+ """API key authentication and role-based permissions for the MCP server.
2
+
3
+ Opt-in: when no AuthConfig is provided, all checks are skipped
4
+ and the server behaves as before (backward compatible).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from enum import Enum
11
+ from typing import Callable
12
+
13
+
14
+ class ToolPermission(Enum):
15
+ """Permission levels for MCP tool access."""
16
+
17
+ READ = "read"
18
+ DIAGNOSE = "diagnose"
19
+ REMEDIATE = "remediate"
20
+ ADMIN = "admin"
21
+
22
+
23
+ @dataclass
24
+ class AgentIdentity:
25
+ """Identifies an authenticated agent making MCP requests."""
26
+
27
+ agent_id: str
28
+ role: str
29
+ session_id: str | None = None
30
+
31
+
32
+ @dataclass
33
+ class AuthConfig:
34
+ """Configuration for API key authentication.
35
+
36
+ Provide either a static api_keys map, a custom validate_key function,
37
+ or both. When both are provided, the static map is checked first.
38
+ """
39
+
40
+ api_keys: dict[str, AgentIdentity] | None = None
41
+ validate_key: Callable[[str], AgentIdentity | None] | None = None
42
+
43
+ def resolve_key(self, key: str) -> AgentIdentity | None:
44
+ """Resolve a bearer token to an agent identity.
45
+
46
+ Returns None if the key is invalid or not found.
47
+ """
48
+ trimmed = key.strip()
49
+ if not trimmed:
50
+ return None
51
+
52
+ if self.api_keys:
53
+ identity = self.api_keys.get(trimmed)
54
+ if identity is not None:
55
+ return identity
56
+
57
+ if self.validate_key:
58
+ return self.validate_key(trimmed)
59
+
60
+ return None
61
+
62
+
63
+ # Default role-to-permission mappings
64
+ _DEFAULT_ROLE_PERMISSIONS: dict[str, set[ToolPermission]] = {
65
+ "observer": {ToolPermission.READ},
66
+ "diagnostician": {ToolPermission.READ, ToolPermission.DIAGNOSE},
67
+ "remediator": {ToolPermission.READ, ToolPermission.DIAGNOSE, ToolPermission.REMEDIATE},
68
+ "admin": {ToolPermission.READ, ToolPermission.DIAGNOSE, ToolPermission.REMEDIATE, ToolPermission.ADMIN},
69
+ }
70
+
71
+ # Default tool-to-required-permission mappings
72
+ _DEFAULT_TOOL_PERMISSIONS: dict[str, ToolPermission] = {
73
+ # READ tools
74
+ "get_service_health": ToolPermission.READ,
75
+ "get_operation_baselines": ToolPermission.READ,
76
+ "get_anomalies": ToolPermission.READ,
77
+ "get_slo_status": ToolPermission.READ,
78
+ "get_dependency_health": ToolPermission.READ,
79
+ "get_topology": ToolPermission.READ,
80
+ "get_incident_context": ToolPermission.READ,
81
+ "get_error_classification": ToolPermission.READ,
82
+ "get_deployment_info": ToolPermission.READ,
83
+ "get_slo_report": ToolPermission.READ,
84
+ "get_executive_summary": ToolPermission.READ,
85
+ "get_trend_analysis": ToolPermission.READ,
86
+ "get_playbook": ToolPermission.READ,
87
+ "get_session": ToolPermission.READ,
88
+ # DIAGNOSE tools
89
+ "verify_remediation_effect": ToolPermission.DIAGNOSE,
90
+ "create_session": ToolPermission.DIAGNOSE,
91
+ "add_session_entry": ToolPermission.DIAGNOSE,
92
+ "suggest_remediation": ToolPermission.DIAGNOSE,
93
+ "get_change_correlation": ToolPermission.DIAGNOSE,
94
+ # REMEDIATE tools
95
+ "execute_remediation": ToolPermission.REMEDIATE,
96
+ "list_remediation_actions": ToolPermission.REMEDIATE,
97
+ }
98
+
99
+
100
+ class ToolPermissionRegistry:
101
+ """Maps roles to granted permissions and tools to required permission levels.
102
+
103
+ Pre-populated with sensible defaults:
104
+ observer -> [READ]
105
+ diagnostician -> [READ, DIAGNOSE]
106
+ remediator -> [READ, DIAGNOSE, REMEDIATE]
107
+ admin -> [READ, DIAGNOSE, REMEDIATE, ADMIN]
108
+ """
109
+
110
+ def __init__(self) -> None:
111
+ self._role_permissions: dict[str, set[ToolPermission]] = {
112
+ k: set(v) for k, v in _DEFAULT_ROLE_PERMISSIONS.items()
113
+ }
114
+ self._tool_permissions: dict[str, ToolPermission] = dict(_DEFAULT_TOOL_PERMISSIONS)
115
+
116
+ def set_role_permissions(self, role: str, perms: list[ToolPermission]) -> None:
117
+ """Set the permissions granted to a role, replacing any existing mapping."""
118
+ self._role_permissions[role.lower()] = set(perms)
119
+
120
+ def set_tool_permission(self, tool_name: str, perm: ToolPermission) -> None:
121
+ """Set the required permission level for a specific tool."""
122
+ self._tool_permissions[tool_name] = perm
123
+
124
+ def is_allowed(self, identity: AgentIdentity, tool_name: str) -> bool:
125
+ """Check whether the given identity has permission to invoke the named tool.
126
+
127
+ Unknown tools default to READ. Unknown roles get observer permissions.
128
+ """
129
+ role = identity.role.lower()
130
+ granted = self._role_permissions.get(
131
+ role, self._role_permissions.get("observer", {ToolPermission.READ})
132
+ )
133
+ required = self._tool_permissions.get(tool_name, ToolPermission.READ)
134
+ return required in granted
@@ -0,0 +1,86 @@
1
+ """Agent context provider for LLM consumption."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from agenttel.agent.health import ServiceHealthAggregator
8
+ from agenttel.anomaly.detector import AnomalyDetector
9
+ from agenttel.baseline.composite import CompositeBaselineProvider
10
+ from agenttel.causality.tracker import CausalityTracker
11
+ from agenttel.slo.tracker import SloTracker
12
+ from agenttel.topology.registry import TopologyRegistry
13
+
14
+
15
+ class AgentContextProvider:
16
+ """Provides structured context for AI agent consumption."""
17
+
18
+ def __init__(
19
+ self,
20
+ topology: TopologyRegistry,
21
+ health_aggregator: ServiceHealthAggregator,
22
+ baseline_provider: CompositeBaselineProvider,
23
+ anomaly_detector: AnomalyDetector,
24
+ slo_tracker: SloTracker,
25
+ causality_tracker: CausalityTracker,
26
+ ) -> None:
27
+ self._topology = topology
28
+ self._health = health_aggregator
29
+ self._baselines = baseline_provider
30
+ self._anomaly = anomaly_detector
31
+ self._slo = slo_tracker
32
+ self._causality = causality_tracker
33
+
34
+ def get_full_context(self) -> dict[str, Any]:
35
+ """Get full service context for incident diagnosis."""
36
+ summary = self._health.get_summary()
37
+ return {
38
+ "service": {
39
+ "name": summary.service_name,
40
+ "status": summary.status,
41
+ "team": self._topology.team,
42
+ "tier": self._topology.tier,
43
+ "domain": self._topology.domain,
44
+ "on_call_channel": self._topology.on_call_channel,
45
+ },
46
+ "operations": [
47
+ {
48
+ "name": op.operation_name,
49
+ "request_count": op.request_count,
50
+ "error_count": op.error_count,
51
+ "error_rate": op.error_count / op.request_count
52
+ if op.request_count > 0
53
+ else 0,
54
+ "latency_p50_ms": op.latency_p50_ms,
55
+ "latency_p99_ms": op.latency_p99_ms,
56
+ }
57
+ for op in summary.operations
58
+ ],
59
+ "dependencies": [
60
+ {
61
+ "name": d.name,
62
+ "type": d.type.value,
63
+ "criticality": d.criticality.value,
64
+ "state": d.state.value,
65
+ "latency_ms": d.latency_ms,
66
+ "error_rate": d.error_rate,
67
+ }
68
+ for d in summary.dependencies
69
+ ],
70
+ "slo_statuses": [
71
+ {
72
+ "name": s.name,
73
+ "target": s.target,
74
+ "budget_remaining": s.budget_remaining,
75
+ "burn_rate": s.burn_rate,
76
+ "alert_level": s.alert_level.value,
77
+ }
78
+ for s in self._slo.get_all_statuses()
79
+ ],
80
+ "causality": None,
81
+ }
82
+
83
+ # Add causal chain if available
84
+ chain = self._causality.build_causal_chain()
85
+ if chain:
86
+ pass
@@ -0,0 +1,142 @@
1
+ """Service health aggregation from span data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ import time
7
+ from collections import deque
8
+
9
+ from agenttel.enums import DependencyState
10
+ from agenttel.models import (
11
+ DependencyHealth,
12
+ OperationStats,
13
+ ServiceHealthSummary,
14
+ )
15
+
16
+
17
+ class ServiceHealthAggregator:
18
+ """Aggregates per-operation and dependency health from span data.
19
+
20
+ Called from the SpanProcessor on_end callback.
21
+ """
22
+
23
+ def __init__(self, service_name: str = "", window_size: int = 1000) -> None:
24
+ self._service_name = service_name
25
+ self._window_size = window_size
26
+ self._lock = threading.Lock()
27
+
28
+ # Per-operation tracking
29
+ self._op_latencies: dict[str, deque[float]] = {}
30
+ self._op_request_count: dict[str, int] = {}
31
+ self._op_error_count: dict[str, int] = {}
32
+
33
+ # Dependency tracking
34
+ self._dep_health: dict[str, DependencyHealth] = {}
35
+
36
+ def record_operation(
37
+ self,
38
+ operation_name: str,
39
+ latency_ms: float,
40
+ is_error: bool = False,
41
+ ) -> None:
42
+ """Record an operation execution."""
43
+ with self._lock:
44
+ if operation_name not in self._op_latencies:
45
+ self._op_latencies[operation_name] = deque(maxlen=self._window_size)
46
+ self._op_request_count[operation_name] = 0
47
+ self._op_error_count[operation_name] = 0
48
+
49
+ self._op_latencies[operation_name].append(latency_ms)
50
+ self._op_request_count[operation_name] += 1
51
+ if is_error:
52
+ self._op_error_count[operation_name] += 1
53
+
54
+ def record_dependency_health(self, health: DependencyHealth) -> None:
55
+ """Update dependency health status."""
56
+ with self._lock:
57
+ self._dep_health[health.name] = health
58
+
59
+ def update_dependency_state(
60
+ self,
61
+ name: str,
62
+ state: DependencyState,
63
+ latency_ms: float = 0.0,
64
+ error_rate: float = 0.0,
65
+ ) -> None:
66
+ """Update a dependency's state."""
67
+ with self._lock:
68
+ existing = self._dep_health.get(name)
69
+ if existing:
70
+ existing.state = state
71
+ existing.latency_ms = latency_ms
72
+ existing.error_rate = error_rate
73
+ existing.last_check = time.time()
74
+
75
+ def get_summary(self) -> ServiceHealthSummary:
76
+ """Get the current service health summary."""
77
+ with self._lock:
78
+ operations = []
79
+ for op_name in self._op_latencies:
80
+ latencies = sorted(self._op_latencies[op_name])
81
+ n = len(latencies)
82
+ if n == 0:
83
+ continue
84
+
85
+ mean = sum(latencies) / n
86
+ operations.append(
87
+ OperationStats(
88
+ operation_name=op_name,
89
+ request_count=self._op_request_count.get(op_name, 0),
90
+ error_count=self._op_error_count.get(op_name, 0),
91
+ latency_p50_ms=_percentile(latencies, 0.50),
92
+ latency_p95_ms=_percentile(latencies, 0.95),
93
+ latency_p99_ms=_percentile(latencies, 0.99),
94
+ latency_mean_ms=mean,
95
+ )
96
+ )
97
+
98
+ dependencies = list(self._dep_health.values())
99
+
100
+ # Determine overall status
101
+ status = "HEALTHY"
102
+ has_errors = any(
103
+ op.error_count > op.request_count * 0.1
104
+ for op in operations
105
+ if op.request_count > 0
106
+ )
107
+ has_failing_deps = any(
108
+ d.state == DependencyState.FAILING for d in dependencies
109
+ )
110
+ if has_failing_deps or has_errors:
111
+ status = "CRITICAL"
112
+ elif any(d.state == DependencyState.DEGRADED for d in dependencies):
113
+ status = "DEGRADED"
114
+
115
+ return ServiceHealthSummary(
116
+ service_name=self._service_name,
117
+ status=status,
118
+ operations=operations,
119
+ dependencies=dependencies,
120
+ )
121
+
122
+ def reset(self) -> None:
123
+ """Reset all aggregated data."""
124
+ with self._lock:
125
+ self._op_latencies.clear()
126
+ self._op_request_count.clear()
127
+ self._op_error_count.clear()
128
+ self._dep_health.clear()
129
+
130
+
131
+ def _percentile(sorted_data: list[float], p: float) -> float:
132
+ """Calculate percentile using linear interpolation."""
133
+ n = len(sorted_data)
134
+ if n == 0:
135
+ return 0.0
136
+ if n == 1:
137
+ return sorted_data[0]
138
+ idx = p * (n - 1)
139
+ lower = int(idx)
140
+ upper = min(lower + 1, n - 1)
141
+ frac = idx - lower
142
+ return sorted_data[lower] + frac * (sorted_data[upper] - sorted_data[lower])
@@ -0,0 +1,104 @@
1
+ """Agent identity and permission management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+ from agenttel.enums import AgentRole
10
+
11
+
12
+ @dataclass
13
+ class AgentIdentity:
14
+ """Represents an AI agent's identity for MCP access."""
15
+
16
+ id: str = field(default_factory=lambda: str(uuid.uuid4()))
17
+ role: AgentRole = AgentRole.OBSERVER
18
+ session_id: str = field(default_factory=lambda: str(uuid.uuid4()))
19
+ name: str = ""
20
+ metadata: dict[str, Any] = field(default_factory=dict)
21
+
22
+ @classmethod
23
+ def from_headers(cls, headers: dict[str, str]) -> AgentIdentity:
24
+ """Extract agent identity from HTTP request headers."""
25
+ return cls(
26
+ id=headers.get("x-agent-id", str(uuid.uuid4())),
27
+ role=AgentRole(headers.get("x-agent-role", "observer")),
28
+ session_id=headers.get("x-agent-session-id", str(uuid.uuid4())),
29
+ name=headers.get("x-agent-name", ""),
30
+ )
31
+
32
+
33
+ # Default tool permissions per role
34
+ _DEFAULT_PERMISSIONS: dict[AgentRole, set[str]] = {
35
+ AgentRole.OBSERVER: {
36
+ "get_service_health",
37
+ "get_operation_baselines",
38
+ "get_anomalies",
39
+ "get_slo_status",
40
+ "get_dependency_health",
41
+ "get_topology",
42
+ "get_error_classification",
43
+ "get_deployment_info",
44
+ },
45
+ AgentRole.DIAGNOSTICIAN: {
46
+ "get_service_health",
47
+ "get_operation_baselines",
48
+ "get_anomalies",
49
+ "get_slo_status",
50
+ "get_dependency_health",
51
+ "get_topology",
52
+ "get_incident_context",
53
+ "get_error_classification",
54
+ "get_deployment_info",
55
+ "get_change_correlation",
56
+ "get_slo_report",
57
+ "get_trend_analysis",
58
+ "get_executive_summary",
59
+ "suggest_remediation",
60
+ },
61
+ AgentRole.REMEDIATOR: {
62
+ "get_service_health",
63
+ "get_operation_baselines",
64
+ "get_anomalies",
65
+ "get_slo_status",
66
+ "get_dependency_health",
67
+ "get_topology",
68
+ "get_incident_context",
69
+ "get_error_classification",
70
+ "get_deployment_info",
71
+ "get_change_correlation",
72
+ "get_slo_report",
73
+ "get_trend_analysis",
74
+ "get_executive_summary",
75
+ "suggest_remediation",
76
+ "execute_remediation",
77
+ },
78
+ AgentRole.ADMIN: set(), # Empty = all tools allowed
79
+ }
80
+
81
+
82
+ class ToolPermissionRegistry:
83
+ """Manages role-based tool access for MCP."""
84
+
85
+ def __init__(self) -> None:
86
+ self._permissions: dict[AgentRole, set[str]] = dict(_DEFAULT_PERMISSIONS)
87
+
88
+ def is_allowed(self, role: AgentRole, tool_name: str) -> bool:
89
+ """Check if a role has permission to use a tool."""
90
+ allowed = self._permissions.get(role)
91
+ if allowed is not None and len(allowed) == 0:
92
+ return True # Empty set for admin = all allowed
93
+ return tool_name in (allowed or set())
94
+
95
+ def grant(self, role: AgentRole, tool_name: str) -> None:
96
+ """Grant a tool permission to a role."""
97
+ if role not in self._permissions:
98
+ self._permissions[role] = set()
99
+ self._permissions[role].add(tool_name)
100
+
101
+ def revoke(self, role: AgentRole, tool_name: str) -> None:
102
+ """Revoke a tool permission from a role."""
103
+ if role in self._permissions:
104
+ self._permissions[role].discard(tool_name)
@@ -0,0 +1,146 @@
1
+ """Incident context builder for AI agent diagnosis."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+ from agenttel.agent.health import ServiceHealthAggregator
10
+ from agenttel.causality.tracker import CausalityTracker
11
+ from agenttel.models import AnomalyResult, SloStatus
12
+
13
+
14
+ @dataclass
15
+ class IncidentContext:
16
+ """Full incident context for AI agent consumption."""
17
+
18
+ service_name: str
19
+ status: str
20
+ timestamp: float = field(default_factory=time.time)
21
+ anomalies: list[dict[str, Any]] = field(default_factory=list)
22
+ slo_violations: list[dict[str, Any]] = field(default_factory=list)
23
+ failing_dependencies: list[str] = field(default_factory=list)
24
+ causal_chain: dict[str, Any] | None = None
25
+ recent_changes: list[dict[str, Any]] = field(default_factory=list)
26
+ suggested_actions: list[str] = field(default_factory=list)
27
+
28
+
29
+ class IncidentContextBuilder:
30
+ """Builds comprehensive incident context from system state."""
31
+
32
+ def __init__(
33
+ self,
34
+ health_aggregator: ServiceHealthAggregator,
35
+ causality_tracker: CausalityTracker,
36
+ ) -> None:
37
+ self._health = health_aggregator
38
+ self._causality = causality_tracker
39
+
40
+ def build(
41
+ self,
42
+ anomalies: list[AnomalyResult] | None = None,
43
+ slo_statuses: list[SloStatus] | None = None,
44
+ ) -> IncidentContext:
45
+ """Build incident context from current state."""
46
+ summary = self._health.get_summary()
47
+
48
+ # Build anomaly list
49
+ anomaly_list = []
50
+ if anomalies:
51
+ for a in anomalies:
52
+ if a.is_anomaly:
53
+ anomaly_list.append({
54
+ "score": a.score,
55
+ "z_score": a.z_score,
56
+ "pattern": a.pattern.value if a.pattern else "unknown",
57
+ })
58
+
59
+ # Build SLO violations
60
+ slo_violations = []
61
+ if slo_statuses:
62
+ for s in slo_statuses:
63
+ if s.alert_level.value != "ok":
64
+ slo_violations.append({
65
+ "name": s.name,
66
+ "target": s.target,
67
+ "budget_remaining": s.budget_remaining,
68
+ "burn_rate": s.burn_rate,
69
+ "alert_level": s.alert_level.value,
70
+ })
71
+
72
+ # Get failing dependencies
73
+ failing = self._causality.get_failing_dependencies()
74
+
75
+ # Get causal chain
76
+ chain = self._causality.build_causal_chain()
77
+ causal_data = None
78
+ if chain:
79
+ causal_data = {
80
+ "root_cause": chain.root_cause,
81
+ "cause_category": chain.cause_category.value,
82
+ "affected_dependencies": chain.affected_dependencies,
83
+ "business_impact": chain.business_impact.value,
84
+ "impact_scope": chain.impact_scope.value,
85
+ }
86
+
87
+ return IncidentContext(
88
+ service_name=summary.service_name,
89
+ status=summary.status,
90
+ anomalies=anomaly_list,
91
+ slo_violations=slo_violations,
92
+ failing_dependencies=failing,
93
+ causal_chain=causal_data,
94
+ )
95
+
96
+
97
+ class ContextFormatter:
98
+ """Formats incident context as structured text for LLM consumption."""
99
+
100
+ @staticmethod
101
+ def format_for_llm(context: IncidentContext) -> str:
102
+ """Format incident context as readable text for LLM."""
103
+ lines = [
104
+ f"# Incident Context: {context.service_name}",
105
+ f"Status: {context.status}",
106
+ f"Timestamp: {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(context.timestamp))}",
107
+ "",
108
+ ]
109
+
110
+ if context.anomalies:
111
+ lines.append("## Anomalies Detected")
112
+ for a in context.anomalies:
113
+ lines.append(f"- Pattern: {a['pattern']}, Score: {a['score']:.2f}, Z-Score: {a['z_score']:.2f}")
114
+ lines.append("")
115
+
116
+ if context.slo_violations:
117
+ lines.append("## SLO Violations")
118
+ for s in context.slo_violations:
119
+ lines.append(
120
+ f"- {s['name']}: target={s['target']}, "
121
+ f"budget_remaining={s['budget_remaining']:.1%}, "
122
+ f"alert={s['alert_level']}"
123
+ )
124
+ lines.append("")
125
+
126
+ if context.failing_dependencies:
127
+ lines.append("## Failing Dependencies")
128
+ for dep in context.failing_dependencies:
129
+ lines.append(f"- {dep}")
130
+ lines.append("")
131
+
132
+ if context.causal_chain:
133
+ c = context.causal_chain
134
+ lines.append("## Causal Analysis")
135
+ lines.append(f"Root Cause: {c['root_cause']}")
136
+ lines.append(f"Category: {c['cause_category']}")
137
+ lines.append(f"Impact: {c['business_impact']} ({c['impact_scope']})")
138
+ lines.append(f"Affected: {', '.join(c['affected_dependencies'])}")
139
+ lines.append("")
140
+
141
+ if context.suggested_actions:
142
+ lines.append("## Suggested Actions")
143
+ for i, action in enumerate(context.suggested_actions, 1):
144
+ lines.append(f"{i}. {action}")
145
+
146
+ return "\n".join(lines)