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.
- kalibr/__init__.py +129 -4
- 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.3a0.dist-info/METADATA +236 -0
- kalibr-1.1.3a0.dist-info/RECORD +48 -0
- kalibr-1.1.3a0.dist-info/entry_points.txt +2 -0
- kalibr-1.1.3a0.dist-info/licenses/LICENSE +21 -0
- kalibr-1.1.3a0.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.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()
|