kalibr 1.1.3a0__py3-none-any.whl → 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
kalibr/router.py ADDED
@@ -0,0 +1,499 @@
1
+ """
2
+ Kalibr Router - Intelligent model routing with outcome learning.
3
+ """
4
+
5
+ import os
6
+ import logging
7
+ import uuid
8
+ from typing import Any, Callable, Dict, List, Optional, Union
9
+
10
+ from opentelemetry import trace as otel_trace
11
+ from opentelemetry.trace import SpanContext, TraceFlags, NonRecordingSpan, set_span_in_context
12
+ from opentelemetry.context import Context
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Type for paths - either string or dict
17
+ PathSpec = Union[str, Dict[str, Any]]
18
+
19
+
20
+ def _create_context_with_trace_id(trace_id_hex: str) -> Optional[Context]:
21
+ """Create an OTel context with a specific trace_id.
22
+
23
+ This allows child spans to inherit the intelligence service's trace_id,
24
+ enabling JOINs between outcomes and traces tables.
25
+ """
26
+ try:
27
+ # Convert 32-char hex string to 128-bit int
28
+ trace_id_int = int(trace_id_hex, 16)
29
+ if trace_id_int == 0:
30
+ return None
31
+
32
+ # Create span context with our trace_id
33
+ span_context = SpanContext(
34
+ trace_id=trace_id_int,
35
+ span_id=0xDEADBEEF, # Placeholder, real span will have its own
36
+ is_remote=True, # Treat as remote parent so new span_id is generated
37
+ trace_flags=TraceFlags(TraceFlags.SAMPLED),
38
+ )
39
+
40
+ # Create a non-recording parent span and set in context
41
+ parent_span = NonRecordingSpan(span_context)
42
+ return set_span_in_context(parent_span)
43
+ except (ValueError, TypeError) as e:
44
+ logger.warning(f"Could not create OTel context with trace_id: {e}")
45
+ return None
46
+
47
+
48
+ class Router:
49
+ """
50
+ Routes LLM requests to the best model based on learned outcomes.
51
+
52
+ Example:
53
+ router = Router(
54
+ goal="summarize",
55
+ paths=["gpt-4o", "claude-3-sonnet"],
56
+ success_when=lambda out: len(out) > 100
57
+ )
58
+ response = router.completion(messages=[...])
59
+
60
+ Examples:
61
+ # Simple auto-reporting
62
+ router = Router(
63
+ goal="extract_email",
64
+ paths=["gpt-4o", "claude-sonnet-4"],
65
+ success_when=lambda out: "@" in out
66
+ )
67
+ response = router.completion(messages=[...])
68
+ # report() called automatically
69
+
70
+ # Manual reporting for complex validation
71
+ router = Router(
72
+ goal="book_meeting",
73
+ paths=["gpt-4o", "claude-sonnet-4"]
74
+ )
75
+ response = router.completion(messages=[...])
76
+ # ... complex validation logic ...
77
+ router.report(success=meeting_booked)
78
+
79
+ Warning:
80
+ Router is not thread-safe. For concurrent requests, create separate
81
+ Router instances per thread/task. For sequential requests in a single
82
+ thread, Router can be reused across multiple completion() calls.
83
+ """
84
+
85
+ def __init__(
86
+ self,
87
+ goal: str,
88
+ paths: Optional[List[PathSpec]] = None,
89
+ success_when: Optional[Callable[[str], bool]] = None,
90
+ exploration_rate: Optional[float] = None,
91
+ auto_register: bool = True,
92
+ ):
93
+ """
94
+ Initialize router.
95
+
96
+ Args:
97
+ goal: Name of the goal (e.g., "book_meeting", "summarize")
98
+ paths: List of models or path configs. Examples:
99
+ ["gpt-4o", "claude-3-sonnet"]
100
+ [{"model": "gpt-4o", "tools": ["search"]}]
101
+ [{"model": "gpt-4o", "params": {"temperature": 0.7}}]
102
+ success_when: Optional function to auto-evaluate success from LLM output.
103
+ Takes the output string and returns True/False.
104
+ When provided, report() is called automatically after completion().
105
+ Use for simple validations (output length, contains key string).
106
+ For complex validation (API calls, multi-step checks), omit this
107
+ and call report() manually.
108
+ Examples:
109
+ success_when=lambda out: len(out) > 0 # Not empty
110
+ success_when=lambda out: "@" in out # Contains email
111
+ exploration_rate: Override exploration rate (0.0-1.0)
112
+ auto_register: If True, register paths on init
113
+ """
114
+ self.goal = goal
115
+
116
+ # Validate required environment variables
117
+ api_key = os.environ.get('KALIBR_API_KEY')
118
+ tenant_id = os.environ.get('KALIBR_TENANT_ID')
119
+
120
+ if not api_key:
121
+ raise ValueError(
122
+ "KALIBR_API_KEY environment variable not set.\n"
123
+ "Get your API key from: https://dashboard.kalibr.systems/settings\n"
124
+ "Then run: export KALIBR_API_KEY=your-key-here"
125
+ )
126
+
127
+ if not tenant_id:
128
+ raise ValueError(
129
+ "KALIBR_TENANT_ID environment variable not set.\n"
130
+ "Find your Tenant ID at: https://dashboard.kalibr.systems/settings\n"
131
+ "Then run: export KALIBR_TENANT_ID=your-tenant-id"
132
+ )
133
+
134
+ self.success_when = success_when
135
+ self.exploration_rate = exploration_rate
136
+ self._last_trace_id: Optional[str] = None
137
+ self._last_model_id: Optional[str] = None
138
+ self._last_decision: Optional[dict] = None
139
+ self._outcome_reported = False
140
+
141
+ # Normalize paths to list of dicts
142
+ self._paths = self._normalize_paths(paths or ["gpt-4o"])
143
+
144
+ # Register paths if requested
145
+ if auto_register:
146
+ self._register_paths()
147
+
148
+ def _normalize_paths(self, paths: List[PathSpec]) -> List[Dict[str, Any]]:
149
+ """Convert paths to consistent format."""
150
+ normalized = []
151
+ for p in paths:
152
+ if isinstance(p, str):
153
+ normalized.append({"model": p, "tools": None, "params": None})
154
+ elif isinstance(p, dict):
155
+ normalized.append({
156
+ "model": p.get("model") or p.get("model_id"),
157
+ "tools": p.get("tools") or p.get("tool_id"),
158
+ "params": p.get("params"),
159
+ })
160
+ else:
161
+ raise ValueError(f"Invalid path spec: {p}")
162
+ return normalized
163
+
164
+ def _register_paths(self):
165
+ """Register paths with intelligence service."""
166
+ from kalibr.intelligence import register_path
167
+
168
+ for path in self._paths:
169
+ try:
170
+ register_path(
171
+ goal=self.goal,
172
+ model_id=path["model"],
173
+ tool_id=path["tools"][0] if isinstance(path["tools"], list) and path["tools"] else path["tools"],
174
+ params=path["params"],
175
+ )
176
+ except Exception as e:
177
+ # Log but don't fail - path might already exist
178
+ logger.debug(f"Path registration note: {e}")
179
+
180
+ def completion(
181
+ self,
182
+ messages: List[Dict[str, str]],
183
+ force_model: Optional[str] = None,
184
+ **kwargs
185
+ ) -> Any:
186
+ """
187
+ Make a completion request with intelligent routing.
188
+
189
+ Args:
190
+ messages: OpenAI-format messages
191
+ force_model: Override routing and use this model
192
+ **kwargs: Additional args passed to provider
193
+
194
+ Returns:
195
+ OpenAI-compatible ChatCompletion response with added attribute:
196
+ - kalibr_trace_id: Trace ID for explicit outcome reporting
197
+ """
198
+ from kalibr.intelligence import decide
199
+
200
+ # Reset state for new request
201
+ self._outcome_reported = False
202
+
203
+ # Step 1: Get routing decision FIRST (before creating span)
204
+ decision = None
205
+ model_id = None
206
+ tool_id = None
207
+ params = {}
208
+
209
+ if force_model:
210
+ model_id = force_model
211
+ self._last_decision = {"model_id": model_id, "forced": True}
212
+ else:
213
+ try:
214
+ decision = decide(goal=self.goal)
215
+ model_id = decision.get("model_id") or self._paths[0]["model"]
216
+ tool_id = decision.get("tool_id")
217
+ params = decision.get("params") or {}
218
+ self._last_decision = decision
219
+ except Exception as e:
220
+ logger.warning(f"Routing failed, using fallback: {e}")
221
+ model_id = self._paths[0]["model"]
222
+ tool_id = self._paths[0].get("tools")
223
+ params = self._paths[0].get("params") or {}
224
+ self._last_decision = {"model_id": model_id, "fallback": True, "error": str(e)}
225
+
226
+ # Step 2: Determine trace_id
227
+ decision_trace_id = self._last_decision.get("trace_id") if self._last_decision else None
228
+
229
+ if decision_trace_id:
230
+ trace_id = decision_trace_id
231
+ else:
232
+ trace_id = uuid.uuid4().hex # Fallback: generate OTel-compatible format
233
+
234
+ self._last_trace_id = trace_id
235
+ self._last_model_id = model_id
236
+
237
+ # Step 3: Create OTel context with intelligence trace_id
238
+ otel_context = _create_context_with_trace_id(trace_id) if trace_id else None
239
+
240
+ # Step 4: Create span with custom context (child spans inherit trace_id)
241
+ tracer = otel_trace.get_tracer("kalibr.router")
242
+
243
+ with tracer.start_as_current_span(
244
+ "kalibr.router.completion",
245
+ context=otel_context,
246
+ attributes={
247
+ "kalibr.goal": self.goal,
248
+ "kalibr.trace_id": trace_id,
249
+ "kalibr.model_id": model_id,
250
+ }
251
+ ) as router_span:
252
+ # Add decision attributes
253
+ if force_model:
254
+ router_span.set_attribute("kalibr.forced", True)
255
+ elif decision:
256
+ router_span.set_attribute("kalibr.path_id", decision.get("path_id", ""))
257
+ router_span.set_attribute("kalibr.reason", decision.get("reason", ""))
258
+ router_span.set_attribute("kalibr.exploration", decision.get("exploration", False))
259
+ router_span.set_attribute("kalibr.confidence", decision.get("confidence", 0.0))
260
+ else:
261
+ router_span.set_attribute("kalibr.fallback", True)
262
+
263
+ # Step 5: Dispatch to provider
264
+ try:
265
+ response = self._dispatch(model_id, messages, tool_id, **{**params, **kwargs})
266
+
267
+ # Auto-report if success_when provided
268
+ if self.success_when and not self._outcome_reported:
269
+ try:
270
+ output = response.choices[0].message.content or ""
271
+ success = self.success_when(output)
272
+ self.report(success=success)
273
+ except Exception as e:
274
+ logger.warning(f"Auto-outcome evaluation failed: {e}")
275
+
276
+ # Add trace_id to response for explicit linkage
277
+ response.kalibr_trace_id = trace_id
278
+ return response
279
+
280
+ except Exception as e:
281
+ router_span.set_attribute("error", True)
282
+ router_span.set_attribute("error.type", type(e).__name__)
283
+
284
+ # Auto-report failure
285
+ if not self._outcome_reported:
286
+ try:
287
+ self.report(success=False, reason=f"provider_error: {type(e).__name__}")
288
+ except:
289
+ pass
290
+ raise
291
+
292
+ def report(
293
+ self,
294
+ success: bool,
295
+ reason: Optional[str] = None,
296
+ score: Optional[float] = None,
297
+ trace_id: Optional[str] = None,
298
+ ):
299
+ """
300
+ Report outcome for the last completion.
301
+
302
+ Args:
303
+ success: Whether the task succeeded
304
+ reason: Optional failure reason
305
+ score: Optional quality score (0.0-1.0)
306
+ trace_id: Optional explicit trace ID (uses last completion's trace_id if not provided)
307
+ """
308
+ if self._outcome_reported:
309
+ logger.warning("Outcome already reported for this completion. Each completion() requires a separate report() call.")
310
+ return
311
+
312
+ from kalibr.intelligence import report_outcome
313
+
314
+ trace_id = trace_id or self._last_trace_id
315
+ if not trace_id:
316
+ raise ValueError("Must call completion() before report(). No trace_id available.")
317
+
318
+ try:
319
+ report_outcome(
320
+ trace_id=trace_id,
321
+ goal=self.goal,
322
+ success=success,
323
+ score=score,
324
+ failure_reason=reason,
325
+ model_id=self._last_model_id,
326
+ )
327
+ self._outcome_reported = True
328
+ except Exception as e:
329
+ logger.warning(f"Failed to report outcome: {e}")
330
+
331
+ def add_path(
332
+ self,
333
+ model: str,
334
+ tools: Optional[List[str]] = None,
335
+ params: Optional[Dict] = None,
336
+ ):
337
+ """Add a new path dynamically."""
338
+ from kalibr.intelligence import register_path
339
+
340
+ path = {"model": model, "tools": tools, "params": params}
341
+ self._paths.append(path)
342
+
343
+ register_path(
344
+ goal=self.goal,
345
+ model_id=model,
346
+ tool_id=tools[0] if tools else None,
347
+ params=params,
348
+ )
349
+
350
+ def _dispatch(
351
+ self,
352
+ model_id: str,
353
+ messages: List[Dict],
354
+ tools: Optional[Any] = None,
355
+ **kwargs
356
+ ) -> Any:
357
+ """Dispatch to the appropriate provider."""
358
+ if model_id.startswith(("gpt-", "o1-", "o3-")):
359
+ return self._call_openai(model_id, messages, tools, **kwargs)
360
+ elif model_id.startswith("claude-"):
361
+ return self._call_anthropic(model_id, messages, tools, **kwargs)
362
+ elif model_id.startswith(("gemini-", "models/gemini")):
363
+ return self._call_google(model_id, messages, tools, **kwargs)
364
+ else:
365
+ # Default to OpenAI-compatible
366
+ logger.info(f"Unknown model prefix '{model_id}', trying OpenAI")
367
+ return self._call_openai(model_id, messages, tools, **kwargs)
368
+
369
+ def _call_openai(self, model: str, messages: List[Dict], tools: Any, **kwargs) -> Any:
370
+ """Call OpenAI API."""
371
+ try:
372
+ from openai import OpenAI
373
+ except ImportError:
374
+ raise ImportError("Install 'openai' package: pip install openai")
375
+
376
+ client = OpenAI()
377
+
378
+ call_kwargs = {"model": model, "messages": messages, **kwargs}
379
+ if tools:
380
+ call_kwargs["tools"] = tools
381
+
382
+ return client.chat.completions.create(**call_kwargs)
383
+
384
+ def _call_anthropic(self, model: str, messages: List[Dict], tools: Any, **kwargs) -> Any:
385
+ """Call Anthropic API and convert response to OpenAI format."""
386
+ try:
387
+ from anthropic import Anthropic
388
+ except ImportError:
389
+ raise ImportError("Install 'anthropic' package: pip install anthropic")
390
+
391
+ client = Anthropic()
392
+
393
+ # Convert messages (handle system message)
394
+ system = None
395
+ anthropic_messages = []
396
+ for m in messages:
397
+ if m["role"] == "system":
398
+ system = m["content"]
399
+ else:
400
+ anthropic_messages.append({"role": m["role"], "content": m["content"]})
401
+
402
+ call_kwargs = {"model": model, "messages": anthropic_messages, "max_tokens": kwargs.pop("max_tokens", 4096)}
403
+ if system:
404
+ call_kwargs["system"] = system
405
+ if tools:
406
+ call_kwargs["tools"] = tools
407
+ call_kwargs.update(kwargs)
408
+
409
+ response = client.messages.create(**call_kwargs)
410
+
411
+ # Convert to OpenAI format
412
+ return self._anthropic_to_openai_response(response, model)
413
+
414
+ def _call_google(self, model: str, messages: List[Dict], tools: Any, **kwargs) -> Any:
415
+ """Call Google API and convert response to OpenAI format."""
416
+ try:
417
+ import google.generativeai as genai
418
+ except ImportError:
419
+ raise ImportError("Install 'google-generativeai' package: pip install google-generativeai")
420
+
421
+ # Configure if API key available
422
+ api_key = os.environ.get("GOOGLE_API_KEY")
423
+ if api_key:
424
+ genai.configure(api_key=api_key)
425
+
426
+ # Convert messages to Google format
427
+ model_name = model.replace("models/", "") if model.startswith("models/") else model
428
+ gmodel = genai.GenerativeModel(model_name)
429
+
430
+ # Simple conversion - concatenate messages
431
+ prompt = "\n".join([f"{m['role']}: {m['content']}" for m in messages])
432
+
433
+ response = gmodel.generate_content(prompt)
434
+
435
+ # Convert to OpenAI format
436
+ return self._google_to_openai_response(response, model)
437
+
438
+ def _anthropic_to_openai_response(self, response: Any, model: str) -> Any:
439
+ """Convert Anthropic response to OpenAI format."""
440
+ from types import SimpleNamespace
441
+
442
+ content = ""
443
+ if response.content:
444
+ content = response.content[0].text if hasattr(response.content[0], "text") else str(response.content[0])
445
+
446
+ return SimpleNamespace(
447
+ id=response.id,
448
+ model=model,
449
+ choices=[
450
+ SimpleNamespace(
451
+ index=0,
452
+ message=SimpleNamespace(
453
+ role="assistant",
454
+ content=content,
455
+ ),
456
+ finish_reason=response.stop_reason,
457
+ )
458
+ ],
459
+ usage=SimpleNamespace(
460
+ prompt_tokens=response.usage.input_tokens,
461
+ completion_tokens=response.usage.output_tokens,
462
+ total_tokens=response.usage.input_tokens + response.usage.output_tokens,
463
+ ),
464
+ )
465
+
466
+ def _google_to_openai_response(self, response: Any, model: str) -> Any:
467
+ """Convert Google response to OpenAI format."""
468
+ from types import SimpleNamespace
469
+ import uuid
470
+
471
+ content = response.text if hasattr(response, "text") else str(response)
472
+
473
+ return SimpleNamespace(
474
+ id=f"google-{uuid.uuid4().hex[:8]}",
475
+ model=model,
476
+ choices=[
477
+ SimpleNamespace(
478
+ index=0,
479
+ message=SimpleNamespace(
480
+ role="assistant",
481
+ content=content,
482
+ ),
483
+ finish_reason="stop",
484
+ )
485
+ ],
486
+ usage=SimpleNamespace(
487
+ prompt_tokens=getattr(response, "usage_metadata", {}).get("prompt_token_count", 0),
488
+ completion_tokens=getattr(response, "usage_metadata", {}).get("candidates_token_count", 0),
489
+ total_tokens=getattr(response, "usage_metadata", {}).get("total_token_count", 0),
490
+ ),
491
+ )
492
+
493
+ def as_langchain(self):
494
+ """Return a LangChain-compatible chat model."""
495
+ try:
496
+ from kalibr_langchain.chat_model import KalibrChatModel
497
+ return KalibrChatModel(router=self)
498
+ except ImportError:
499
+ raise ImportError("Install 'kalibr-langchain' package for LangChain integration")
kalibr/simple_tracer.py CHANGED
@@ -19,6 +19,8 @@ Capsule Usage (automatic when middleware is active):
19
19
  def process_request(request: Request, prompt: str):
