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
evaluators/models.py ADDED
@@ -0,0 +1,180 @@
1
+ """
2
+ models.py
3
+ ---------
4
+ Pydantic models shared by all 3 evaluators and the RCA Engine.
5
+
6
+ EvaluationResult is the canonical output of every evaluator. Every field
7
+ is designed to be stored as-is in MongoDB, written to JSONL, and fed to the
8
+ RCA Engine without any transformation.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import uuid
14
+ from datetime import datetime, timezone
15
+ from enum import Enum
16
+ from typing import Any, Optional
17
+
18
+ from pydantic import BaseModel, Field
19
+
20
+
21
+ # ── Enums ──────────────────────────────────────────────────────────────────
22
+
23
+ class EvaluationStatus(str, Enum):
24
+ PASS = "PASS"
25
+ FAIL = "FAIL"
26
+ WARNING = "WARNING" # non-blocking issue worth noting
27
+ SKIP = "SKIP" # evaluator not applicable to this node type
28
+
29
+
30
+ class Severity(str, Enum):
31
+ """Severity of a FAIL/WARNING. Only set when status != PASS/SKIP."""
32
+ LOW = "LOW"
33
+ MEDIUM = "MEDIUM"
34
+ HIGH = "HIGH"
35
+ CRITICAL = "CRITICAL"
36
+
37
+
38
+ # ── Core result model ──────────────────────────────────────────────────────
39
+
40
+ class EvaluationResult(BaseModel):
41
+ """
42
+ The standard output of every evaluator.
43
+
44
+ Stored in 3 places:
45
+ 1. MongoDB — as an `evaluations` document referencing its node_id
46
+ 2. JSONL — appended to evaluation_results.jsonl
47
+ 3. Python — returned from evaluator.evaluate(node, graph)
48
+ """
49
+
50
+ # ── Identity ─────────────────────────────────────────────────────────
51
+ evaluation_id: str = Field(default_factory=lambda: str(uuid.uuid4())[:16])
52
+ evaluator: str # "InputEvaluator" | "OutputEvaluator" | "ToolAgentEvaluator"
53
+
54
+ # ── Links back to the execution ───────────────────────────────────────
55
+ node_id: str
56
+ node_name: str
57
+ node_type: str
58
+ execution_id: str
59
+ session_id: Optional[str] = None
60
+
61
+ # ── Verdict ───────────────────────────────────────────────────────────
62
+ status: EvaluationStatus
63
+ severity: Optional[Severity] = None # None when status=PASS or SKIP
64
+ confidence: float = 0.0 # 0.0–1.0
65
+
66
+ # ── Human-readable explanation ────────────────────────────────────────
67
+ reason: str
68
+ suggestion: Optional[str] = None # null when status=PASS
69
+
70
+ # ── Evaluator-specific sub-checks ─────────────────────────────────────
71
+ # Each evaluator populates this with its own structured checks dict.
72
+ checks: dict[str, Any] = Field(default_factory=dict)
73
+
74
+ # ── Operational metadata ──────────────────────────────────────────────
75
+ timestamp: str = Field(
76
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
77
+ )
78
+ metadata: dict[str, Any] = Field(default_factory=dict)
79
+
80
+ class Config:
81
+ use_enum_values = True
82
+
83
+ def to_dict(self) -> dict:
84
+ return self.model_dump()
85
+
86
+ def is_ok(self) -> bool:
87
+ return self.status in (EvaluationStatus.PASS, EvaluationStatus.SKIP)
88
+
89
+ def worst_severity(self) -> str | None:
90
+ """Return severity value string, or None."""
91
+ return self.severity if self.severity else None
92
+
93
+
94
+ # ── Aggregated result for one node ─────────────────────────────────────────
95
+
96
+ class NodeEvaluationSummary(BaseModel):
97
+ """
98
+ Bundles all 3 evaluator results for one ExecutionNode.
99
+ Written to the node document in MongoDB as flat props + JSON blob.
100
+ """
101
+ node_id: str
102
+ node_name: str
103
+ execution_id: str
104
+ node_type: str = ""
105
+
106
+ input_result: Optional[EvaluationResult] = None
107
+ output_result: Optional[EvaluationResult] = None
108
+ tool_agent_result: Optional[EvaluationResult] = None
109
+
110
+ @property
111
+ def overall_status(self) -> str:
112
+ results = [r for r in [self.input_result, self.output_result, self.tool_agent_result] if r]
113
+ statuses = {r.status for r in results}
114
+ if EvaluationStatus.FAIL in statuses:
115
+ return "FAIL"
116
+ if EvaluationStatus.WARNING in statuses:
117
+ return "WARNING"
118
+ return "PASS"
119
+
120
+ @property
121
+ def worst_severity(self) -> str | None:
122
+ order = [Severity.CRITICAL, Severity.HIGH, Severity.MEDIUM, Severity.LOW]
123
+ results = [r for r in [self.input_result, self.output_result, self.tool_agent_result] if r]
124
+ for sev in order:
125
+ if any(r.severity == sev for r in results):
126
+ return sev.value
127
+ return None
128
+
129
+ @property
130
+ def has_hallucination(self) -> bool:
131
+ if self.output_result and self.output_result.checks:
132
+ return bool(self.output_result.checks.get("has_hallucination", False))
133
+ return False
134
+
135
+ @property
136
+ def has_injection(self) -> bool:
137
+ if self.input_result and self.input_result.checks:
138
+ return bool(self.input_result.checks.get("is_prompt_injected", False))
139
+ return False
140
+
141
+ @property
142
+ def has_missing_context(self) -> bool:
143
+ if self.input_result and self.input_result.checks:
144
+ missing = self.input_result.checks.get("missing_context", [])
145
+ return len(missing) > 0
146
+ return False
147
+
148
+ @property
149
+ def tool_selection_correct(self) -> bool | None:
150
+ """
151
+ True/False only for Agent/Router nodes with a judged tool_selection verdict
152
+ (the agent's own decision — the canonical source). None for Tool/Retriever
153
+ nodes (which only carry a redundant copy) and for nodes where the check
154
+ wasn't applicable (verdict N/A).
155
+ """
156
+ if not self.tool_agent_result or not self.tool_agent_result.checks:
157
+ return None
158
+ if self.node_type not in ("Agent", "Router"):
159
+ return None
160
+ ts = self.tool_agent_result.checks.get("tool_selection", {})
161
+ if ts.get("verdict") in (None, "N/A"):
162
+ return None
163
+ return bool(ts.get("is_right_tool"))
164
+
165
+ def all_results(self) -> list[EvaluationResult]:
166
+ return [r for r in [self.input_result, self.output_result, self.tool_agent_result] if r]
167
+
168
+ def flat_eval_props(self) -> dict:
169
+ """Flat boolean properties written directly to the node document for fast filtering."""
170
+ return {
171
+ "eval_overall_status": self.overall_status,
172
+ "eval_severity": self.worst_severity,
173
+ "eval_input_passed": self.input_result.status == "PASS" if self.input_result else None,
174
+ "eval_output_passed": self.output_result.status == "PASS" if self.output_result else None,
175
+ "eval_tool_passed": self.tool_agent_result.status == "PASS" if self.tool_agent_result else None,
176
+ "tool_selection_correct": self.tool_selection_correct,
177
+ "has_hallucination": self.has_hallucination,
178
+ "has_injection": self.has_injection,
179
+ "has_missing_context": self.has_missing_context,
180
+ }
@@ -0,0 +1,247 @@
1
+ """
2
+ output_evaluator.py
3
+ -------------------
4
+ Evaluator 2 — Output Evaluator
5
+
6
+ Validates the OUTPUT of every execution node.
7
+ Sees: node.input (full context) + node.output.
8
+
9
+ Auto-detects output type:
10
+ - Structured → dict/JSON: checks consistency, structure correctness, handoff relevance
11
+ - Unstructured → string: checks hallucination, toxicity, relevance, completeness
12
+
13
+ checks schema for STRUCTURED output:
14
+ {
15
+ "output_type": "structured",
16
+ "is_relevant_to_input": bool,
17
+ "is_structurally_correct": bool,
18
+ "is_internally_consistent": bool,
19
+ "handoff_is_valid": bool | null, # null if no handoff
20
+ "schema_violations": [str],
21
+ "consistency_issues": [str]
22
+ }
23
+
24
+ checks schema for UNSTRUCTURED output:
25
+ {
26
+ "output_type": "unstructured",
27
+ "is_relevant": bool,
28
+ "is_complete": bool,
29
+ "has_hallucination": bool,
30
+ "is_toxic": bool,
31
+ "hallucination_evidence": [str], # specific unsupported claims
32
+ "toxicity_evidence": str | null,
33
+ "completeness_gaps": [str] # what's missing from the answer
34
+ }
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import json
40
+
41
+ from graph_builder.models import ExecutionGraph, ExecutionNode
42
+ from .base_evaluator import BaseEvaluator
43
+ from .models import EvaluationResult, EvaluationStatus, Severity
44
+
45
+
46
+ _PROMPT_STRUCTURED = """\
47
+ You are an AI agent execution auditor. Evaluate the OUTPUT of an AI agent node that produced STRUCTURED (JSON/dict) output.
48
+
49
+ ## Node Being Evaluated
50
+ {node_context}
51
+
52
+ ## Full Context (what this node had access to)
53
+ {full_context}
54
+
55
+ ## Output Produced
56
+ {output}
57
+
58
+ ## Instructions
59
+ Analyze whether this structured output is correct. Check for:
60
+
61
+ 1. **Relevance** — Does the output address the input request? Is it relevant?
62
+ 2. **Structural Correctness** — Is the JSON/dict well-formed? Are required keys present?
63
+ 3. **Internal Consistency** — Are there contradictions within the output itself?
64
+ 4. **Handoff Validity** — If this output routes to another agent (handoff), is that routing decision appropriate given the input?
65
+ 5. **Schema Violations** — List any fields that are wrong type, missing, or extraneous.
66
+
67
+ ## Severity Guide
68
+ - CRITICAL: Output is completely wrong or will corrupt downstream agents
69
+ - HIGH: Major structural violations or highly irrelevant output
70
+ - MEDIUM: Inconsistencies or partially wrong structure
71
+ - LOW: Minor style issues or unnecessary fields
72
+
73
+ ## Response Format
74
+ Respond ONLY with a valid JSON object. No explanation outside the JSON.
75
+
76
+ {{
77
+ "status": "PASS" | "FAIL" | "WARNING",
78
+ "severity": "LOW" | "MEDIUM" | "HIGH" | "CRITICAL" | null,
79
+ "confidence": <float 0.0–1.0>,
80
+ "reason": "<1-2 sentence explanation>",
81
+ "suggestion": "<specific fix or null if PASS>",
82
+ "checks": {{
83
+ "output_type": "structured",
84
+ "is_relevant_to_input": true | false,
85
+ "is_structurally_correct": true | false,
86
+ "is_internally_consistent": true | false,
87
+ "handoff_is_valid": true | false | null,
88
+ "schema_violations": ["<violation1>"],
89
+ "consistency_issues": ["<issue1>"]
90
+ }}
91
+ }}
92
+ """
93
+
94
+ _PROMPT_UNSTRUCTURED = """\
95
+ You are an AI agent execution auditor. Evaluate the OUTPUT of an AI agent node that produced UNSTRUCTURED (text) output.
96
+
97
+ ## Node Being Evaluated
98
+ {node_context}
99
+
100
+ ## Full Context (what this node had access to)
101
+ {full_context}
102
+
103
+ ## Output / Response Produced
104
+ {output}
105
+
106
+ ## Instructions
107
+ Analyze whether this text output is correct. Check for:
108
+
109
+ 1. **Relevance** — Does the response directly address the input question/task?
110
+ 2. **Completeness** — Does it answer fully, or are important parts missing?
111
+ 3. **Hallucination** — Does it make specific factual claims NOT supported by the context provided? List each claim separately.
112
+ 4. **Toxicity** — Does the output contain harmful, offensive, or inappropriate content?
113
+
114
+ ## Important Hallucination Guide
115
+ Only flag hallucination if the output makes specific, verifiable claims about facts, names, numbers, or events that are not present in or supportable by the context. Do NOT flag it as hallucination if the node is doing legitimate reasoning or summarization.
116
+
117
+ ## Severity Guide
118
+ - CRITICAL: Toxic content, or severe hallucination with multiple unsupported facts
119
+ - HIGH: Clear hallucination of key facts, or output is completely off-topic
120
+ - MEDIUM: Partial hallucination or significantly incomplete answer
121
+ - LOW: Minor incompleteness or slightly off-topic
122
+
123
+ ## Response Format
124
+ Respond ONLY with a valid JSON object. No explanation outside the JSON.
125
+
126
+ {{
127
+ "status": "PASS" | "FAIL" | "WARNING",
128
+ "severity": "LOW" | "MEDIUM" | "HIGH" | "CRITICAL" | null,
129
+ "confidence": <float 0.0–1.0>,
130
+ "reason": "<1-2 sentence explanation>",
131
+ "suggestion": "<specific fix or null if PASS>",
132
+ "checks": {{
133
+ "output_type": "unstructured",
134
+ "is_relevant": true | false,
135
+ "is_complete": true | false,
136
+ "has_hallucination": true | false,
137
+ "is_toxic": true | false,
138
+ "hallucination_evidence": ["<specific unsupported claim 1>"],
139
+ "toxicity_evidence": "<description or null>",
140
+ "completeness_gaps": ["<missing item 1>"]
141
+ }}
142
+ }}
143
+ """
144
+
145
+
146
+ def _detect_output_type(output: object) -> str:
147
+ """Return 'structured' if output is a dict/list, else 'unstructured'."""
148
+ if isinstance(output, (dict, list)):
149
+ return "structured"
150
+ if isinstance(output, str):
151
+ stripped = output.strip()
152
+ if stripped.startswith("{") or stripped.startswith("["):
153
+ try:
154
+ json.loads(stripped)
155
+ return "structured"
156
+ except (json.JSONDecodeError, ValueError):
157
+ pass
158
+ return "unstructured"
159
+
160
+
161
+ class OutputEvaluator(BaseEvaluator):
162
+ """Evaluator 2: Validates output quality, hallucination, structure, and handoffs."""
163
+
164
+ name = "OutputEvaluator"
165
+
166
+ def should_run(self, node: ExecutionNode, graph: ExecutionGraph) -> bool:
167
+ # Runs on every node with actual content — a pure lifecycle/
168
+ # bookkeeping span (zero I/O) produced no output to judge.
169
+ return self._has_content(node)
170
+
171
+ def _run(self, node: ExecutionNode, graph: ExecutionGraph) -> EvaluationResult:
172
+ output = node.output or node.response
173
+ output_type = _detect_output_type(output)
174
+
175
+ # Build full context: parent + sibling inputs
176
+ full_context = self._build_full_context(node, graph)
177
+
178
+ if output_type == "structured":
179
+ prompt = _PROMPT_STRUCTURED.format(
180
+ node_context=self._format_node(node),
181
+ full_context=full_context,
182
+ output=self._safe_json(output),
183
+ )
184
+ else:
185
+ prompt = _PROMPT_UNSTRUCTURED.format(
186
+ node_context=self._format_node(node),
187
+ full_context=full_context,
188
+ output=self._safe_json(output)[:3000],
189
+ )
190
+
191
+ parsed = self._call_and_parse(prompt)
192
+
193
+ status_raw = parsed.get("status", "FAIL")
194
+ try:
195
+ status = EvaluationStatus(status_raw)
196
+ except ValueError:
197
+ status = EvaluationStatus.FAIL
198
+
199
+ checks = parsed.get("checks", {})
200
+
201
+ severity = self._severity_from_status(parsed, default=Severity.MEDIUM)
202
+ # Override: toxicity is always CRITICAL
203
+ if checks.get("is_toxic"):
204
+ status = EvaluationStatus.FAIL
205
+ severity = Severity.CRITICAL
206
+
207
+ return EvaluationResult(
208
+ evaluator=self.name,
209
+ node_id=node.node_id,
210
+ node_name=node.name,
211
+ node_type=node.node_type,
212
+ execution_id=node.execution_id,
213
+ session_id=node.session_id,
214
+ status=status,
215
+ severity=severity if status != EvaluationStatus.PASS else None,
216
+ confidence=float(parsed.get("confidence", 0.5)),
217
+ reason=parsed.get("reason", ""),
218
+ suggestion=parsed.get("suggestion"),
219
+ checks=checks,
220
+ )
221
+
222
+ def _build_full_context(self, node: ExecutionNode, graph: ExecutionGraph) -> str:
223
+ """
224
+ Build a human-readable string of everything this node had access to:
225
+ - Parent node's input/output
226
+ - Sibling nodes (same parent) that ran before this node
227
+ """
228
+ lines = []
229
+ parent = self._get_parent(node, graph)
230
+ if parent:
231
+ lines.append(f"[Parent Node: {parent.name} ({parent.node_type})]")
232
+ lines.append(f" Parent Input: {str(parent.input or '')[:1000]}")
233
+ lines.append(f" Parent Output: {str(parent.output or '')[:1000]}")
234
+
235
+ # Siblings that ran before (ordered by timestamp)
236
+ if parent:
237
+ siblings = [
238
+ n for n in graph.get_children(parent.node_id)
239
+ if n.node_id != node.node_id
240
+ and (n.timestamp or "") < (node.timestamp or "")
241
+ ]
242
+ for sib in siblings:
243
+ lines.append(f"\n[Prior Sibling: {sib.name} ({sib.node_type})]")
244
+ lines.append(f" Input: {str(sib.input or '')[:500]}")
245
+ lines.append(f" Output: {str(sib.output or '')[:500]}")
246
+
247
+ return "\n".join(lines) if lines else "(No additional context available)"