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,229 @@
|
|
|
1
|
+
"""
|
|
2
|
+
init.py
|
|
3
|
+
-------
|
|
4
|
+
The public entry point for direct OpenAI SDK agents.
|
|
5
|
+
|
|
6
|
+
from continuous_intelligence_layer.openai import init
|
|
7
|
+
init()
|
|
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
|
+
OpenAIInstrumentor (see instrumentation.py). This patches the
|
|
21
|
+
`openai.OpenAI()` client to emit an OTel LLM span (with tool_calls
|
|
22
|
+
metadata) for every `chat.completions.create` call — completely
|
|
23
|
+
automatically.
|
|
24
|
+
5. When the user's agent runs, spans flow:
|
|
25
|
+
openai SDK → OpenInference patch → OTel SDK → SimpleSpanProcessor
|
|
26
|
+
→ ExecutionSpanExporter.export() → JSONL file on disk
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import uuid
|
|
32
|
+
import requests
|
|
33
|
+
|
|
34
|
+
from opentelemetry import trace
|
|
35
|
+
from opentelemetry.sdk.resources import Resource
|
|
36
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
37
|
+
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
|
38
|
+
|
|
39
|
+
from .._core.exporter import ExecutionSpanExporter
|
|
40
|
+
from .instrumentation import instrument
|
|
41
|
+
|
|
42
|
+
# Guard against double-init in the same process
|
|
43
|
+
_INITIALIZED: bool = False
|
|
44
|
+
_PROVIDER: TracerProvider | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def init(
|
|
48
|
+
api_key: str,
|
|
49
|
+
output_path: str = "./execution_trace.jsonl",
|
|
50
|
+
service_name: str = "continuous-intelligence-layer",
|
|
51
|
+
execution_id: str | None = None,
|
|
52
|
+
session_id: str | None = None,
|
|
53
|
+
store: str | None = "mongodb",
|
|
54
|
+
write_jsonl: bool = False,
|
|
55
|
+
verbose: bool = True,
|
|
56
|
+
base_url: str = "http://localhost:8080",
|
|
57
|
+
run_evaluations: bool = True,
|
|
58
|
+
evaluation_api_key: str = "",
|
|
59
|
+
evaluation_provider: str = "openai",
|
|
60
|
+
evaluation_model: str = "gpt-5.6-luna",
|
|
61
|
+
) -> dict:
|
|
62
|
+
"""
|
|
63
|
+
Initialize the Continuous Intelligence Layer SDK for direct OpenAI SDK agents.
|
|
64
|
+
|
|
65
|
+
Call this ONCE, at the very top of your script, before instantiating any
|
|
66
|
+
`openai.OpenAI()` client. No other changes to your agent code are required.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
output_path :
|
|
71
|
+
Path to the JSONL file where spans will be appended, if
|
|
72
|
+
``write_jsonl=True``. File is created automatically; parent dirs are
|
|
73
|
+
created if needed. Each call to init() with a new execution_id
|
|
74
|
+
appends to the same file, so you can accumulate traces from multiple
|
|
75
|
+
runs.
|
|
76
|
+
write_jsonl :
|
|
77
|
+
If True, also append every span to a local JSONL file at
|
|
78
|
+
``output_path``. Defaults to False when ``store="mongodb"`` — MongoDB
|
|
79
|
+
is the single source of truth by default, and JSONL is only an
|
|
80
|
+
opt-in local debug/backup mechanism. Automatically forced on when
|
|
81
|
+
``store`` is not "mongodb" (otherwise nothing would be persisted).
|
|
82
|
+
service_name :
|
|
83
|
+
An arbitrary label written to every span as ``service.name``.
|
|
84
|
+
Useful when you have multiple agents writing to the same file.
|
|
85
|
+
execution_id :
|
|
86
|
+
A short identifier for *this* pipeline run. Auto-generated (12-char
|
|
87
|
+
UUID fragment) if not provided. Stored on every span as
|
|
88
|
+
``execution_id``, so you can filter a single run later.
|
|
89
|
+
session_id :
|
|
90
|
+
Identifier for the conversational session/thread.
|
|
91
|
+
store :
|
|
92
|
+
Optional. If set to "mongodb", also buffers spans and saves them to
|
|
93
|
+
MongoDB on shutdown.
|
|
94
|
+
verbose :
|
|
95
|
+
Print a one-line confirmation when init succeeds.
|
|
96
|
+
run_evaluations :
|
|
97
|
+
If True (the default), evaluations and RCA run automatically on the
|
|
98
|
+
backend right after the trace finishes ingesting — no need to call
|
|
99
|
+
the ``run-evaluations`` endpoint or click "Run Evaluation" in the UI
|
|
100
|
+
separately. Pass False to keep today's behavior: evaluations only
|
|
101
|
+
run when triggered manually.
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
dict
|
|
106
|
+
``{"execution_id": str, "output_path": str}``
|
|
107
|
+
Keep a reference if you need to correlate spans to a run ID later.
|
|
108
|
+
|
|
109
|
+
Notes
|
|
110
|
+
-----
|
|
111
|
+
Calling init() a second time in the same process is a no-op — the existing
|
|
112
|
+
provider is reused. If you genuinely need a fresh provider (e.g., in
|
|
113
|
+
tests), call ``reset()`` first.
|
|
114
|
+
"""
|
|
115
|
+
global _INITIALIZED, _PROVIDER
|
|
116
|
+
|
|
117
|
+
if _INITIALIZED:
|
|
118
|
+
if verbose:
|
|
119
|
+
print(
|
|
120
|
+
f"[continuous-intelligence-layer] ⚠ Already initialized — skipping. "
|
|
121
|
+
f"Call reset() first if you need a fresh provider."
|
|
122
|
+
)
|
|
123
|
+
return {"execution_id": execution_id or "unknown", "output_path": output_path}
|
|
124
|
+
|
|
125
|
+
execution_id = execution_id or str(uuid.uuid4())[:12]
|
|
126
|
+
session_id = session_id or str(uuid.uuid4())[:12]
|
|
127
|
+
|
|
128
|
+
# ── Step 1: Build the span exporter ─────────────────────────────────────
|
|
129
|
+
write_jsonl = write_jsonl or store != "mongodb"
|
|
130
|
+
exporter = ExecutionSpanExporter(
|
|
131
|
+
output_path=output_path,
|
|
132
|
+
execution_id=execution_id,
|
|
133
|
+
session_id=session_id,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# ── Step 2: Sync Project & Build TracerProvider ────────────────────────
|
|
137
|
+
if not api_key:
|
|
138
|
+
raise ValueError("API Key is not provided. An API Key is required to initialize the SDK.")
|
|
139
|
+
|
|
140
|
+
if not evaluation_api_key:
|
|
141
|
+
raise ValueError(
|
|
142
|
+
"evaluation_api_key is not provided. An LLM API key (OpenAI, Anthropic, "
|
|
143
|
+
"or Gemini) is required to initialize the SDK -- it's used to run "
|
|
144
|
+
"evaluations and RCA on your traces. We never use our own key for your work."
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
res = requests.post(
|
|
149
|
+
f"{base_url}/projects/sync",
|
|
150
|
+
json={
|
|
151
|
+
"name": service_name,
|
|
152
|
+
"execution_id": execution_id,
|
|
153
|
+
"evaluation_provider": evaluation_provider,
|
|
154
|
+
"evaluation_model": evaluation_model,
|
|
155
|
+
"evaluation_api_key": evaluation_api_key,
|
|
156
|
+
},
|
|
157
|
+
headers={"x-api-key": api_key}
|
|
158
|
+
)
|
|
159
|
+
if res.status_code == 401:
|
|
160
|
+
raise ValueError("API Key is not valid.")
|
|
161
|
+
res.raise_for_status()
|
|
162
|
+
project_id = res.json()["id"]
|
|
163
|
+
except requests.exceptions.RequestException as e:
|
|
164
|
+
raise ValueError(f"Failed to verify API key with backend: {e}")
|
|
165
|
+
|
|
166
|
+
resource_attrs = {
|
|
167
|
+
"service.name": service_name,
|
|
168
|
+
"project.id": project_id,
|
|
169
|
+
"execution.id": execution_id,
|
|
170
|
+
"sdk.name": "continuous-intelligence-layer",
|
|
171
|
+
"sdk.version": "0.1.0",
|
|
172
|
+
}
|
|
173
|
+
if session_id:
|
|
174
|
+
resource_attrs["session.id"] = session_id
|
|
175
|
+
|
|
176
|
+
resource = Resource.create(resource_attrs)
|
|
177
|
+
provider = TracerProvider(resource=resource)
|
|
178
|
+
|
|
179
|
+
# ── Step 3: Attach SimpleSpanProcessor ────────────────────────────────
|
|
180
|
+
if write_jsonl:
|
|
181
|
+
provider.add_span_processor(SimpleSpanProcessor(exporter))
|
|
182
|
+
|
|
183
|
+
if store == "mongodb":
|
|
184
|
+
from .._core.graph_exporter import GraphSpanExporter
|
|
185
|
+
graph_exporter = GraphSpanExporter(
|
|
186
|
+
exporter,
|
|
187
|
+
session_id=session_id,
|
|
188
|
+
base_url=base_url,
|
|
189
|
+
api_key=api_key,
|
|
190
|
+
project_id=project_id,
|
|
191
|
+
run_evaluations=run_evaluations,
|
|
192
|
+
)
|
|
193
|
+
provider.add_span_processor(SimpleSpanProcessor(graph_exporter))
|
|
194
|
+
|
|
195
|
+
# ── Step 4: Set as global OTel provider ───────────────────────────────
|
|
196
|
+
trace.set_tracer_provider(provider)
|
|
197
|
+
_PROVIDER = provider
|
|
198
|
+
|
|
199
|
+
# ── Step 5: Auto-instrument the direct OpenAI SDK ──────────────────────
|
|
200
|
+
instrument(provider)
|
|
201
|
+
|
|
202
|
+
_INITIALIZED = True
|
|
203
|
+
|
|
204
|
+
if verbose:
|
|
205
|
+
store_line = f" Store : MongoDB (project={service_name})\n" if store == "mongodb" else ""
|
|
206
|
+
jsonl_line = f" Output file : {output_path}\n" if write_jsonl else ""
|
|
207
|
+
print(
|
|
208
|
+
f"[continuous-intelligence-layer] ✅ Initialized\n"
|
|
209
|
+
f" Execution ID : {execution_id}\n"
|
|
210
|
+
f"{store_line}"
|
|
211
|
+
f"{jsonl_line}"
|
|
212
|
+
f" Service name : {service_name}"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return {"execution_id": execution_id, "output_path": output_path}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def reset() -> None:
|
|
219
|
+
"""
|
|
220
|
+
Reset the SDK so init() can be called again.
|
|
221
|
+
|
|
222
|
+
Primarily useful in test suites where each test needs a fresh provider.
|
|
223
|
+
Calling this in production code is rarely needed.
|
|
224
|
+
"""
|
|
225
|
+
global _INITIALIZED, _PROVIDER
|
|
226
|
+
from .instrumentation import uninstrument
|
|
227
|
+
uninstrument()
|
|
228
|
+
_INITIALIZED = False
|
|
229
|
+
_PROVIDER = None
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
instrumentation.py
|
|
3
|
+
------------------
|
|
4
|
+
Activates OpenInference auto-instrumentation for direct OpenAI SDK agents.
|
|
5
|
+
|
|
6
|
+
How OpenInference works internally
|
|
7
|
+
------------------------------------
|
|
8
|
+
The instrumentor patches the `openai.OpenAI()` client at import/instantiation
|
|
9
|
+
time and emits OTel spans using the vendor-neutral semantic-convention
|
|
10
|
+
attribute names (openinference.span.kind, input.value, llm.model_name,
|
|
11
|
+
tool.name, etc. — see `continuous_intelligence_layer/_core/exporter.py`).
|
|
12
|
+
Because every OpenInference instrumentor speaks that shared vocabulary, the
|
|
13
|
+
rest of the SDK (`_core/`, `graph_builder/`, `evaluators/`, `rca_engine/`)
|
|
14
|
+
doesn't need to know which framework produced a given span.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
20
|
+
|
|
21
|
+
_instrumentor = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def instrument(provider: TracerProvider) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Activate direct OpenAI SDK OpenInference auto-instrumentation.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
provider :
|
|
31
|
+
The TracerProvider to attach spans to. Must be the same provider that
|
|
32
|
+
was set as the global OTel provider.
|
|
33
|
+
"""
|
|
34
|
+
global _instrumentor
|
|
35
|
+
if _instrumentor is not None:
|
|
36
|
+
# Already instrumented — avoid double-patching
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
from openinference.instrumentation.openai import OpenAIInstrumentor
|
|
41
|
+
except ImportError:
|
|
42
|
+
print(
|
|
43
|
+
"[continuous-intelligence-layer] ⚠️ openinference-instrumentation-openai not found.\n"
|
|
44
|
+
" openai spans will NOT be captured automatically.\n"
|
|
45
|
+
" Fix: pip install openinference-instrumentation-openai"
|
|
46
|
+
)
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
instrumentor = OpenAIInstrumentor()
|
|
51
|
+
instrumentor.instrument(tracer_provider=provider)
|
|
52
|
+
_instrumentor = instrumentor
|
|
53
|
+
print("[continuous-intelligence-layer] 🔌 openai auto-instrumentation active.")
|
|
54
|
+
except Exception as exc:
|
|
55
|
+
print(f"[continuous-intelligence-layer] ⚠️ Instrumentation of 'openai' failed: {exc}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def uninstrument() -> None:
|
|
59
|
+
"""Remove direct OpenAI SDK OpenInference instrumentation."""
|
|
60
|
+
global _instrumentor
|
|
61
|
+
if _instrumentor is None:
|
|
62
|
+
return
|
|
63
|
+
try:
|
|
64
|
+
_instrumentor.uninstrument()
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
_instrumentor = None
|