20
20
  # Capsule automatically updated with this hop
21
21
  return llm_call(prompt)
22
+
23
+ Note: Uses centralized pricing from kalibr.pricing module.
22
24
  """
23
25
 
24
26
  import json
@@ -31,6 +33,8 @@ from datetime import datetime, timezone
31
33
  from functools import wraps
32
34
  from typing import Callable, Optional
33
35
 
36
+ from kalibr.pricing import compute_cost
37
+
34
38
  try:
35
39
  import requests
36
40
  except ImportError:
@@ -53,7 +57,7 @@ def send_event(payload: dict):
53
57
  print("[Kalibr SDK] ❌ requests library not available")
54
58
  return
55
59
 
56
- url = os.getenv("KALIBR_COLLECTOR_URL", "https://api.kalibr.systems/api/ingest")
60
+ url = os.getenv("KALIBR_COLLECTOR_URL", "https://kalibr-backend.fly.dev/api/ingest")
57
61
  api_key = os.getenv("KALIBR_API_KEY")
58
62
  if not api_key:
59
63
  print("[Kalibr SDK] ⚠️ KALIBR_API_KEY not set, traces will not be sent")
@@ -155,21 +159,18 @@ def trace(
155
159
  actual_input_tokens = input_tokens or kwargs.get("input_tokens", 1000)
156
160
  actual_output_tokens = output_tokens or kwargs.get("output_tokens", 500)
157
161
 
158
- # Cost calculation (simplified pricing)
159
- # OpenAI GPT-4o: ~$2.50/1M input, ~$10/1M output
160
- # Anthropic Claude-3-Sonnet: ~$3/1M input, ~$15/1M output
161
- pricing_map = {
162
- "openai": {"gpt-4o": 0.00000250, "gpt-4": 0.00003000},
163
- "anthropic": {"claude-3-sonnet": 0.00000300, "claude-3-opus": 0.00001500},
164
- "google": {"gemini-pro": 0.00000125},
165
- }
166
-
167
- # Get unit price
168
- provider_pricing = pricing_map.get(provider, {})
169
- unit_price_usd = provider_pricing.get(model, 0.00002000) # Default $0.02/1M
162
+ # Cost calculation using centralized pricing
163
+ # This ensures consistency with all other cost adapters
164
+ total_cost_usd = compute_cost(
165
+ vendor=provider,
166
+ model_name=model,
167
+ input_tokens=actual_input_tokens,
168
+ output_tokens=actual_output_tokens,
169
+ )
170
170
 
171
- # Calculate total cost
172
- total_cost_usd = (actual_input_tokens + actual_output_tokens) * unit_price_usd
171
+ # Calculate unit price for backward compatibility (total cost / total tokens)
172
+ total_tokens = actual_input_tokens + actual_output_tokens
173
+ unit_price_usd = total_cost_usd / total_tokens if total_tokens > 0 else 0.0
173
174
 
174
175
  # Build payload
175
176
  payload = {
kalibr/trace_capsule.py CHANGED
@@ -28,6 +28,7 @@ Usage:
28
28
  """
