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.
- ai_lib_python/__init__.py +43 -0
- ai_lib_python/batch/__init__.py +15 -0
- ai_lib_python/batch/collector.py +244 -0
- ai_lib_python/batch/executor.py +224 -0
- ai_lib_python/cache/__init__.py +26 -0
- ai_lib_python/cache/backends.py +380 -0
- ai_lib_python/cache/key.py +237 -0
- ai_lib_python/cache/manager.py +332 -0
- ai_lib_python/client/__init__.py +37 -0
- ai_lib_python/client/builder.py +528 -0
- ai_lib_python/client/cancel.py +368 -0
- ai_lib_python/client/core.py +433 -0
- ai_lib_python/client/response.py +134 -0
- ai_lib_python/embeddings/__init__.py +36 -0
- ai_lib_python/embeddings/client.py +339 -0
- ai_lib_python/embeddings/types.py +234 -0
- ai_lib_python/embeddings/vectors.py +246 -0
- ai_lib_python/errors/__init__.py +41 -0
- ai_lib_python/errors/base.py +316 -0
- ai_lib_python/errors/classification.py +210 -0
- ai_lib_python/guardrails/__init__.py +35 -0
- ai_lib_python/guardrails/base.py +336 -0
- ai_lib_python/guardrails/filters.py +583 -0
- ai_lib_python/guardrails/validators.py +475 -0
- ai_lib_python/pipeline/__init__.py +55 -0
- ai_lib_python/pipeline/accumulate.py +248 -0
- ai_lib_python/pipeline/base.py +240 -0
- ai_lib_python/pipeline/decode.py +281 -0
- ai_lib_python/pipeline/event_map.py +506 -0
- ai_lib_python/pipeline/fan_out.py +284 -0
- ai_lib_python/pipeline/select.py +297 -0
- ai_lib_python/plugins/__init__.py +32 -0
- ai_lib_python/plugins/base.py +294 -0
- ai_lib_python/plugins/hooks.py +296 -0
- ai_lib_python/plugins/middleware.py +285 -0
- ai_lib_python/plugins/registry.py +294 -0
- ai_lib_python/protocol/__init__.py +71 -0
- ai_lib_python/protocol/loader.py +317 -0
- ai_lib_python/protocol/manifest.py +385 -0
- ai_lib_python/protocol/validator.py +460 -0
- ai_lib_python/py.typed +1 -0
- ai_lib_python/resilience/__init__.py +102 -0
- ai_lib_python/resilience/backpressure.py +225 -0
- ai_lib_python/resilience/circuit_breaker.py +318 -0
- ai_lib_python/resilience/executor.py +343 -0
- ai_lib_python/resilience/fallback.py +341 -0
- ai_lib_python/resilience/preflight.py +413 -0
- ai_lib_python/resilience/rate_limiter.py +291 -0
- ai_lib_python/resilience/retry.py +299 -0
- ai_lib_python/resilience/signals.py +283 -0
- ai_lib_python/routing/__init__.py +118 -0
- ai_lib_python/routing/manager.py +593 -0
- ai_lib_python/routing/strategy.py +345 -0
- ai_lib_python/routing/types.py +397 -0
- ai_lib_python/structured/__init__.py +33 -0
- ai_lib_python/structured/json_mode.py +281 -0
- ai_lib_python/structured/schema.py +316 -0
- ai_lib_python/structured/validator.py +334 -0
- ai_lib_python/telemetry/__init__.py +127 -0
- ai_lib_python/telemetry/exporters/__init__.py +9 -0
- ai_lib_python/telemetry/exporters/prometheus.py +111 -0
- ai_lib_python/telemetry/feedback.py +446 -0
- ai_lib_python/telemetry/health.py +409 -0
- ai_lib_python/telemetry/logger.py +389 -0
- ai_lib_python/telemetry/metrics.py +496 -0
- ai_lib_python/telemetry/tracer.py +473 -0
- ai_lib_python/tokens/__init__.py +25 -0
- ai_lib_python/tokens/counter.py +282 -0
- ai_lib_python/tokens/estimator.py +286 -0
- ai_lib_python/transport/__init__.py +34 -0
- ai_lib_python/transport/auth.py +141 -0
- ai_lib_python/transport/http.py +364 -0
- ai_lib_python/transport/pool.py +425 -0
- ai_lib_python/types/__init__.py +41 -0
- ai_lib_python/types/events.py +343 -0
- ai_lib_python/types/message.py +332 -0
- ai_lib_python/types/tool.py +191 -0
- ai_lib_python/utils/__init__.py +21 -0
- ai_lib_python/utils/tool_call_assembler.py +317 -0
- ai_lib_python-0.5.0.dist-info/METADATA +837 -0
- ai_lib_python-0.5.0.dist-info/RECORD +84 -0
- ai_lib_python-0.5.0.dist-info/WHEEL +4 -0
- ai_lib_python-0.5.0.dist-info/licenses/LICENSE-APACHE +201 -0
- 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
|