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,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"]