ai-lib-python 0.5.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.
Files changed (84) hide show
  1. ai_lib_python/__init__.py +43 -0
  2. ai_lib_python/batch/__init__.py +15 -0
  3. ai_lib_python/batch/collector.py +244 -0
  4. ai_lib_python/batch/executor.py +224 -0
  5. ai_lib_python/cache/__init__.py +26 -0
  6. ai_lib_python/cache/backends.py +380 -0
  7. ai_lib_python/cache/key.py +237 -0
  8. ai_lib_python/cache/manager.py +332 -0
  9. ai_lib_python/client/__init__.py +37 -0
  10. ai_lib_python/client/builder.py +528 -0
  11. ai_lib_python/client/cancel.py +368 -0
  12. ai_lib_python/client/core.py +433 -0
  13. ai_lib_python/client/response.py +134 -0
  14. ai_lib_python/embeddings/__init__.py +36 -0
  15. ai_lib_python/embeddings/client.py +339 -0
  16. ai_lib_python/embeddings/types.py +234 -0
  17. ai_lib_python/embeddings/vectors.py +246 -0
  18. ai_lib_python/errors/__init__.py +41 -0
  19. ai_lib_python/errors/base.py +316 -0
  20. ai_lib_python/errors/classification.py +210 -0
  21. ai_lib_python/guardrails/__init__.py +35 -0
  22. ai_lib_python/guardrails/base.py +336 -0
  23. ai_lib_python/guardrails/filters.py +583 -0
  24. ai_lib_python/guardrails/validators.py +475 -0
  25. ai_lib_python/pipeline/__init__.py +55 -0
  26. ai_lib_python/pipeline/accumulate.py +248 -0
  27. ai_lib_python/pipeline/base.py +240 -0
  28. ai_lib_python/pipeline/decode.py +281 -0
  29. ai_lib_python/pipeline/event_map.py +506 -0
  30. ai_lib_python/pipeline/fan_out.py +284 -0
  31. ai_lib_python/pipeline/select.py +297 -0
  32. ai_lib_python/plugins/__init__.py +32 -0
  33. ai_lib_python/plugins/base.py +294 -0
  34. ai_lib_python/plugins/hooks.py +296 -0
  35. ai_lib_python/plugins/middleware.py +285 -0
  36. ai_lib_python/plugins/registry.py +294 -0
  37. ai_lib_python/protocol/__init__.py +71 -0
  38. ai_lib_python/protocol/loader.py +317 -0
  39. ai_lib_python/protocol/manifest.py +385 -0
  40. ai_lib_python/protocol/validator.py +460 -0
  41. ai_lib_python/py.typed +1 -0
  42. ai_lib_python/resilience/__init__.py +102 -0
  43. ai_lib_python/resilience/backpressure.py +225 -0
  44. ai_lib_python/resilience/circuit_breaker.py +318 -0
  45. ai_lib_python/resilience/executor.py +343 -0
  46. ai_lib_python/resilience/fallback.py +341 -0
  47. ai_lib_python/resilience/preflight.py +413 -0
  48. ai_lib_python/resilience/rate_limiter.py +291 -0
  49. ai_lib_python/resilience/retry.py +299 -0
  50. ai_lib_python/resilience/signals.py +283 -0
  51. ai_lib_python/routing/__init__.py +118 -0
  52. ai_lib_python/routing/manager.py +593 -0
  53. ai_lib_python/routing/strategy.py +345 -0
  54. ai_lib_python/routing/types.py +397 -0
  55. ai_lib_python/structured/__init__.py +33 -0
  56. ai_lib_python/structured/json_mode.py +281 -0
  57. ai_lib_python/structured/schema.py +316 -0
  58. ai_lib_python/structured/validator.py +334 -0
  59. ai_lib_python/telemetry/__init__.py +127 -0
  60. ai_lib_python/telemetry/exporters/__init__.py +9 -0
  61. ai_lib_python/telemetry/exporters/prometheus.py +111 -0
  62. ai_lib_python/telemetry/feedback.py +446 -0
  63. ai_lib_python/telemetry/health.py +409 -0
  64. ai_lib_python/telemetry/logger.py +389 -0
  65. ai_lib_python/telemetry/metrics.py +496 -0
  66. ai_lib_python/telemetry/tracer.py +473 -0
  67. ai_lib_python/tokens/__init__.py +25 -0
  68. ai_lib_python/tokens/counter.py +282 -0
  69. ai_lib_python/tokens/estimator.py +286 -0
  70. ai_lib_python/transport/__init__.py +34 -0
  71. ai_lib_python/transport/auth.py +141 -0
  72. ai_lib_python/transport/http.py +364 -0
  73. ai_lib_python/transport/pool.py +425 -0
  74. ai_lib_python/types/__init__.py +41 -0
  75. ai_lib_python/types/events.py +343 -0
  76. ai_lib_python/types/message.py +332 -0
  77. ai_lib_python/types/tool.py +191 -0
  78. ai_lib_python/utils/__init__.py +21 -0
  79. ai_lib_python/utils/tool_call_assembler.py +317 -0
  80. ai_lib_python-0.5.0.dist-info/METADATA +837 -0
  81. ai_lib_python-0.5.0.dist-info/RECORD +84 -0
  82. ai_lib_python-0.5.0.dist-info/WHEEL +4 -0
  83. ai_lib_python-0.5.0.dist-info/licenses/LICENSE-APACHE +201 -0
  84. ai_lib_python-0.5.0.dist-info/licenses/LICENSE-MIT +21 -0
