ai-lib-python 0.7.1__py3-none-any.whl → 0.8.3__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 +1 -1
- ai_lib_python/batch/collector.py +4 -1
- ai_lib_python/cache/manager.py +1 -0
- ai_lib_python/client/__init__.py +2 -2
- ai_lib_python/client/builder.py +33 -0
- ai_lib_python/client/cancel.py +7 -3
- ai_lib_python/client/core.py +97 -14
- ai_lib_python/computer_use/__init__.py +0 -1
- ai_lib_python/drivers/anthropic.py +10 -0
- ai_lib_python/drivers/gemini.py +11 -1
- ai_lib_python/drivers/openai.py +16 -12
- ai_lib_python/embeddings/client.py +9 -5
- ai_lib_python/guardrails/__init__.py +3 -3
- ai_lib_python/guardrails/base.py +336 -336
- ai_lib_python/guardrails/filters.py +561 -583
- ai_lib_python/guardrails/validators.py +475 -475
- ai_lib_python/multimodal/__init__.py +5 -0
- ai_lib_python/pipeline/accumulate.py +2 -2
- ai_lib_python/pipeline/base.py +3 -3
- ai_lib_python/pipeline/event_map.py +1 -1
- ai_lib_python/pipeline/fan_out.py +3 -3
- ai_lib_python/pipeline/select.py +1 -1
- ai_lib_python/protocol/loader.py +18 -6
- ai_lib_python/protocol/manifest.py +3 -1
- ai_lib_python/protocol/v2/capabilities.py +224 -198
- ai_lib_python/protocol/v2/manifest.py +45 -9
- ai_lib_python/registry/__init__.py +1 -2
- ai_lib_python/rerank/__init__.py +20 -0
- ai_lib_python/rerank/client.py +144 -0
- ai_lib_python/resilience/executor.py +9 -5
- ai_lib_python/resilience/fallback.py +3 -3
- ai_lib_python/resilience/preflight.py +15 -27
- ai_lib_python/resilience/retry.py +5 -4
- ai_lib_python/resilience/signals.py +7 -7
- ai_lib_python/structured/json_mode.py +6 -2
- ai_lib_python/structured/schema.py +10 -4
- ai_lib_python/structured/validator.py +18 -8
- ai_lib_python/stt/__init__.py +22 -0
- ai_lib_python/stt/client.py +171 -0
- ai_lib_python/telemetry/health.py +1 -1
- ai_lib_python/telemetry/logger.py +4 -1
- ai_lib_python/transport/auth.py +2 -2
- ai_lib_python/transport/http.py +22 -3
- ai_lib_python/transport/pool.py +1 -0
- ai_lib_python/tts/__init__.py +22 -0
- ai_lib_python/tts/client.py +164 -0
- ai_lib_python/types/message.py +25 -5
- ai_lib_python/types/tool.py +19 -1
- {ai_lib_python-0.7.1.dist-info → ai_lib_python-0.8.3.dist-info}/METADATA +12 -1
- ai_lib_python-0.8.3.dist-info/RECORD +103 -0
- ai_lib_python-0.7.1.dist-info/RECORD +0 -97
- {ai_lib_python-0.7.1.dist-info → ai_lib_python-0.8.3.dist-info}/WHEEL +0 -0
- {ai_lib_python-0.7.1.dist-info → ai_lib_python-0.8.3.dist-info}/licenses/LICENSE-APACHE +0 -0
- {ai_lib_python-0.7.1.dist-info → ai_lib_python-0.8.3.dist-info}/licenses/LICENSE-MIT +0 -0
ai_lib_python/__init__.py
CHANGED
ai_lib_python/batch/collector.py
CHANGED
|
@@ -191,7 +191,10 @@ class BatchCollector(Generic[T, R]):
|
|
|
191
191
|
|
|
192
192
|
try:
|
|
193
193
|
# Execute batch
|
|
194
|
-
|
|
194
|
+
executor = self._executor
|
|
195
|
+
if executor is None:
|
|
196
|
+
raise RuntimeError("No executor set")
|
|
197
|
+
results = await executor(data_list)
|
|
195
198
|
|
|
196
199
|
# Resolve futures
|
|
197
200
|
for request, result in zip(requests, results, strict=False):
|
ai_lib_python/cache/manager.py
CHANGED
ai_lib_python/client/__init__.py
CHANGED
|
@@ -10,8 +10,8 @@ This module provides:
|
|
|
10
10
|
|
|
11
11
|
from ai_lib_python.client.builder import AiClientBuilder, ChatRequestBuilder
|
|
12
12
|
from ai_lib_python.client.cancel import (
|
|
13
|
-
CancellableStream,
|
|
14
13
|
CancelHandle,
|
|
14
|
+
CancellableStream,
|
|
15
15
|
CancelReason,
|
|
16
16
|
CancelState,
|
|
17
17
|
CancelToken,
|
|
@@ -25,11 +25,11 @@ __all__ = [
|
|
|
25
25
|
"AiClient",
|
|
26
26
|
"AiClientBuilder",
|
|
27
27
|
"CallStats",
|
|
28
|
-
"CancellableStream",
|
|
29
28
|
"CancelHandle",
|
|
30
29
|
"CancelReason",
|
|
31
30
|
"CancelState",
|
|
32
31
|
"CancelToken",
|
|
32
|
+
"CancellableStream",
|
|
33
33
|
"ChatRequestBuilder",
|
|
34
34
|
"ChatResponse",
|
|
35
35
|
"create_cancel_pair",
|
ai_lib_python/client/builder.py
CHANGED
|
@@ -52,6 +52,7 @@ class AiClientBuilder:
|
|
|
52
52
|
self._rate_limit_config: RateLimiterConfig | None = None
|
|
53
53
|
self._circuit_breaker_config: CircuitBreakerConfig | None = None
|
|
54
54
|
self._resilient_config: ResilientConfig | None = None
|
|
55
|
+
self._api_keys: dict[str, str] = {}
|
|
55
56
|
|
|
56
57
|
def model(self, model_id: str) -> AiClientBuilder:
|
|
57
58
|
"""Set the model to use.
|
|
@@ -149,6 +150,37 @@ class AiClientBuilder:
|
|
|
149
150
|
self._max_inflight = n
|
|
150
151
|
return self
|
|
151
152
|
|
|
153
|
+
def retry(self, max_attempts: int = 3, backoff: float = 1.0) -> AiClientBuilder:
|
|
154
|
+
"""Configure retry policy with simple parameters.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
max_attempts: Maximum number of attempts (including initial)
|
|
158
|
+
backoff: Backoff base in seconds
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Self for chaining
|
|
162
|
+
"""
|
|
163
|
+
from ai_lib_python.resilience import RetryConfig
|
|
164
|
+
|
|
165
|
+
self._retry_config = RetryConfig(
|
|
166
|
+
max_retries=max_attempts - 1,
|
|
167
|
+
min_delay_ms=int(backoff * 1000),
|
|
168
|
+
)
|
|
169
|
+
return self
|
|
170
|
+
|
|
171
|
+
def api_key_for(self, model_id: str, key: str) -> AiClientBuilder:
|
|
172
|
+
"""Set API key for a specific fallback model.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
model_id: Model identifier
|
|
176
|
+
key: API key to use for this model
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Self for chaining
|
|
180
|
+
"""
|
|
181
|
+
self._api_keys[model_id] = key
|
|
182
|
+
return self
|
|
183
|
+
|
|
152
184
|
def with_retry(self, config: RetryConfig) -> AiClientBuilder:
|
|
153
185
|
"""Configure retry policy.
|
|
154
186
|
|
|
@@ -252,6 +284,7 @@ class AiClientBuilder:
|
|
|
252
284
|
timeout=self._timeout,
|
|
253
285
|
hot_reload=self._hot_reload,
|
|
254
286
|
resilient_config=resilient_config,
|
|
287
|
+
api_keys=self._api_keys,
|
|
255
288
|
)
|
|
256
289
|
|
|
257
290
|
|
ai_lib_python/client/cancel.py
CHANGED
|
@@ -77,8 +77,12 @@ class CancelToken:
|
|
|
77
77
|
|
|
78
78
|
def _start_timeout(self) -> None:
|
|
79
79
|
"""Start the timeout task."""
|
|
80
|
+
timeout = self._timeout
|
|
81
|
+
if timeout is None:
|
|
82
|
+
return
|
|
83
|
+
|
|
80
84
|
async def timeout_handler() -> None:
|
|
81
|
-
await asyncio.sleep(
|
|
85
|
+
await asyncio.sleep(timeout)
|
|
82
86
|
if not self._state.cancelled:
|
|
83
87
|
self.cancel(CancelReason.TIMEOUT)
|
|
84
88
|
|
|
@@ -125,7 +129,7 @@ class CancelToken:
|
|
|
125
129
|
try:
|
|
126
130
|
result = callback(reason)
|
|
127
131
|
if asyncio.iscoroutine(result):
|
|
128
|
-
_ = asyncio.create_task(result) #
|
|
132
|
+
_ = asyncio.create_task(result) # noqa: RUF006
|
|
129
133
|
except Exception:
|
|
130
134
|
pass
|
|
131
135
|
|
|
@@ -185,7 +189,7 @@ class CancelToken:
|
|
|
185
189
|
try:
|
|
186
190
|
result = callback(self._state.reason)
|
|
187
191
|
if asyncio.iscoroutine(result):
|
|
188
|
-
_ = asyncio.create_task(result) #
|
|
192
|
+
_ = asyncio.create_task(result) # noqa: RUF006
|
|
189
193
|
except Exception:
|
|
190
194
|
pass
|
|
191
195
|
return self
|
ai_lib_python/client/core.py
CHANGED
|
@@ -56,6 +56,10 @@ class AiClient:
|
|
|
56
56
|
model_id: str,
|
|
57
57
|
fallbacks: list[str] | None = None,
|
|
58
58
|
executor: ResilientExecutor | None = None,
|
|
59
|
+
loader: ProtocolLoader | None = None,
|
|
60
|
+
api_keys: dict[str, str] | None = None,
|
|
61
|
+
base_url_override: str | None = None,
|
|
62
|
+
timeout: float | None = None,
|
|
59
63
|
) -> None:
|
|
60
64
|
"""Initialize the client (internal use).
|
|
61
65
|
|
|
@@ -67,6 +71,10 @@ class AiClient:
|
|
|
67
71
|
self._model_id = model_id
|
|
68
72
|
self._fallbacks = fallbacks or []
|
|
69
73
|
self._executor = executor
|
|
74
|
+
self._loader = loader
|
|
75
|
+
self._api_keys = api_keys or {}
|
|
76
|
+
self._base_url_override = base_url_override
|
|
77
|
+
self._timeout = timeout
|
|
70
78
|
|
|
71
79
|
@classmethod
|
|
72
80
|
async def create(
|
|
@@ -131,6 +139,7 @@ class AiClient:
|
|
|
131
139
|
timeout: float | None = None,
|
|
132
140
|
hot_reload: bool = False,
|
|
133
141
|
resilient_config: ResilientConfig | None = None,
|
|
142
|
+
api_keys: dict[str, str] | None = None,
|
|
134
143
|
) -> AiClient:
|
|
135
144
|
"""Internal creation method.
|
|
136
145
|
|
|
@@ -181,9 +190,13 @@ class AiClient:
|
|
|
181
190
|
manifest=manifest,
|
|
182
191
|
transport=transport,
|
|
183
192
|
pipeline=pipeline,
|
|
184
|
-
model_id=
|
|
193
|
+
model_id=model, # Keep the full model name including provider
|
|
185
194
|
fallbacks=fallbacks,
|
|
186
195
|
executor=executor,
|
|
196
|
+
loader=loader,
|
|
197
|
+
api_keys=api_keys,
|
|
198
|
+
base_url_override=base_url_override,
|
|
199
|
+
timeout=timeout,
|
|
187
200
|
)
|
|
188
201
|
|
|
189
202
|
def chat(self) -> ChatRequestBuilder:
|
|
@@ -203,7 +216,7 @@ class AiClient:
|
|
|
203
216
|
return ChatRequestBuilder(self)
|
|
204
217
|
|
|
205
218
|
async def _execute_chat(self, builder: ChatRequestBuilder) -> ChatResponse:
|
|
206
|
-
"""Execute a non-streaming chat request.
|
|
219
|
+
"""Execute a non-streaming chat request with fallback support.
|
|
207
220
|
|
|
208
221
|
Args:
|
|
209
222
|
builder: Configured request builder
|
|
@@ -211,19 +224,89 @@ class AiClient:
|
|
|
211
224
|
Returns:
|
|
212
225
|
ChatResponse with the completion
|
|
213
226
|
"""
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
endpoint = self._manifest.get_chat_endpoint()
|
|
227
|
+
models_to_try = [self._model_id, *self._fallbacks]
|
|
228
|
+
last_error = None
|
|
217
229
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
230
|
+
for model in models_to_try:
|
|
231
|
+
try:
|
|
232
|
+
# 1. Resolve manifest and transport for this model
|
|
233
|
+
if model == self._model_id:
|
|
234
|
+
manifest = self._manifest
|
|
235
|
+
transport = self._transport
|
|
236
|
+
pipeline = self._pipeline
|
|
237
|
+
else:
|
|
238
|
+
# Dynamic load for fallback
|
|
239
|
+
if not self._loader:
|
|
240
|
+
raise ValueError("ProtocolLoader missing for fallback")
|
|
241
|
+
manifest = await self._loader.load_model(model)
|
|
242
|
+
|
|
243
|
+
parts = model.split("/")
|
|
244
|
+
m_id = parts[1] if len(parts) >= 2 else model
|
|
245
|
+
|
|
246
|
+
# Resolve key for this model
|
|
247
|
+
m_key = self._api_keys.get(model)
|
|
248
|
+
|
|
249
|
+
from ai_lib_python.transport import HttpTransport
|
|
250
|
+
transport = HttpTransport(
|
|
251
|
+
manifest=manifest,
|
|
252
|
+
model_id=m_id,
|
|
253
|
+
api_key=m_key,
|
|
254
|
+
base_url_override=self._base_url_override,
|
|
255
|
+
timeout=self._timeout,
|
|
256
|
+
)
|
|
257
|
+
pipeline = Pipeline.from_manifest(manifest)
|
|
258
|
+
|
|
259
|
+
async def do_request(
|
|
260
|
+
m: ProtocolManifest = manifest,
|
|
261
|
+
t: HttpTransport = transport,
|
|
262
|
+
p: Pipeline = pipeline,
|
|
263
|
+
mid: str = model,
|
|
264
|
+
) -> ChatResponse:
|
|
265
|
+
# Debug print for model being used
|
|
266
|
+
print(f"DEBUG: Executing request for model: {mid}, manifest ID: {m.id}")
|
|
267
|
+
|
|
268
|
+
# Update builder's client temporary context?
|
|
269
|
+
# Actually builder.build_payload() uses self._client._model_id
|
|
270
|
+
# This is tricky as builder is bound to the primary client.
|
|
271
|
+
# We need to temporarily override the client context in the builder.
|
|
272
|
+
|
|
273
|
+
# Create a temporary builder/payload
|
|
274
|
+
# For simplicity, we'll just manually build the payload here or
|
|
275
|
+
# temporarily swap self._model_id (hacky but it works for this pattern)
|
|
276
|
+
original_model_id = self._model_id
|
|
277
|
+
original_manifest = self._manifest
|
|
278
|
+
try:
|
|
279
|
+
self._model_id = mid
|
|
280
|
+
self._manifest = m
|
|
281
|
+
payload = builder.build_payload()
|
|
282
|
+
print(f"DEBUG: Payload model: {payload.get('model')}")
|
|
283
|
+
finally:
|
|
284
|
+
self._model_id = original_model_id
|
|
285
|
+
self._manifest = original_manifest
|
|
286
|
+
|
|
287
|
+
endpoint = m.get_chat_endpoint()
|
|
288
|
+
print(f"DEBUG: Endpoint: {endpoint}")
|
|
289
|
+
response = await t.post(endpoint, json=payload)
|
|
290
|
+
data = response.json()
|
|
291
|
+
|
|
292
|
+
# Parse using the correct pipeline
|
|
293
|
+
return self._parse_response(data)
|
|
294
|
+
|
|
295
|
+
# Use executor if available for resilience
|
|
296
|
+
if self._executor:
|
|
297
|
+
return await self._executor.execute(do_request)
|
|
298
|
+
return await do_request()
|
|
299
|
+
|
|
300
|
+
except Exception as e:
|
|
301
|
+
from ai_lib_python.errors import is_fallbackable
|
|
302
|
+
# Check if we should fallback
|
|
303
|
+
error_class = getattr(e, "error_class", None)
|
|
304
|
+
if model != models_to_try[-1] and (error_class is None or is_fallbackable(error_class)):
|
|
305
|
+
last_error = e
|
|
306
|
+
continue
|
|
307
|
+
raise e
|
|
308
|
+
|
|
309
|
+
raise last_error or RuntimeError("Fallback exhausted")
|
|
227
310
|
|
|
228
311
|
async def _execute_chat_with_stats(
|
|
229
312
|
self, builder: ChatRequestBuilder
|
|
@@ -162,6 +162,16 @@ class AnthropicDriver(ProviderDriver):
|
|
|
162
162
|
system_parts.append(m.content)
|
|
163
163
|
continue
|
|
164
164
|
|
|
165
|
+
if role == "tool":
|
|
166
|
+
# Anthropic: tool results as user message with tool_result block
|
|
167
|
+
tool_id = getattr(m, "tool_call_id", None)
|
|
168
|
+
if tool_id and isinstance(m.content, str):
|
|
169
|
+
msgs.append({
|
|
170
|
+
"role": "user",
|
|
171
|
+
"content": [{"type": "tool_result", "tool_use_id": tool_id, "content": m.content}],
|
|
172
|
+
})
|
|
173
|
+
continue
|
|
174
|
+
|
|
165
175
|
if isinstance(m.content, str):
|
|
166
176
|
content: Any = [{"type": "text", "text": m.content}]
|
|
167
177
|
else:
|
ai_lib_python/drivers/gemini.py
CHANGED
|
@@ -55,7 +55,7 @@ class GeminiDriver(ProviderDriver):
|
|
|
55
55
|
def build_request(
|
|
56
56
|
self,
|
|
57
57
|
messages: list[Message],
|
|
58
|
-
|
|
58
|
+
_model: str,
|
|
59
59
|
*,
|
|
60
60
|
temperature: float | None = None,
|
|
61
61
|
max_tokens: int | None = None,
|
|
@@ -161,6 +161,16 @@ class GeminiDriver(ProviderDriver):
|
|
|
161
161
|
system_parts.append(m.content)
|
|
162
162
|
continue
|
|
163
163
|
|
|
164
|
+
if role == "tool":
|
|
165
|
+
# Gemini: function_response with name (tool_call_id) and response
|
|
166
|
+
tool_id = getattr(m, "tool_call_id", None)
|
|
167
|
+
if tool_id and isinstance(m.content, str):
|
|
168
|
+
contents.append({
|
|
169
|
+
"role": "user",
|
|
170
|
+
"parts": [{"functionResponse": {"name": tool_id, "response": {"result": m.content}}}],
|
|
171
|
+
})
|
|
172
|
+
continue
|
|
173
|
+
|
|
164
174
|
gemini_role = "model" if role == "assistant" else "user"
|
|
165
175
|
|
|
166
176
|
if isinstance(m.content, str):
|
ai_lib_python/drivers/openai.py
CHANGED
|
@@ -18,7 +18,7 @@ from ai_lib_python.drivers import (
|
|
|
18
18
|
from ai_lib_python.protocol.v2.capabilities import Capability
|
|
19
19
|
from ai_lib_python.protocol.v2.manifest import ApiStyle
|
|
20
20
|
from ai_lib_python.types.events import StreamingEvent
|
|
21
|
-
from ai_lib_python.types.message import
|
|
21
|
+
from ai_lib_python.types.message import Message
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
class OpenAiDriver(ProviderDriver):
|
|
@@ -120,14 +120,18 @@ class OpenAiDriver(ProviderDriver):
|
|
|
120
120
|
# role is stored as str because model uses use_enum_values=True
|
|
121
121
|
role = m.role if isinstance(m.role, str) else m.role.value
|
|
122
122
|
if isinstance(m.content, str):
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
123
|
+
out: dict[str, Any] = {"role": role, "content": m.content}
|
|
124
|
+
else:
|
|
125
|
+
# list[ContentBlock] → OpenAI content array
|
|
126
|
+
blocks = []
|
|
127
|
+
for b in m.content:
|
|
128
|
+
if b.type == "text":
|
|
129
|
+
blocks.append({"type": "text", "text": b.text})
|
|
130
|
+
elif b.type == "image":
|
|
131
|
+
blocks.append(b.model_dump(by_alias=True))
|
|
132
|
+
else:
|
|
133
|
+
blocks.append(b.model_dump(by_alias=True))
|
|
134
|
+
out = {"role": role, "content": blocks}
|
|
135
|
+
if role == "tool" and getattr(m, "tool_call_id", None):
|
|
136
|
+
out["tool_call_id"] = m.tool_call_id
|
|
137
|
+
return out
|
|
@@ -193,9 +193,12 @@ class EmbeddingClient:
|
|
|
193
193
|
Returns:
|
|
194
194
|
Endpoint path
|
|
195
195
|
"""
|
|
196
|
-
#
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
# Prefer manifest endpoint mapping when present.
|
|
197
|
+
embedding_cfg = self._manifest.endpoints.get("embeddings")
|
|
198
|
+
if isinstance(embedding_cfg, dict):
|
|
199
|
+
path = embedding_cfg.get("path")
|
|
200
|
+
if isinstance(path, str):
|
|
201
|
+
return path
|
|
199
202
|
return "/v1/embeddings"
|
|
200
203
|
|
|
201
204
|
@property
|
|
@@ -325,8 +328,9 @@ class EmbeddingClientBuilder:
|
|
|
325
328
|
manifest = await loader.load_provider(provider_id)
|
|
326
329
|
|
|
327
330
|
# Create transport
|
|
328
|
-
transport = HttpTransport
|
|
329
|
-
manifest,
|
|
331
|
+
transport = HttpTransport(
|
|
332
|
+
manifest=manifest,
|
|
333
|
+
model_id=model_id,
|
|
330
334
|
api_key=self._api_key,
|
|
331
335
|
base_url_override=self._base_url,
|
|
332
336
|
timeout=self._timeout,
|
|
@@ -7,14 +7,14 @@ both user inputs and AI model outputs to ensure safety and compliance.
|
|
|
7
7
|
Core principle: All logic is operators, all configuration is protocol.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
from ai_lib_python.guardrails.base import Guardrail,
|
|
10
|
+
from ai_lib_python.guardrails.base import Guardrail, GuardrailResult, GuardrailViolation
|
|
11
11
|
from ai_lib_python.guardrails.filters import (
|
|
12
|
+
EmailFilter,
|
|
12
13
|
KeywordFilter,
|
|
13
|
-
RegexFilter,
|
|
14
14
|
LengthFilter,
|
|
15
15
|
ProfanityFilter,
|
|
16
|
+
RegexFilter,
|
|
16
17
|
UrlFilter,
|
|
17
|
-
EmailFilter,
|
|
18
18
|
)
|
|
19
19
|
from ai_lib_python.guardrails.validators import ContentValidator
|
|
20
20
|
|