kalibr 1.0.28__py3-none-any.whl → 1.1.3a0__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 (61) hide show
  1. kalibr/__init__.py +129 -4
  2. kalibr/__main__.py +3 -203
  3. kalibr/capsule_middleware.py +108 -0
  4. kalibr/cli/__init__.py +5 -0
  5. kalibr/cli/capsule_cmd.py +174 -0
  6. kalibr/cli/deploy_cmd.py +114 -0
  7. kalibr/cli/main.py +67 -0
  8. kalibr/cli/run.py +203 -0
  9. kalibr/cli/serve.py +59 -0
  10. kalibr/client.py +293 -0
  11. kalibr/collector.py +173 -0
  12. kalibr/context.py +132 -0
  13. kalibr/cost_adapter.py +222 -0
  14. kalibr/decorators.py +140 -0
  15. kalibr/instrumentation/__init__.py +13 -0
  16. kalibr/instrumentation/anthropic_instr.py +282 -0
  17. kalibr/instrumentation/base.py +108 -0
  18. kalibr/instrumentation/google_instr.py +281 -0
  19. kalibr/instrumentation/openai_instr.py +265 -0
  20. kalibr/instrumentation/registry.py +153 -0
  21. kalibr/kalibr.py +144 -230
  22. kalibr/kalibr_app.py +53 -314
  23. kalibr/middleware/__init__.py +5 -0
  24. kalibr/middleware/auto_tracer.py +356 -0
  25. kalibr/models.py +41 -0
  26. kalibr/redaction.py +44 -0
  27. kalibr/schemas.py +116 -0
  28. kalibr/simple_tracer.py +258 -0
  29. kalibr/tokens.py +52 -0
  30. kalibr/trace_capsule.py +296 -0
  31. kalibr/trace_models.py +201 -0
  32. kalibr/tracer.py +354 -0
  33. kalibr/types.py +25 -93
  34. kalibr/utils.py +198 -0
  35. kalibr-1.1.3a0.dist-info/METADATA +236 -0
  36. kalibr-1.1.3a0.dist-info/RECORD +48 -0
  37. kalibr-1.1.3a0.dist-info/entry_points.txt +2 -0
  38. kalibr-1.1.3a0.dist-info/licenses/LICENSE +21 -0
  39. kalibr-1.1.3a0.dist-info/top_level.txt +4 -0
  40. kalibr_crewai/__init__.py +65 -0
  41. kalibr_crewai/callbacks.py +539 -0
  42. kalibr_crewai/instrumentor.py +513 -0
  43. kalibr_langchain/__init__.py +47 -0
  44. kalibr_langchain/async_callback.py +850 -0
  45. kalibr_langchain/callback.py +1064 -0
  46. kalibr_openai_agents/__init__.py +43 -0
  47. kalibr_openai_agents/processor.py +554 -0
  48. kalibr/deployment.py +0 -41
  49. kalibr/packager.py +0 -43
  50. kalibr/runtime_router.py +0 -138
  51. kalibr/schema_generators.py +0 -159
  52. kalibr/validator.py +0 -70
  53. kalibr-1.0.28.data/data/examples/README.md +0 -173
  54. kalibr-1.0.28.data/data/examples/basic_kalibr_example.py +0 -66
  55. kalibr-1.0.28.data/data/examples/enhanced_kalibr_example.py +0 -347
  56. kalibr-1.0.28.dist-info/METADATA +0 -175
  57. kalibr-1.0.28.dist-info/RECORD +0 -19
  58. kalibr-1.0.28.dist-info/entry_points.txt +0 -2
  59. kalibr-1.0.28.dist-info/licenses/LICENSE +0 -11
  60. kalibr-1.0.28.dist-info/top_level.txt +0 -1
  61. {kalibr-1.0.28.dist-info → kalibr-1.1.3a0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,539 @@
1
+ """Kalibr Callbacks for CrewAI.
2
+
3
+ This module provides callback classes for CrewAI agents and tasks.
4
+ """
5
+
6
+ import atexit
7
+ import os
8
+ import queue
9
+ import threading
10
+ import time
11
+ import traceback
12
+ import uuid
13
+ from datetime import datetime, timezone
14
+ from typing import Any, Callable, Dict, List, Optional, Union
15
+
16
+ import httpx
17
+
18
+ # Import Kalibr cost adapters
19
+ try:
20
+ from kalibr.cost_adapter import CostAdapterFactory
21
+ except ImportError:
22
+ CostAdapterFactory = None
23
+
24
+ # Import tiktoken for token counting
25
+ try:
26
+ import tiktoken
27
+ HAS_TIKTOKEN = True
28
+ except ImportError:
29
+ HAS_TIKTOKEN = False
30
+
31
+
32
+ def _count_tokens(text: str, model: str = "gpt-4") -> int:
33
+ """Count tokens for given text."""
34
+ if not text:
35
+ return 0
36
+
37
+ if HAS_TIKTOKEN:
38
+ try:
39
+ encoding = tiktoken.encoding_for_model(model)
40
+ return len(encoding.encode(text))
41
+ except Exception:
42
+ pass
43
+
44
+ return len(str(text)) // 4
45
+
46
+
47
+ def _get_provider_from_model(model: str) -> str:
48
+ """Infer provider from model name."""
49
+ if not model:
50
+ return "openai"
51
+ model_lower = model.lower()
52
+
53
+ if any(x in model_lower for x in ["gpt", "text-davinci", "o1", "o3"]):
54
+ return "openai"
55
+ elif any(x in model_lower for x in ["claude"]):
56
+ return "anthropic"
57
+ elif any(x in model_lower for x in ["gemini", "palm"]):
58
+ return "google"
59
+ else:
60
+ return "openai"
61
+
62
+
63
+ class EventBatcher:
64
+ """Shared event batching for callbacks."""
65
+
66
+ _instances: Dict[str, "EventBatcher"] = {}
67
+ _lock = threading.Lock()
68
+
69
+ def __init__(
70
+ self,
71
+ endpoint: str,
72
+ api_key: str,
73
+ batch_size: int = 100,
74
+ flush_interval: float = 2.0,
75
+ ):
76
+ self.endpoint = endpoint
77
+ self.api_key = api_key
78
+ self.batch_size = batch_size
79
+ self.flush_interval = flush_interval
80
+
81
+ self._event_queue: queue.Queue = queue.Queue(maxsize=5000)
82
+ self._client = httpx.Client(timeout=10.0)
83
+ self._shutdown = False
84
+
85
+ self._flush_thread = threading.Thread(target=self._flush_loop, daemon=True)
86
+ self._flush_thread.start()
87
+
88
+ atexit.register(self.shutdown)
89
+
90
+ @classmethod
91
+ def get_instance(
92
+ cls,
93
+ endpoint: str,
94
+ api_key: str,
95
+ batch_size: int = 100,
96
+ flush_interval: float = 2.0,
97
+ ) -> "EventBatcher":
98
+ """Get or create a shared EventBatcher instance."""
99
+ key = f"{endpoint}:{api_key}"
100
+ with cls._lock:
101
+ if key not in cls._instances:
102
+ cls._instances[key] = cls(
103
+ endpoint=endpoint,
104
+ api_key=api_key,
105
+ batch_size=batch_size,
106
+ flush_interval=flush_interval,
107
+ )
108
+ return cls._instances[key]
109
+
110
+ def enqueue(self, event: Dict[str, Any]):
111
+ """Add event to queue."""
112
+ try:
113
+ self._event_queue.put_nowait(event)
114
+ except queue.Full:
115
+ try:
116
+ self._event_queue.get_nowait()
117
+ self._event_queue.put_nowait(event)
118
+ except:
119
+ pass
120
+
121
+ def _flush_loop(self):
122
+ """Background thread to flush events."""
123
+ batch = []
124
+ last_flush = time.time()
125
+
126
+ while not self._shutdown:
127
+ try:
128
+ try:
129
+ event = self._event_queue.get(timeout=0.1)
130
+ batch.append(event)
131
+ except queue.Empty:
132
+ pass
133
+
134
+ now = time.time()
135
+ should_flush = (
136
+ len(batch) >= self.batch_size or
137
+ (batch and now - last_flush >= self.flush_interval)
138
+ )
139
+
140
+ if should_flush:
141
+ self._send_batch(batch)
142
+ batch = []
143
+ last_flush = now
144
+ except Exception:
145
+ pass
146
+
147
+ if batch:
148
+ self._send_batch(batch)
149
+
150
+ def _send_batch(self, batch: List[Dict[str, Any]]):
151
+ """Send batch to backend."""
152
+ if not batch:
153
+ return
154
+
155
+ try:
156
+ payload = {"events": batch}
157
+ headers = {}
158
+ if self.api_key:
159
+ headers["X-API-Key"] = self.api_key
160
+
161
+ self._client.post(self.endpoint, json=payload, headers=headers)
162
+ except Exception:
163
+ pass
164
+
165
+ def shutdown(self):
166
+ """Shutdown batcher."""
167
+ if self._shutdown:
168
+ return
169
+ self._shutdown = True
170
+ if self._flush_thread.is_alive():
171
+ self._flush_thread.join(timeout=5.0)
172
+ self._client.close()
173
+
174
+ def flush(self):
175
+ """Force flush pending events."""
176
+ events = []
177
+ while True:
178
+ try:
179
+ event = self._event_queue.get_nowait()
180
+ events.append(event)
181
+ except queue.Empty:
182
+ break
183
+ if events:
184
+ self._send_batch(events)
185
+
186
+
187
+ class KalibrAgentCallback:
188
+ """Callback for CrewAI Agent step_callback.
189
+
190
+ This callback is invoked after each step in an agent's execution,
191
+ capturing tool calls, agent actions, and intermediate results.
192
+
193
+ Args:
194
+ api_key: Kalibr API key
195
+ endpoint: Backend endpoint URL
196
+ tenant_id: Tenant identifier
197
+ environment: Environment (prod/staging/dev)
198
+ service: Service name
199
+ workflow_id: Workflow identifier
200
+ metadata: Additional metadata for all events
201
+
202
+ Usage:
203
+ from kalibr_crewai import KalibrAgentCallback
204
+ from crewai import Agent
205
+
206
+ callback = KalibrAgentCallback(tenant_id="my-tenant")
207
+
208
+ agent = Agent(
209
+ role="Researcher",
210
+ goal="Find information",
211
+ step_callback=callback,
212
+ )
213
+ """
214
+
215
+ def __init__(
216
+ self,
217
+ api_key: Optional[str] = None,
218
+ endpoint: Optional[str] = None,
219
+ tenant_id: Optional[str] = None,
220
+ environment: Optional[str] = None,
221
+ service: Optional[str] = None,
222
+ workflow_id: Optional[str] = None,
223
+ metadata: Optional[Dict[str, Any]] = None,
224
+ ):
225
+ self.api_key = api_key or os.getenv("KALIBR_API_KEY", "")
226
+ self.endpoint = endpoint or os.getenv(
227
+ "KALIBR_ENDPOINT",
228
+ os.getenv("KALIBR_API_ENDPOINT", "https://api.kalibr.systems/api/v1/traces")
229
+ )
230
+ self.tenant_id = tenant_id or os.getenv("KALIBR_TENANT_ID", "default")
231
+ self.environment = environment or os.getenv("KALIBR_ENVIRONMENT", "prod")
232
+ self.service = service or os.getenv("KALIBR_SERVICE", "crewai-app")
233
+ self.workflow_id = workflow_id or os.getenv("KALIBR_WORKFLOW_ID", "default-workflow")
234
+ self.default_metadata = metadata or {}
235
+
236
+ # Get shared batcher
237
+ self._batcher = EventBatcher.get_instance(
238
+ endpoint=self.endpoint,
239
+ api_key=self.api_key,
240
+ )
241
+
242
+ # Trace context
243
+ self._trace_id: Optional[str] = None
244
+ self._agent_span_id: Optional[str] = None
245
+ self._step_count: int = 0
246
+
247
+ def __call__(self, step_output: Any) -> None:
248
+ """Called after each agent step.
249
+
250
+ Args:
251
+ step_output: The output from the agent step. Can be:
252
+ - AgentAction (tool call)
253
+ - AgentFinish (final answer)
254
+ - Other step output types
255
+ """
256
+ try:
257
+ self._handle_step(step_output)
258
+ except Exception as e:
259
+ # Don't let callback errors break the agent
260
+ pass
261
+
262
+ def _handle_step(self, step_output: Any):
263
+ """Process step output and create trace event."""
264
+ now = datetime.now(timezone.utc)
265
+ self._step_count += 1
266
+
267
+ # Initialize trace on first step
268
+ if not self._trace_id:
269
+ self._trace_id = str(uuid.uuid4())
270
+ self._agent_span_id = str(uuid.uuid4())
271
+
272
+ span_id = str(uuid.uuid4())
273
+
274
+ # Extract step information
275
+ step_type = "agent_step"
276
+ operation = "agent_step"
277
+ tool_name = None
278
+ tool_input = None
279
+ output_text = ""
280
+ status = "success"
281
+
282
+ # Handle different step output types
283
+ if hasattr(step_output, "tool"):
284
+ # AgentAction - tool call
285
+ step_type = "tool_call"
286
+ tool_name = step_output.tool
287
+ operation = f"tool:{tool_name}"
288
+ if hasattr(step_output, "tool_input"):
289
+ tool_input = str(step_output.tool_input)
290
+
291
+ elif hasattr(step_output, "return_values"):
292
+ # AgentFinish - final output
293
+ step_type = "agent_finish"
294
+ operation = "agent_finish"
295
+ output_text = str(step_output.return_values)
296
+
297
+ elif hasattr(step_output, "output"):
298
+ # Generic step with output
299
+ output_text = str(step_output.output)
300
+
301
+ elif hasattr(step_output, "log"):
302
+ # Step with log
303
+ output_text = str(step_output.log)
304
+
305
+ else:
306
+ # Fallback
307
+ output_text = str(step_output)
308
+
309
+ # Count tokens
310
+ input_tokens = _count_tokens(tool_input or "", "gpt-4")
311
+ output_tokens = _count_tokens(output_text, "gpt-4")
312
+
313
+ # Build event
314
+ event = {
315
+ "schema_version": "1.0",
316
+ "trace_id": self._trace_id,
317
+ "span_id": span_id,
318
+ "parent_span_id": self._agent_span_id,
319
+ "tenant_id": self.tenant_id,
320
+ "workflow_id": self.workflow_id,
321
+ "provider": "crewai",
322
+ "model_id": "agent",
323
+ "model_name": "crewai-agent",
324
+ "operation": operation,
325
+ "endpoint": operation,
326
+ "duration_ms": 0, # Step timing not available
327
+ "latency_ms": 0,
328
+ "input_tokens": input_tokens,
329
+ "output_tokens": output_tokens,
330
+ "total_tokens": input_tokens + output_tokens,
331
+ "cost_usd": 0.0, # Cost tracked at LLM level
332
+ "total_cost_usd": 0.0,
333
+ "status": status,
334
+ "timestamp": now.isoformat(),
335
+ "ts_start": now.isoformat(),
336
+ "ts_end": now.isoformat(),
337
+ "environment": self.environment,
338
+ "service": self.service,
339
+ "runtime_env": os.getenv("RUNTIME_ENV", "local"),
340
+ "sandbox_id": os.getenv("SANDBOX_ID", "local"),
341
+ "metadata": {
342
+ **self.default_metadata,
343
+ "span_type": step_type,
344
+ "crewai": True,
345
+ "step_number": self._step_count,
346
+ "tool_name": tool_name,
347
+ "tool_input": tool_input[:500] if tool_input else None,
348
+ "output_preview": output_text[:500] if output_text else None,
349
+ },
350
+ }
351
+
352
+ self._batcher.enqueue(event)
353
+
354
+ def reset(self):
355
+ """Reset trace context for new agent run."""
356
+ self._trace_id = None
357
+ self._agent_span_id = None
358
+ self._step_count = 0
359
+
360
+ def flush(self):
361
+ """Force flush pending events."""
362
+ self._batcher.flush()
363
+
364
+
365
+ class KalibrTaskCallback:
366
+ """Callback for CrewAI Task callback.
367
+
368
+ This callback is invoked when a task completes, capturing the
369
+ full task execution including description, output, and metrics.
370
+
371
+ Args:
372
+ api_key: Kalibr API key
373
+ endpoint: Backend endpoint URL
374
+ tenant_id: Tenant identifier
375
+ environment: Environment (prod/staging/dev)
376
+ service: Service name
377
+ workflow_id: Workflow identifier
378
+ metadata: Additional metadata for all events
379
+
380
+ Usage:
381
+ from kalibr_crewai import KalibrTaskCallback
382
+ from crewai import Task
383
+
384
+ callback = KalibrTaskCallback(tenant_id="my-tenant")
385
+
386
+ task = Task(
387
+ description="Research AI trends",
388
+ agent=my_agent,
389
+ callback=callback,
390
+ )
391
+ """
392
+
393
+ def __init__(
394
+ self,
395
+ api_key: Optional[str] = None,
396
+ endpoint: Optional[str] = None,
397
+ tenant_id: Optional[str] = None,
398
+ environment: Optional[str] = None,
399
+ service: Optional[str] = None,
400
+ workflow_id: Optional[str] = None,
401
+ metadata: Optional[Dict[str, Any]] = None,
402
+ ):
403
+ self.api_key = api_key or os.getenv("KALIBR_API_KEY", "")
404
+ self.endpoint = endpoint or os.getenv(
405
+ "KALIBR_ENDPOINT",
406
+ os.getenv("KALIBR_API_ENDPOINT", "https://api.kalibr.systems/api/v1/traces")
407
+ )
408
+ self.tenant_id = tenant_id or os.getenv("KALIBR_TENANT_ID", "default")
409
+ self.environment = environment or os.getenv("KALIBR_ENVIRONMENT", "prod")
410
+ self.service = service or os.getenv("KALIBR_SERVICE", "crewai-app")
411
+ self.workflow_id = workflow_id or os.getenv("KALIBR_WORKFLOW_ID", "default-workflow")
412
+ self.default_metadata = metadata or {}
413
+
414
+ # Get shared batcher
415
+ self._batcher = EventBatcher.get_instance(
416
+ endpoint=self.endpoint,
417
+ api_key=self.api_key,
418
+ )
419
+
420
+ # Trace context
421
+ self._trace_id: Optional[str] = None
422
+ self._crew_span_id: Optional[str] = None
423
+
424
+ def __call__(self, task_output: Any) -> None:
425
+ """Called when task completes.
426
+
427
+ Args:
428
+ task_output: TaskOutput object with:
429
+ - description: Task description
430
+ - raw: Raw output string
431
+ - pydantic: Optional Pydantic model output
432
+ - json_dict: Optional JSON dict output
433
+ - agent: Agent role that executed the task
434
+ - output_format: Output format type
435
+ """
436
+ try:
437
+ self._handle_task_complete(task_output)
438
+ except Exception as e:
439
+ # Don't let callback errors break the task
440
+ pass
441
+
442
+ def _handle_task_complete(self, task_output: Any):
443
+ """Process task output and create trace event."""
444
+ now = datetime.now(timezone.utc)
445
+
446
+ # Initialize trace if needed
447
+ if not self._trace_id:
448
+ self._trace_id = str(uuid.uuid4())
449
+
450
+ span_id = str(uuid.uuid4())
451
+
452
+ # Extract task information
453
+ description = ""
454
+ raw_output = ""
455
+ agent_role = "unknown"
456
+
457
+ if hasattr(task_output, "description"):
458
+ description = str(task_output.description)
459
+
460
+ if hasattr(task_output, "raw"):
461
+ raw_output = str(task_output.raw)
462
+ elif hasattr(task_output, "output"):
463
+ raw_output = str(task_output.output)
464
+ else:
465
+ raw_output = str(task_output)
466
+
467
+ if hasattr(task_output, "agent"):
468
+ agent_role = str(task_output.agent)
469
+
470
+ # Token counting
471
+ input_tokens = _count_tokens(description, "gpt-4")
472
+ output_tokens = _count_tokens(raw_output, "gpt-4")
473
+
474
+ # Build operation name from description
475
+ operation = "task_complete"
476
+ if description:
477
+ # Create short operation name from description
478
+ words = description.split()[:5]
479
+ operation = f"task:{' '.join(words)}"[:64]
480
+
481
+ # Build event
482
+ event = {
483
+ "schema_version": "1.0",
484
+ "trace_id": self._trace_id,
485
+ "span_id": span_id,
486
+ "parent_span_id": self._crew_span_id,
487
+ "tenant_id": self.tenant_id,
488
+ "workflow_id": self.workflow_id,
489
+ "provider": "crewai",
490
+ "model_id": "task",
491
+ "model_name": agent_role,
492
+ "operation": operation,
493
+ "endpoint": "task_complete",
494
+ "duration_ms": 0, # Task timing not available in callback
495
+ "latency_ms": 0,
496
+ "input_tokens": input_tokens,
497
+ "output_tokens": output_tokens,
498
+ "total_tokens": input_tokens + output_tokens,
499
+ "cost_usd": 0.0, # Cost tracked at LLM level
500
+ "total_cost_usd": 0.0,
501
+ "status": "success",
502
+ "timestamp": now.isoformat(),
503
+ "ts_start": now.isoformat(),
504
+ "ts_end": now.isoformat(),
505
+ "environment": self.environment,
506
+ "service": self.service,
507
+ "runtime_env": os.getenv("RUNTIME_ENV", "local"),
508
+ "sandbox_id": os.getenv("SANDBOX_ID", "local"),
509
+ "metadata": {
510
+ **self.default_metadata,
511
+ "span_type": "task",
512
+ "crewai": True,
513
+ "task_description": description[:500] if description else None,
514
+ "agent_role": agent_role,
515
+ "output_preview": raw_output[:500] if raw_output else None,
516
+ "output_format": getattr(task_output, "output_format", None),
517
+ },
518
+ }
519
+
520
+ self._batcher.enqueue(event)
521
+
522
+ def set_trace_context(self, trace_id: str, crew_span_id: Optional[str] = None):
523
+ """Set trace context for linking tasks to a crew execution.
524
+
525
+ Args:
526
+ trace_id: Parent trace ID
527
+ crew_span_id: Parent crew span ID
528
+ """
529
+ self._trace_id = trace_id
530
+ self._crew_span_id = crew_span_id
531
+
532
+ def reset(self):
533
+ """Reset trace context for new crew run."""
534
+ self._trace_id = None
535
+ self._crew_span_id = None
536
+
537
+ def flush(self):
538
+ """Force flush pending events."""
539
+ self._batcher.flush()