kalibr 1.1.3a0__py3-none-any.whl → 1.4.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/intelligence.py ADDED
@@ -0,0 +1,662 @@
1
+ """Kalibr Intelligence Client - Query execution intelligence and report outcomes.
2
+
3
+ This module enables the outcome-conditioned routing loop:
4
+ 1. Before executing: query get_policy() to get the best path for your goal
5
+ 2. After executing: call report_outcome() to teach Kalibr what worked
6
+
7
+ Example - Policy-based routing:
8
+ from kalibr import get_policy, report_outcome
9
+
10
+ # Before executing - get best path
11
+ policy = get_policy(goal="book_meeting")
12
+ model = policy["recommended_model"] # Use this model
13
+
14
+ # After executing - report what happened
15
+ report_outcome(
16
+ trace_id=trace_id,
17
+ goal="book_meeting",
18
+ success=True
19
+ )
20
+
21
+ Example - Path registration and intelligent routing:
22
+ from kalibr import register_path, decide
23
+
24
+ # Register paths for a goal
25
+ register_path(goal="book_meeting", model_id="gpt-4", tool_id="calendar_tool")
26
+ register_path(goal="book_meeting", model_id="claude-3-opus")
27
+
28
+ # Get intelligent routing decision
29
+ decision = decide(goal="book_meeting")
30
+ model = decision["model_id"] # Selected based on outcomes
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import os
36
+ import threading
37
+ from typing import Any, Optional
38
+
39
+ import httpx
40
+
41
+ # Default intelligence API endpoint
42
+ DEFAULT_INTELLIGENCE_URL = "https://kalibr-intelligence.fly.dev"
43
+
44
+
45
+ class KalibrIntelligence:
46
+ """Client for Kalibr Intelligence API.
47
+
48
+ Provides methods to query execution policies and report outcomes
49
+ for the outcome-conditioned routing loop.
50
+
51
+ Args:
52
+ api_key: Kalibr API key (or set KALIBR_API_KEY env var)
53
+ tenant_id: Tenant identifier (or set KALIBR_TENANT_ID env var)
54
+ base_url: Intelligence API base URL (or set KALIBR_INTELLIGENCE_URL env var)
55
+ timeout: Request timeout in seconds
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ api_key: str | None = None,
61
+ tenant_id: str | None = None,
62
+ base_url: str | None = None,
63
+ timeout: float = 10.0,
64
+ ):
65
+ self.api_key = api_key or os.getenv("KALIBR_API_KEY", "")
66
+ self.tenant_id = tenant_id or os.getenv("KALIBR_TENANT_ID", "")
67
+ self.base_url = (
68
+ base_url
69
+ or os.getenv("KALIBR_INTELLIGENCE_URL", DEFAULT_INTELLIGENCE_URL)
70
+ ).rstrip("/")
71
+ self.timeout = timeout
72
+ self._client = httpx.Client(timeout=timeout)
73
+
74
+ def _request(
75
+ self,
76
+ method: str,
77
+ path: str,
78
+ json: dict | None = None,
79
+ params: dict | None = None,
80
+ ) -> httpx.Response:
81
+ """Make authenticated request to intelligence API."""
82
+ headers = {
83
+ "X-API-Key": self.api_key,
84
+ "X-Tenant-ID": self.tenant_id,
85
+ "Content-Type": "application/json",
86
+ }
87
+
88
+ url = f"{self.base_url}{path}"
89
+ response = self._client.request(method, url, json=json, params=params, headers=headers)
90
+ response.raise_for_status()
91
+ return response
92
+
93
+ def get_policy(
94
+ self,
95
+ goal: str,
96
+ task_type: str | None = None,
97
+ constraints: dict | None = None,
98
+ window_hours: int = 168,
99
+ ) -> dict[str, Any]:
100
+ """Get execution policy for a goal.
101
+
102
+ Returns the historically best-performing path for achieving
103
+ the specified goal, based on outcome data.
104
+
105
+ Args:
106
+ goal: The goal to optimize for (e.g., "book_meeting", "resolve_ticket")
107
+ task_type: Optional task type filter (e.g., "code", "summarize")
108
+ constraints: Optional constraints dict with keys:
109
+ - max_cost_usd: Maximum cost per request
110
+ - max_latency_ms: Maximum latency
111
+ - min_quality: Minimum quality score (0-1)
112
+ - min_confidence: Minimum statistical confidence (0-1)
113
+ - max_risk: Maximum risk score (0-1)
114
+ window_hours: Time window for pattern analysis (default 1 week)
115
+
116
+ Returns:
117
+ dict with:
118
+ - goal: The goal queried
119
+ - recommended_model: Best model for this goal
120
+ - recommended_provider: Provider for the recommended model
121
+ - outcome_success_rate: Historical success rate (0-1)
122
+ - outcome_sample_count: Number of outcomes in the data
123
+ - confidence: Statistical confidence in recommendation
124
+ - risk_score: Risk score (lower is better)
125
+ - reasoning: Human-readable explanation
126
+ - alternatives: List of alternative models
127
+
128
+ Raises:
129
+ httpx.HTTPStatusError: If the API returns an error
130
+
131
+ Example:
132
+ policy = intelligence.get_policy(goal="book_meeting")
133
+ print(f"Use {policy['recommended_model']} - {policy['outcome_success_rate']:.0%} success rate")
134
+ """
135
+ response = self._request(
136
+ "POST",
137
+ "/api/v1/intelligence/policy",
138
+ json={
139
+ "goal": goal,
140
+ "task_type": task_type,
141
+ "constraints": constraints,
142
+ "window_hours": window_hours,
143
+ },
144
+ )
145
+ return response.json()
146
+
147
+ def report_outcome(
148
+ self,
149
+ trace_id: str,
150
+ goal: str,
151
+ success: bool,
152
+ score: float | None = None,
153
+ failure_reason: str | None = None,
154
+ metadata: dict | None = None,
155
+ tool_id: str | None = None,
156
+ execution_params: dict | None = None,
157
+ model_id: str | None = None,
158
+ ) -> dict[str, Any]:
159
+ """Report execution outcome for a goal.
160
+
161
+ This is the feedback loop that teaches Kalibr what works.
162
+ Call this after your agent completes (or fails) a task.
163
+
164
+ Args:
165
+ trace_id: The trace ID from the execution
166
+ goal: The goal this execution was trying to achieve
167
+ success: Whether the goal was achieved
168
+ score: Optional quality score (0-1) for more granular feedback
169
+ failure_reason: Optional reason for failure (helps with debugging)
170
+ metadata: Optional additional context as a dict
171
+ tool_id: Optional tool that was used (e.g., "serper", "browserless")
172
+ execution_params: Optional execution parameters (e.g., {"temperature": 0.3})
173
+
174
+ Returns:
175
+ dict with:
176
+ - status: "accepted" if successful
177
+ - trace_id: The trace ID recorded
178
+ - goal: The goal recorded
179
+
180
+ Raises:
181
+ httpx.HTTPStatusError: If the API returns an error
182
+
183
+ Example:
184
+ # Success case
185
+ report_outcome(trace_id="abc123", goal="book_meeting", success=True)
186
+
187
+ # Failure case with reason
188
+ report_outcome(
189
+ trace_id="abc123",
190
+ goal="book_meeting",
191
+ success=False,
192
+ failure_reason="calendar_conflict"
193
+ )
194
+ """
195
+ response = self._request(
196
+ "POST",
197
+ "/api/v1/intelligence/report-outcome",
198
+ json={
199
+ "trace_id": trace_id,
200
+ "goal": goal,
201
+ "success": success,
202
+ "score": score,
203
+ "failure_reason": failure_reason,
204
+ "metadata": metadata,
205
+ "tool_id": tool_id,
206
+ "execution_params": execution_params,
207
+ "model_id": model_id,
208
+ },
209
+ )
210
+ return response.json()
211
+
212
+ def get_recommendation(
213
+ self,
214
+ task_type: str,
215
+ goal: str | None = None,
216
+ optimize_for: str = "balanced",
217
+ constraints: dict | None = None,
218
+ window_hours: int = 168,
219
+ ) -> dict[str, Any]:
220
+ """Get model recommendation for a task type.
221
+
222
+ This is the original recommendation endpoint. For goal-based
223
+ optimization, prefer get_policy() instead.
224
+
225
+ Args:
226
+ task_type: Type of task (e.g., "summarize", "code", "qa")
227
+ goal: Optional goal for outcome-based optimization
228
+ optimize_for: Optimization target - one of:
229
+ - "cost": Minimize cost
230
+ - "quality": Maximize output quality
231
+ - "latency": Minimize response time
232
+ - "balanced": Balance all factors (default)
233
+ - "cost_efficiency": Maximize quality-per-dollar
234
+ - "outcome": Optimize for goal success rate
235
+ constraints: Optional constraints dict
236
+ window_hours: Time window for pattern analysis
237
+
238
+ Returns:
239
+ dict with recommendation, alternatives, stats, reasoning
240
+ """
241
+ response = self._request(
242
+ "POST",
243
+ "/api/v1/intelligence/recommend",
244
+ json={
245
+ "task_type": task_type,
246
+ "goal": goal,
247
+ "optimize_for": optimize_for,
248
+ "constraints": constraints,
249
+ "window_hours": window_hours,
250
+ },
251
+ )
252
+ return response.json()
253
+
254
+ # =========================================================================
255
+ # ROUTING METHODS
256
+ # =========================================================================
257
+
258
+ def register_path(
259
+ self,
260
+ goal: str,
261
+ model_id: str,
262
+ tool_id: str | None = None,
263
+ params: dict | None = None,
264
+ risk_level: str = "low",
265
+ ) -> dict[str, Any]:
266
+ """Register a new routing path for a goal.
267
+
268
+ Creates a path that maps a goal to a specific model (and optionally tool)
269
+ configuration. This path can then be selected by the decide() method.
270
+
271
+ Args:
272
+ goal: The goal this path is for (e.g., "book_meeting", "resolve_ticket")
273
+ model_id: The model identifier to use (e.g., "gpt-4", "claude-3-opus")
274
+ tool_id: Optional tool identifier if this path uses a specific tool
275
+ params: Optional parameters dict for the path configuration
276
+ risk_level: Risk level for this path - "low", "medium", or "high"
277
+
278
+ Returns:
279
+ dict with the created path including:
280
+ - path_id: Unique identifier for the path
281
+ - goal: The goal
282
+ - model_id: The model
283
+ - tool_id: The tool (if specified)
284
+ - params: The parameters (if specified)
285
+ - risk_level: The risk level
286
+ - created_at: Creation timestamp
287
+
288
+ Raises:
289
+ httpx.HTTPStatusError: If the API returns an error
290
+
291
+ Example:
292
+ path = intelligence.register_path(
293
+ goal="book_meeting",
294
+ model_id="gpt-4",
295
+ tool_id="calendar_tool",
296
+ risk_level="low"
297
+ )
298
+ print(f"Created path: {path['path_id']}")
299
+ """
300
+ response = self._request(
301
+ "POST",
302
+ "/api/v1/routing/paths",
303
+ json={
304
+ "goal": goal,
305
+ "model_id": model_id,
306
+ "tool_id": tool_id,
307
+ "params": params,
308
+ "risk_level": risk_level,
309
+ },
310
+ )
311
+ return response.json()
312
+
313
+ def list_paths(
314
+ self,
315
+ goal: str | None = None,
316
+ include_disabled: bool = False,
317
+ ) -> dict[str, Any]:
318
+ """List registered routing paths.
319
+
320
+ Args:
321
+ goal: Optional goal to filter paths by
322
+ include_disabled: Whether to include disabled paths (default False)
323
+
324
+ Returns:
325
+ dict with:
326
+ - paths: List of path objects
327
+
328
+ Raises:
329
+ httpx.HTTPStatusError: If the API returns an error
330
+
331
+ Example:
332
+ result = intelligence.list_paths(goal="book_meeting")
333
+ for path in result["paths"]:
334
+ print(f"{path['path_id']}: {path['model_id']}")
335
+ """
336
+ params = {}
337
+ if goal is not None:
338
+ params["goal"] = goal
339
+ if include_disabled:
340
+ params["include_disabled"] = "true"
341
+
342
+ response = self._request(
343
+ "GET",
344
+ "/api/v1/routing/paths",
345
+ params=params if params else None,
346
+ )
347
+ return response.json()
348
+
349
+ def disable_path(self, path_id: str) -> dict[str, Any]:
350
+ """Disable a routing path.
351
+
352
+ Disables a path so it won't be selected by decide(). The path
353
+ data is retained for historical analysis.
354
+
355
+ Args:
356
+ path_id: The unique identifier of the path to disable
357
+
358
+ Returns:
359
+ dict with:
360
+ - status: "disabled" if successful
361
+ - path_id: The disabled path ID
362
+
363
+ Raises:
364
+ httpx.HTTPStatusError: If the API returns an error
365
+
366
+ Example:
367
+ result = intelligence.disable_path("path_abc123")
368
+ print(f"Status: {result['status']}")
369
+ """
370
+ response = self._request(
371
+ "DELETE",
372
+ f"/api/v1/routing/paths/{path_id}",
373
+ )
374
+ return response.json()
375
+
376
+ def decide(
377
+ self,
378
+ goal: str,
379
+ task_risk_level: str = "low",
380
+ ) -> dict[str, Any]:
381
+ """Get routing decision for a goal.
382
+
383
+ Uses outcome data and exploration/exploitation strategy to decide
384
+ which path to use for achieving the specified goal.
385
+
386
+ Args:
387
+ goal: The goal to route for (e.g., "book_meeting")
388
+ task_risk_level: Risk tolerance for this task - "low", "medium", or "high"
389
+
390
+ Returns:
391
+ dict with:
392
+ - model_id: The selected model
393
+ - tool_id: The selected tool (if any)
394
+ - params: Additional parameters (if any)
395
+ - reason: Human-readable explanation of the decision
396
+ - confidence: Confidence score (0-1)
397
+ - is_exploration: Whether this is an exploration choice
398
+ - path_id: The selected path ID
399
+
400
+ Raises:
401
+ httpx.HTTPStatusError: If the API returns an error
402
+
403
+ Example:
404
+ decision = intelligence.decide(goal="book_meeting")
405
+ model = decision["model_id"]
406
+ print(f"Using {model} ({decision['reason']})")
407
+ """
408
+ response = self._request(
409
+ "POST",
410
+ "/api/v1/routing/decide",
411
+ json={
412
+ "goal": goal,
413
+ "task_risk_level": task_risk_level,
414
+ },
415
+ )
416
+ return response.json()
417
+
418
+ def set_exploration_config(
419
+ self,
420
+ goal: str = "*",
421
+ exploration_rate: float = 0.1,
422
+ min_samples_before_exploit: int = 20,
423
+ rollback_threshold: float = 0.3,
424
+ staleness_days: int = 7,
425
+ exploration_on_high_risk: bool = False,
426
+ ) -> dict[str, Any]:
427
+ """Set exploration/exploitation configuration for routing.
428
+
429
+ Configures how the decide() method balances exploring new paths
430
+ vs exploiting known good paths.
431
+
432
+ Args:
433
+ goal: Goal to configure, or "*" for default config
434
+ exploration_rate: Probability of exploring (0-1, default 0.1)
435
+ min_samples_before_exploit: Minimum outcomes before exploiting (default 20)
436
+ rollback_threshold: Performance drop threshold to rollback (default 0.3)
437
+ staleness_days: Days before reexploring stale paths (default 7)
438
+ exploration_on_high_risk: Whether to explore on high-risk tasks (default False)
439
+
440
+ Returns:
441
+ dict with the saved configuration
442
+
443
+ Raises:
444
+ httpx.HTTPStatusError: If the API returns an error
445
+
446
+ Example:
447
+ config = intelligence.set_exploration_config(
448
+ goal="book_meeting",
449
+ exploration_rate=0.2,
450
+ min_samples_before_exploit=10
451
+ )
452
+ """
453
+ response = self._request(
454
+ "POST",
455
+ "/api/v1/routing/config",
456
+ json={
457
+ "goal": goal,
458
+ "exploration_rate": exploration_rate,
459
+ "min_samples_before_exploit": min_samples_before_exploit,
460
+ "rollback_threshold": rollback_threshold,
461
+ "staleness_days": staleness_days,
462
+ "exploration_on_high_risk": exploration_on_high_risk,
463
+ },
464
+ )
465
+ return response.json()
466
+
467
+ def get_exploration_config(self, goal: str | None = None) -> dict[str, Any]:
468
+ """Get exploration/exploitation configuration.
469
+
470
+ Args:
471
+ goal: Optional goal to get config for (returns default if not found)
472
+
473
+ Returns:
474
+ dict with configuration values:
475
+ - goal: The goal this config applies to
476
+ - exploration_rate: Exploration probability
477
+ - min_samples_before_exploit: Minimum samples before exploiting
478
+ - rollback_threshold: Rollback threshold
479
+ - staleness_days: Staleness threshold in days
480
+ - exploration_on_high_risk: Whether exploration is allowed on high-risk
481
+
482
+ Raises:
483
+ httpx.HTTPStatusError: If the API returns an error
484
+
485
+ Example:
486
+ config = intelligence.get_exploration_config(goal="book_meeting")
487
+ print(f"Exploration rate: {config['exploration_rate']}")
488
+ """
489
+ params = {}
490
+ if goal is not None:
491
+ params["goal"] = goal
492
+
493
+ response = self._request(
494
+ "GET",
495
+ "/api/v1/routing/config",
496
+ params=params if params else None,
497
+ )
498
+ return response.json()
499
+
500
+ def close(self):
501
+ """Close the HTTP client."""
502
+ self._client.close()
503
+
504
+ def __enter__(self):
505
+ return self
506
+
507
+ def __exit__(self, *args):
508
+ self.close()
509
+
510
+
511
+ # Module-level singleton for convenience functions
512
+ _intelligence_client: KalibrIntelligence | None = None
513
+ _client_lock = threading.Lock()
514
+
515
+
516
+ def _get_intelligence_client() -> KalibrIntelligence:
517
+ """Get or create the singleton intelligence client.
518
+
519
+ Thread-safe singleton pattern using double-checked locking.
520
+ """
521
+ global _intelligence_client
522
+ if _intelligence_client is None:
523
+ with _client_lock:
524
+ # Double-check inside lock to prevent race condition
525
+ if _intelligence_client is None:
526
+ _intelligence_client = KalibrIntelligence()
527
+ return _intelligence_client
528
+
529
+
530
+ def get_policy(goal: str, tenant_id: str | None = None, **kwargs) -> dict[str, Any]:
531
+ """Get execution policy for a goal.
532
+
533
+ Convenience function that uses the default intelligence client.
534
+ See KalibrIntelligence.get_policy for full documentation.
535
+
536
+ Args:
537
+ goal: The goal to optimize for
538
+ tenant_id: Optional tenant ID override (default: uses KALIBR_TENANT_ID env var)
539
+ **kwargs: Additional arguments (task_type, constraints, window_hours)
540
+
541
+ Returns:
542
+ Policy dict with recommended_model, outcome_success_rate, etc.
543
+
544
+ Example:
545
+ from kalibr import get_policy
546
+
547
+ policy = get_policy(goal="book_meeting")
548
+ model = policy["recommended_model"]
549
+ """
550
+ if tenant_id:
551
+ # Use context manager to ensure client is properly closed
552
+ with KalibrIntelligence(tenant_id=tenant_id) as client:
553
+ return client.get_policy(goal, **kwargs)
554
+ return _get_intelligence_client().get_policy(goal, **kwargs)
555
+
556
+
557
+ def report_outcome(trace_id: str, goal: str, success: bool, tenant_id: str | None = None, **kwargs) -> dict[str, Any]:
558
+ """Report execution outcome for a goal.
559
+
560
+ Convenience function that uses the default intelligence client.
561
+ See KalibrIntelligence.report_outcome for full documentation.
562
+
563
+ Args:
564
+ trace_id: The trace ID from the execution
565
+ goal: The goal this execution was trying to achieve
566
+ success: Whether the goal was achieved
567
+ tenant_id: Optional tenant ID override (default: uses KALIBR_TENANT_ID env var)
568
+ **kwargs: Additional arguments (score, failure_reason, metadata, tool_id, execution_params)
569
+
570
+ Returns:
571
+ Response dict with status confirmation
572
+
573
+ Example:
574
+ from kalibr import report_outcome
575
+
576
+ report_outcome(trace_id="abc123", goal="book_meeting", success=True)
577
+ """
578
+ if tenant_id:
579
+ # Use context manager to ensure client is properly closed
580
+ with KalibrIntelligence(tenant_id=tenant_id) as client:
581
+ return client.report_outcome(trace_id, goal, success, **kwargs)
582
+ return _get_intelligence_client().report_outcome(trace_id, goal, success, **kwargs)
583
+
584
+
585
+ def get_recommendation(task_type: str, **kwargs) -> dict[str, Any]:
586
+ """Get model recommendation for a task type.
587
+
588
+ Convenience function that uses the default intelligence client.
589
+ See KalibrIntelligence.get_recommendation for full documentation.
590
+ """
591
+ return _get_intelligence_client().get_recommendation(task_type, **kwargs)
592
+
593
+
594
+ def register_path(
595
+ goal: str,
596
+ model_id: str,
597
+ tool_id: str | None = None,
598
+ params: dict | None = None,
599
+ risk_level: str = "low",
600
+ tenant_id: str | None = None,
601
+ ) -> dict[str, Any]:
602
+ """Register a new routing path for a goal.
603
+
604
+ Convenience function that uses the default intelligence client.
605
+ See KalibrIntelligence.register_path for full documentation.
606
+
607
+ Args:
608
+ goal: The goal this path is for
609
+ model_id: The model identifier to use
610
+ tool_id: Optional tool identifier
611
+ params: Optional parameters dict
612
+ risk_level: Risk level - "low", "medium", or "high"
613
+ tenant_id: Optional tenant ID override
614
+
615
+ Returns:
616
+ dict with the created path
617
+
618
+ Example:
619
+ from kalibr import register_path
620
+
621
+ path = register_path(
622
+ goal="book_meeting",
623
+ model_id="gpt-4",
624
+ tool_id="calendar_tool"
625
+ )
626
+ """
627
+ if tenant_id:
628
+ # Use context manager to ensure client is properly closed
629
+ with KalibrIntelligence(tenant_id=tenant_id) as client:
630
+ return client.register_path(goal, model_id, tool_id, params, risk_level)
631
+ return _get_intelligence_client().register_path(goal, model_id, tool_id, params, risk_level)
632
+
633
+
634
+ def decide(
635
+ goal: str,
636
+ task_risk_level: str = "low",
637
+ tenant_id: str | None = None,
638
+ ) -> dict[str, Any]:
639
+ """Get routing decision for a goal.
640
+
641
+ Convenience function that uses the default intelligence client.
642
+ See KalibrIntelligence.decide for full documentation.
643
+
644
+ Args:
645
+ goal: The goal to route for
646
+ task_risk_level: Risk tolerance - "low", "medium", or "high"
647
+ tenant_id: Optional tenant ID override
648
+
649
+ Returns:
650
+ dict with model_id, tool_id, params, reason, confidence, etc.
651
+
652
+ Example:
653
+ from kalibr import decide
654
+
655
+ decision = decide(goal="book_meeting")
656
+ model = decision["model_id"]
657
+ """
658
+ if tenant_id:
659
+ # Use context manager to ensure client is properly closed
660
+ with KalibrIntelligence(tenant_id=tenant_id) as client:
661
+ return client.decide(goal, task_risk_level)
662
+ return _get_intelligence_client().decide(goal, task_risk_level)
@@ -54,7 +54,7 @@ class AutoTracerMiddleware(BaseHTTPMiddleware):
54
54
 
55
55
  # Collector config
56
56
  self.collector_url = collector_url or os.getenv(
57
- "KALIBR_COLLECTOR_URL", "https://api.kalibr.systems/api/ingest"
57
+ "KALIBR_COLLECTOR_URL", "https://kalibr-backend.fly.dev/api/ingest"
58
58
  )
59
59
  self.api_key = api_key or os.getenv("KALIBR_API_KEY", "")
60
60
  self.tenant_id = tenant_id or os.getenv("KALIBR_TENANT_ID", "default")