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,225 @@
1
+ """
2
+ Backpressure control using semaphores.
3
+
4
+ Limits concurrent operations to prevent overload.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import os
11
+ from contextlib import asynccontextmanager
12
+ from dataclasses import dataclass
13
+ from typing import TYPE_CHECKING, TypeVar
14
+
15
+ if TYPE_CHECKING:
16
+ from collections.abc import AsyncIterator, Awaitable, Callable
17
+
18
+ T = TypeVar("T")
19
+
20
+
21
+ @dataclass
22
+ class BackpressureConfig:
23
+ """Configuration for backpressure control.
24
+
25
+ Attributes:
26
+ max_concurrent: Maximum concurrent operations
27
+ queue_timeout: Timeout waiting for permit (None = wait forever)
28
+ """
29
+
30
+ max_concurrent: int = 10
31
+ queue_timeout: float | None = None
32
+
33
+ @classmethod
34
+ def from_env(cls) -> BackpressureConfig:
35
+ """Create configuration from environment variables."""
36
+ max_concurrent = int(os.getenv("AI_LIB_MAX_INFLIGHT", "10"))
37
+ timeout_str = os.getenv("AI_LIB_QUEUE_TIMEOUT")
38
+ queue_timeout = float(timeout_str) if timeout_str else None
39
+
40
+ return cls(
41
+ max_concurrent=max_concurrent,
42
+ queue_timeout=queue_timeout,
43
+ )
44
+
45
+ @classmethod
46
+ def unlimited(cls) -> BackpressureConfig:
47
+ """Create config with no limit."""
48
+ return cls(max_concurrent=0)
49
+
50
+
51
+ class BackpressureError(Exception):
52
+ """Raised when backpressure limit is exceeded."""
53
+
54
+ def __init__(self, message: str = "Backpressure limit exceeded") -> None:
55
+ super().__init__(message)
56
+
57
+
58
+ class Backpressure:
59
+ """Backpressure control using semaphores.
60
+
61
+ Limits the number of concurrent operations to prevent
62
+ overwhelming downstream services.
63
+
64
+ Example:
65
+ >>> bp = Backpressure(BackpressureConfig(max_concurrent=5))
66
+ >>> async with bp.acquire():
67
+ ... await make_request()
68
+
69
+ >>> # Or with execute
70
+ >>> result = await bp.execute(async_operation)
71
+ """
72
+
73
+ def __init__(self, config: BackpressureConfig | None = None) -> None:
74
+ """Initialize backpressure control.
75
+
76
+ Args:
77
+ config: Backpressure configuration
78
+ """
79
+ self._config = config or BackpressureConfig()
80
+
81
+ # Create semaphore if limiting is enabled
82
+ if self._config.max_concurrent > 0:
83
+ self._semaphore: asyncio.Semaphore | None = asyncio.Semaphore(
84
+ self._config.max_concurrent
85
+ )
86
+ else:
87
+ self._semaphore = None
88
+
89
+ # Statistics
90
+ self._current_inflight = 0
91
+ self._peak_inflight = 0
92
+ self._total_acquired = 0
93
+ self._total_rejected = 0
94
+
95
+ @property
96
+ def current_inflight(self) -> int:
97
+ """Get current number of in-flight operations."""
98
+ return self._current_inflight
99
+
100
+ @property
101
+ def available_permits(self) -> int:
102
+ """Get number of available permits."""
103
+ if self._semaphore is None:
104
+ return float("inf") # type: ignore
105
+ return self._config.max_concurrent - self._current_inflight
106
+
107
+ @property
108
+ def is_limited(self) -> bool:
109
+ """Check if backpressure limiting is enabled."""
110
+ return self._semaphore is not None
111
+
112
+ @asynccontextmanager
113
+ async def acquire(self) -> AsyncIterator[None]:
114
+ """Acquire a permit for an operation.
115
+
116
+ Yields:
117
+ None when permit acquired
118
+
119
+ Raises:
120
+ BackpressureError: If timeout exceeded
121
+ asyncio.TimeoutError: If timeout exceeded
122
+ """
123
+ if self._semaphore is None:
124
+ # No limiting
125
+ self._current_inflight += 1
126
+ self._peak_inflight = max(self._peak_inflight, self._current_inflight)
127
+ self._total_acquired += 1
128
+ try:
129
+ yield
130
+ finally:
131
+ self._current_inflight -= 1
132
+ return
133
+
134
+ try:
135
+ if self._config.queue_timeout is not None:
136
+ # Wait with timeout
137
+ await asyncio.wait_for(
138
+ self._semaphore.acquire(),
139
+ timeout=self._config.queue_timeout,
140
+ )
141
+ else:
142
+ # Wait indefinitely
143
+ await self._semaphore.acquire()
144
+
145
+ self._current_inflight += 1
146
+ self._peak_inflight = max(self._peak_inflight, self._current_inflight)
147
+ self._total_acquired += 1
148
+
149
+ try:
150
+ yield
151
+ finally:
152
+ self._current_inflight -= 1
153
+ self._semaphore.release()
154
+
155
+ except asyncio.TimeoutError:
156
+ self._total_rejected += 1
157
+ raise BackpressureError("Timeout waiting for permit") from None
158
+
159
+ async def try_acquire(self) -> bool:
160
+ """Try to acquire a permit without waiting.
161
+
162
+ Returns:
163
+ True if permit acquired, False otherwise
164
+ """
165
+ if self._semaphore is None:
166
+ self._current_inflight += 1
167
+ self._peak_inflight = max(self._peak_inflight, self._current_inflight)
168
+ self._total_acquired += 1
169
+ return True
170
+
171
+ # Try to acquire without waiting
172
+ acquired = self._semaphore.locked() is False
173
+ if acquired:
174
+ await self._semaphore.acquire()
175
+ self._current_inflight += 1
176
+ self._peak_inflight = max(self._peak_inflight, self._current_inflight)
177
+ self._total_acquired += 1
178
+
179
+ return acquired
180
+
181
+ def release(self) -> None:
182
+ """Release a permit (if using try_acquire)."""
183
+ self._current_inflight -= 1
184
+ if self._semaphore is not None:
185
+ self._semaphore.release()
186
+
187
+ async def execute(
188
+ self,
189
+ operation: Callable[[], Awaitable[T]],
190
+ ) -> T:
191
+ """Execute an operation with backpressure control.
192
+
193
+ Args:
194
+ operation: Async operation to execute
195
+
196
+ Returns:
197
+ Operation result
198
+
199
+ Raises:
200
+ BackpressureError: If timeout exceeded
201
+ """
202
+ async with self.acquire():
203
+ return await operation()
204
+
205
+ def get_stats(self) -> dict[str, int]:
206
+ """Get backpressure statistics.
207
+
208
+ Returns:
209
+ Dict with statistics
210
+ """
211
+ return {
212
+ "current_inflight": self._current_inflight,
213
+ "peak_inflight": self._peak_inflight,
214
+ "total_acquired": self._total_acquired,
215
+ "total_rejected": self._total_rejected,
216
+ "max_concurrent": self._config.max_concurrent,
217
+ }
218
+
219
+ def __repr__(self) -> str:
220
+ if self._semaphore is None:
221
+ return "Backpressure(unlimited)"
222
+ return (
223
+ f"Backpressure("
224
+ f"inflight={self._current_inflight}/{self._config.max_concurrent})"
225
+ )
@@ -0,0 +1,318 @@
1
+ """
2
+ Circuit breaker for fault isolation.
3
+
4
+ Implements the circuit breaker pattern with three states:
5
+ - Closed: Normal operation, requests pass through
6
+ - Open: Circuit tripped, requests fail fast
7
+ - Half-Open: Testing if service recovered
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import time
14
+ from dataclasses import dataclass
15
+ from enum import Enum
16
+ from typing import TYPE_CHECKING, TypeVar
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Awaitable, Callable
20
+
21
+ T = TypeVar("T")
22
+
23
+
24
+ class CircuitState(str, Enum):
25
+ """Circuit breaker states."""
26
+
27
+ CLOSED = "closed"
28
+ OPEN = "open"
29
+ HALF_OPEN = "half_open"
30
+
31
+
32
+ @dataclass
33
+ class CircuitBreakerConfig:
34
+ """Configuration for circuit breaker.
35
+
36
+ Attributes:
37
+ failure_threshold: Number of failures to trip the circuit
38
+ success_threshold: Number of successes in half-open to close
39
+ cooldown_seconds: Time to wait before testing (half-open)
40
+ timeout_seconds: Optional timeout for operations
41
+ """
42
+
43
+ failure_threshold: int = 5
44
+ success_threshold: int = 2
45
+ cooldown_seconds: float = 30.0
46
+ timeout_seconds: float | None = None
47
+ half_open_max_concurrent: int = 1
48
+
49
+ @classmethod
50
+ def default(cls) -> CircuitBreakerConfig:
51
+ """Create default configuration."""
52
+ return cls()
53
+
54
+ @classmethod
55
+ def from_env(cls) -> CircuitBreakerConfig:
56
+ """Create configuration from environment variables."""
57
+ import os
58
+
59
+ failure_threshold = int(
60
+ os.getenv("AI_LIB_BREAKER_FAILURE_THRESHOLD", "5")
61
+ )
62
+ cooldown_seconds = float(
63
+ os.getenv("AI_LIB_BREAKER_COOLDOWN_SECS", "30")
64
+ )
65
+
66
+ return cls(
67
+ failure_threshold=failure_threshold,
68
+ cooldown_seconds=cooldown_seconds,
69
+ )
70
+
71
+
72
+ class CircuitOpenError(Exception):
73
+ """Raised when circuit is open and request is rejected."""
74
+
75
+ def __init__(
76
+ self,
77
+ message: str = "Circuit breaker is open",
78
+ time_until_retry: float | None = None,
79
+ ) -> None:
80
+ super().__init__(message)
81
+ self.time_until_retry = time_until_retry
82
+
83
+
84
+ @dataclass
85
+ class CircuitStats:
86
+ """Statistics for circuit breaker."""
87
+
88
+ total_requests: int = 0
89
+ successful_requests: int = 0
90
+ failed_requests: int = 0
91
+ rejected_requests: int = 0
92
+ state_changes: int = 0
93
+ last_failure_time: float | None = None
94
+ last_success_time: float | None = None
95
+
96
+
97
+ class CircuitBreaker:
98
+ """Circuit breaker for fault isolation.
99
+
100
+ Prevents cascading failures by failing fast when a service is unhealthy.
101
+
102
+ Example:
103
+ >>> breaker = CircuitBreaker(CircuitBreakerConfig(failure_threshold=3))
104
+ >>> try:
105
+ ... result = await breaker.execute(async_operation)
106
+ ... except CircuitOpenError:
107
+ ... print("Service unavailable")
108
+ """
109
+
110
+ def __init__(self, config: CircuitBreakerConfig | None = None) -> None:
111
+ """Initialize circuit breaker.
112
+
113
+ Args:
114
+ config: Circuit breaker configuration
115
+ """
116
+ self._config = config or CircuitBreakerConfig()
117
+ self._state = CircuitState.CLOSED
118
+ self._lock = asyncio.Lock()
119
+
120
+ # Failure tracking
121
+ self._failure_count = 0
122
+ self._success_count = 0
123
+ self._last_failure_time: float | None = None
124
+ self._opened_at: float | None = None
125
+
126
+ # Half-open state management
127
+ self._half_open_semaphore = asyncio.Semaphore(
128
+ self._config.half_open_max_concurrent
129
+ )
130
+
131
+ # Statistics
132
+ self._stats = CircuitStats()
133
+
134
+ @property
135
+ def state(self) -> CircuitState:
136
+ """Get current circuit state."""
137
+ return self._state
138
+
139
+ @property
140
+ def is_closed(self) -> bool:
141
+ """Check if circuit is closed (normal operation)."""
142
+ return self._state == CircuitState.CLOSED
143
+
144
+ @property
145
+ def is_open(self) -> bool:
146
+ """Check if circuit is open (failing fast)."""
147
+ return self._state == CircuitState.OPEN
148
+
149
+ @property
150
+ def is_half_open(self) -> bool:
151
+ """Check if circuit is half-open (testing)."""
152
+ return self._state == CircuitState.HALF_OPEN
153
+
154
+ def _check_state_transition(self) -> None:
155
+ """Check if state should transition."""
156
+ now = time.monotonic()
157
+
158
+ if self._state == CircuitState.OPEN and self._opened_at is not None:
159
+ # Check if cooldown has passed
160
+ elapsed = now - self._opened_at
161
+ if elapsed >= self._config.cooldown_seconds:
162
+ self._transition_to(CircuitState.HALF_OPEN)
163
+
164
+ def _transition_to(self, new_state: CircuitState) -> None:
165
+ """Transition to a new state.
166
+
167
+ Args:
168
+ new_state: Target state
169
+ """
170
+ if new_state == self._state:
171
+ return
172
+
173
+ self._state = new_state
174
+ self._stats.state_changes += 1
175
+
176
+ if new_state == CircuitState.OPEN:
177
+ self._opened_at = time.monotonic()
178
+ elif new_state == CircuitState.HALF_OPEN:
179
+ self._success_count = 0
180
+ elif new_state == CircuitState.CLOSED:
181
+ self._failure_count = 0
182
+ self._opened_at = None
183
+
184
+ def _record_success(self) -> None:
185
+ """Record a successful operation."""
186
+ self._stats.successful_requests += 1
187
+ self._stats.last_success_time = time.monotonic()
188
+
189
+ if self._state == CircuitState.HALF_OPEN:
190
+ self._success_count += 1
191
+ if self._success_count >= self._config.success_threshold:
192
+ self._transition_to(CircuitState.CLOSED)
193
+ elif self._state == CircuitState.CLOSED:
194
+ # Reset failure count on success
195
+ self._failure_count = max(0, self._failure_count - 1)
196
+
197
+ def _record_failure(self) -> None:
198
+ """Record a failed operation."""
199
+ now = time.monotonic()
200
+ self._stats.failed_requests += 1
201
+ self._stats.last_failure_time = now
202
+ self._last_failure_time = now
203
+
204
+ if self._state == CircuitState.HALF_OPEN:
205
+ # Single failure in half-open trips back to open
206
+ self._transition_to(CircuitState.OPEN)
207
+ elif self._state == CircuitState.CLOSED:
208
+ self._failure_count += 1
209
+ if self._failure_count >= self._config.failure_threshold:
210
+ self._transition_to(CircuitState.OPEN)
211
+
212
+ def get_time_until_retry(self) -> float | None:
213
+ """Get time until circuit will transition to half-open.
214
+
215
+ Returns:
216
+ Seconds until retry, or None if not open
217
+ """
218
+ if self._state != CircuitState.OPEN or self._opened_at is None:
219
+ return None
220
+
221
+ elapsed = time.monotonic() - self._opened_at
222
+ remaining = self._config.cooldown_seconds - elapsed
223
+ return max(0, remaining)
224
+
225
+ async def execute(
226
+ self,
227
+ operation: Callable[[], Awaitable[T]],
228
+ fallback: Callable[[], Awaitable[T]] | None = None,
229
+ ) -> T:
230
+ """Execute an operation through the circuit breaker.
231
+
232
+ Args:
233
+ operation: Async operation to execute
234
+ fallback: Optional fallback if circuit is open
235
+
236
+ Returns:
237
+ Operation result
238
+
239
+ Raises:
240
+ CircuitOpenError: If circuit is open and no fallback
241
+ """
242
+ async with self._lock:
243
+ self._check_state_transition()
244
+ self._stats.total_requests += 1
245
+
246
+ if self._state == CircuitState.OPEN:
247
+ self._stats.rejected_requests += 1
248
+ if fallback:
249
+ return await fallback()
250
+ raise CircuitOpenError(
251
+ time_until_retry=self.get_time_until_retry()
252
+ )
253
+
254
+ # Execute operation
255
+ try:
256
+ if self._state == CircuitState.HALF_OPEN:
257
+ # Limit concurrent requests in half-open state
258
+ async with self._half_open_semaphore:
259
+ result = await self._execute_with_timeout(operation)
260
+ else:
261
+ result = await self._execute_with_timeout(operation)
262
+
263
+ async with self._lock:
264
+ self._record_success()
265
+
266
+ return result
267
+
268
+ except Exception:
269
+ async with self._lock:
270
+ self._record_failure()
271
+ raise
272
+
273
+ async def _execute_with_timeout(
274
+ self, operation: Callable[[], Awaitable[T]]
275
+ ) -> T:
276
+ """Execute operation with optional timeout.
277
+
278
+ Args:
279
+ operation: Async operation
280
+
281
+ Returns:
282
+ Operation result
283
+ """
284
+ if self._config.timeout_seconds:
285
+ return await asyncio.wait_for(
286
+ operation(),
287
+ timeout=self._config.timeout_seconds,
288
+ )
289
+ return await operation()
290
+
291
+ def reset(self) -> None:
292
+ """Reset circuit breaker to closed state."""
293
+ self._state = CircuitState.CLOSED
294
+ self._failure_count = 0
295
+ self._success_count = 0
296
+ self._opened_at = None
297
+
298
+ def get_stats(self) -> CircuitStats:
299
+ """Get circuit breaker statistics.
300
+
301
+ Returns:
302
+ CircuitStats with current statistics
303
+ """
304
+ return CircuitStats(
305
+ total_requests=self._stats.total_requests,
306
+ successful_requests=self._stats.successful_requests,
307
+ failed_requests=self._stats.failed_requests,
308
+ rejected_requests=self._stats.rejected_requests,
309
+ state_changes=self._stats.state_changes,
310
+ last_failure_time=self._stats.last_failure_time,
311
+ last_success_time=self._stats.last_success_time,
312
+ )
313
+
314
+ def __repr__(self) -> str:
315
+ return (
316
+ f"CircuitBreaker(state={self._state.value}, "
317
+ f"failures={self._failure_count}/{self._config.failure_threshold})"
318
+ )