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.
Files changed (38) hide show
  1. continuous_intelligence_layer/__init__.py +28 -0
  2. continuous_intelligence_layer/_core/__init__.py +6 -0
  3. continuous_intelligence_layer/_core/exporter.py +517 -0
  4. continuous_intelligence_layer/_core/graph_exporter.py +92 -0
  5. continuous_intelligence_layer/_core/utils.py +170 -0
  6. continuous_intelligence_layer/anthropic/__init__.py +9 -0
  7. continuous_intelligence_layer/anthropic/init.py +232 -0
  8. continuous_intelligence_layer/anthropic/instrumentation.py +91 -0
  9. continuous_intelligence_layer/crewai/__init__.py +9 -0
  10. continuous_intelligence_layer/crewai/init.py +228 -0
  11. continuous_intelligence_layer/crewai/instrumentation.py +83 -0
  12. continuous_intelligence_layer/langgraph/__init__.py +12 -0
  13. continuous_intelligence_layer/langgraph/init.py +253 -0
  14. continuous_intelligence_layer/langgraph/instrumentation.py +71 -0
  15. continuous_intelligence_layer/openai/__init__.py +9 -0
  16. continuous_intelligence_layer/openai/init.py +229 -0
  17. continuous_intelligence_layer/openai/instrumentation.py +67 -0
  18. continuous_intelligence_layer-0.1.0.dist-info/METADATA +633 -0
  19. continuous_intelligence_layer-0.1.0.dist-info/RECORD +38 -0
  20. continuous_intelligence_layer-0.1.0.dist-info/WHEEL +4 -0
  21. continuous_intelligence_layer-0.1.0.dist-info/licenses/LICENSE +21 -0
  22. evaluators/__init__.py +32 -0
  23. evaluators/base_evaluator.py +181 -0
  24. evaluators/crewai_input_evaluator.py +249 -0
  25. evaluators/input_evaluator.py +121 -0
  26. evaluators/models.py +180 -0
  27. evaluators/output_evaluator.py +247 -0
  28. evaluators/runner.py +313 -0
  29. evaluators/tool_agent_evaluator.py +305 -0
  30. graph_builder/__init__.py +6 -0
  31. graph_builder/builder.py +212 -0
  32. graph_builder/models.py +159 -0
  33. graph_builder/mongo_store.py +588 -0
  34. llm_router/__init__.py +3 -0
  35. llm_router/router.py +71 -0
  36. rca_engine/__init__.py +5 -0
  37. rca_engine/incident_report.py +162 -0
  38. rca_engine/rca_engine.py +202 -0
@@ -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
+ )
@@ -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
+ }