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
graph_builder/builder.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""
|
|
2
|
+
builder.py
|
|
3
|
+
----------
|
|
4
|
+
Converts a flat list of execution spans (from the continuous_intelligence_layer
|
|
5
|
+
SDK or from TraceStore) into an ExecutionGraph of ExecutionNodes and ExecutionEdges.
|
|
6
|
+
|
|
7
|
+
Algorithm:
|
|
8
|
+
1. Sort spans by timestamp.
|
|
9
|
+
2. Each span → ExecutionNode (reading from the continuous_intelligence_layer
|
|
10
|
+
exporter schema, shared by every framework subpackage).
|
|
11
|
+
3. For every span with a parent_span_id, create an edge:
|
|
12
|
+
parent → child (EdgeType.CALLS)
|
|
13
|
+
4. Additionally create NEXT edges between sibling nodes (same parent)
|
|
14
|
+
ordered by timestamp.
|
|
15
|
+
|
|
16
|
+
Span field mapping (continuous_intelligence_layer exporter → ExecutionNode):
|
|
17
|
+
"span_id" → node_id
|
|
18
|
+
"parent_span_id" → parent_id
|
|
19
|
+
"execution_id" → execution_id
|
|
20
|
+
"session_id" → session_id
|
|
21
|
+
"trace_id" → trace_id
|
|
22
|
+
"node_type" → node_type
|
|
23
|
+
"operation" → name ← was wrongly "name" before
|
|
24
|
+
"model" → model
|
|
25
|
+
"tool_name" → tool_name
|
|
26
|
+
"tool_desc" → tool_desc
|
|
27
|
+
"retrieved_docs" → retrieved_docs
|
|
28
|
+
"available_tools"→ available_tools
|
|
29
|
+
"tool_calls" → tool_calls
|
|
30
|
+
"input" → input
|
|
31
|
+
"output" → output
|
|
32
|
+
"prompt" → prompt
|
|
33
|
+
"response" → response
|
|
34
|
+
"latency_ms" → latency_ms
|
|
35
|
+
"tokens" → tokens
|
|
36
|
+
"error" → error
|
|
37
|
+
"timestamp" → timestamp
|
|
38
|
+
"end_timestamp" → end_timestamp
|
|
39
|
+
"metadata" → metadata ← was wrongly "attributes" before
|
|
40
|
+
"status" → status
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import json
|
|
46
|
+
from typing import Any
|
|
47
|
+
|
|
48
|
+
from .models import (
|
|
49
|
+
EdgeType,
|
|
50
|
+
ExecutionEdge,
|
|
51
|
+
ExecutionGraph,
|
|
52
|
+
ExecutionNode,
|
|
53
|
+
NodeType,
|
|
54
|
+
TokenUsage,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _safe_str(val: Any, max_len: int = 4000) -> str | None:
|
|
59
|
+
"""Safely convert any value to a string, capping length."""
|
|
60
|
+
if val is None:
|
|
61
|
+
return None
|
|
62
|
+
if isinstance(val, str):
|
|
63
|
+
return val[:max_len]
|
|
64
|
+
try:
|
|
65
|
+
return json.dumps(val, default=str)[:max_len]
|
|
66
|
+
except Exception:
|
|
67
|
+
return str(val)[:max_len]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _node_type(raw: str) -> NodeType:
|
|
71
|
+
mapping = {
|
|
72
|
+
"llm": NodeType.LLM,
|
|
73
|
+
"tool": NodeType.Tool,
|
|
74
|
+
"retriever": NodeType.Retriever,
|
|
75
|
+
"agent": NodeType.Agent,
|
|
76
|
+
"memory": NodeType.Memory,
|
|
77
|
+
"router": NodeType.Router,
|
|
78
|
+
"embedding": NodeType.Embedding,
|
|
79
|
+
"reranker": NodeType.Reranker,
|
|
80
|
+
"guardrail": NodeType.Guardrail,
|
|
81
|
+
}
|
|
82
|
+
return mapping.get(raw.lower(), NodeType.Unknown)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TraceToGraphBuilder:
|
|
86
|
+
"""
|
|
87
|
+
Converts a list of span dicts (from continuous_intelligence_layer SDK
|
|
88
|
+
output) into an ExecutionGraph ready to be persisted in MongoDB.
|
|
89
|
+
|
|
90
|
+
Usage:
|
|
91
|
+
from graph_builder.builder import TraceToGraphBuilder
|
|
92
|
+
from continuous_intelligence_layer.langgraph import load_traces
|
|
93
|
+
|
|
94
|
+
spans = load_traces("./execution_trace.jsonl")
|
|
95
|
+
graph = TraceToGraphBuilder().build(spans, execution_id="abc123")
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def build(
|
|
99
|
+
self,
|
|
100
|
+
spans: list[dict],
|
|
101
|
+
execution_id: str,
|
|
102
|
+
session_id: str | None = None,
|
|
103
|
+
) -> ExecutionGraph:
|
|
104
|
+
if not spans:
|
|
105
|
+
return ExecutionGraph(execution_id=execution_id, session_id=session_id)
|
|
106
|
+
|
|
107
|
+
# ── 1. Sort by timestamp ───────────────────────────────────────────
|
|
108
|
+
sorted_spans = sorted(
|
|
109
|
+
spans,
|
|
110
|
+
key=lambda s: s.get("timestamp") or "",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# ── 2. Build nodes ────────────────────────────────────────────────
|
|
114
|
+
node_map: dict[str, ExecutionNode] = {}
|
|
115
|
+
for span in sorted_spans:
|
|
116
|
+
node = self._span_to_node(span, session_id=session_id)
|
|
117
|
+
node_map[node.node_id] = node
|
|
118
|
+
|
|
119
|
+
# ── 3. Build CALLS edges (parent → child) ─────────────────────────
|
|
120
|
+
edges: list[ExecutionEdge] = []
|
|
121
|
+
for node in node_map.values():
|
|
122
|
+
if node.parent_id and node.parent_id in node_map:
|
|
123
|
+
edges.append(ExecutionEdge(
|
|
124
|
+
source_id=node.parent_id,
|
|
125
|
+
target_id=node.node_id,
|
|
126
|
+
edge_type=EdgeType.CALLS,
|
|
127
|
+
))
|
|
128
|
+
|
|
129
|
+
# ── 4. Build NEXT edges between siblings ──────────────────────────
|
|
130
|
+
# Group by parent_id — siblings are nodes sharing the same parent
|
|
131
|
+
siblings: dict[str | None, list[ExecutionNode]] = {}
|
|
132
|
+
for node in node_map.values():
|
|
133
|
+
siblings.setdefault(node.parent_id, []).append(node)
|
|
134
|
+
|
|
135
|
+
for group in siblings.values():
|
|
136
|
+
ordered = sorted(group, key=lambda n: n.timestamp or "")
|
|
137
|
+
for i in range(len(ordered) - 1):
|
|
138
|
+
edges.append(ExecutionEdge(
|
|
139
|
+
source_id=ordered[i].node_id,
|
|
140
|
+
target_id=ordered[i + 1].node_id,
|
|
141
|
+
edge_type=EdgeType.NEXT,
|
|
142
|
+
))
|
|
143
|
+
|
|
144
|
+
graph = ExecutionGraph(
|
|
145
|
+
execution_id=execution_id,
|
|
146
|
+
session_id=session_id,
|
|
147
|
+
nodes=list(node_map.values()),
|
|
148
|
+
edges=edges,
|
|
149
|
+
)
|
|
150
|
+
return graph
|
|
151
|
+
|
|
152
|
+
# ── Private helpers ────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
def _span_to_node(
|
|
155
|
+
self,
|
|
156
|
+
span: dict,
|
|
157
|
+
session_id: str | None = None,
|
|
158
|
+
) -> ExecutionNode:
|
|
159
|
+
tokens_raw = span.get("tokens", {}) or {}
|
|
160
|
+
token_usage = TokenUsage(
|
|
161
|
+
prompt=tokens_raw.get("prompt"),
|
|
162
|
+
completion=tokens_raw.get("completion"),
|
|
163
|
+
total=tokens_raw.get("total"),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# session_id: prefer what's on the span, fall back to caller-supplied
|
|
167
|
+
s_id = span.get("session_id") or session_id
|
|
168
|
+
|
|
169
|
+
return ExecutionNode(
|
|
170
|
+
node_id=span["span_id"],
|
|
171
|
+
parent_id=span.get("parent_span_id"),
|
|
172
|
+
execution_id=span.get("execution_id", "unknown"),
|
|
173
|
+
session_id=s_id,
|
|
174
|
+
project_id=span.get("project_id"),
|
|
175
|
+
trace_id=span.get("trace_id", ""),
|
|
176
|
+
|
|
177
|
+
# ── Classification ──────────────────────────────────────────
|
|
178
|
+
node_type=_node_type(span.get("node_type", "Unknown")),
|
|
179
|
+
name=span.get("operation") or span.get("name") or "unnamed", # Fixed: was "name" only
|
|
180
|
+
|
|
181
|
+
# ── LLM-specific ────────────────────────────────────────────
|
|
182
|
+
model=span.get("model"),
|
|
183
|
+
|
|
184
|
+
# ── Tool-specific ───────────────────────────────────────────
|
|
185
|
+
tool_name=span.get("tool_name"),
|
|
186
|
+
tool_desc=_safe_str(span.get("tool_desc"), max_len=500),
|
|
187
|
+
retrieved_docs=_safe_str(span.get("retrieved_docs"), max_len=2000),
|
|
188
|
+
|
|
189
|
+
# ── Tool-calling decision (node-local) ─────────────────────
|
|
190
|
+
available_tools=span.get("available_tools"),
|
|
191
|
+
tool_calls=span.get("tool_calls"),
|
|
192
|
+
|
|
193
|
+
# ── I/O ─────────────────────────────────────────────────────
|
|
194
|
+
input=_safe_str(span.get("input")),
|
|
195
|
+
output=_safe_str(span.get("output")),
|
|
196
|
+
prompt=_safe_str(span.get("prompt")),
|
|
197
|
+
response=_safe_str(span.get("response")),
|
|
198
|
+
|
|
199
|
+
# ── Metrics ─────────────────────────────────────────────────
|
|
200
|
+
latency_ms=span.get("latency_ms", 0.0),
|
|
201
|
+
tokens=token_usage,
|
|
202
|
+
error=span.get("error"),
|
|
203
|
+
|
|
204
|
+
# ── Temporal ────────────────────────────────────────────────
|
|
205
|
+
timestamp=span.get("timestamp"),
|
|
206
|
+
end_timestamp=span.get("end_timestamp"),
|
|
207
|
+
|
|
208
|
+
# ── Metadata ────────────────────────────────────────────────
|
|
209
|
+
# Fixed: was span.get("attributes") — exporter writes "metadata"
|
|
210
|
+
metadata=span.get("metadata") or {},
|
|
211
|
+
status=span.get("status", "UNSET"),
|
|
212
|
+
)
|
graph_builder/models.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
models.py
|
|
3
|
+
---------
|
|
4
|
+
Pydantic models for the execution graph.
|
|
5
|
+
|
|
6
|
+
ExecutionNode — represents a single span in the graph
|
|
7
|
+
ExecutionEdge — represents a relationship between two nodes
|
|
8
|
+
ExecutionGraph — container for nodes + edges with helper methods
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, Field
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NodeType(str, Enum):
|
|
20
|
+
LLM = "LLM"
|
|
21
|
+
Tool = "Tool"
|
|
22
|
+
Retriever = "Retriever"
|
|
23
|
+
Agent = "Agent"
|
|
24
|
+
Memory = "Memory"
|
|
25
|
+
Router = "Router"
|
|
26
|
+
Embedding = "Embedding"
|
|
27
|
+
Reranker = "Reranker"
|
|
28
|
+
Guardrail = "Guardrail"
|
|
29
|
+
Unknown = "Unknown"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class EdgeType(str, Enum):
|
|
33
|
+
CALLS = "CALLS"
|
|
34
|
+
RETURNS = "RETURNS"
|
|
35
|
+
USES = "USES"
|
|
36
|
+
READS = "READS"
|
|
37
|
+
WRITES = "WRITES"
|
|
38
|
+
NEXT = "NEXT"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TokenUsage(BaseModel):
|
|
42
|
+
prompt: Optional[int] = None
|
|
43
|
+
completion: Optional[int] = None
|
|
44
|
+
total: Optional[int] = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ExecutionNode(BaseModel):
|
|
48
|
+
"""One execution span = one graph node."""
|
|
49
|
+
|
|
50
|
+
# ── Identity ────────────────────────────────────────────────────────────
|
|
51
|
+
node_id: str
|
|
52
|
+
parent_id: Optional[str] = None
|
|
53
|
+
execution_id: str # one graph.invoke() call
|
|
54
|
+
session_id: Optional[str] = None # one user conversation / thread
|
|
55
|
+
project_id: Optional[str] = None # for multi-tenancy
|
|
56
|
+
trace_id: str
|
|
57
|
+
|
|
58
|
+
# ── Classification ─────────────────────────────────────────────────────
|
|
59
|
+
node_type: NodeType = NodeType.Unknown
|
|
60
|
+
name: str # operation name (e.g. "supervisor", "ChatOpenAI")
|
|
61
|
+
|
|
62
|
+
# ── LLM-specific ───────────────────────────────────────────────────────
|
|
63
|
+
model: Optional[str] = None # e.g. "gpt-4o-mini"
|
|
64
|
+
|
|
65
|
+
# ── Tool / Retriever-specific ──────────────────────────────────────────
|
|
66
|
+
tool_name: Optional[str] = None
|
|
67
|
+
tool_desc: Optional[str] = None
|
|
68
|
+
retrieved_docs: Optional[str] = None # JSON-serialised list of docs
|
|
69
|
+
|
|
70
|
+
# ── Tool-calling decision (LLM-node-local; no child Tool span needed) ──
|
|
71
|
+
available_tools: Optional[list[Any]] = None # tool schemas offered to the model
|
|
72
|
+
tool_calls: Optional[list[dict]] = None # tool(s) the model actually chose
|
|
73
|
+
|
|
74
|
+
# ── Execution data ─────────────────────────────────────────────────────
|
|
75
|
+
input: Optional[Any] = None
|
|
76
|
+
output: Optional[Any] = None
|
|
77
|
+
prompt: Optional[str] = None
|
|
78
|
+
response: Optional[str] = None
|
|
79
|
+
|
|
80
|
+
# ── Runtime metrics ────────────────────────────────────────────────────
|
|
81
|
+
latency_ms: float = 0.0
|
|
82
|
+
tokens: TokenUsage = Field(default_factory=TokenUsage)
|
|
83
|
+
error: Optional[str] = None
|
|
84
|
+
|
|
85
|
+
# ── Temporal ──────────────────────────────────────────────────────────
|
|
86
|
+
timestamp: Optional[str] = None
|
|
87
|
+
end_timestamp: Optional[str] = None
|
|
88
|
+
|
|
89
|
+
# ── Extra attributes ──────────────────────────────────────────────────
|
|
90
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
91
|
+
status: str = "UNSET"
|
|
92
|
+
|
|
93
|
+
# ── Validation state (filled by Validator Engine) ─────────────────────
|
|
94
|
+
validation_results: list[dict] = Field(default_factory=list)
|
|
95
|
+
|
|
96
|
+
class Config:
|
|
97
|
+
use_enum_values = True
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ExecutionEdge(BaseModel):
|
|
101
|
+
"""Directed relationship between two nodes."""
|
|
102
|
+
|
|
103
|
+
source_id: str
|
|
104
|
+
target_id: str
|
|
105
|
+
edge_type: EdgeType = EdgeType.CALLS
|
|
106
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
107
|
+
|
|
108
|
+
class Config:
|
|
109
|
+
use_enum_values = True
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ExecutionGraph(BaseModel):
|
|
113
|
+
"""Container for the full execution graph of one run."""
|
|
114
|
+
|
|
115
|
+
execution_id: str
|
|
116
|
+
session_id: Optional[str] = None
|
|
117
|
+
project_id: Optional[str] = None
|
|
118
|
+
nodes: list[ExecutionNode] = Field(default_factory=list)
|
|
119
|
+
edges: list[ExecutionEdge] = Field(default_factory=list)
|
|
120
|
+
|
|
121
|
+
# ── Helpers ────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
def get_node(self, node_id: str) -> Optional[ExecutionNode]:
|
|
124
|
+
for node in self.nodes:
|
|
125
|
+
if node.node_id == node_id:
|
|
126
|
+
return node
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
def get_children(self, node_id: str) -> list[ExecutionNode]:
|
|
130
|
+
"""Return all nodes whose parent is node_id."""
|
|
131
|
+
return [n for n in self.nodes if n.parent_id == node_id]
|
|
132
|
+
|
|
133
|
+
def get_root_nodes(self) -> list[ExecutionNode]:
|
|
134
|
+
"""Return nodes with no parent (top-level spans)."""
|
|
135
|
+
return [n for n in self.nodes if n.parent_id is None]
|
|
136
|
+
|
|
137
|
+
def get_nodes_by_type(self, node_type: NodeType | str) -> list[ExecutionNode]:
|
|
138
|
+
nt = node_type.value if isinstance(node_type, NodeType) else node_type
|
|
139
|
+
return [n for n in self.nodes if n.node_type == nt]
|
|
140
|
+
|
|
141
|
+
def get_failed_nodes(self) -> list[ExecutionNode]:
|
|
142
|
+
return [n for n in self.nodes if n.error or n.status == "ERROR"]
|
|
143
|
+
|
|
144
|
+
def summary(self) -> dict:
|
|
145
|
+
total_latency = sum(n.latency_ms for n in self.nodes)
|
|
146
|
+
total_tokens = sum((n.tokens.total or 0) for n in self.nodes)
|
|
147
|
+
return {
|
|
148
|
+
"execution_id": self.execution_id,
|
|
149
|
+
"session_id": self.session_id,
|
|
150
|
+
"total_nodes": len(self.nodes),
|
|
151
|
+
"total_edges": len(self.edges),
|
|
152
|
+
"total_latency_ms": round(total_latency, 2),
|
|
153
|
+
"total_tokens": total_tokens,
|
|
154
|
+
"failed_nodes": len(self.get_failed_nodes()),
|
|
155
|
+
"node_type_counts": {
|
|
156
|
+
nt: len(self.get_nodes_by_type(nt))
|
|
157
|
+
for nt in [e.value for e in NodeType]
|
|
158
|
+
},
|
|
159
|
+
}
|