29
29
 
30
30
  import json
31
+ import threading
31
32
  import uuid
32
33
  from datetime import datetime, timezone
33
34
  from typing import Any, Dict, List, Optional
@@ -85,12 +86,16 @@ class TraceCapsule:
85
86
  # Phase 3C: Context token propagation (keep as UUID for consistency)
86
87
  self.context_token = context_token or str(uuid.uuid4())
87
88
  self.parent_context_token = parent_context_token
89
+ # Thread-safety: Lock for protecting concurrent append_hop operations
90
+ self._lock = threading.Lock()
88
91
 
89
92
  def append_hop(self, hop: Dict[str, Any]) -> None:
90
93
  """Append a new hop to the capsule.
91
94
 
92
95
  Maintains a rolling window of last N hops to keep payload compact.
93
96
  Updates aggregate metrics automatically.
97
+
98
+ Thread-safe: Uses internal lock to protect concurrent modifications.
94
99
 
95
100
  Args:
96
101
  hop: Dictionary containing hop metadata
@@ -111,22 +116,24 @@ class TraceCapsule:
111
116
  "agent_name": "code-writer"
112
117
  })
113
118
  """
114
- # Add hop_index
115
- hop["hop_index"] = len(self.last_n_hops)
119
+ # Thread-safe update of capsule state
120
+ with self._lock:
121
+ # Add hop_index
122
+ hop["hop_index"] = len(self.last_n_hops)
116
123
 
