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