continuous-intelligence-layer 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- continuous_intelligence_layer/__init__.py +28 -0
- continuous_intelligence_layer/_core/__init__.py +6 -0
- continuous_intelligence_layer/_core/exporter.py +517 -0
- continuous_intelligence_layer/_core/graph_exporter.py +92 -0
- continuous_intelligence_layer/_core/utils.py +170 -0
- continuous_intelligence_layer/anthropic/__init__.py +9 -0
- continuous_intelligence_layer/anthropic/init.py +232 -0
- continuous_intelligence_layer/anthropic/instrumentation.py +91 -0
- continuous_intelligence_layer/crewai/__init__.py +9 -0
- continuous_intelligence_layer/crewai/init.py +228 -0
- continuous_intelligence_layer/crewai/instrumentation.py +83 -0
- continuous_intelligence_layer/langgraph/__init__.py +12 -0
- continuous_intelligence_layer/langgraph/init.py +253 -0
- continuous_intelligence_layer/langgraph/instrumentation.py +71 -0
- continuous_intelligence_layer/openai/__init__.py +9 -0
- continuous_intelligence_layer/openai/init.py +229 -0
- continuous_intelligence_layer/openai/instrumentation.py +67 -0
- continuous_intelligence_layer-0.1.0.dist-info/METADATA +633 -0
- continuous_intelligence_layer-0.1.0.dist-info/RECORD +38 -0
- continuous_intelligence_layer-0.1.0.dist-info/WHEEL +4 -0
- continuous_intelligence_layer-0.1.0.dist-info/licenses/LICENSE +21 -0
- evaluators/__init__.py +32 -0
- evaluators/base_evaluator.py +181 -0
- evaluators/crewai_input_evaluator.py +249 -0
- evaluators/input_evaluator.py +121 -0
- evaluators/models.py +180 -0
- evaluators/output_evaluator.py +247 -0
- evaluators/runner.py +313 -0
- evaluators/tool_agent_evaluator.py +305 -0
- graph_builder/__init__.py +6 -0
- graph_builder/builder.py +212 -0
- graph_builder/models.py +159 -0
- graph_builder/mongo_store.py +588 -0
- llm_router/__init__.py +3 -0
- llm_router/router.py +71 -0
- rca_engine/__init__.py +5 -0
- rca_engine/incident_report.py +162 -0
- rca_engine/rca_engine.py +202 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
incident_report.py
|
|
3
|
+
------------------
|
|
4
|
+
Generates a human-readable Markdown incident report from an RCAResult.
|
|
5
|
+
|
|
6
|
+
Includes:
|
|
7
|
+
- Execution metadata
|
|
8
|
+
- Overall status
|
|
9
|
+
- Primary root cause
|
|
10
|
+
- Failure propagation chain (ASCII diagram)
|
|
11
|
+
- Evidence table
|
|
12
|
+
- Contributing factors
|
|
13
|
+
- Recommendations
|
|
14
|
+
- Confidence score
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from graph_builder.models import ExecutionGraph
|
|
24
|
+
from .rca_engine import RCAResult
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class IncidentReportGenerator:
|
|
28
|
+
"""Generates Markdown incident reports from RCA results."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, output_dir: str = "./reports"):
|
|
31
|
+
self._output_dir = Path(output_dir)
|
|
32
|
+
self._output_dir.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
|
|
34
|
+
def generate(self, rca: RCAResult, graph: ExecutionGraph) -> str:
|
|
35
|
+
"""Generate a Markdown incident report and save it to disk."""
|
|
36
|
+
report = self._build_report(rca, graph)
|
|
37
|
+
filepath = self._save(rca.execution_id, report)
|
|
38
|
+
return filepath
|
|
39
|
+
|
|
40
|
+
def _build_report(self, rca: RCAResult, graph: ExecutionGraph) -> str:
|
|
41
|
+
summary = graph.summary()
|
|
42
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
43
|
+
status_badge = "🔴 FAILED" if rca.overall_status == "FAIL" else (
|
|
44
|
+
"🟡 PARTIAL" if rca.overall_status == "PARTIAL" else "🟢 PASSED")
|
|
45
|
+
|
|
46
|
+
lines = [
|
|
47
|
+
f"# Incident Report — Execution `{rca.execution_id}`",
|
|
48
|
+
"",
|
|
49
|
+
f"**Generated:** {now}",
|
|
50
|
+
"",
|
|
51
|
+
"---",
|
|
52
|
+
"",
|
|
53
|
+
"## Overview",
|
|
54
|
+
"",
|
|
55
|
+
f"| Field | Value |",
|
|
56
|
+
f"|---|---|",
|
|
57
|
+
f"| Execution ID | `{rca.execution_id}` |",
|
|
58
|
+
f"| Status | {status_badge} |",
|
|
59
|
+
f"| Total Nodes | {summary['total_nodes']} |",
|
|
60
|
+
f"| Failed Nodes | {summary['failed_nodes']} |",
|
|
61
|
+
f"| Total Latency | {summary['total_latency_ms']} ms |",
|
|
62
|
+
f"| Total Tokens | {summary['total_tokens']} |",
|
|
63
|
+
f"| RCA Confidence | {rca.confidence * 100:.1f}% |",
|
|
64
|
+
"",
|
|
65
|
+
"---",
|
|
66
|
+
"",
|
|
67
|
+
"## 🔍 Primary Root Cause",
|
|
68
|
+
"",
|
|
69
|
+
f"> **{rca.root_cause}**",
|
|
70
|
+
"",
|
|
71
|
+
"---",
|
|
72
|
+
"",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
# Propagation chain
|
|
76
|
+
if rca.propagation_chain:
|
|
77
|
+
lines += [
|
|
78
|
+
"## 🔗 Failure Propagation Chain",
|
|
79
|
+
"",
|
|
80
|
+
"```",
|
|
81
|
+
self._ascii_chain(rca.propagation_chain),
|
|
82
|
+
"```",
|
|
83
|
+
"",
|
|
84
|
+
"---",
|
|
85
|
+
"",
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
# Evidence table
|
|
89
|
+
lines += [
|
|
90
|
+
"## 🧾 Evidence",
|
|
91
|
+
"",
|
|
92
|
+
"| Node | Validator | Status | Reason |",
|
|
93
|
+
"|---|---|:---:|---|",
|
|
94
|
+
]
|
|
95
|
+
for ev in rca.evidence:
|
|
96
|
+
status_str = "❌ FAIL" if ev.status == "FAIL" else "✅ PASS"
|
|
97
|
+
reason = ev.reason.replace("|", "\\|")[:120]
|
|
98
|
+
lines.append(f"| `{ev.node_name}` | {ev.validator} | {status_str} | {reason} |")
|
|
99
|
+
|
|
100
|
+
if not rca.evidence:
|
|
101
|
+
lines.append("| — | — | — | No failures detected |")
|
|
102
|
+
|
|
103
|
+
lines += ["", "---", ""]
|
|
104
|
+
|
|
105
|
+
# Contributing factors
|
|
106
|
+
if rca.contributing_factors:
|
|
107
|
+
lines += [
|
|
108
|
+
"## ⚠️ Contributing Factors",
|
|
109
|
+
"",
|
|
110
|
+
]
|
|
111
|
+
for factor in rca.contributing_factors:
|
|
112
|
+
lines.append(f"- {factor}")
|
|
113
|
+
lines += ["", "---", ""]
|
|
114
|
+
|
|
115
|
+
# Recommendations
|
|
116
|
+
lines += [
|
|
117
|
+
"## ✅ Recommendations",
|
|
118
|
+
"",
|
|
119
|
+
]
|
|
120
|
+
for i, rec in enumerate(rca.recommendations, 1):
|
|
121
|
+
lines.append(f"{i}. {rec}")
|
|
122
|
+
if not rca.recommendations:
|
|
123
|
+
lines.append("No specific recommendations generated.")
|
|
124
|
+
|
|
125
|
+
lines += [
|
|
126
|
+
"",
|
|
127
|
+
"---",
|
|
128
|
+
"",
|
|
129
|
+
"## 📊 Node Summary",
|
|
130
|
+
"",
|
|
131
|
+
"| Node | Type | Status | Latency (ms) | Tokens |",
|
|
132
|
+
"|---|---|:---:|---:|---:|",
|
|
133
|
+
]
|
|
134
|
+
for node in graph.nodes:
|
|
135
|
+
n_status = "❌ ERROR" if node.error else "✅ OK"
|
|
136
|
+
tokens = node.tokens.total or 0
|
|
137
|
+
lines.append(
|
|
138
|
+
f"| `{node.name[:35]}` | {node.node_type} | {n_status} "
|
|
139
|
+
f"| {node.latency_ms:.1f} | {tokens} |"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
lines += [
|
|
143
|
+
"",
|
|
144
|
+
"---",
|
|
145
|
+
"",
|
|
146
|
+
f"*Report generated by AgentOPS Intelligent Layer — confidence: {rca.confidence*100:.1f}%*",
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
return "\n".join(lines)
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def _ascii_chain(chain: list[str]) -> str:
|
|
153
|
+
if not chain:
|
|
154
|
+
return "(empty)"
|
|
155
|
+
return "\n↓\n".join(chain)
|
|
156
|
+
|
|
157
|
+
def _save(self, execution_id: str, content: str) -> str:
|
|
158
|
+
ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S")
|
|
159
|
+
name = f"incident_{execution_id}_{ts}.md"
|
|
160
|
+
path = self._output_dir / name
|
|
161
|
+
path.write_text(content, encoding="utf-8")
|
|
162
|
+
return str(path)
|
rca_engine/rca_engine.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""
|
|
2
|
+
rca_engine.py
|
|
3
|
+
-------------
|
|
4
|
+
RCA Engine — Phase 4 Intelligence Layer
|
|
5
|
+
|
|
6
|
+
Input:
|
|
7
|
+
- ExecutionGraph (with validation_results attached to every node)
|
|
8
|
+
|
|
9
|
+
Responsibilities:
|
|
10
|
+
1. Identify the primary root cause
|
|
11
|
+
2. Identify contributing factors
|
|
12
|
+
3. Explain failure propagation (which node failure caused downstream failures)
|
|
13
|
+
4. Suggest fixes
|
|
14
|
+
5. Produce confidence scores
|
|
15
|
+
|
|
16
|
+
Output: RCAResult (structured JSON + evidence list)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import re
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from pydantic import BaseModel
|
|
26
|
+
|
|
27
|
+
from graph_builder.models import ExecutionGraph, ExecutionNode
|
|
28
|
+
from llm_router import LLMRouter
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _parse_json(text: str) -> dict:
|
|
32
|
+
text = re.sub(r"```(?:json)?", "", text).replace("```", "").strip()
|
|
33
|
+
match = re.search(r"\{.*\}", text, re.DOTALL)
|
|
34
|
+
if match:
|
|
35
|
+
return json.loads(match.group())
|
|
36
|
+
raise ValueError(f"No JSON in LLM response: {text[:400]}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ── Result model ───────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
class EvidenceItem(BaseModel):
|
|
42
|
+
node_id: str
|
|
43
|
+
node_name: str
|
|
44
|
+
validator: str
|
|
45
|
+
status: str
|
|
46
|
+
reason: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RCAResult(BaseModel):
|
|
50
|
+
execution_id: str
|
|
51
|
+
project_id: str | None = None
|
|
52
|
+
overall_status: str # "PASS" | "FAIL" | "PARTIAL"
|
|
53
|
+
root_cause: str
|
|
54
|
+
confidence: float
|
|
55
|
+
propagation_chain: list[str] # node names in order of failure spread
|
|
56
|
+
contributing_factors: list[str]
|
|
57
|
+
evidence: list[EvidenceItem]
|
|
58
|
+
recommendations: list[str]
|
|
59
|
+
raw_llm_analysis: str = ""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ── Engine ─────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
_PROMPT_TEMPLATE = """
|
|
65
|
+
You are an expert AI systems reliability engineer performing Root Cause Analysis (RCA)
|
|
66
|
+
on a failed multi-agent AI system execution.
|
|
67
|
+
|
|
68
|
+
## Execution Summary
|
|
69
|
+
Execution ID: {execution_id}
|
|
70
|
+
Total Nodes: {total_nodes}
|
|
71
|
+
Failed Nodes: {failed_nodes}
|
|
72
|
+
Total Latency: {total_latency_ms} ms
|
|
73
|
+
Total Tokens: {total_tokens}
|
|
74
|
+
|
|
75
|
+
## Execution Graph (node hierarchy)
|
|
76
|
+
{graph_structure}
|
|
77
|
+
|
|
78
|
+
## Validation Failures (Evidence)
|
|
79
|
+
{validation_failures}
|
|
80
|
+
|
|
81
|
+
## All Validation Results
|
|
82
|
+
{all_validations}
|
|
83
|
+
|
|
84
|
+
## Your Task
|
|
85
|
+
Perform a thorough Root Cause Analysis.
|
|
86
|
+
|
|
87
|
+
1. Identify the PRIMARY ROOT CAUSE — what single issue was most responsible for the failure?
|
|
88
|
+
2. Identify CONTRIBUTING FACTORS — secondary issues that amplified the failure.
|
|
89
|
+
3. Explain FAILURE PROPAGATION — how did the root cause cascade through the execution graph?
|
|
90
|
+
4. Provide specific, actionable RECOMMENDATIONS to fix the issues.
|
|
91
|
+
5. Assign a CONFIDENCE SCORE (0.0-1.0) to your analysis.
|
|
92
|
+
|
|
93
|
+
Consider:
|
|
94
|
+
- Which node failed FIRST chronologically?
|
|
95
|
+
- Did that failure cause downstream nodes to fail?
|
|
96
|
+
- Was the issue in the input, the decision, the execution, or the goal?
|
|
97
|
+
- What could have prevented this?
|
|
98
|
+
|
|
99
|
+
## Response Format
|
|
100
|
+
Respond ONLY with a valid JSON object. No explanation outside the JSON. No markdown.
|
|
101
|
+
|
|
102
|
+
{{
|
|
103
|
+
"overall_status": "PASS" or "FAIL" or "PARTIAL",
|
|
104
|
+
"root_cause": "<concise primary root cause in 1-2 sentences>",
|
|
105
|
+
"confidence": <float 0.0-1.0>,
|
|
106
|
+
"propagation_chain": ["<node_name_1>", "<node_name_2>", ...],
|
|
107
|
+
"contributing_factors": ["<factor 1>", "<factor 2>", ...],
|
|
108
|
+
"recommendations": ["<specific fix 1>", "<specific fix 2>", ...]
|
|
109
|
+
}}
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class RCAEngine:
|
|
114
|
+
"""Analyzes an ExecutionGraph's validation results and produces an RCA."""
|
|
115
|
+
|
|
116
|
+
def __init__(self, router: LLMRouter):
|
|
117
|
+
self._router = router
|
|
118
|
+
|
|
119
|
+
def analyze(self, graph: ExecutionGraph) -> RCAResult:
|
|
120
|
+
summary = graph.summary()
|
|
121
|
+
|
|
122
|
+
# ── Collect evidence ───────────────────────────────────────────────
|
|
123
|
+
all_evidence: list[EvidenceItem] = []
|
|
124
|
+
failures: list[EvidenceItem] = []
|
|
125
|
+
graph_structure_lines = []
|
|
126
|
+
|
|
127
|
+
for node in graph.nodes:
|
|
128
|
+
indent = " " if node.parent_id else ""
|
|
129
|
+
graph_structure_lines.append(
|
|
130
|
+
f"{indent}• [{node.node_type}] {node.name} (id={node.node_id[:8]})"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
for vr in node.validation_results:
|
|
134
|
+
if not isinstance(vr, dict):
|
|
135
|
+
continue
|
|
136
|
+
item = EvidenceItem(
|
|
137
|
+
node_id = node.node_id,
|
|
138
|
+
node_name = node.name,
|
|
139
|
+
validator = vr.get("evaluator", "Unknown"),
|
|
140
|
+
status = vr.get("status", "UNKNOWN"),
|
|
141
|
+
reason = vr.get("reason", "")[:300],
|
|
142
|
+
)
|
|
143
|
+
all_evidence.append(item)
|
|
144
|
+
if vr.get("status") == "FAIL":
|
|
145
|
+
failures.append(item)
|
|
146
|
+
|
|
147
|
+
# ── Format for LLM ─────────────────────────────────────────────────
|
|
148
|
+
graph_structure = "\n".join(graph_structure_lines)
|
|
149
|
+
|
|
150
|
+
validation_failures_str = (
|
|
151
|
+
"\n".join(
|
|
152
|
+
f"- [{f.validator}] Node '{f.node_name}': {f.reason}"
|
|
153
|
+
for f in failures
|
|
154
|
+
)
|
|
155
|
+
if failures else "No failures detected."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
all_validations_str = json.dumps(
|
|
159
|
+
{node.node_id: node.validation_results for node in graph.nodes},
|
|
160
|
+
indent=2, default=str
|
|
161
|
+
)[:8000] # cap to avoid huge prompts
|
|
162
|
+
|
|
163
|
+
prompt = _PROMPT_TEMPLATE.format(
|
|
164
|
+
execution_id = graph.execution_id,
|
|
165
|
+
total_nodes = summary["total_nodes"],
|
|
166
|
+
failed_nodes = summary["failed_nodes"],
|
|
167
|
+
total_latency_ms = summary["total_latency_ms"],
|
|
168
|
+
total_tokens = summary["total_tokens"],
|
|
169
|
+
graph_structure = graph_structure,
|
|
170
|
+
validation_failures = validation_failures_str,
|
|
171
|
+
all_validations = all_validations_str,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# ── Call LLM ──────────────────────────────────────────────────────
|
|
175
|
+
raw_text = self._router.call(prompt)
|
|
176
|
+
try:
|
|
177
|
+
parsed = _parse_json(raw_text)
|
|
178
|
+
except Exception as exc:
|
|
179
|
+
parsed = {
|
|
180
|
+
"overall_status": "FAIL" if failures else "PASS",
|
|
181
|
+
"root_cause": f"RCA LLM parsing failed: {exc}",
|
|
182
|
+
"confidence": 0.3,
|
|
183
|
+
"propagation_chain": [],
|
|
184
|
+
"contributing_factors": [],
|
|
185
|
+
"recommendations": ["Review validation failures manually."],
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# ── Determine overall status if not provided ───────────────────────
|
|
189
|
+
overall = parsed.get("overall_status", "FAIL" if failures else "PASS")
|
|
190
|
+
|
|
191
|
+
return RCAResult(
|
|
192
|
+
execution_id = graph.execution_id,
|
|
193
|
+
project_id = graph.project_id,
|
|
194
|
+
overall_status = overall,
|
|
195
|
+
root_cause = parsed.get("root_cause", "Unknown"),
|
|
196
|
+
confidence = float(parsed.get("confidence", 0.5)),
|
|
197
|
+
propagation_chain = parsed.get("propagation_chain", []),
|
|
198
|
+
contributing_factors = parsed.get("contributing_factors", []),
|
|
199
|
+
evidence = failures,
|
|
200
|
+
recommendations = parsed.get("recommendations", []),
|
|
201
|
+
raw_llm_analysis = raw_text,
|
|
202
|
+
)
|