117
- # Append to history
118
- self.last_n_hops.append(hop)
124
+ # Append to history
125
+ self.last_n_hops.append(hop)
119
126
 
120
- # Maintain rolling window (keep last N hops)
121
- if len(self.last_n_hops) > self.MAX_HOPS:
122
- self.last_n_hops.pop(0)
127
+ # Maintain rolling window (keep last N hops)
128
+ if len(self.last_n_hops) > self.MAX_HOPS:
129
+ self.last_n_hops.pop(0)
123
130
 
124
- # Update aggregates
125
- self.aggregate_cost_usd += hop.get("cost_usd", 0.0)
126
- self.aggregate_latency_ms += hop.get("duration_ms", 0.0)
131
+ # Update aggregates
132
+ self.aggregate_cost_usd += hop.get("cost_usd", 0.0)
133
+ self.aggregate_latency_ms += hop.get("duration_ms", 0.0)
127
134
 
128
- # Update timestamp
129
- self.timestamp = datetime.now(timezone.utc).isoformat()
135
+ # Update timestamp
136
+ self.timestamp = datetime.now(timezone.utc).isoformat()
130
137
 
131
138
  def get_last_hop(self) -> Optional[Dict[str, Any]]:
132
139
  """Get the most recent hop.
kalibr/utils.py CHANGED
@@ -38,8 +38,8 @@ def load_config_from_env() -> Dict[str, str]:
38
38
  "workflow_id": os.getenv("KALIBR_WORKFLOW_ID", "default-workflow"),
39
39
  "sandbox_id": os.getenv("SANDBOX_ID", "local"),
40
40
  "runtime_env": os.getenv("RUNTIME_ENV", "local"),
41
- "api_endpoint": os.getenv("KALIBR_API_ENDPOINT", "https://api.kalibr.systems/api/v1/traces"),
42
- "collector_url": os.getenv("KALIBR_COLLECTOR_URL", "https://api.kalibr.systems/api/ingest"),
41
+ "api_endpoint": os.getenv("KALIBR_API_ENDPOINT", "https://kalibr-backend.fly.dev/api/v1/traces"),
42
+ "collector_url": os.getenv("KALIBR_COLLECTOR_URL", "https://kalibr-backend.fly.dev/api/ingest"),
43
43
  }
44
44
  return config
45
45