@@ -0,0 +1,409 @@
1
+ """
2
+ Health check utilities for ai-lib-python.
3
+
4
+ Provides health status tracking and provider availability detection.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import time
11
+ from dataclasses import dataclass, field
12
+ from enum import Enum
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ if TYPE_CHECKING:
16
+ from collections.abc import Awaitable, Callable
17
+
18
+
19
+ class HealthStatus(str, Enum):
20
+ """Health status levels."""
21
+
22
+ HEALTHY = "healthy"
23
+ DEGRADED = "degraded"
24
+ UNHEALTHY = "unhealthy"
25
+ UNKNOWN = "unknown"
26
+
27
+
28
+ @dataclass
29
+ class HealthCheckResult:
30
+ """Result of a health check.
31
+
32
+ Attributes:
33
+ name: Check name
34
+ status: Health status
35
+ message: Status message
36
+ latency_ms: Check latency in milliseconds
37
+ timestamp: Check timestamp
38
+ details: Additional details
39
+ """
40
+
41
+ name: str
42
+ status: HealthStatus
43
+ message: str = ""
44
+ latency_ms: float = 0.0
45
+ timestamp: float = field(default_factory=time.time)
46
+ details: dict[str, Any] = field(default_factory=dict)
47
+
48
+ def to_dict(self) -> dict[str, Any]:
49
+ """Convert to dictionary."""
50
+ return {
51
+ "name": self.name,
52
+ "status": self.status.value,
53
+ "message": self.message,
54
+ "latency_ms": self.latency_ms,
55
+ "timestamp": self.timestamp,
56
+ "details": self.details,
57
+ }
58
+
59
+
60
+ @dataclass
61
+ class AggregatedHealth:
62
+ """Aggregated health status.
63
+
64
+ Attributes:
65
+ status: Overall health status
66
+ checks: Individual check results
67
+ timestamp: Aggregation timestamp
68
+ """
69
+
70
+ status: HealthStatus
71
+ checks: list[HealthCheckResult] = field(default_factory=list)
72
+ timestamp: float = field(default_factory=time.time)
73
+
74
+ def to_dict(self) -> dict[str, Any]:
75
+ """Convert to dictionary."""
76
+ return {
77
+ "status": self.status.value,
78
+ "checks": [c.to_dict() for c in self.checks],
79
+ "timestamp": self.timestamp,
80
+ }
81
+
82
+
83
+ class HealthChecker:
84
+ """Health checker for ai-lib-python components.
85
+
86
+ Example:
87
+ >>> checker = HealthChecker()
88
+ >>> checker.register("openai", check_openai_health)
89
+ >>> health = await checker.check_all()
90
+ >>> print(health.status)
91
+ """
92
+
93
+ def __init__(self) -> None:
94
+ """Initialize health checker."""
95
+ self._checks: dict[str, Callable[[], Awaitable[HealthCheckResult]]] = {}
96
+ self._last_results: dict[str, HealthCheckResult] = {}
97
+
98
+ def register(
99
+ self,
100
+ name: str,
101
+ check: Callable[[], Awaitable[HealthCheckResult]],
102
+ ) -> None:
103
+ """Register a health check.
104
+
105
+ Args:
106
+ name: Check name
107
+ check: Async function returning HealthCheckResult
108
+ """
109
+ self._checks[name] = check
110
+
111
+ def unregister(self, name: str) -> None:
112
+ """Unregister a health check.
113
+
114
+ Args:
115
+ name: Check name
116
+ """
117
+ self._checks.pop(name, None)
118
+ self._last_results.pop(name, None)
119
+
120
+ async def check(self, name: str) -> HealthCheckResult:
121
+ """Run a specific health check.
122
+
123
+ Args:
124
+ name: Check name
125
+
126
+ Returns:
127
+ HealthCheckResult
128
+
129
+ Raises:
130
+ KeyError: If check not found
131
+ """
132
+ if name not in self._checks:
133
+ raise KeyError(f"Health check not found: {name}")
134
+
135
+ check_fn = self._checks[name]
136
+ start = time.time()
137
+
138
+ try:
139
+ result = await check_fn()
140
+ result.latency_ms = (time.time() - start) * 1000
141
+ except Exception as e:
142
+ result = HealthCheckResult(
143
+ name=name,
144
+ status=HealthStatus.UNHEALTHY,
145
+ message=str(e),
146
+ latency_ms=(time.time() - start) * 1000,
147
+ )
148
+
149
+ self._last_results[name] = result
150
+ return result
151
+
152
+ async def check_all(self, timeout: float = 30.0) -> AggregatedHealth:
153
+ """Run all health checks.
154
+
155
+ Args:
156
+ timeout: Timeout for all checks in seconds
157
+
158
+ Returns:
159
+ AggregatedHealth with all results
160
+ """
161
+ if not self._checks:
162
+ return AggregatedHealth(status=HealthStatus.UNKNOWN)
163
+
164
+ # Run all checks concurrently
165
+ tasks = [self.check(name) for name in self._checks]
166
+
167
+ try:
168
+ results = await asyncio.wait_for(
169
+ asyncio.gather(*tasks, return_exceptions=True),
170
+ timeout=timeout,
171
+ )
172
+ except asyncio.TimeoutError:
173
+ # Return unhealthy if timeout
174
+ return AggregatedHealth(
175
+ status=HealthStatus.UNHEALTHY,
176
+ checks=[
177
+ HealthCheckResult(
178
+ name="_timeout",
179
+ status=HealthStatus.UNHEALTHY,
180
+ message=f"Health check timeout after {timeout}s",
181
+ )
182
+ ],
183
+ )
184
+
185
+ # Process results
186
+ check_results: list[HealthCheckResult] = []
187
+ for result in results:
188
+ if isinstance(result, Exception):
189
+ check_results.append(
190
+ HealthCheckResult(
191
+ name="_error",
192
+ status=HealthStatus.UNHEALTHY,
193
+ message=str(result),
194
+ )
195
+ )
196
+ else:
197
+ check_results.append(result)
198
+
199
+ # Determine overall status
200
+ statuses = [r.status for r in check_results]
201
+ if all(s == HealthStatus.HEALTHY for s in statuses):
202
+ overall = HealthStatus.HEALTHY
203
+ elif any(s == HealthStatus.UNHEALTHY for s in statuses):
204
+ overall = HealthStatus.UNHEALTHY
205
+ elif any(s == HealthStatus.DEGRADED for s in statuses):
206
+ overall = HealthStatus.DEGRADED
207
+ else:
208
+ overall = HealthStatus.UNKNOWN
209
+
210
+ return AggregatedHealth(status=overall, checks=check_results)
211
+
212
+ def get_last_result(self, name: str) -> HealthCheckResult | None:
213
+ """Get last result for a check.
214
+
215
+ Args:
216
+ name: Check name
217
+
218
+ Returns:
219
+ Last result or None
220
+ """
221
+ return self._last_results.get(name)
222
+
223
+ def get_all_last_results(self) -> dict[str, HealthCheckResult]:
224
+ """Get all last results.
225
+
226
+ Returns:
227
+ Dict of name to result
228
+ """
229
+ return dict(self._last_results)
230
+
231
+
232
+ class ProviderHealthTracker:
233
+ """Tracks health of AI providers based on request outcomes.
234
+
235
+ Automatically tracks success/failure rates and determines
236
+ provider health status.
237
+
238
+ Example:
239
+ >>> tracker = ProviderHealthTracker()
240
+ >>> tracker.record_success("openai")
241
+ >>> tracker.record_failure("openai", "rate_limited")
242
+ >>> status = tracker.get_status("openai")
243
+ """
244
+
245
+ def __init__(
246
+ self,
247
+ window_size: int = 100,
248
+ unhealthy_threshold: float = 0.5,
249
+ degraded_threshold: float = 0.1,
250
+ ) -> None:
251
+ """Initialize tracker.
252
+
253
+ Args:
254
+ window_size: Number of requests to consider
255
+ unhealthy_threshold: Error rate for unhealthy status
256
+ degraded_threshold: Error rate for degraded status
257
+ """
258
+ self._window_size = window_size
259
+ self._unhealthy_threshold = unhealthy_threshold
260
+ self._degraded_threshold = degraded_threshold
261
+
262
+ # Sliding window of outcomes (True = success, False = failure)
263
+ self._outcomes: dict[str, list[bool]] = {}
264
+ self._last_error: dict[str, str] = {}
265
+ self._last_success_time: dict[str, float] = {}
266
+ self._last_failure_time: dict[str, float] = {}
267
+
268
+ def record_success(self, provider: str) -> None:
269
+ """Record a successful request.
270
+
271
+ Args:
272
+ provider: Provider name
273
+ """
274
+ if provider not in self._outcomes:
275
+ self._outcomes[provider] = []
276
+
277
+ self._outcomes[provider].append(True)
278
+ if len(self._outcomes[provider]) > self._window_size:
279
+ self._outcomes[provider].pop(0)
280
+
281
+ self._last_success_time[provider] = time.time()
282
+
283
+ def record_failure(self, provider: str, error: str = "") -> None:
284
+ """Record a failed request.
285
+
286
+ Args:
287
+ provider: Provider name
288
+ error: Error description
289
+ """
290
+ if provider not in self._outcomes:
291
+ self._outcomes[provider] = []
292
+
293
+ self._outcomes[provider].append(False)
294
+ if len(self._outcomes[provider]) > self._window_size:
295
+ self._outcomes[provider].pop(0)
296
+
297
+ self._last_error[provider] = error
298
+ self._last_failure_time[provider] = time.time()
299
+
300
+ def get_status(self, provider: str) -> HealthStatus:
301
+ """Get health status for a provider.
302
+
303
+ Args:
304
+ provider: Provider name
305
+
306
+ Returns:
307
+ HealthStatus
308
+ """
309
+ if provider not in self._outcomes or not self._outcomes[provider]:
310
+ return HealthStatus.UNKNOWN
311
+
312
+ outcomes = self._outcomes[provider]
313
+ error_rate = 1 - (sum(outcomes) / len(outcomes))
314
+
315
+ if error_rate >= self._unhealthy_threshold:
316
+ return HealthStatus.UNHEALTHY
317
+ elif error_rate >= self._degraded_threshold:
318
+ return HealthStatus.DEGRADED
319
+ else:
320
+ return HealthStatus.HEALTHY
321
+
322
+ def get_error_rate(self, provider: str) -> float:
323
+ """Get error rate for a provider.
324
+
325
+ Args:
326
+ provider: Provider name
327
+
328
+ Returns:
329
+ Error rate (0.0 to 1.0)
330
+ """
331
+ if provider not in self._outcomes or not self._outcomes[provider]:
332
+ return 0.0
333
+
334
+ outcomes = self._outcomes[provider]
335
+ return 1 - (sum(outcomes) / len(outcomes))
336
+
337
+ def get_details(self, provider: str) -> dict[str, Any]:
338
+ """Get detailed health information for a provider.
339
+
340
+ Args:
341
+ provider: Provider name
342
+
343
+ Returns:
344
+ Dict with health details
345
+ """
346
+ outcomes = self._outcomes.get(provider, [])
347
+ return {
348
+ "provider": provider,
349
+ "status": self.get_status(provider).value,
350
+ "error_rate": self.get_error_rate(provider),
351
+ "sample_count": len(outcomes),
352
+ "last_error": self._last_error.get(provider),
353
+ "last_success_time": self._last_success_time.get(provider),
354
+ "last_failure_time": self._last_failure_time.get(provider),
355
+ }
356
+
357
+ def get_all_providers(self) -> list[str]:
358
+ """Get all tracked providers.
359
+
360
+ Returns:
361
+ List of provider names
362
+ """
363
+ return list(self._outcomes.keys())
364
+
365
+ def reset(self, provider: str | None = None) -> None:
366
+ """Reset health tracking.
367
+
368
+ Args:
369
+ provider: Provider to reset (all if None)
370
+ """
371
+ if provider:
372
+ self._outcomes.pop(provider, None)
373
+ self._last_error.pop(provider, None)
374
+ self._last_success_time.pop(provider, None)
375
+ self._last_failure_time.pop(provider, None)
376
+ else:
377
+ self._outcomes.clear()
378
+ self._last_error.clear()
379
+ self._last_success_time.clear()
380
+ self._last_failure_time.clear()
381
+
382
+
383
+ # Global health checker and tracker
384
+ _global_health_checker: HealthChecker | None = None
385
+ _global_health_tracker: ProviderHealthTracker | None = None
386
+
387
+
388
+ def get_health_checker() -> HealthChecker:
389
+ """Get the global health checker.
390
+
391
+ Returns:
392
+ Global HealthChecker instance
393
+ """
394
+ global _global_health_checker
395
+ if _global_health_checker is None:
396
+ _global_health_checker = HealthChecker()
397
+ return _global_health_checker
398
+
399
+
400
+ def get_health_tracker() -> ProviderHealthTracker:
401
+ """Get the global provider health tracker.
402
+
403
+ Returns:
404
+ Global ProviderHealthTracker instance
405
+ """
406
+ global _global_health_tracker
407
+ if _global_health_tracker is None:
408
+ _global_health_tracker = ProviderHealthTracker()
409
+ return _global_health_tracker