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