kalibr 1.0.28__py3-none-any.whl → 1.1.2a0__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.
- kalibr/__init__.py +170 -3
- kalibr/__main__.py +3 -203
- kalibr/capsule_middleware.py +108 -0
- kalibr/cli/__init__.py +5 -0
- kalibr/cli/capsule_cmd.py +174 -0
- kalibr/cli/deploy_cmd.py +114 -0
- kalibr/cli/main.py +67 -0
- kalibr/cli/run.py +203 -0
- kalibr/cli/serve.py +59 -0
- kalibr/client.py +293 -0
- kalibr/collector.py +173 -0
- kalibr/context.py +132 -0
- kalibr/cost_adapter.py +222 -0
- kalibr/decorators.py +140 -0
- kalibr/instrumentation/__init__.py +13 -0
- kalibr/instrumentation/anthropic_instr.py +282 -0
- kalibr/instrumentation/base.py +108 -0
- kalibr/instrumentation/google_instr.py +281 -0
- kalibr/instrumentation/openai_instr.py +265 -0
- kalibr/instrumentation/registry.py +153 -0
- kalibr/kalibr.py +144 -230
- kalibr/kalibr_app.py +53 -314
- kalibr/middleware/__init__.py +5 -0
- kalibr/middleware/auto_tracer.py +356 -0
- kalibr/models.py +41 -0
- kalibr/redaction.py +44 -0
- kalibr/schemas.py +116 -0
- kalibr/simple_tracer.py +258 -0
- kalibr/tokens.py +52 -0
- kalibr/trace_capsule.py +296 -0
- kalibr/trace_models.py +201 -0
- kalibr/tracer.py +354 -0
- kalibr/types.py +25 -93
- kalibr/utils.py +198 -0
- kalibr-1.1.2a0.dist-info/METADATA +236 -0
- kalibr-1.1.2a0.dist-info/RECORD +48 -0
- kalibr-1.1.2a0.dist-info/entry_points.txt +2 -0
- kalibr-1.1.2a0.dist-info/licenses/LICENSE +21 -0
- kalibr-1.1.2a0.dist-info/top_level.txt +4 -0
- kalibr_crewai/__init__.py +65 -0
- kalibr_crewai/callbacks.py +539 -0
- kalibr_crewai/instrumentor.py +513 -0
- kalibr_langchain/__init__.py +47 -0
- kalibr_langchain/async_callback.py +850 -0
- kalibr_langchain/callback.py +1064 -0
- kalibr_openai_agents/__init__.py +43 -0
- kalibr_openai_agents/processor.py +554 -0
- kalibr/deployment.py +0 -41
- kalibr/packager.py +0 -43
- kalibr/runtime_router.py +0 -138
- kalibr/schema_generators.py +0 -159
- kalibr/validator.py +0 -70
- kalibr-1.0.28.data/data/examples/README.md +0 -173
- kalibr-1.0.28.data/data/examples/basic_kalibr_example.py +0 -66
- kalibr-1.0.28.data/data/examples/enhanced_kalibr_example.py +0 -347
- kalibr-1.0.28.dist-info/METADATA +0 -175
- kalibr-1.0.28.dist-info/RECORD +0 -19
- kalibr-1.0.28.dist-info/entry_points.txt +0 -2
- kalibr-1.0.28.dist-info/licenses/LICENSE +0 -11
- kalibr-1.0.28.dist-info/top_level.txt +0 -1
- {kalibr-1.0.28.dist-info → kalibr-1.1.2a0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"""Kalibr Auto-Instrumentation for CrewAI.
|
|
2
|
+
|
|
3
|
+
This module provides automatic instrumentation for CrewAI applications
|
|
4
|
+
by wrapping key methods to capture telemetry.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
import traceback
|
|
10
|
+
import uuid
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from functools import wraps
|
|
13
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
from .callbacks import EventBatcher, _count_tokens, _get_provider_from_model
|
|
16
|
+
|
|
17
|
+
# Import Kalibr cost adapters
|
|
18
|
+
try:
|
|
19
|
+
from kalibr.cost_adapter import CostAdapterFactory
|
|
20
|
+
except ImportError:
|
|
21
|
+
CostAdapterFactory = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class KalibrCrewAIInstrumentor:
|
|
25
|
+
"""Auto-instrumentation for CrewAI.
|
|
26
|
+
|
|
27
|
+
This instrumentor wraps CrewAI's Crew, Agent, and Task classes to
|
|
28
|
+
automatically capture telemetry without requiring manual callbacks.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
api_key: Kalibr API key
|
|
32
|
+
endpoint: Backend endpoint URL
|
|
33
|
+
tenant_id: Tenant identifier
|
|
34
|
+
environment: Environment (prod/staging/dev)
|
|
35
|
+
service: Service name
|
|
36
|
+
workflow_id: Workflow identifier
|
|
37
|
+
capture_input: Whether to capture inputs (default: True)
|
|
38
|
+
capture_output: Whether to capture outputs (default: True)
|
|
39
|
+
|
|
40
|
+
Usage:
|
|
41
|
+
from kalibr_crewai import KalibrCrewAIInstrumentor
|
|
42
|
+
|
|
43
|
+
# Instrument before creating crews
|
|
44
|
+
instrumentor = KalibrCrewAIInstrumentor(tenant_id="my-tenant")
|
|
45
|
+
instrumentor.instrument()
|
|
46
|
+
|
|
47
|
+
# Use CrewAI normally - all operations are traced
|
|
48
|
+
crew = Crew(agents=[...], tasks=[...])
|
|
49
|
+
result = crew.kickoff()
|
|
50
|
+
|
|
51
|
+
# Optionally uninstrument
|
|
52
|
+
instrumentor.uninstrument()
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
api_key: Optional[str] = None,
|
|
58
|
+
endpoint: Optional[str] = None,
|
|
59
|
+
tenant_id: Optional[str] = None,
|
|
60
|
+
environment: Optional[str] = None,
|
|
61
|
+
service: Optional[str] = None,
|
|
62
|
+
workflow_id: Optional[str] = None,
|
|
63
|
+
capture_input: bool = True,
|
|
64
|
+
capture_output: bool = True,
|
|
65
|
+
):
|
|
66
|
+
self.api_key = api_key or os.getenv("KALIBR_API_KEY", "")
|
|
67
|
+
self.endpoint = endpoint or os.getenv(
|
|
68
|
+
"KALIBR_ENDPOINT",
|
|
69
|
+
os.getenv("KALIBR_API_ENDPOINT", "http://localhost:8001/api/v1/traces")
|
|
70
|
+
)
|
|
71
|
+
self.tenant_id = tenant_id or os.getenv("KALIBR_TENANT_ID", "default")
|
|
72
|
+
self.environment = environment or os.getenv("KALIBR_ENVIRONMENT", "prod")
|
|
73
|
+
self.service = service or os.getenv("KALIBR_SERVICE", "crewai-app")
|
|
74
|
+
self.workflow_id = workflow_id or os.getenv("KALIBR_WORKFLOW_ID", "default-workflow")
|
|
75
|
+
self.capture_input = capture_input
|
|
76
|
+
self.capture_output = capture_output
|
|
77
|
+
|
|
78
|
+
# Batcher for sending events
|
|
79
|
+
self._batcher: Optional[EventBatcher] = None
|
|
80
|
+
|
|
81
|
+
# Original methods to restore on uninstrument
|
|
82
|
+
self._originals: Dict[str, Any] = {}
|
|
83
|
+
|
|
84
|
+
# Instrumentation state
|
|
85
|
+
self._is_instrumented = False
|
|
86
|
+
|
|
87
|
+
def instrument(self) -> bool:
|
|
88
|
+
"""Instrument CrewAI classes.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
True if instrumentation was successful
|
|
92
|
+
"""
|
|
93
|
+
if self._is_instrumented:
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
from crewai import Crew, Agent, Task
|
|
98
|
+
except ImportError:
|
|
99
|
+
print("[Kalibr] CrewAI not installed, skipping instrumentation")
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
# Initialize batcher
|
|
103
|
+
self._batcher = EventBatcher.get_instance(
|
|
104
|
+
endpoint=self.endpoint,
|
|
105
|
+
api_key=self.api_key,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Instrument Crew.kickoff
|
|
109
|
+
self._originals["Crew.kickoff"] = Crew.kickoff
|
|
110
|
+
Crew.kickoff = self._wrap_crew_kickoff(Crew.kickoff)
|
|
111
|
+
|
|
112
|
+
# Instrument Crew.kickoff_async if available
|
|
113
|
+
if hasattr(Crew, "kickoff_async"):
|
|
114
|
+
self._originals["Crew.kickoff_async"] = Crew.kickoff_async
|
|
115
|
+
Crew.kickoff_async = self._wrap_crew_kickoff_async(Crew.kickoff_async)
|
|
116
|
+
|
|
117
|
+
# Instrument Agent.execute_task
|
|
118
|
+
if hasattr(Agent, "execute_task"):
|
|
119
|
+
self._originals["Agent.execute_task"] = Agent.execute_task
|
|
120
|
+
Agent.execute_task = self._wrap_agent_execute(Agent.execute_task)
|
|
121
|
+
|
|
122
|
+
# Instrument Task.execute_sync if available
|
|
123
|
+
if hasattr(Task, "execute_sync"):
|
|
124
|
+
self._originals["Task.execute_sync"] = Task.execute_sync
|
|
125
|
+
Task.execute_sync = self._wrap_task_execute(Task.execute_sync)
|
|
126
|
+
|
|
127
|
+
self._is_instrumented = True
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
def uninstrument(self) -> bool:
|
|
131
|
+
"""Remove instrumentation and restore original methods.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
True if uninstrumentation was successful
|
|
135
|
+
"""
|
|
136
|
+
if not self._is_instrumented:
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
from crewai import Crew, Agent, Task
|
|
141
|
+
|
|
142
|
+
# Restore original methods
|
|
143
|
+
if "Crew.kickoff" in self._originals:
|
|
144
|
+
Crew.kickoff = self._originals["Crew.kickoff"]
|
|
145
|
+
|
|
146
|
+
if "Crew.kickoff_async" in self._originals:
|
|
147
|
+
Crew.kickoff_async = self._originals["Crew.kickoff_async"]
|
|
148
|
+
|
|
149
|
+
if "Agent.execute_task" in self._originals:
|
|
150
|
+
Agent.execute_task = self._originals["Agent.execute_task"]
|
|
151
|
+
|
|
152
|
+
if "Task.execute_sync" in self._originals:
|
|
153
|
+
Task.execute_sync = self._originals["Task.execute_sync"]
|
|
154
|
+
|
|
155
|
+
self._originals.clear()
|
|
156
|
+
self._is_instrumented = False
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
except ImportError:
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
def _wrap_crew_kickoff(self, original_method: Callable) -> Callable:
|
|
163
|
+
"""Wrap Crew.kickoff to capture crew execution."""
|
|
164
|
+
instrumentor = self
|
|
165
|
+
|
|
166
|
+
@wraps(original_method)
|
|
167
|
+
def wrapper(crew_self, *args, **kwargs):
|
|
168
|
+
trace_id = str(uuid.uuid4())
|
|
169
|
+
span_id = str(uuid.uuid4())
|
|
170
|
+
start_time = time.time()
|
|
171
|
+
ts_start = datetime.now(timezone.utc)
|
|
172
|
+
|
|
173
|
+
# Capture crew info
|
|
174
|
+
crew_name = getattr(crew_self, "name", None) or "unnamed_crew"
|
|
175
|
+
agent_count = len(getattr(crew_self, "agents", []))
|
|
176
|
+
task_count = len(getattr(crew_self, "tasks", []))
|
|
177
|
+
|
|
178
|
+
status = "success"
|
|
179
|
+
error_type = None
|
|
180
|
+
error_message = None
|
|
181
|
+
result = None
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
result = original_method(crew_self, *args, **kwargs)
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
except Exception as e:
|
|
188
|
+
status = "error"
|
|
189
|
+
error_type = type(e).__name__
|
|
190
|
+
error_message = str(e)[:512]
|
|
191
|
+
raise
|
|
192
|
+
|
|
193
|
+
finally:
|
|
194
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
195
|
+
ts_end = datetime.now(timezone.utc)
|
|
196
|
+
|
|
197
|
+
# Build output info
|
|
198
|
+
output_preview = None
|
|
199
|
+
if instrumentor.capture_output and result is not None:
|
|
200
|
+
output_preview = str(result)[:500]
|
|
201
|
+
|
|
202
|
+
# Create event
|
|
203
|
+
event = {
|
|
204
|
+
"schema_version": "1.0",
|
|
205
|
+
"trace_id": trace_id,
|
|
206
|
+
"span_id": span_id,
|
|
207
|
+
"parent_span_id": None,
|
|
208
|
+
"tenant_id": instrumentor.tenant_id,
|
|
209
|
+
"workflow_id": instrumentor.workflow_id,
|
|
210
|
+
"provider": "crewai",
|
|
211
|
+
"model_id": "crew",
|
|
212
|
+
"model_name": crew_name,
|
|
213
|
+
"operation": f"crew:{crew_name}",
|
|
214
|
+
"endpoint": "crew.kickoff",
|
|
215
|
+
"duration_ms": duration_ms,
|
|
216
|
+
"latency_ms": duration_ms,
|
|
217
|
+
"input_tokens": 0,
|
|
218
|
+
"output_tokens": 0,
|
|
219
|
+
"total_tokens": 0,
|
|
220
|
+
"cost_usd": 0.0,
|
|
221
|
+
"total_cost_usd": 0.0,
|
|
222
|
+
"status": status,
|
|
223
|
+
"error_type": error_type,
|
|
224
|
+
"error_message": error_message,
|
|
225
|
+
"timestamp": ts_start.isoformat(),
|
|
226
|
+
"ts_start": ts_start.isoformat(),
|
|
227
|
+
"ts_end": ts_end.isoformat(),
|
|
228
|
+
"environment": instrumentor.environment,
|
|
229
|
+
"service": instrumentor.service,
|
|
230
|
+
"runtime_env": os.getenv("RUNTIME_ENV", "local"),
|
|
231
|
+
"sandbox_id": os.getenv("SANDBOX_ID", "local"),
|
|
232
|
+
"metadata": {
|
|
233
|
+
"span_type": "crew",
|
|
234
|
+
"crewai": True,
|
|
235
|
+
"crew_name": crew_name,
|
|
236
|
+
"agent_count": agent_count,
|
|
237
|
+
"task_count": task_count,
|
|
238
|
+
"output_preview": output_preview,
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if instrumentor._batcher:
|
|
243
|
+
instrumentor._batcher.enqueue(event)
|
|
244
|
+
|
|
245
|
+
return wrapper
|
|
246
|
+
|
|
247
|
+
def _wrap_crew_kickoff_async(self, original_method: Callable) -> Callable:
|
|
248
|
+
"""Wrap Crew.kickoff_async to capture async crew execution."""
|
|
249
|
+
instrumentor = self
|
|
250
|
+
|
|
251
|
+
@wraps(original_method)
|
|
252
|
+
async def wrapper(crew_self, *args, **kwargs):
|
|
253
|
+
trace_id = str(uuid.uuid4())
|
|
254
|
+
span_id = str(uuid.uuid4())
|
|
255
|
+
start_time = time.time()
|
|
256
|
+
ts_start = datetime.now(timezone.utc)
|
|
257
|
+
|
|
258
|
+
crew_name = getattr(crew_self, "name", None) or "unnamed_crew"
|
|
259
|
+
agent_count = len(getattr(crew_self, "agents", []))
|
|
260
|
+
task_count = len(getattr(crew_self, "tasks", []))
|
|
261
|
+
|
|
262
|
+
status = "success"
|
|
263
|
+
error_type = None
|
|
264
|
+
error_message = None
|
|
265
|
+
result = None
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
result = await original_method(crew_self, *args, **kwargs)
|
|
269
|
+
return result
|
|
270
|
+
|
|
271
|
+
except Exception as e:
|
|
272
|
+
status = "error"
|
|
273
|
+
error_type = type(e).__name__
|
|
274
|
+
error_message = str(e)[:512]
|
|
275
|
+
raise
|
|
276
|
+
|
|
277
|
+
finally:
|
|
278
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
279
|
+
ts_end = datetime.now(timezone.utc)
|
|
280
|
+
|
|
281
|
+
output_preview = None
|
|
282
|
+
if instrumentor.capture_output and result is not None:
|
|
283
|
+
output_preview = str(result)[:500]
|
|
284
|
+
|
|
285
|
+
event = {
|
|
286
|
+
"schema_version": "1.0",
|
|
287
|
+
"trace_id": trace_id,
|
|
288
|
+
"span_id": span_id,
|
|
289
|
+
"parent_span_id": None,
|
|
290
|
+
"tenant_id": instrumentor.tenant_id,
|
|
291
|
+
"workflow_id": instrumentor.workflow_id,
|
|
292
|
+
"provider": "crewai",
|
|
293
|
+
"model_id": "crew",
|
|
294
|
+
"model_name": crew_name,
|
|
295
|
+
"operation": f"crew:{crew_name}",
|
|
296
|
+
"endpoint": "crew.kickoff_async",
|
|
297
|
+
"duration_ms": duration_ms,
|
|
298
|
+
"latency_ms": duration_ms,
|
|
299
|
+
"input_tokens": 0,
|
|
300
|
+
"output_tokens": 0,
|
|
301
|
+
"total_tokens": 0,
|
|
302
|
+
"cost_usd": 0.0,
|
|
303
|
+
"total_cost_usd": 0.0,
|
|
304
|
+
"status": status,
|
|
305
|
+
"error_type": error_type,
|
|
306
|
+
"error_message": error_message,
|
|
307
|
+
"timestamp": ts_start.isoformat(),
|
|
308
|
+
"ts_start": ts_start.isoformat(),
|
|
309
|
+
"ts_end": ts_end.isoformat(),
|
|
310
|
+
"environment": instrumentor.environment,
|
|
311
|
+
"service": instrumentor.service,
|
|
312
|
+
"runtime_env": os.getenv("RUNTIME_ENV", "local"),
|
|
313
|
+
"sandbox_id": os.getenv("SANDBOX_ID", "local"),
|
|
314
|
+
"metadata": {
|
|
315
|
+
"span_type": "crew",
|
|
316
|
+
"crewai": True,
|
|
317
|
+
"async": True,
|
|
318
|
+
"crew_name": crew_name,
|
|
319
|
+
"agent_count": agent_count,
|
|
320
|
+
"task_count": task_count,
|
|
321
|
+
"output_preview": output_preview,
|
|
322
|
+
},
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if instrumentor._batcher:
|
|
326
|
+
instrumentor._batcher.enqueue(event)
|
|
327
|
+
|
|
328
|
+
return wrapper
|
|
329
|
+
|
|
330
|
+
def _wrap_agent_execute(self, original_method: Callable) -> Callable:
|
|
331
|
+
"""Wrap Agent.execute_task to capture agent execution."""
|
|
332
|
+
instrumentor = self
|
|
333
|
+
|
|
334
|
+
@wraps(original_method)
|
|
335
|
+
def wrapper(agent_self, task, *args, **kwargs):
|
|
336
|
+
span_id = str(uuid.uuid4())
|
|
337
|
+
start_time = time.time()
|
|
338
|
+
ts_start = datetime.now(timezone.utc)
|
|
339
|
+
|
|
340
|
+
# Get agent info
|
|
341
|
+
role = getattr(agent_self, "role", "unknown")
|
|
342
|
+
goal = getattr(agent_self, "goal", "")
|
|
343
|
+
|
|
344
|
+
# Get task info
|
|
345
|
+
task_description = ""
|
|
346
|
+
if hasattr(task, "description"):
|
|
347
|
+
task_description = str(task.description)
|
|
348
|
+
|
|
349
|
+
status = "success"
|
|
350
|
+
error_type = None
|
|
351
|
+
error_message = None
|
|
352
|
+
result = None
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
result = original_method(agent_self, task, *args, **kwargs)
|
|
356
|
+
return result
|
|
357
|
+
|
|
358
|
+
except Exception as e:
|
|
359
|
+
status = "error"
|
|
360
|
+
error_type = type(e).__name__
|
|
361
|
+
error_message = str(e)[:512]
|
|
362
|
+
raise
|
|
363
|
+
|
|
364
|
+
finally:
|
|
365
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
366
|
+
ts_end = datetime.now(timezone.utc)
|
|
367
|
+
|
|
368
|
+
output_preview = None
|
|
369
|
+
if instrumentor.capture_output and result is not None:
|
|
370
|
+
output_preview = str(result)[:500]
|
|
371
|
+
|
|
372
|
+
# Token estimation
|
|
373
|
+
input_tokens = _count_tokens(task_description + goal, "gpt-4")
|
|
374
|
+
output_tokens = _count_tokens(output_preview or "", "gpt-4")
|
|
375
|
+
|
|
376
|
+
event = {
|
|
377
|
+
"schema_version": "1.0",
|
|
378
|
+
"trace_id": str(uuid.uuid4()), # TODO: Link to crew trace
|
|
379
|
+
"span_id": span_id,
|
|
380
|
+
"parent_span_id": None,
|
|
381
|
+
"tenant_id": instrumentor.tenant_id,
|
|
382
|
+
"workflow_id": instrumentor.workflow_id,
|
|
383
|
+
"provider": "crewai",
|
|
384
|
+
"model_id": "agent",
|
|
385
|
+
"model_name": role,
|
|
386
|
+
"operation": f"agent:{role}",
|
|
387
|
+
"endpoint": "agent.execute_task",
|
|
388
|
+
"duration_ms": duration_ms,
|
|
389
|
+
"latency_ms": duration_ms,
|
|
390
|
+
"input_tokens": input_tokens,
|
|
391
|
+
"output_tokens": output_tokens,
|
|
392
|
+
"total_tokens": input_tokens + output_tokens,
|
|
393
|
+
"cost_usd": 0.0,
|
|
394
|
+
"total_cost_usd": 0.0,
|
|
395
|
+
"status": status,
|
|
396
|
+
"error_type": error_type,
|
|
397
|
+
"error_message": error_message,
|
|
398
|
+
"timestamp": ts_start.isoformat(),
|
|
399
|
+
"ts_start": ts_start.isoformat(),
|
|
400
|
+
"ts_end": ts_end.isoformat(),
|
|
401
|
+
"environment": instrumentor.environment,
|
|
402
|
+
"service": instrumentor.service,
|
|
403
|
+
"runtime_env": os.getenv("RUNTIME_ENV", "local"),
|
|
404
|
+
"sandbox_id": os.getenv("SANDBOX_ID", "local"),
|
|
405
|
+
"metadata": {
|
|
406
|
+
"span_type": "agent",
|
|
407
|
+
"crewai": True,
|
|
408
|
+
"agent_role": role,
|
|
409
|
+
"agent_goal": goal[:200] if goal else None,
|
|
410
|
+
"task_description": task_description[:200] if task_description else None,
|
|
411
|
+
"output_preview": output_preview,
|
|
412
|
+
},
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if instrumentor._batcher:
|
|
416
|
+
instrumentor._batcher.enqueue(event)
|
|
417
|
+
|
|
418
|
+
return wrapper
|
|
419
|
+
|
|
420
|
+
def _wrap_task_execute(self, original_method: Callable) -> Callable:
|
|
421
|
+
"""Wrap Task.execute_sync to capture task execution."""
|
|
422
|
+
instrumentor = self
|
|
423
|
+
|
|
424
|
+
@wraps(original_method)
|
|
425
|
+
def wrapper(task_self, *args, **kwargs):
|
|
426
|
+
span_id = str(uuid.uuid4())
|
|
427
|
+
start_time = time.time()
|
|
428
|
+
ts_start = datetime.now(timezone.utc)
|
|
429
|
+
|
|
430
|
+
description = getattr(task_self, "description", "")
|
|
431
|
+
expected_output = getattr(task_self, "expected_output", "")
|
|
432
|
+
|
|
433
|
+
status = "success"
|
|
434
|
+
error_type = None
|
|
435
|
+
error_message = None
|
|
436
|
+
result = None
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
result = original_method(task_self, *args, **kwargs)
|
|
440
|
+
return result
|
|
441
|
+
|
|
442
|
+
except Exception as e:
|
|
443
|
+
status = "error"
|
|
444
|
+
error_type = type(e).__name__
|
|
445
|
+
error_message = str(e)[:512]
|
|
446
|
+
raise
|
|
447
|
+
|
|
448
|
+
finally:
|
|
449
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
450
|
+
ts_end = datetime.now(timezone.utc)
|
|
451
|
+
|
|
452
|
+
output_preview = None
|
|
453
|
+
if instrumentor.capture_output and result is not None:
|
|
454
|
+
if hasattr(result, "raw"):
|
|
455
|
+
output_preview = str(result.raw)[:500]
|
|
456
|
+
else:
|
|
457
|
+
output_preview = str(result)[:500]
|
|
458
|
+
|
|
459
|
+
input_tokens = _count_tokens(description, "gpt-4")
|
|
460
|
+
output_tokens = _count_tokens(output_preview or "", "gpt-4")
|
|
461
|
+
|
|
462
|
+
event = {
|
|
463
|
+
"schema_version": "1.0",
|
|
464
|
+
"trace_id": str(uuid.uuid4()),
|
|
465
|
+
"span_id": span_id,
|
|
466
|
+
"parent_span_id": None,
|
|
467
|
+
"tenant_id": instrumentor.tenant_id,
|
|
468
|
+
"workflow_id": instrumentor.workflow_id,
|
|
469
|
+
"provider": "crewai",
|
|
470
|
+
"model_id": "task",
|
|
471
|
+
"model_name": "crewai-task",
|
|
472
|
+
"operation": f"task:{description[:30]}..." if len(description) > 30 else f"task:{description}",
|
|
473
|
+
"endpoint": "task.execute_sync",
|
|
474
|
+
"duration_ms": duration_ms,
|
|
475
|
+
"latency_ms": duration_ms,
|
|
476
|
+
"input_tokens": input_tokens,
|
|
477
|
+
"output_tokens": output_tokens,
|
|
478
|
+
"total_tokens": input_tokens + output_tokens,
|
|
479
|
+
"cost_usd": 0.0,
|
|
480
|
+
"total_cost_usd": 0.0,
|
|
481
|
+
"status": status,
|
|
482
|
+
"error_type": error_type,
|
|
483
|
+
"error_message": error_message,
|
|
484
|
+
"timestamp": ts_start.isoformat(),
|
|
485
|
+
"ts_start": ts_start.isoformat(),
|
|
486
|
+
"ts_end": ts_end.isoformat(),
|
|
487
|
+
"environment": instrumentor.environment,
|
|
488
|
+
"service": instrumentor.service,
|
|
489
|
+
"runtime_env": os.getenv("RUNTIME_ENV", "local"),
|
|
490
|
+
"sandbox_id": os.getenv("SANDBOX_ID", "local"),
|
|
491
|
+
"metadata": {
|
|
492
|
+
"span_type": "task",
|
|
493
|
+
"crewai": True,
|
|
494
|
+
"task_description": description[:200] if description else None,
|
|
495
|
+
"expected_output": expected_output[:200] if expected_output else None,
|
|
496
|
+
"output_preview": output_preview,
|
|
497
|
+
},
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if instrumentor._batcher:
|
|
501
|
+
instrumentor._batcher.enqueue(event)
|
|
502
|
+
|
|
503
|
+
return wrapper
|
|
504
|
+
|
|
505
|
+
def flush(self):
|
|
506
|
+
"""Force flush pending events."""
|
|
507
|
+
if self._batcher:
|
|
508
|
+
self._batcher.flush()
|
|
509
|
+
|
|
510
|
+
@property
|
|
511
|
+
def is_instrumented(self) -> bool:
|
|
512
|
+
"""Check if CrewAI is currently instrumented."""
|
|
513
|
+
return self._is_instrumented
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Kalibr LangChain Integration - Observability for LangChain applications.
|
|
2
|
+
|
|
3
|
+
This package provides a callback handler that integrates LangChain with
|
|
4
|
+
Kalibr's observability platform, capturing:
|
|
5
|
+
- LLM calls with token usage and costs
|
|
6
|
+
- Chain executions with timing
|
|
7
|
+
- Tool/Agent invocations
|
|
8
|
+
- Retrieval operations
|
|
9
|
+
- Error tracking with stack traces
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
from kalibr_langchain import KalibrCallbackHandler
|
|
13
|
+
from langchain_openai import ChatOpenAI
|
|
14
|
+
|
|
15
|
+
# Create callback handler
|
|
16
|
+
handler = KalibrCallbackHandler(
|
|
17
|
+
api_key="your-api-key",
|
|
18
|
+
tenant_id="my-tenant",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Use with LangChain
|
|
22
|
+
llm = ChatOpenAI(model="gpt-4", callbacks=[handler])
|
|
23
|
+
response = llm.invoke("Hello, world!")
|
|
24
|
+
|
|
25
|
+
# Or attach to all components
|
|
26
|
+
from langchain.globals import set_llm_cache
|
|
27
|
+
chain = prompt | llm | parser
|
|
28
|
+
chain.invoke({"input": "Hello"}, config={"callbacks": [handler]})
|
|
29
|
+
|
|
30
|
+
Environment Variables:
|
|
31
|
+
KALIBR_API_KEY: API key for authentication
|
|
32
|
+
KALIBR_ENDPOINT: Backend endpoint URL
|
|
33
|
+
KALIBR_TENANT_ID: Tenant identifier
|
|
34
|
+
KALIBR_ENVIRONMENT: Environment (prod/staging/dev)
|
|
35
|
+
KALIBR_SERVICE: Service name
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
__version__ = "0.1.0"
|
|
39
|
+
|
|
40
|
+
from .callback import KalibrCallbackHandler
|
|
41
|
+
from .async_callback import AsyncKalibrCallbackHandler
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"KalibrCallbackHandler",
|
|
45
|
+
"AsyncKalibrCallbackHandler",
|
|
46
|
+
"__version__",
|
|
47
|
+
]
|