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,170 @@
1
+ """
2
+ utils.py
3
+ --------
4
+ Helpers for reading and displaying traces after a run.
5
+
6
+ These are convenience functions โ€” the SDK works without them.
7
+ Use them for quick terminal inspection or to load traces into pandas/analysis.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+
17
+ def load_traces(path: str) -> list[dict]:
18
+ """
19
+ Load all spans from a JSONL trace file into a list of dicts.
20
+
21
+ Parameters
22
+ ----------
23
+ path :
24
+ Path to the .jsonl file written by ExecutionSpanExporter.
25
+
26
+ Returns
27
+ -------
28
+ list[dict]
29
+ One dict per span, in the order they were written.
30
+
31
+ Example
32
+ -------
33
+ records = load_traces("./execution_trace.jsonl")
34
+ llm_spans = [r for r in records if r["node_type"] == "LLM"]
35
+ total_tokens = sum(r["tokens"]["total"] or 0 for r in llm_spans)
36
+ """
37
+ records: list[dict] = []
38
+ p = Path(path)
39
+ if not p.exists():
40
+ return records
41
+ with open(p, encoding="utf-8") as fh:
42
+ for line in fh:
43
+ line = line.strip()
44
+ if line:
45
+ try:
46
+ records.append(json.loads(line))
47
+ except json.JSONDecodeError:
48
+ pass # skip malformed lines
49
+ return records
50
+
51
+
52
+ def print_trace_summary(path: str) -> None:
53
+ """
54
+ Print a human-readable summary of all spans in a JSONL trace file.
55
+
56
+ Shows the call tree with indentation (root spans vs child spans),
57
+ node type, operation name, latency, token usage, and errors.
58
+
59
+ Parameters
60
+ ----------
61
+ path :
62
+ Path to the .jsonl file written by ExecutionSpanExporter.
63
+ """
64
+ records = load_traces(path)
65
+ if not records:
66
+ print(f"\n[continuous-intelligence-layer] No spans found in {path}")
67
+ return
68
+
69
+ # Count by type
70
+ type_counts: dict[str, int] = {}
71
+ total_tokens = 0
72
+ total_latency = 0.0
73
+ errors = 0
74
+
75
+ print(f"\n{'โ”'*70}")
76
+ print(f" ๐Ÿ“Š Execution Trace ยท {path}")
77
+ print(f" Total Spans: {len(records)}")
78
+ print(f"{'โ”'*70}")
79
+
80
+ # Build a parent lookup for indentation
81
+ span_ids = {r["span_id"] for r in records}
82
+
83
+ for r in records:
84
+ node_type = r.get("node_type", "Unknown")
85
+ operation = r.get("operation", "?")
86
+ latency = r.get("latency_ms", 0.0)
87
+ status = r.get("status", "UNSET")
88
+ error = r.get("error")
89
+ tokens = r.get("tokens", {}) or {}
90
+ tok_total = tokens.get("total")
91
+ model = r.get("model") or ""
92
+ tool = r.get("tool_name") or ""
93
+ parent_id = r.get("parent_span_id")
94
+
95
+ # Indent child spans
96
+ indent = " " if (parent_id and parent_id in span_ids) else ""
97
+
98
+ # Status icon
99
+ if status == "OK":
100
+ icon = "โœ…"
101
+ elif status == "ERROR":
102
+ icon = "โŒ"
103
+ else:
104
+ icon = "โšช"
105
+
106
+ # Extra label
107
+ extra = ""
108
+ if model:
109
+ extra = f" [{model}]"
110
+ elif tool:
111
+ extra = f" [{tool}]"
112
+
113
+ tok_str = f" tokens={tok_total}" if tok_total is not None else ""
114
+
115
+ print(
116
+ f" {indent}{icon} [{node_type:<10}] {operation:<38}"
117
+ f"{latency:>8.1f} ms{tok_str}{extra}"
118
+ )
119
+
120
+ if error:
121
+ print(f" {indent} โš  Error: {str(error)[:80]}")
122
+
123
+ # Accumulate stats
124
+ type_counts[node_type] = type_counts.get(node_type, 0) + 1
125
+ if tok_total:
126
+ total_tokens += tok_total
127
+ total_latency += latency
128
+ if status == "ERROR":
129
+ errors += 1
130
+
131
+ print(f"{'โ”'*70}")
132
+ print(f" Node types : {type_counts}")
133
+ print(f" Total tokens: {total_tokens}")
134
+ print(f" Total latency: {total_latency:.1f} ms")
135
+ print(f" Errors : {errors}")
136
+ print(f"{'โ”'*70}\n")
137
+
138
+
139
+ def spans_to_dataframe(path: str) -> Any:
140
+ """
141
+ Load traces into a pandas DataFrame.
142
+
143
+ Requires pandas to be installed. Returns None if pandas is not available.
144
+
145
+ Parameters
146
+ ----------
147
+ path : str
148
+ Path to the JSONL trace file.
149
+ """
150
+ try:
151
+ import pandas as pd # type: ignore[import]
152
+ except ImportError:
153
+ print("[continuous-intelligence-layer] pandas not installed. Run: pip install pandas")
154
+ return None
155
+
156
+ records = load_traces(path)
157
+ if not records:
158
+ return pd.DataFrame()
159
+
160
+ # Flatten the tokens dict into columns
161
+ rows = []
162
+ for r in records:
163
+ flat = dict(r)
164
+ tokens = flat.pop("tokens", {}) or {}
165
+ flat["tokens_prompt"] = tokens.get("prompt")
166
+ flat["tokens_completion"] = tokens.get("completion")
167
+ flat["tokens_total"] = tokens.get("total")
168
+ rows.append(flat)
169
+
170
+ return pd.DataFrame(rows)
@@ -0,0 +1,9 @@
1
+ """Direct Anthropic SDK integration for continuous_intelligence_layer.
2
+
3
+ from continuous_intelligence_layer.anthropic import init
4
+ init(api_key=...)
5
+ """
6
+ from .init import init, reset
7
+ from .._core.utils import load_traces, print_trace_summary
8
+
9
+ __all__ = ["init", "reset", "load_traces", "print_trace_summary"]
@@ -0,0 +1,232 @@
1
+ """
2
+ init.py
3
+ -------
4
+ The public entry point for direct Anthropic SDK agents.
5
+
6
+ from continuous_intelligence_layer.anthropic import init
7
+ init(api_key=...)
8
+
9
+ That's all the user needs to write. Everything else is automatic.
10
+
11
+ How it works (internals)
12
+ ------------------------
13
+ 1. We create an ExecutionSpanExporter โ€” our custom JSONL writer.
14
+ 2. We build an OpenTelemetry TracerProvider and attach the exporter via a
15
+ SimpleSpanProcessor. SimpleSpanProcessor calls exporter.export() *synchronously*
16
+ on every span that finishes, so nothing is lost even if the process exits abruptly.
17
+ 3. We set this provider as the GLOBAL OTel provider for the Python process.
18
+ Any library that uses `opentelemetry.trace.get_tracer(...)` will now use it.
19
+ 4. We call instrument(provider) which activates the OpenInference
20
+ AnthropicInstrumentor (see instrumentation.py). This patches the
21
+ `anthropic.resources.messages.Messages`/`AsyncMessages` classes to emit
22
+ an OTel LLM span (with tool_calls metadata, normalized token counts) for
23
+ every `messages.create`/`.stream`/`.parse` call โ€” completely
24
+ automatically, regardless of which file in the caller's codebase
25
+ instantiates `anthropic.Anthropic()` or issues the call.
26
+ 5. When the user's agent runs, spans flow:
27
+ anthropic SDK โ†’ OpenInference patch โ†’ OTel SDK โ†’ SimpleSpanProcessor
28
+ โ†’ ExecutionSpanExporter.export() โ†’ JSONL file on disk
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import uuid
34
+ import requests
35
+
36
+ from opentelemetry import trace
37
+ from opentelemetry.sdk.resources import Resource
38
+ from opentelemetry.sdk.trace import TracerProvider
39
+ from opentelemetry.sdk.trace.export import SimpleSpanProcessor
40
+
41
+ from .._core.exporter import ExecutionSpanExporter
42
+ from .instrumentation import instrument
43
+
44
+ # Guard against double-init in the same process
45
+ _INITIALIZED: bool = False
46
+ _PROVIDER: TracerProvider | None = None
47
+
48
+
49
+ def init(
50
+ api_key: str,
51
+ output_path: str = "./execution_trace.jsonl",
52
+ service_name: str = "continuous-intelligence-layer",
53
+ execution_id: str | None = None,
54
+ session_id: str | None = None,
55
+ store: str | None = "mongodb",
56
+ write_jsonl: bool = False,
57
+ verbose: bool = True,
58
+ base_url: str = "http://localhost:8080",
59
+ run_evaluations: bool = True,
60
+ evaluation_api_key: str = "",
61
+ evaluation_provider: str = "openai",
62
+ evaluation_model: str = "gpt-5.6-luna",
63
+ ) -> dict:
64
+ """
65
+ Initialize the Continuous Intelligence Layer SDK for direct Anthropic SDK agents.
66
+
67
+ Call this ONCE, at the very top of your script, before instantiating any
68
+ `anthropic.Anthropic()` client. No other changes to your agent code are
69
+ required.
70
+
71
+ Parameters
72
+ ----------
73
+ output_path :
74
+ Path to the JSONL file where spans will be appended, if
75
+ ``write_jsonl=True``. File is created automatically; parent dirs are
76
+ created if needed. Each call to init() with a new execution_id
77
+ appends to the same file, so you can accumulate traces from multiple
78
+ runs.
79
+ write_jsonl :
80
+ If True, also append every span to a local JSONL file at
81
+ ``output_path``. Defaults to False when ``store="mongodb"`` โ€” MongoDB
82
+ is the single source of truth by default, and JSONL is only an
83
+ opt-in local debug/backup mechanism. Automatically forced on when
84
+ ``store`` is not "mongodb" (otherwise nothing would be persisted).
85
+ service_name :
86
+ An arbitrary label written to every span as ``service.name``.
87
+ Useful when you have multiple agents writing to the same file.
88
+ execution_id :
89
+ A short identifier for *this* pipeline run. Auto-generated (12-char
90
+ UUID fragment) if not provided. Stored on every span as
91
+ ``execution_id``, so you can filter a single run later.
92
+ session_id :
93
+ Identifier for the conversational session/thread.
94
+ store :
95
+ Optional. If set to "mongodb", also buffers spans and saves them to
96
+ MongoDB on shutdown.
97
+ verbose :
98
+ Print a one-line confirmation when init succeeds.
99
+ run_evaluations :
100
+ If True (the default), evaluations and RCA run automatically on the
101
+ backend right after the trace finishes ingesting โ€” no need to call
102
+ the ``run-evaluations`` endpoint or click "Run Evaluation" in the UI
103
+ separately. Pass False to keep today's behavior: evaluations only
104
+ run when triggered manually.
105
+
106
+ Returns
107
+ -------
108
+ dict
109
+ ``{"execution_id": str, "output_path": str}``
110
+ Keep a reference if you need to correlate spans to a run ID later.
111
+
112
+ Notes
113
+ -----
114
+ Calling init() a second time in the same process is a no-op โ€” the existing
115
+ provider is reused. If you genuinely need a fresh provider (e.g., in
116
+ tests), call ``reset()`` first.
117
+ """
118
+ global _INITIALIZED, _PROVIDER
119
+
120
+ if _INITIALIZED:
121
+ if verbose:
122
+ print(
123
+ f"[continuous-intelligence-layer] โš  Already initialized โ€” skipping. "
124
+ f"Call reset() first if you need a fresh provider."
125
+ )
126
+ return {"execution_id": execution_id or "unknown", "output_path": output_path}
127
+
128
+ execution_id = execution_id or str(uuid.uuid4())[:12]
129
+ session_id = session_id or str(uuid.uuid4())[:12]
130
+
131
+ # โ”€โ”€ Step 1: Build the span exporter โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
132
+ write_jsonl = write_jsonl or store != "mongodb"
133
+ exporter = ExecutionSpanExporter(
134
+ output_path=output_path,
135
+ execution_id=execution_id,
136
+ session_id=session_id,
137
+ )
138
+
139
+ # โ”€โ”€ Step 2: Sync Project & Build TracerProvider โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
140
+ if not api_key:
141
+ raise ValueError("API Key is not provided. An API Key is required to initialize the SDK.")
142
+
143
+ if not evaluation_api_key:
144
+ raise ValueError(
145
+ "evaluation_api_key is not provided. An LLM API key (OpenAI, Anthropic, "
146
+ "or Gemini) is required to initialize the SDK -- it's used to run "
147
+ "evaluations and RCA on your traces. We never use our own key for your work."
148
+ )
149
+
150
+ try:
151
+ res = requests.post(
152
+ f"{base_url}/projects/sync",
153
+ json={
154
+ "name": service_name,
155
+ "execution_id": execution_id,
156
+ "evaluation_provider": evaluation_provider,
157
+ "evaluation_model": evaluation_model,
158
+ "evaluation_api_key": evaluation_api_key,
159
+ },
160
+ headers={"x-api-key": api_key}
161
+ )
162
+ if res.status_code == 401:
163
+ raise ValueError("API Key is not valid.")
164
+ res.raise_for_status()
165
+ project_id = res.json()["id"]
166
+ except requests.exceptions.RequestException as e:
167
+ raise ValueError(f"Failed to verify API key with backend: {e}")
168
+
169
+ resource_attrs = {
170
+ "service.name": service_name,
171
+ "project.id": project_id,
172
+ "execution.id": execution_id,
173
+ "sdk.name": "continuous-intelligence-layer",
174
+ "sdk.version": "0.1.0",
175
+ }
176
+ if session_id:
177
+ resource_attrs["session.id"] = session_id
178
+
179
+ resource = Resource.create(resource_attrs)
180
+ provider = TracerProvider(resource=resource)
181
+
182
+ # โ”€โ”€ Step 3: Attach SimpleSpanProcessor โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
183
+ if write_jsonl:
184
+ provider.add_span_processor(SimpleSpanProcessor(exporter))
185
+
186
+ if store == "mongodb":
187
+ from .._core.graph_exporter import GraphSpanExporter
188
+ graph_exporter = GraphSpanExporter(
189
+ exporter,
190
+ session_id=session_id,
191
+ base_url=base_url,
192
+ api_key=api_key,
193
+ project_id=project_id,
194
+ run_evaluations=run_evaluations,
195
+ )
196
+ provider.add_span_processor(SimpleSpanProcessor(graph_exporter))
197
+
198
+ # โ”€โ”€ Step 4: Set as global OTel provider โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
199
+ trace.set_tracer_provider(provider)
200
+ _PROVIDER = provider
201
+
202
+ # โ”€โ”€ Step 5: Auto-instrument the direct Anthropic SDK โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
203
+ instrument(provider)
204
+
205
+ _INITIALIZED = True
206
+
207
+ if verbose:
208
+ store_line = f" Store : MongoDB (project={service_name})\n" if store == "mongodb" else ""
209
+ jsonl_line = f" Output file : {output_path}\n" if write_jsonl else ""
210
+ print(
211
+ f"[continuous-intelligence-layer] โœ… Initialized\n"
212
+ f" Execution ID : {execution_id}\n"
213
+ f"{store_line}"
214
+ f"{jsonl_line}"
215
+ f" Service name : {service_name}"
216
+ )
217
+
218
+ return {"execution_id": execution_id, "output_path": output_path}
219
+
220
+
221
+ def reset() -> None:
222
+ """
223
+ Reset the SDK so init() can be called again.
224
+
225
+ Primarily useful in test suites where each test needs a fresh provider.
226
+ Calling this in production code is rarely needed.
227
+ """
228
+ global _INITIALIZED, _PROVIDER
229
+ from .instrumentation import uninstrument
230
+ uninstrument()
231
+ _INITIALIZED = False
232
+ _PROVIDER = None
@@ -0,0 +1,91 @@
1
+ """
2
+ instrumentation.py
3
+ ------------------
4
+ Activates OpenInference auto-instrumentation for direct Anthropic SDK agents.
5
+
6
+ How OpenInference works internally
7
+ ------------------------------------
8
+ `openinference-instrumentation-anthropic`'s `AnthropicInstrumentor` patches
9
+ the `anthropic.resources.messages.Messages`/`AsyncMessages` (and
10
+ `beta.messages`) classes directly โ€” `Messages.create`, `.stream`, `.parse`,
11
+ and their async variants โ€” not any particular `anthropic.Anthropic()`
12
+ instance. Because the patch lives on the shared class, it's active for every
13
+ client built anywhere in the process (any file, any module, instantiated
14
+ before or after `init()` โ€” only the actual `.create()`/`.stream()` call needs
15
+ to happen after `instrument()` has run), exactly like the direct OpenAI SDK
16
+ integration.
17
+
18
+ Key-name mapping (Anthropic โ†’ OpenInference)
19
+ ---------------------------------------------
20
+ Anthropic's Messages API has different field names than OpenAI's Chat
21
+ Completions API; OpenInference normalizes them onto the same
22
+ semantic-convention keys `_core/exporter.py` already reads โ€” no exporter
23
+ changes were needed to support this framework:
24
+ usage.input_tokens (+ cache_creation_input_tokens + cache_read_input_tokens)
25
+ โ†’ llm.token_count.prompt
26
+ usage.output_tokens โ†’ llm.token_count.completion
27
+ message.model โ†’ llm.model_name
28
+ system param + messages list โ†’ llm.input_messages (role="system" synthesized
29
+ as message 0 from the top-level `system` kwarg,
30
+ which Anthropic โ€” unlike OpenAI โ€” passes
31
+ separately from `messages`)
32
+ response.content blocks โ†’ llm.output_messages
33
+ tool_use content blocks โ†’ llm.output_messages.*.message.tool_calls.*
34
+ tools param โ†’ llm.tools.*.tool.json_schema
35
+
36
+ Anthropic-specific attribute `llm.system = "anthropic"` (openinference.instrumentation.LLM_SYSTEM)
37
+ is also set on every span; it lands in `metadata` since `_core/exporter.py`
38
+ only pulls out the vocabulary it knows about โ€” this is expected, not a gap.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ from opentelemetry.sdk.trace import TracerProvider
44
+
45
+ _instrumentor = None
46
+
47
+
48
+ def instrument(provider: TracerProvider) -> None:
49
+ """
50
+ Activate direct Anthropic SDK OpenInference auto-instrumentation.
51
+
52
+ Parameters
53
+ ----------
54
+ provider :
55
+ The TracerProvider to attach spans to. Must be the same provider that
56
+ was set as the global OTel provider.
57
+ """
58
+ global _instrumentor
59
+ if _instrumentor is not None:
60
+ # Already instrumented โ€” avoid double-patching
61
+ return
62
+
63
+ try:
64
+ from openinference.instrumentation.anthropic import AnthropicInstrumentor
65
+ except ImportError:
66
+ print(
67
+ "[continuous-intelligence-layer] โš ๏ธ openinference-instrumentation-anthropic not found.\n"
68
+ " anthropic spans will NOT be captured automatically.\n"
69
+ " Fix: pip install openinference-instrumentation-anthropic"
70
+ )
71
+ return
72
+
73
+ try:
74
+ instrumentor = AnthropicInstrumentor()
75
+ instrumentor.instrument(tracer_provider=provider)
76
+ _instrumentor = instrumentor
77
+ print("[continuous-intelligence-layer] ๐Ÿ”Œ anthropic auto-instrumentation active.")
78
+ except Exception as exc:
79
+ print(f"[continuous-intelligence-layer] โš ๏ธ Instrumentation of 'anthropic' failed: {exc}")
80
+
81
+
82
+ def uninstrument() -> None:
83
+ """Remove direct Anthropic SDK OpenInference instrumentation."""
84
+ global _instrumentor
85
+ if _instrumentor is None:
86
+ return
87
+ try:
88
+ _instrumentor.uninstrument()
89
+ except Exception:
90
+ pass
91
+ _instrumentor = None
@@ -0,0 +1,9 @@
1
+ """CrewAI integration for continuous_intelligence_layer.
2
+
3
+ from continuous_intelligence_layer.crewai import init
4
+ init(api_key=...)
5
+ """
6
+ from .init import init, reset
7
+ from .._core.utils import load_traces, print_trace_summary
8
+
9
+ __all__ = ["init", "reset", "load_traces", "print_trace_summary"]