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,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
init.py
|
|
3
|
+
-------
|
|
4
|
+
The public entry point for CrewAI agents.
|
|
5
|
+
|
|
6
|
+
from continuous_intelligence_layer.crewai 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
|
+
CrewAIInstrumentor (see instrumentation.py). This monkey-patches CrewAI's
|
|
21
|
+
internals to emit OTel spans for every crew kickoff, agent task, LLM
|
|
22
|
+
call, tool call, etc. — completely automatically.
|
|
23
|
+
5. When the user's agent runs, spans flow:
|
|
24
|
+
CrewAI → OpenInference patch → OTel SDK → SimpleSpanProcessor
|
|
25
|
+
→ ExecutionSpanExporter.export() → JSONL file on disk
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import uuid
|
|
31
|
+
import requests
|
|
32
|
+
|
|
33
|
+
from opentelemetry import trace
|
|
34
|
+
from opentelemetry.sdk.resources import Resource
|
|
35
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
36
|
+
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
|
37
|
+
|
|
38
|
+
from .._core.exporter import ExecutionSpanExporter
|
|
39
|
+
from .instrumentation import instrument
|
|
40
|
+
|
|
41
|
+
# Guard against double-init in the same process
|
|
42
|
+
_INITIALIZED: bool = False
|
|
43
|
+
_PROVIDER: TracerProvider | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def init(
|
|
47
|
+
api_key: str,
|
|
48
|
+
output_path: str = "./execution_trace.jsonl",
|
|
49
|
+
service_name: str = "continuous-intelligence-layer",
|
|
50
|
+
execution_id: str | None = None,
|
|
51
|
+
session_id: str | None = None,
|
|
52
|
+
store: str | None = "mongodb",
|
|
53
|
+
write_jsonl: bool = False,
|
|
54
|
+
verbose: bool = True,
|
|
55
|
+
base_url: str = "http://localhost:8080",
|
|
56
|
+
run_evaluations: bool = True,
|
|
57
|
+
evaluation_api_key: str = "",
|
|
58
|
+
evaluation_provider: str = "openai",
|
|
59
|
+
evaluation_model: str = "gpt-5.6-luna",
|
|
60
|
+
) -> dict:
|
|
61
|
+
"""
|
|
62
|
+
Initialize the Continuous Intelligence Layer SDK for CrewAI.
|
|
63
|
+
|
|
64
|
+
Call this ONCE, at the very top of your script, before creating or running
|
|
65
|
+
any Crew. No other changes to your agent code are required.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
output_path :
|
|
70
|
+
Path to the JSONL file where spans will be appended, if
|
|
71
|
+
``write_jsonl=True``. File is created automatically; parent dirs are
|
|
72
|
+
created if needed. Each call to init() with a new execution_id
|
|
73
|
+
appends to the same file, so you can accumulate traces from multiple
|
|
74
|
+
runs.
|
|
75
|
+
write_jsonl :
|
|
76
|
+
If True, also append every span to a local JSONL file at
|
|
77
|
+
``output_path``. Defaults to False when ``store="mongodb"`` — MongoDB
|
|
78
|
+
is the single source of truth by default, and JSONL is only an
|
|
79
|
+
opt-in local debug/backup mechanism. Automatically forced on when
|
|
80
|
+
``store`` is not "mongodb" (otherwise nothing would be persisted).
|
|
81
|
+
service_name :
|
|
82
|
+
An arbitrary label written to every span as ``service.name``.
|
|
83
|
+
Useful when you have multiple agents writing to the same file.
|
|
84
|
+
execution_id :
|
|
85
|
+
A short identifier for *this* pipeline run. Auto-generated (12-char
|
|
86
|
+
UUID fragment) if not provided. Stored on every span as
|
|
87
|
+
``execution_id``, so you can filter a single run later.
|
|
88
|
+
session_id :
|
|
89
|
+
Identifier for the conversational session/thread.
|
|
90
|
+
store :
|
|
91
|
+
Optional. If set to "mongodb", also buffers spans and saves them to
|
|
92
|
+
MongoDB on shutdown.
|
|
93
|
+
verbose :
|
|
94
|
+
Print a one-line confirmation when init succeeds.
|
|
95
|
+
run_evaluations :
|
|
96
|
+
If True (the default), evaluations and RCA run automatically on the
|
|
97
|
+
backend right after the trace finishes ingesting — no need to call
|
|
98
|
+
the ``run-evaluations`` endpoint or click "Run Evaluation" in the UI
|
|
99
|
+
separately. Pass False to keep today's behavior: evaluations only
|
|
100
|
+
run when triggered manually.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
dict
|
|
105
|
+
``{"execution_id": str, "output_path": str}``
|
|
106
|
+
Keep a reference if you need to correlate spans to a run ID later.
|
|
107
|
+
|
|
108
|
+
Notes
|
|
109
|
+
-----
|
|
110
|
+
Calling init() a second time in the same process is a no-op — the existing
|
|
111
|
+
provider is reused. If you genuinely need a fresh provider (e.g., in
|
|
112
|
+
tests), call ``reset()`` first.
|
|
113
|
+
"""
|
|
114
|
+
global _INITIALIZED, _PROVIDER
|
|
115
|
+
|
|
116
|
+
if _INITIALIZED:
|
|
117
|
+
if verbose:
|
|
118
|
+
print(
|
|
119
|
+
f"[continuous-intelligence-layer] ⚠ Already initialized — skipping. "
|
|
120
|
+
f"Call reset() first if you need a fresh provider."
|
|
121
|
+
)
|
|
122
|
+
return {"execution_id": execution_id or "unknown", "output_path": output_path}
|
|
123
|
+
|
|
124
|
+
execution_id = execution_id or str(uuid.uuid4())[:12]
|
|
125
|
+
session_id = session_id or str(uuid.uuid4())[:12]
|
|
126
|
+
|
|
127
|
+
# ── Step 1: Build the span exporter ─────────────────────────────────────
|
|
128
|
+
write_jsonl = write_jsonl or store != "mongodb"
|
|
129
|
+
exporter = ExecutionSpanExporter(
|
|
130
|
+
output_path=output_path,
|
|
131
|
+
execution_id=execution_id,
|
|
132
|
+
session_id=session_id,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# ── Step 2: Sync Project & Build TracerProvider ────────────────────────
|
|
136
|
+
if not api_key:
|
|
137
|
+
raise ValueError("API Key is not provided. An API Key is required to initialize the SDK.")
|
|
138
|
+
|
|
139
|
+
if not evaluation_api_key:
|
|
140
|
+
raise ValueError(
|
|
141
|
+
"evaluation_api_key is not provided. An LLM API key (OpenAI, Anthropic, "
|
|
142
|
+
"or Gemini) is required to initialize the SDK -- it's used to run "
|
|
143
|
+
"evaluations and RCA on your traces. We never use our own key for your work."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
res = requests.post(
|
|
148
|
+
f"{base_url}/projects/sync",
|
|
149
|
+
json={
|
|
150
|
+
"name": service_name,
|
|
151
|
+
"execution_id": execution_id,
|
|
152
|
+
"evaluation_provider": evaluation_provider,
|
|
153
|
+
"evaluation_model": evaluation_model,
|
|
154
|
+
"evaluation_api_key": evaluation_api_key,
|
|
155
|
+
},
|
|
156
|
+
headers={"x-api-key": api_key}
|
|
157
|
+
)
|
|
158
|
+
if res.status_code == 401:
|
|
159
|
+
raise ValueError("API Key is not valid.")
|
|
160
|
+
res.raise_for_status()
|
|
161
|
+
project_id = res.json()["id"]
|
|
162
|
+
except requests.exceptions.RequestException as e:
|
|
163
|
+
raise ValueError(f"Failed to verify API key with backend: {e}")
|
|
164
|
+
|
|
165
|
+
resource_attrs = {
|
|
166
|
+
"service.name": service_name,
|
|
167
|
+
"project.id": project_id,
|
|
168
|
+
"execution.id": execution_id,
|
|
169
|
+
"sdk.name": "continuous-intelligence-layer",
|
|
170
|
+
"sdk.version": "0.1.0",
|
|
171
|
+
}
|
|
172
|
+
if session_id:
|
|
173
|
+
resource_attrs["session.id"] = session_id
|
|
174
|
+
|
|
175
|
+
resource = Resource.create(resource_attrs)
|
|
176
|
+
provider = TracerProvider(resource=resource)
|
|
177
|
+
|
|
178
|
+
# ── Step 3: Attach SimpleSpanProcessor ────────────────────────────────
|
|
179
|
+
if write_jsonl:
|
|
180
|
+
provider.add_span_processor(SimpleSpanProcessor(exporter))
|
|
181
|
+
|
|
182
|
+
if store == "mongodb":
|
|
183
|
+
from .._core.graph_exporter import GraphSpanExporter
|
|
184
|
+
graph_exporter = GraphSpanExporter(
|
|
185
|
+
exporter,
|
|
186
|
+
session_id=session_id,
|
|
187
|
+
base_url=base_url,
|
|
188
|
+
api_key=api_key,
|
|
189
|
+
project_id=project_id,
|
|
190
|
+
run_evaluations=run_evaluations,
|
|
191
|
+
)
|
|
192
|
+
provider.add_span_processor(SimpleSpanProcessor(graph_exporter))
|
|
193
|
+
|
|
194
|
+
# ── Step 4: Set as global OTel provider ───────────────────────────────
|
|
195
|
+
trace.set_tracer_provider(provider)
|
|
196
|
+
_PROVIDER = provider
|
|
197
|
+
|
|
198
|
+
# ── Step 5: Auto-instrument CrewAI ─────────────────────────────────────
|
|
199
|
+
instrument(provider)
|
|
200
|
+
|
|
201
|
+
_INITIALIZED = True
|
|
202
|
+
|
|
203
|
+
if verbose:
|
|
204
|
+
store_line = f" Store : MongoDB (project={service_name})\n" if store == "mongodb" else ""
|
|
205
|
+
jsonl_line = f" Output file : {output_path}\n" if write_jsonl else ""
|
|
206
|
+
print(
|
|
207
|
+
f"[continuous-intelligence-layer] ✅ Initialized\n"
|
|
208
|
+
f" Execution ID : {execution_id}\n"
|
|
209
|
+
f"{store_line}"
|
|
210
|
+
f"{jsonl_line}"
|
|
211
|
+
f" Service name : {service_name}"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return {"execution_id": execution_id, "output_path": output_path}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def reset() -> None:
|
|
218
|
+
"""
|
|
219
|
+
Reset the SDK so init() can be called again.
|
|
220
|
+
|
|
221
|
+
Primarily useful in test suites where each test needs a fresh provider.
|
|
222
|
+
Calling this in production code is rarely needed.
|
|
223
|
+
"""
|
|
224
|
+
global _INITIALIZED, _PROVIDER
|
|
225
|
+
from .instrumentation import uninstrument
|
|
226
|
+
uninstrument()
|
|
227
|
+
_INITIALIZED = False
|
|
228
|
+
_PROVIDER = None
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
instrumentation.py
|
|
3
|
+
------------------
|
|
4
|
+
Activates OpenInference auto-instrumentation for CrewAI agents.
|
|
5
|
+
|
|
6
|
+
How OpenInference works internally
|
|
7
|
+
------------------------------------
|
|
8
|
+
The instrumentor hooks into CrewAI's own event bus and emits OTel spans using
|
|
9
|
+
the vendor-neutral semantic-convention attribute names (openinference.span.kind,
|
|
10
|
+
input.value, llm.model_name, tool.name, etc. — see
|
|
11
|
+
`continuous_intelligence_layer/_core/exporter.py`). Because every OpenInference
|
|
12
|
+
instrumentor speaks that shared vocabulary, the rest of the SDK (`_core/`,
|
|
13
|
+
`graph_builder/`, `evaluators/`, `rca_engine/`) doesn't need to know which
|
|
14
|
+
framework produced a given span.
|
|
15
|
+
|
|
16
|
+
use_event_listener=True (not the package default)
|
|
17
|
+
---------------------------------------------------
|
|
18
|
+
`openinference-instrumentation-crewai`'s DEFAULT mode ("legacy wrapper") only
|
|
19
|
+
wraps `Task._execute_core`/`Crew.kickoff`/`Flow.*`/tool `.run()`/memory
|
|
20
|
+
`.save()`/`.search()` — it never wraps the actual LLM call, so no LLM-kind
|
|
21
|
+
span (and therefore no captured prompt) is ever produced for CrewAI agents
|
|
22
|
+
under that mode. The event-listener mode additionally emits a real
|
|
23
|
+
`"<model>.llm_call"` LLM span per model call, and — critically — the
|
|
24
|
+
Agent-execution span itself gets `input.value` populated directly from the
|
|
25
|
+
resolved `task_prompt`/`task_description` (see
|
|
26
|
+
`openinference/instrumentation/crewai/_event_listener.py::_build_agent_start_spec`),
|
|
27
|
+
instead of only carrying static agent config. This is required for
|
|
28
|
+
`evaluators/crewai_input_evaluator.py` and `evaluators/tool_agent_evaluator.py`
|
|
29
|
+
(whose docstring already assumes per-iteration CrewAI LLM spans exist) to see
|
|
30
|
+
real content.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
36
|
+
|
|
37
|
+
_instrumentor = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def instrument(provider: TracerProvider) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Activate CrewAI OpenInference auto-instrumentation.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
provider :
|
|
47
|
+
The TracerProvider to attach spans to. Must be the same provider that
|
|
48
|
+
was set as the global OTel provider.
|
|
49
|
+
"""
|
|
50
|
+
global _instrumentor
|
|
51
|
+
if _instrumentor is not None:
|
|
52
|
+
# Already instrumented — avoid double-patching
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
from openinference.instrumentation.crewai import CrewAIInstrumentor
|
|
57
|
+
except ImportError:
|
|
58
|
+
print(
|
|
59
|
+
"[continuous-intelligence-layer] ⚠️ openinference-instrumentation-crewai not found.\n"
|
|
60
|
+
" crewai spans will NOT be captured automatically.\n"
|
|
61
|
+
" Fix: pip install openinference-instrumentation-crewai"
|
|
62
|
+
)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
instrumentor = CrewAIInstrumentor()
|
|
67
|
+
instrumentor.instrument(tracer_provider=provider, use_event_listener=True)
|
|
68
|
+
_instrumentor = instrumentor
|
|
69
|
+
print("[continuous-intelligence-layer] 🔌 crewai auto-instrumentation active.")
|
|
70
|
+
except Exception as exc:
|
|
71
|
+
print(f"[continuous-intelligence-layer] ⚠️ Instrumentation of 'crewai' failed: {exc}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def uninstrument() -> None:
|
|
75
|
+
"""Remove CrewAI OpenInference instrumentation."""
|
|
76
|
+
global _instrumentor
|
|
77
|
+
if _instrumentor is None:
|
|
78
|
+
return
|
|
79
|
+
try:
|
|
80
|
+
_instrumentor.uninstrument()
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
_instrumentor = None
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""LangGraph/LangChain integration for continuous_intelligence_layer.
|
|
2
|
+
|
|
3
|
+
Also covers plain LangChain usage — both map to the same OpenInference
|
|
4
|
+
LangChainInstrumentor, so there is no separate `langchain` folder.
|
|
5
|
+
|
|
6
|
+
from continuous_intelligence_layer.langgraph import init
|
|
7
|
+
init(api_key=...)
|
|
8
|
+
"""
|
|
9
|
+
from .init import init, reset
|
|
10
|
+
from .._core.utils import load_traces, print_trace_summary
|
|
11
|
+
|
|
12
|
+
__all__ = ["init", "reset", "load_traces", "print_trace_summary"]
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""
|
|
2
|
+
init.py
|
|
3
|
+
-------
|
|
4
|
+
The public entry point for LangGraph/LangChain agents.
|
|
5
|
+
|
|
6
|
+
from continuous_intelligence_layer.langgraph 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
|
+
LangChainInstrumentor (covers both LangGraph and plain LangChain — see
|
|
21
|
+
instrumentation.py). This monkey-patches LangChain's internals to emit
|
|
22
|
+
OTel spans for every node, LLM call, tool call, retriever call, etc. —
|
|
23
|
+
completely automatically.
|
|
24
|
+
5. When the user's agent runs, spans flow:
|
|
25
|
+
LangGraph/LangChain → 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 LangGraph/LangChain.
|
|
64
|
+
|
|
65
|
+
Call this ONCE, at the very top of your script, before creating or running
|
|
66
|
+
any LangGraph graph. 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
|
+
#
|
|
130
|
+
# ExecutionSpanExporter is a standard OTel SpanExporter that knows how to
|
|
131
|
+
# parse a ReadableSpan into a flat dict (via ._extract). GraphSpanExporter
|
|
132
|
+
# reuses that parsing logic to build the MongoDB graph, so this object is
|
|
133
|
+
# always constructed — but it's only wired up to actually *write* a JSONL
|
|
134
|
+
# file to disk if write_jsonl is True. When store="mongodb", MongoDB is
|
|
135
|
+
# the single source of truth by default; when store is not "mongodb"
|
|
136
|
+
# there'd be nowhere else for spans to go, so JSONL is forced on.
|
|
137
|
+
#
|
|
138
|
+
write_jsonl = write_jsonl or store != "mongodb"
|
|
139
|
+
exporter = ExecutionSpanExporter(
|
|
140
|
+
output_path=output_path,
|
|
141
|
+
execution_id=execution_id,
|
|
142
|
+
session_id=session_id,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# ── Step 2: Sync Project & Build TracerProvider ────────────────────────
|
|
146
|
+
if not api_key:
|
|
147
|
+
raise ValueError("API Key is not provided. An API Key is required to initialize the SDK.")
|
|
148
|
+
|
|
149
|
+
if not evaluation_api_key:
|
|
150
|
+
raise ValueError(
|
|
151
|
+
"evaluation_api_key is not provided. An LLM API key (OpenAI, Anthropic, "
|
|
152
|
+
"or Gemini) is required to initialize the SDK -- it's used to run "
|
|
153
|
+
"evaluations and RCA on your traces. We never use our own key for your work."
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
res = requests.post(
|
|
158
|
+
f"{base_url}/projects/sync",
|
|
159
|
+
json={
|
|
160
|
+
"name": service_name,
|
|
161
|
+
"execution_id": execution_id,
|
|
162
|
+
"evaluation_provider": evaluation_provider,
|
|
163
|
+
"evaluation_model": evaluation_model,
|
|
164
|
+
"evaluation_api_key": evaluation_api_key,
|
|
165
|
+
},
|
|
166
|
+
headers={"x-api-key": api_key}
|
|
167
|
+
)
|
|
168
|
+
if res.status_code == 401:
|
|
169
|
+
raise ValueError("API Key is not valid.")
|
|
170
|
+
res.raise_for_status()
|
|
171
|
+
project_id = res.json()["id"]
|
|
172
|
+
except requests.exceptions.RequestException as e:
|
|
173
|
+
raise ValueError(f"Failed to verify API key with backend: {e}")
|
|
174
|
+
|
|
175
|
+
resource_attrs = {
|
|
176
|
+
"service.name": service_name,
|
|
177
|
+
"project.id": project_id,
|
|
178
|
+
"execution.id": execution_id,
|
|
179
|
+
"sdk.name": "continuous-intelligence-layer",
|
|
180
|
+
"sdk.version": "0.1.0",
|
|
181
|
+
}
|
|
182
|
+
if session_id:
|
|
183
|
+
resource_attrs["session.id"] = session_id
|
|
184
|
+
|
|
185
|
+
resource = Resource.create(resource_attrs)
|
|
186
|
+
provider = TracerProvider(resource=resource)
|
|
187
|
+
|
|
188
|
+
# ── Step 3: Attach SimpleSpanProcessor ────────────────────────────────
|
|
189
|
+
#
|
|
190
|
+
# SimpleSpanProcessor calls exporter.export([span]) synchronously on every
|
|
191
|
+
# span.end(). This guarantees no spans are lost even if the process crashes.
|
|
192
|
+
# (For high-throughput production use, replace with BatchSpanProcessor.)
|
|
193
|
+
#
|
|
194
|
+
if write_jsonl:
|
|
195
|
+
provider.add_span_processor(SimpleSpanProcessor(exporter))
|
|
196
|
+
|
|
197
|
+
if store == "mongodb":
|
|
198
|
+
from .._core.graph_exporter import GraphSpanExporter
|
|
199
|
+
graph_exporter = GraphSpanExporter(
|
|
200
|
+
exporter,
|
|
201
|
+
session_id=session_id,
|
|
202
|
+
base_url=base_url,
|
|
203
|
+
api_key=api_key,
|
|
204
|
+
project_id=project_id,
|
|
205
|
+
run_evaluations=run_evaluations,
|
|
206
|
+
)
|
|
207
|
+
provider.add_span_processor(SimpleSpanProcessor(graph_exporter))
|
|
208
|
+
|
|
209
|
+
# ── Step 4: Set as global OTel provider ───────────────────────────────
|
|
210
|
+
#
|
|
211
|
+
# Any library that calls opentelemetry.trace.get_tracer() anywhere in the
|
|
212
|
+
# process will now route spans through our provider.
|
|
213
|
+
#
|
|
214
|
+
trace.set_tracer_provider(provider)
|
|
215
|
+
_PROVIDER = provider
|
|
216
|
+
|
|
217
|
+
# ── Step 5: Auto-instrument LangGraph/LangChain ────────────────────────
|
|
218
|
+
#
|
|
219
|
+
# The OpenInference LangChainInstrumentor monkey-patches LangChain's own
|
|
220
|
+
# callback/hook system. After this call, LLM calls, agent/tool
|
|
221
|
+
# invocations, and retriever calls are intercepted and an OTel span is
|
|
222
|
+
# emitted. The user's agent code never needs to know this is happening.
|
|
223
|
+
#
|
|
224
|
+
instrument(provider)
|
|
225
|
+
|
|
226
|
+
_INITIALIZED = True
|
|
227
|
+
|
|
228
|
+
if verbose:
|
|
229
|
+
store_line = f" Store : MongoDB (project={service_name})\n" if store == "mongodb" else ""
|
|
230
|
+
jsonl_line = f" Output file : {output_path}\n" if write_jsonl else ""
|
|
231
|
+
print(
|
|
232
|
+
f"[continuous-intelligence-layer] ✅ Initialized\n"
|
|
233
|
+
f" Execution ID : {execution_id}\n"
|
|
234
|
+
f"{store_line}"
|
|
235
|
+
f"{jsonl_line}"
|
|
236
|
+
f" Service name : {service_name}"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
return {"execution_id": execution_id, "output_path": output_path}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def reset() -> None:
|
|
243
|
+
"""
|
|
244
|
+
Reset the SDK so init() can be called again.
|
|
245
|
+
|
|
246
|
+
Primarily useful in test suites where each test needs a fresh provider.
|
|
247
|
+
Calling this in production code is rarely needed.
|
|
248
|
+
"""
|
|
249
|
+
global _INITIALIZED, _PROVIDER
|
|
250
|
+
from .instrumentation import uninstrument
|
|
251
|
+
uninstrument()
|
|
252
|
+
_INITIALIZED = False
|
|
253
|
+
_PROVIDER = None
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
instrumentation.py
|
|
3
|
+
------------------
|
|
4
|
+
Activates OpenInference auto-instrumentation for LangGraph/LangChain agents.
|
|
5
|
+
|
|
6
|
+
Both LangGraph and plain LangChain map to the same
|
|
7
|
+
`openinference.instrumentation.langchain.LangChainInstrumentor` — there is no
|
|
8
|
+
separate instrumentor class for LangGraph, so this single folder covers both.
|
|
9
|
+
|
|
10
|
+
How OpenInference works internally
|
|
11
|
+
------------------------------------
|
|
12
|
+
The instrumentor hooks into LangChain's own callback-handler system and emits
|
|
13
|
+
OTel spans using the vendor-neutral semantic-convention attribute names
|
|
14
|
+
(openinference.span.kind, input.value, llm.model_name, tool.name, etc. — see
|
|
15
|
+
`continuous_intelligence_layer/_core/exporter.py`). Because every OpenInference
|
|
16
|
+
instrumentor speaks that shared vocabulary, the rest of the SDK (`_core/`,
|
|
17
|
+
`graph_builder/`, `evaluators/`, `rca_engine/`) doesn't need to know which
|
|
18
|
+
framework produced a given span.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
24
|
+
|
|
25
|
+
_instrumentor = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def instrument(provider: TracerProvider) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Activate LangChain/LangGraph OpenInference auto-instrumentation.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
provider :
|
|
35
|
+
The TracerProvider to attach spans to. Must be the same provider that
|
|
36
|
+
was set as the global OTel provider.
|
|
37
|
+
"""
|
|
38
|
+
global _instrumentor
|
|
39
|
+
if _instrumentor is not None:
|
|
40
|
+
# Already instrumented — avoid double-patching
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
from openinference.instrumentation.langchain import LangChainInstrumentor
|
|
45
|
+
except ImportError:
|
|
46
|
+
print(
|
|
47
|
+
"[continuous-intelligence-layer] ⚠️ openinference-instrumentation-langchain not found.\n"
|
|
48
|
+
" langgraph/langchain spans will NOT be captured automatically.\n"
|
|
49
|
+
" Fix: pip install openinference-instrumentation-langchain"
|
|
50
|
+
)
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
instrumentor = LangChainInstrumentor()
|
|
55
|
+
instrumentor.instrument(tracer_provider=provider)
|
|
56
|
+
_instrumentor = instrumentor
|
|
57
|
+
print("[continuous-intelligence-layer] 🔌 langgraph auto-instrumentation active.")
|
|
58
|
+
except Exception as exc:
|
|
59
|
+
print(f"[continuous-intelligence-layer] ⚠️ Instrumentation of 'langgraph' failed: {exc}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def uninstrument() -> None:
|
|
63
|
+
"""Remove LangChain/LangGraph OpenInference instrumentation."""
|
|
64
|
+
global _instrumentor
|
|
65
|
+
if _instrumentor is None:
|
|
66
|
+
return
|
|
67
|
+
try:
|
|
68
|
+
_instrumentor.uninstrument()
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
_instrumentor = None
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Direct OpenAI SDK integration for continuous_intelligence_layer.
|
|
2
|
+
|
|
3
|
+
from continuous_intelligence_layer.openai 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"]
|