kalibr 1.0.25__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.25.data/data/examples/README.md +0 -173
- kalibr-1.0.25.data/data/examples/basic_kalibr_example.py +0 -66
- kalibr-1.0.25.data/data/examples/enhanced_kalibr_example.py +0 -347
- kalibr-1.0.25.dist-info/METADATA +0 -231
- kalibr-1.0.25.dist-info/RECORD +0 -19
- kalibr-1.0.25.dist-info/entry_points.txt +0 -2
- kalibr-1.0.25.dist-info/licenses/LICENSE +0 -11
- kalibr-1.0.25.dist-info/top_level.txt +0 -1
- {kalibr-1.0.25.dist-info → kalibr-1.1.2a0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Kalibr OpenAI Agents SDK Integration - Observability for OpenAI Agents.
|
|
2
|
+
|
|
3
|
+
This package provides a TracingProcessor that integrates OpenAI's Agents SDK
|
|
4
|
+
with Kalibr's observability platform, capturing:
|
|
5
|
+
- Agent executions and handoffs
|
|
6
|
+
- LLM generation spans with tokens and model info
|
|
7
|
+
- Function/tool invocations
|
|
8
|
+
- Guardrail checks
|
|
9
|
+
- Full trace hierarchy
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
from kalibr_openai_agents import KalibrTracingProcessor, setup_kalibr_tracing
|
|
13
|
+
from agents import Agent, Runner
|
|
14
|
+
|
|
15
|
+
# Option 1: Quick setup (adds to existing processors)
|
|
16
|
+
setup_kalibr_tracing(tenant_id="my-tenant")
|
|
17
|
+
|
|
18
|
+
# Option 2: Manual setup with more control
|
|
19
|
+
from agents.tracing import add_trace_processor
|
|
20
|
+
processor = KalibrTracingProcessor(tenant_id="my-tenant")
|
|
21
|
+
add_trace_processor(processor)
|
|
22
|
+
|
|
23
|
+
# Use OpenAI Agents normally
|
|
24
|
+
agent = Agent(name="Assistant", instructions="You are helpful.")
|
|
25
|
+
result = Runner.run_sync(agent, "Hello!")
|
|
26
|
+
|
|
27
|
+
Environment Variables:
|
|
28
|
+
KALIBR_API_KEY: API key for authentication
|
|
29
|
+
KALIBR_ENDPOINT: Backend endpoint URL
|
|
30
|
+
KALIBR_TENANT_ID: Tenant identifier
|
|
31
|
+
KALIBR_ENVIRONMENT: Environment (prod/staging/dev)
|
|
32
|
+
KALIBR_SERVICE: Service name
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
__version__ = "0.1.0"
|
|
36
|
+
|
|
37
|
+
from .processor import KalibrTracingProcessor, setup_kalibr_tracing
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"KalibrTracingProcessor",
|
|
41
|
+
"setup_kalibr_tracing",
|
|
42
|
+
"__version__",
|
|
43
|
+
]
|
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
"""Kalibr TracingProcessor for OpenAI Agents SDK.
|
|
2
|
+
|
|
3
|
+
This module implements a TracingProcessor that captures telemetry from
|
|
4
|
+
OpenAI Agents and sends it to the Kalibr backend.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import atexit
|
|
8
|
+
import os
|
|
9
|
+
import queue
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
import uuid
|
|
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 fallback
|
|
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(str(text)))
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
return len(str(text)) // 4
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class EventBatcher:
|
|
48
|
+
"""Batches events for efficient sending to backend."""
|
|
49
|
+
|
|
50
|
+
_instances: Dict[str, "EventBatcher"] = {}
|
|
51
|
+
_lock = threading.Lock()
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
endpoint: str,
|
|
56
|
+
api_key: str,
|
|
57
|
+
batch_size: int = 100,
|
|
58
|
+
flush_interval: float = 2.0,
|
|
59
|
+
):
|
|
60
|
+
self.endpoint = endpoint
|
|
61
|
+
self.api_key = api_key
|
|
62
|
+
self.batch_size = batch_size
|
|
63
|
+
self.flush_interval = flush_interval
|
|
64
|
+
|
|
65
|
+
self._event_queue: queue.Queue = queue.Queue(maxsize=5000)
|
|
66
|
+
self._client = httpx.Client(timeout=10.0)
|
|
67
|
+
self._shutdown = False
|
|
68
|
+
|
|
69
|
+
self._flush_thread = threading.Thread(target=self._flush_loop, daemon=True)
|
|
70
|
+
self._flush_thread.start()
|
|
71
|
+
|
|
72
|
+
atexit.register(self.shutdown)
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def get_instance(
|
|
76
|
+
cls,
|
|
77
|
+
endpoint: str,
|
|
78
|
+
api_key: str,
|
|
79
|
+
batch_size: int = 100,
|
|
80
|
+
flush_interval: float = 2.0,
|
|
81
|
+
) -> "EventBatcher":
|
|
82
|
+
"""Get or create a shared EventBatcher instance."""
|
|
83
|
+
key = f"{endpoint}:{api_key}"
|
|
84
|
+
with cls._lock:
|
|
85
|
+
if key not in cls._instances:
|
|
86
|
+
cls._instances[key] = cls(
|
|
87
|
+
endpoint=endpoint,
|
|
88
|
+
api_key=api_key,
|
|
89
|
+
batch_size=batch_size,
|
|
90
|
+
flush_interval=flush_interval,
|
|
91
|
+
)
|
|
92
|
+
return cls._instances[key]
|
|
93
|
+
|
|
94
|
+
def enqueue(self, event: Dict[str, Any]):
|
|
95
|
+
"""Add event to queue."""
|
|
96
|
+
try:
|
|
97
|
+
self._event_queue.put_nowait(event)
|
|
98
|
+
except queue.Full:
|
|
99
|
+
try:
|
|
100
|
+
self._event_queue.get_nowait()
|
|
101
|
+
self._event_queue.put_nowait(event)
|
|
102
|
+
except:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
def _flush_loop(self):
|
|
106
|
+
"""Background thread to flush events."""
|
|
107
|
+
batch = []
|
|
108
|
+
last_flush = time.time()
|
|
109
|
+
|
|
110
|
+
while not self._shutdown:
|
|
111
|
+
try:
|
|
112
|
+
try:
|
|
113
|
+
event = self._event_queue.get(timeout=0.1)
|
|
114
|
+
batch.append(event)
|
|
115
|
+
except queue.Empty:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
now = time.time()
|
|
119
|
+
should_flush = (
|
|
120
|
+
len(batch) >= self.batch_size or
|
|
121
|
+
(batch and now - last_flush >= self.flush_interval)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if should_flush:
|
|
125
|
+
self._send_batch(batch)
|
|
126
|
+
batch = []
|
|
127
|
+
last_flush = now
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
if batch:
|
|
132
|
+
self._send_batch(batch)
|
|
133
|
+
|
|
134
|
+
def _send_batch(self, batch: List[Dict[str, Any]]):
|
|
135
|
+
"""Send batch to backend."""
|
|
136
|
+
if not batch:
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
payload = {"events": batch}
|
|
141
|
+
headers = {}
|
|
142
|
+
if self.api_key:
|
|
143
|
+
headers["X-API-Key"] = self.api_key
|
|
144
|
+
|
|
145
|
+
self._client.post(self.endpoint, json=payload, headers=headers)
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
def shutdown(self):
|
|
150
|
+
"""Shutdown batcher."""
|
|
151
|
+
if self._shutdown:
|
|
152
|
+
return
|
|
153
|
+
self._shutdown = True
|
|
154
|
+
if self._flush_thread.is_alive():
|
|
155
|
+
self._flush_thread.join(timeout=5.0)
|
|
156
|
+
self._client.close()
|
|
157
|
+
|
|
158
|
+
def flush(self):
|
|
159
|
+
"""Force flush pending events."""
|
|
160
|
+
events = []
|
|
161
|
+
while True:
|
|
162
|
+
try:
|
|
163
|
+
event = self._event_queue.get_nowait()
|
|
164
|
+
events.append(event)
|
|
165
|
+
except queue.Empty:
|
|
166
|
+
break
|
|
167
|
+
if events:
|
|
168
|
+
self._send_batch(events)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class KalibrTracingProcessor:
|
|
172
|
+
"""OpenAI Agents SDK TracingProcessor for Kalibr observability.
|
|
173
|
+
|
|
174
|
+
This processor implements the TracingProcessor interface from OpenAI's
|
|
175
|
+
Agents SDK and sends telemetry to the Kalibr backend.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
api_key: Kalibr API key
|
|
179
|
+
endpoint: Backend endpoint URL
|
|
180
|
+
tenant_id: Tenant identifier
|
|
181
|
+
environment: Environment (prod/staging/dev)
|
|
182
|
+
service: Service name
|
|
183
|
+
workflow_id: Workflow identifier
|
|
184
|
+
capture_input: Whether to capture inputs (default: True)
|
|
185
|
+
capture_output: Whether to capture outputs (default: True)
|
|
186
|
+
|
|
187
|
+
Usage:
|
|
188
|
+
from agents.tracing import add_trace_processor
|
|
189
|
+
from kalibr_openai_agents import KalibrTracingProcessor
|
|
190
|
+
|
|
191
|
+
processor = KalibrTracingProcessor(tenant_id="my-tenant")
|
|
192
|
+
add_trace_processor(processor)
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
def __init__(
|
|
196
|
+
self,
|
|
197
|
+
api_key: Optional[str] = None,
|
|
198
|
+
endpoint: Optional[str] = None,
|
|
199
|
+
tenant_id: Optional[str] = None,
|
|
200
|
+
environment: Optional[str] = None,
|
|
201
|
+
service: Optional[str] = None,
|
|
202
|
+
workflow_id: Optional[str] = None,
|
|
203
|
+
capture_input: bool = True,
|
|
204
|
+
capture_output: bool = True,
|
|
205
|
+
):
|
|
206
|
+
self.api_key = api_key or os.getenv("KALIBR_API_KEY", "")
|
|
207
|
+
self.endpoint = endpoint or os.getenv(
|
|
208
|
+
"KALIBR_ENDPOINT",
|
|
209
|
+
os.getenv("KALIBR_API_ENDPOINT", "http://localhost:8001/api/v1/traces")
|
|
210
|
+
)
|
|
211
|
+
self.tenant_id = tenant_id or os.getenv("KALIBR_TENANT_ID", "default")
|
|
212
|
+
self.environment = environment or os.getenv("KALIBR_ENVIRONMENT", "prod")
|
|
213
|
+
self.service = service or os.getenv("KALIBR_SERVICE", "openai-agents-app")
|
|
214
|
+
self.workflow_id = workflow_id or os.getenv("KALIBR_WORKFLOW_ID", "default-workflow")
|
|
215
|
+
self.capture_input = capture_input
|
|
216
|
+
self.capture_output = capture_output
|
|
217
|
+
|
|
218
|
+
# Get shared batcher
|
|
219
|
+
self._batcher = EventBatcher.get_instance(
|
|
220
|
+
endpoint=self.endpoint,
|
|
221
|
+
api_key=self.api_key,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Track active traces for context
|
|
225
|
+
self._active_traces: Dict[str, Dict[str, Any]] = {}
|
|
226
|
+
self._lock = threading.Lock()
|
|
227
|
+
|
|
228
|
+
def on_trace_start(self, trace: Any) -> None:
|
|
229
|
+
"""Called when a trace starts.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
trace: The trace object with trace_id, workflow_name, etc.
|
|
233
|
+
"""
|
|
234
|
+
try:
|
|
235
|
+
trace_id = getattr(trace, "trace_id", str(uuid.uuid4()))
|
|
236
|
+
workflow_name = getattr(trace, "workflow_name", "unknown")
|
|
237
|
+
|
|
238
|
+
with self._lock:
|
|
239
|
+
self._active_traces[trace_id] = {
|
|
240
|
+
"trace_id": trace_id,
|
|
241
|
+
"workflow_name": workflow_name,
|
|
242
|
+
"started_at": datetime.now(timezone.utc),
|
|
243
|
+
"spans": [],
|
|
244
|
+
}
|
|
245
|
+
except Exception:
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
def on_trace_end(self, trace: Any) -> None:
|
|
249
|
+
"""Called when a trace ends.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
trace: The completed trace object
|
|
253
|
+
"""
|
|
254
|
+
try:
|
|
255
|
+
trace_id = getattr(trace, "trace_id", None)
|
|
256
|
+
if not trace_id:
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
with self._lock:
|
|
260
|
+
trace_data = self._active_traces.pop(trace_id, None)
|
|
261
|
+
|
|
262
|
+
if not trace_data:
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
ended_at = datetime.now(timezone.utc)
|
|
266
|
+
started_at = trace_data.get("started_at", ended_at)
|
|
267
|
+
duration_ms = int((ended_at - started_at).total_seconds() * 1000)
|
|
268
|
+
|
|
269
|
+
# Create trace-level event
|
|
270
|
+
event = {
|
|
271
|
+
"schema_version": "1.0",
|
|
272
|
+
"trace_id": trace_id,
|
|
273
|
+
"span_id": str(uuid.uuid4()),
|
|
274
|
+
"parent_span_id": None,
|
|
275
|
+
"tenant_id": self.tenant_id,
|
|
276
|
+
"workflow_id": self.workflow_id,
|
|
277
|
+
"provider": "openai",
|
|
278
|
+
"model_id": "agents",
|
|
279
|
+
"model_name": trace_data.get("workflow_name", "unknown"),
|
|
280
|
+
"operation": f"trace:{trace_data.get('workflow_name', 'unknown')}",
|
|
281
|
+
"endpoint": "agents.trace",
|
|
282
|
+
"duration_ms": duration_ms,
|
|
283
|
+
"latency_ms": duration_ms,
|
|
284
|
+
"input_tokens": 0,
|
|
285
|
+
"output_tokens": 0,
|
|
286
|
+
"total_tokens": 0,
|
|
287
|
+
"cost_usd": 0.0,
|
|
288
|
+
"total_cost_usd": 0.0,
|
|
289
|
+
"status": "success",
|
|
290
|
+
"timestamp": started_at.isoformat(),
|
|
291
|
+
"ts_start": started_at.isoformat(),
|
|
292
|
+
"ts_end": ended_at.isoformat(),
|
|
293
|
+
"environment": self.environment,
|
|
294
|
+
"service": self.service,
|
|
295
|
+
"runtime_env": os.getenv("RUNTIME_ENV", "local"),
|
|
296
|
+
"sandbox_id": os.getenv("SANDBOX_ID", "local"),
|
|
297
|
+
"metadata": {
|
|
298
|
+
"span_type": "trace",
|
|
299
|
+
"openai_agents": True,
|
|
300
|
+
"workflow_name": trace_data.get("workflow_name"),
|
|
301
|
+
"span_count": len(trace_data.get("spans", [])),
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
self._batcher.enqueue(event)
|
|
306
|
+
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
309
|
+
|
|
310
|
+
def on_span_start(self, span: Any) -> None:
|
|
311
|
+
"""Called when a span starts.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
span: The span object with span_id, trace_id, span_data, etc.
|
|
315
|
+
"""
|
|
316
|
+
# We process spans on end to have complete data
|
|
317
|
+
pass
|
|
318
|
+
|
|
319
|
+
def on_span_end(self, span: Any) -> None:
|
|
320
|
+
"""Called when a span ends.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
span: The completed span object
|
|
324
|
+
"""
|
|
325
|
+
try:
|
|
326
|
+
self._process_span(span)
|
|
327
|
+
except Exception:
|
|
328
|
+
pass
|
|
329
|
+
|
|
330
|
+
def _process_span(self, span: Any):
|
|
331
|
+
"""Process a completed span and create Kalibr event."""
|
|
332
|
+
# Extract span attributes
|
|
333
|
+
span_id = getattr(span, "span_id", str(uuid.uuid4()))
|
|
334
|
+
trace_id = getattr(span, "trace_id", str(uuid.uuid4()))
|
|
335
|
+
parent_id = getattr(span, "parent_id", None)
|
|
336
|
+
|
|
337
|
+
# Get timing
|
|
338
|
+
started_at = getattr(span, "started_at", None)
|
|
339
|
+
ended_at = getattr(span, "ended_at", None)
|
|
340
|
+
|
|
341
|
+
if isinstance(started_at, str):
|
|
342
|
+
started_at = datetime.fromisoformat(started_at.replace("Z", "+00:00"))
|
|
343
|
+
if isinstance(ended_at, str):
|
|
344
|
+
ended_at = datetime.fromisoformat(ended_at.replace("Z", "+00:00"))
|
|
345
|
+
|
|
346
|
+
if not started_at:
|
|
347
|
+
started_at = datetime.now(timezone.utc)
|
|
348
|
+
if not ended_at:
|
|
349
|
+
ended_at = datetime.now(timezone.utc)
|
|
350
|
+
|
|
351
|
+
duration_ms = int((ended_at - started_at).total_seconds() * 1000)
|
|
352
|
+
|
|
353
|
+
# Get span data
|
|
354
|
+
span_data = getattr(span, "span_data", None)
|
|
355
|
+
|
|
356
|
+
# Determine span type and extract relevant data
|
|
357
|
+
span_type = "unknown"
|
|
358
|
+
operation = "span"
|
|
359
|
+
model = "unknown"
|
|
360
|
+
input_tokens = 0
|
|
361
|
+
output_tokens = 0
|
|
362
|
+
input_preview = None
|
|
363
|
+
output_preview = None
|
|
364
|
+
|
|
365
|
+
if span_data is not None:
|
|
366
|
+
span_type = type(span_data).__name__
|
|
367
|
+
|
|
368
|
+
# Handle GenerationSpanData (LLM calls)
|
|
369
|
+
if "Generation" in span_type:
|
|
370
|
+
span_type = "generation"
|
|
371
|
+
operation = "llm_generation"
|
|
372
|
+
model = getattr(span_data, "model", "gpt-4")
|
|
373
|
+
|
|
374
|
+
# Extract token counts from response
|
|
375
|
+
response = getattr(span_data, "response", None)
|
|
376
|
+
if response:
|
|
377
|
+
usage = getattr(response, "usage", None)
|
|
378
|
+
if usage:
|
|
379
|
+
input_tokens = getattr(usage, "prompt_tokens", 0) or 0
|
|
380
|
+
output_tokens = getattr(usage, "completion_tokens", 0) or 0
|
|
381
|
+
|
|
382
|
+
# Extract input/output if capturing
|
|
383
|
+
if self.capture_input:
|
|
384
|
+
input_data = getattr(span_data, "input", None)
|
|
385
|
+
if input_data:
|
|
386
|
+
input_preview = str(input_data)[:500]
|
|
387
|
+
|
|
388
|
+
if self.capture_output:
|
|
389
|
+
output_data = getattr(span_data, "output", None)
|
|
390
|
+
if output_data:
|
|
391
|
+
output_preview = str(output_data)[:500]
|
|
392
|
+
|
|
393
|
+
# Handle AgentSpanData
|
|
394
|
+
elif "Agent" in span_type:
|
|
395
|
+
span_type = "agent"
|
|
396
|
+
agent_name = getattr(span_data, "name", "unknown")
|
|
397
|
+
operation = f"agent:{agent_name}"
|
|
398
|
+
|
|
399
|
+
# Handle FunctionSpanData (tool calls)
|
|
400
|
+
elif "Function" in span_type:
|
|
401
|
+
span_type = "function"
|
|
402
|
+
func_name = getattr(span_data, "name", "unknown")
|
|
403
|
+
operation = f"function:{func_name}"
|
|
404
|
+
|
|
405
|
+
if self.capture_input:
|
|
406
|
+
input_data = getattr(span_data, "input", None)
|
|
407
|
+
if input_data:
|
|
408
|
+
input_preview = str(input_data)[:500]
|
|
409
|
+
|
|
410
|
+
if self.capture_output:
|
|
411
|
+
output_data = getattr(span_data, "output", None)
|
|
412
|
+
if output_data:
|
|
413
|
+
output_preview = str(output_data)[:500]
|
|
414
|
+
|
|
415
|
+
# Handle HandoffSpanData
|
|
416
|
+
elif "Handoff" in span_type:
|
|
417
|
+
span_type = "handoff"
|
|
418
|
+
from_agent = getattr(span_data, "from_agent", "unknown")
|
|
419
|
+
to_agent = getattr(span_data, "to_agent", "unknown")
|
|
420
|
+
operation = f"handoff:{from_agent}->{to_agent}"
|
|
421
|
+
|
|
422
|
+
# Handle GuardrailSpanData
|
|
423
|
+
elif "Guardrail" in span_type:
|
|
424
|
+
span_type = "guardrail"
|
|
425
|
+
guardrail_name = getattr(span_data, "name", "unknown")
|
|
426
|
+
operation = f"guardrail:{guardrail_name}"
|
|
427
|
+
|
|
428
|
+
else:
|
|
429
|
+
# Generic span
|
|
430
|
+
operation = f"span:{span_type}"
|
|
431
|
+
|
|
432
|
+
# Calculate cost for generation spans
|
|
433
|
+
cost_usd = 0.0
|
|
434
|
+
if span_type == "generation" and CostAdapterFactory is not None:
|
|
435
|
+
cost_usd = CostAdapterFactory.compute_cost(
|
|
436
|
+
vendor="openai",
|
|
437
|
+
model_name=model,
|
|
438
|
+
tokens_in=input_tokens,
|
|
439
|
+
tokens_out=output_tokens,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# Get error info
|
|
443
|
+
error = getattr(span, "error", None)
|
|
444
|
+
status = "error" if error else "success"
|
|
445
|
+
error_type = None
|
|
446
|
+
error_message = None
|
|
447
|
+
if error:
|
|
448
|
+
error_type = type(error).__name__
|
|
449
|
+
error_message = str(error)[:512]
|
|
450
|
+
|
|
451
|
+
# Build event
|
|
452
|
+
event = {
|
|
453
|
+
"schema_version": "1.0",
|
|
454
|
+
"trace_id": trace_id,
|
|
455
|
+
"span_id": span_id,
|
|
456
|
+
"parent_span_id": parent_id,
|
|
457
|
+
"tenant_id": self.tenant_id,
|
|
458
|
+
"workflow_id": self.workflow_id,
|
|
459
|
+
"provider": "openai",
|
|
460
|
+
"model_id": model,
|
|
461
|
+
"model_name": model,
|
|
462
|
+
"operation": operation,
|
|
463
|
+
"endpoint": f"agents.{span_type}",
|
|
464
|
+
"duration_ms": duration_ms,
|
|
465
|
+
"latency_ms": duration_ms,
|
|
466
|
+
"input_tokens": input_tokens,
|
|
467
|
+
"output_tokens": output_tokens,
|
|
468
|
+
"total_tokens": input_tokens + output_tokens,
|
|
469
|
+
"cost_usd": cost_usd,
|
|
470
|
+
"total_cost_usd": cost_usd,
|
|
471
|
+
"status": status,
|
|
472
|
+
"error_type": error_type,
|
|
473
|
+
"error_message": error_message,
|
|
474
|
+
"timestamp": started_at.isoformat(),
|
|
475
|
+
"ts_start": started_at.isoformat(),
|
|
476
|
+
"ts_end": ended_at.isoformat(),
|
|
477
|
+
"environment": self.environment,
|
|
478
|
+
"service": self.service,
|
|
479
|
+
"runtime_env": os.getenv("RUNTIME_ENV", "local"),
|
|
480
|
+
"sandbox_id": os.getenv("SANDBOX_ID", "local"),
|
|
481
|
+
"metadata": {
|
|
482
|
+
"span_type": span_type,
|
|
483
|
+
"openai_agents": True,
|
|
484
|
+
"input_preview": input_preview,
|
|
485
|
+
"output_preview": output_preview,
|
|
486
|
+
},
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
self._batcher.enqueue(event)
|
|
490
|
+
|
|
491
|
+
def shutdown(self) -> None:
|
|
492
|
+
"""Shutdown the processor and flush pending events."""
|
|
493
|
+
self._batcher.shutdown()
|
|
494
|
+
|
|
495
|
+
def force_flush(self) -> None:
|
|
496
|
+
"""Force flush all pending events."""
|
|
497
|
+
self._batcher.flush()
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def setup_kalibr_tracing(
|
|
501
|
+
api_key: Optional[str] = None,
|
|
502
|
+
endpoint: Optional[str] = None,
|
|
503
|
+
tenant_id: Optional[str] = None,
|
|
504
|
+
environment: Optional[str] = None,
|
|
505
|
+
service: Optional[str] = None,
|
|
506
|
+
workflow_id: Optional[str] = None,
|
|
507
|
+
capture_input: bool = True,
|
|
508
|
+
capture_output: bool = True,
|
|
509
|
+
) -> KalibrTracingProcessor:
|
|
510
|
+
"""Set up Kalibr tracing for OpenAI Agents SDK.
|
|
511
|
+
|
|
512
|
+
This is a convenience function that creates a KalibrTracingProcessor
|
|
513
|
+
and adds it to the OpenAI Agents SDK's trace processors.
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
api_key: Kalibr API key
|
|
517
|
+
endpoint: Backend endpoint URL
|
|
518
|
+
tenant_id: Tenant identifier
|
|
519
|
+
environment: Environment (prod/staging/dev)
|
|
520
|
+
service: Service name
|
|
521
|
+
workflow_id: Workflow identifier
|
|
522
|
+
capture_input: Whether to capture inputs
|
|
523
|
+
capture_output: Whether to capture outputs
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
The configured KalibrTracingProcessor
|
|
527
|
+
|
|
528
|
+
Usage:
|
|
529
|
+
from kalibr_openai_agents import setup_kalibr_tracing
|
|
530
|
+
|
|
531
|
+
# Quick setup
|
|
532
|
+
processor = setup_kalibr_tracing(tenant_id="my-tenant")
|
|
533
|
+
|
|
534
|
+
# Now use OpenAI Agents normally
|
|
535
|
+
"""
|
|
536
|
+
processor = KalibrTracingProcessor(
|
|
537
|
+
api_key=api_key,
|
|
538
|
+
endpoint=endpoint,
|
|
539
|
+
tenant_id=tenant_id,
|
|
540
|
+
environment=environment,
|
|
541
|
+
service=service,
|
|
542
|
+
workflow_id=workflow_id,
|
|
543
|
+
capture_input=capture_input,
|
|
544
|
+
capture_output=capture_output,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
# Try to add to OpenAI Agents SDK trace processors
|
|
548
|
+
try:
|
|
549
|
+
from agents.tracing import add_trace_processor
|
|
550
|
+
add_trace_processor(processor)
|
|
551
|
+
except ImportError:
|
|
552
|
+
print("[Kalibr] OpenAI Agents SDK not installed, processor created but not registered")
|
|
553
|
+
|
|
554
|
+
return processor
|
kalibr/deployment.py
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Kalibr Deployment
|
|
3
|
-
-----------------
|
|
4
|
-
Thin wrapper that forwards to the runtime router.
|
|
5
|
-
Keeps a simple API surface for backwards-compat commands.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
from dataclasses import dataclass, field
|
|
10
|
-
from typing import Dict, Any
|
|
11
|
-
from kalibr.runtime_router import deploy as router_deploy
|
|
12
|
-
|
|
13
|
-
@dataclass
|
|
14
|
-
class DeploymentConfig:
|
|
15
|
-
app_name: str
|
|
16
|
-
memory_mb: int = 512
|
|
17
|
-
timeout_seconds: int = 30
|
|
18
|
-
environment_vars: Dict[str, str] = field(default_factory=dict)
|
|
19
|
-
|
|
20
|
-
def deploy_app(file_path: str, config: DeploymentConfig, platform: str = "local") -> Dict[str, Any]:
|
|
21
|
-
# Map older "platform" to runtime names used by router
|
|
22
|
-
runtime = {
|
|
23
|
-
"local": "local",
|
|
24
|
-
"fly": "fly",
|
|
25
|
-
"aws-lambda": "local", # not supported; punt to local
|
|
26
|
-
"render": "render",
|
|
27
|
-
}.get(platform, platform)
|
|
28
|
-
|
|
29
|
-
result = router_deploy(runtime=runtime, app_name=config.app_name, app_file=file_path)
|
|
30
|
-
if result.get("status") in ("success", "started"):
|
|
31
|
-
eps = result.get("endpoints", {})
|
|
32
|
-
return {
|
|
33
|
-
"status": "success",
|
|
34
|
-
"endpoints": {
|
|
35
|
-
"root": eps.get("root", ""),
|
|
36
|
-
"mcp": eps.get("mcp", ""),
|
|
37
|
-
"openapi": eps.get("openapi", ""),
|
|
38
|
-
"health": eps.get("health", ""),
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return {"status": "error", "error": "unknown deploy outcome", "raw": result}
|
kalibr/packager.py
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Packager
|
|
3
|
-
--------
|
|
4
|
-
Create a deployable MCP bundle (code + manifests + metadata).
|
|
5
|
-
This does not host anything; it prepares artifacts for any runtime.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
import shutil
|
|
11
|
-
import json
|
|
12
|
-
import tempfile
|
|
13
|
-
from typing import Dict, Any, Optional
|
|
14
|
-
|
|
15
|
-
DEFAULT_BUNDLE = "kalibr_bundle.zip"
|
|
16
|
-
|
|
17
|
-
def package_app(app_dir: str = ".", output: str = DEFAULT_BUNDLE, models_supported: Optional[list] = None, kalibr_version: str = "unknown") -> str:
|
|
18
|
-
app_dir = Path(app_dir).resolve()
|
|
19
|
-
out_path = Path(output).resolve()
|
|
20
|
-
|
|
21
|
-
# Assemble temp dir with metadata
|
|
22
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
23
|
-
tmpdir = Path(tmp)
|
|
24
|
-
# Copy source tree
|
|
25
|
-
for item in app_dir.iterdir():
|
|
26
|
-
if item.name == out_path.name:
|
|
27
|
-
continue
|
|
28
|
-
dest = tmpdir / item.name
|
|
29
|
-
if item.is_dir():
|
|
30
|
-
shutil.copytree(item, dest)
|
|
31
|
-
else:
|
|
32
|
-
shutil.copy2(item, dest)
|
|
33
|
-
|
|
34
|
-
# Write bundle metadata
|
|
35
|
-
(tmpdir / "kalibr_manifest.json").write_text(json.dumps({
|
|
36
|
-
"kalibr_version": kalibr_version,
|
|
37
|
-
"models_supported": models_supported or ["mcp", "gpt-actions", "gemini", "copilot"],
|
|
38
|
-
}, indent=2))
|
|
39
|
-
|
|
40
|
-
# Zip
|
|
41
|
-
shutil.make_archive(out_path.with_suffix(""), "zip", tmpdir)
|
|
42
|
-
|
|
43
|
-
return str(out_path)
|