ai-lib-python 0.7.0__tar.gz → 0.8.2__tar.gz
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-0.7.0 → ai_lib_python-0.8.2}/PKG-INFO +28 -1
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/README.md +24 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/pyproject.toml +10 -2
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/__init__.py +1 -1
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/batch/collector.py +4 -1
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/cache/manager.py +1 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/client/__init__.py +2 -2
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/client/builder.py +33 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/client/cancel.py +7 -3
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/client/core.py +97 -14
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/computer_use/__init__.py +0 -1
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/drivers/anthropic.py +10 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/drivers/gemini.py +11 -1
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/drivers/openai.py +16 -12
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/embeddings/client.py +9 -5
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/guardrails/__init__.py +3 -3
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/guardrails/base.py +10 -10
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/guardrails/filters.py +28 -50
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/guardrails/validators.py +12 -12
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/multimodal/__init__.py +5 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/pipeline/accumulate.py +2 -2
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/pipeline/base.py +3 -3
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/pipeline/event_map.py +1 -1
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/pipeline/fan_out.py +3 -3
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/pipeline/select.py +1 -1
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/protocol/loader.py +18 -6
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/protocol/manifest.py +3 -1
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/protocol/v2/capabilities.py +27 -1
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/protocol/v2/manifest.py +45 -9
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/registry/__init__.py +1 -2
- ai_lib_python-0.8.2/src/ai_lib_python/rerank/__init__.py +20 -0
- ai_lib_python-0.8.2/src/ai_lib_python/rerank/client.py +144 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/resilience/executor.py +9 -5
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/resilience/fallback.py +3 -3
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/resilience/preflight.py +15 -27
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/resilience/retry.py +5 -4
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/resilience/signals.py +7 -7
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/structured/json_mode.py +6 -2
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/structured/schema.py +10 -4
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/structured/validator.py +18 -8
- ai_lib_python-0.8.2/src/ai_lib_python/stt/__init__.py +22 -0
- ai_lib_python-0.8.2/src/ai_lib_python/stt/client.py +171 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/telemetry/health.py +1 -1
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/telemetry/logger.py +4 -1
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/transport/auth.py +2 -2
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/transport/http.py +22 -3
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/transport/pool.py +1 -0
- ai_lib_python-0.8.2/src/ai_lib_python/tts/__init__.py +22 -0
- ai_lib_python-0.8.2/src/ai_lib_python/tts/client.py +164 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/types/message.py +25 -5
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/types/tool.py +19 -1
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/.gitignore +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/LICENSE-APACHE +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/LICENSE-MIT +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/_features.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/batch/__init__.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/batch/executor.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/cache/__init__.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/cache/backends.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/cache/key.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/client/response.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/drivers/__init__.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/embeddings/__init__.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/embeddings/types.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/embeddings/vectors.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/errors/__init__.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/errors/base.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/errors/classification.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/errors/standard_codes.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/mcp/__init__.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/pipeline/__init__.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/pipeline/decode.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/plugins/__init__.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/plugins/base.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/plugins/hooks.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/plugins/middleware.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/plugins/registry.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/protocol/__init__.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/protocol/v2/__init__.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/protocol/validator.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/py.typed +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/resilience/__init__.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/resilience/backpressure.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/resilience/circuit_breaker.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/resilience/rate_limiter.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/routing/__init__.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/routing/manager.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/routing/strategy.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/routing/types.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/structured/__init__.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/telemetry/__init__.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/telemetry/exporters/__init__.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/telemetry/exporters/prometheus.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/telemetry/feedback.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/telemetry/metrics.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/telemetry/tracer.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/tokens/__init__.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/tokens/counter.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/tokens/estimator.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/transport/__init__.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/types/__init__.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/types/events.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/utils/__init__.py +0 -0
- {ai_lib_python-0.7.0 → ai_lib_python-0.8.2}/src/ai_lib_python/utils/tool_call_assembler.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ai-lib-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.2
|
|
4
4
|
Summary: Official Python Runtime for AI-Protocol - The canonical Pythonic implementation for unified AI model interaction
|
|
5
5
|
Project-URL: Homepage, https://github.com/hiddenpath/ai-lib-python
|
|
6
6
|
Project-URL: Documentation, https://github.com/hiddenpath/ai-lib-python#readme
|
|
@@ -57,13 +57,16 @@ Requires-Dist: tiktoken>=0.5; extra == 'full'
|
|
|
57
57
|
Requires-Dist: watchdog>=3.0; extra == 'full'
|
|
58
58
|
Provides-Extra: jupyter
|
|
59
59
|
Requires-Dist: ipywidgets>=8.0; extra == 'jupyter'
|
|
60
|
+
Provides-Extra: reranking
|
|
60
61
|
Provides-Extra: structured
|
|
62
|
+
Provides-Extra: stt
|
|
61
63
|
Provides-Extra: telemetry
|
|
62
64
|
Requires-Dist: opentelemetry-api>=1.20; extra == 'telemetry'
|
|
63
65
|
Requires-Dist: opentelemetry-exporter-otlp>=1.20; extra == 'telemetry'
|
|
64
66
|
Requires-Dist: opentelemetry-sdk>=1.20; extra == 'telemetry'
|
|
65
67
|
Provides-Extra: tokenizer
|
|
66
68
|
Requires-Dist: tiktoken>=0.5; extra == 'tokenizer'
|
|
69
|
+
Provides-Extra: tts
|
|
67
70
|
Provides-Extra: vision
|
|
68
71
|
Requires-Dist: pillow>=10.0; extra == 'vision'
|
|
69
72
|
Description-Content-Type: text/markdown
|
|
@@ -176,6 +179,25 @@ COMPLIANCE_DIR=../ai-protocol/tests/compliance pytest tests/compliance/ -v
|
|
|
176
179
|
|
|
177
180
|
For details, see [CROSS_RUNTIME.md](https://github.com/hiddenpath/ai-protocol/blob/main/docs/CROSS_RUNTIME.md).
|
|
178
181
|
|
|
182
|
+
### Testing with ai-protocol-mock
|
|
183
|
+
|
|
184
|
+
For integration and MCP e2e tests without real API calls, use [ai-protocol-mock](https://github.com/hiddenpath/ai-protocol-mock):
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# Start mock server (from ai-protocol-mock repo)
|
|
188
|
+
docker-compose up -d
|
|
189
|
+
|
|
190
|
+
# Run tests with mock
|
|
191
|
+
MOCK_HTTP_URL=http://localhost:4010 MOCK_MCP_URL=http://localhost:4010/mcp pytest tests/ -v
|
|
192
|
+
|
|
193
|
+
# Run only mock E2E tests (chat, streaming, tools, MCP)
|
|
194
|
+
MOCK_HTTP_URL=http://localhost:4010 pytest tests/integration/test_mock_chat_e2e.py tests/integration/test_mcp_bridge_e2e.py -v
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
With proxy: set `NO_PROXY` to include the mock server IP (e.g. `NO_PROXY=192.168.2.13,localhost,127.0.0.1`).
|
|
198
|
+
|
|
199
|
+
Or in code: `AiClient.create("openai/gpt-4o", base_url="http://localhost:4010")`
|
|
200
|
+
|
|
179
201
|
## 📦 Installation
|
|
180
202
|
|
|
181
203
|
```bash
|
|
@@ -195,6 +217,9 @@ pip install ai-lib-python[embeddings] # Embedding generation
|
|
|
195
217
|
pip install ai-lib-python[structured] # Structured output / JSON mode
|
|
196
218
|
pip install ai-lib-python[batch] # Batch processing
|
|
197
219
|
pip install ai-lib-python[agentic] # Agent workflow support
|
|
220
|
+
pip install ai-lib-python[stt] # Speech-to-Text (STT)
|
|
221
|
+
pip install ai-lib-python[tts] # Text-to-Speech (TTS)
|
|
222
|
+
pip install ai-lib-python[reranking] # Document reranking
|
|
198
223
|
|
|
199
224
|
# Infrastructure extras
|
|
200
225
|
pip install ai-lib-python[telemetry] # OpenTelemetry integration
|
|
@@ -228,6 +253,8 @@ Provider manifests are resolved in a backward-compatible order:
|
|
|
228
253
|
| `AI_LIB_RPS` | Rate limit (requests per second) | - |
|
|
229
254
|
| `AI_LIB_BREAKER_FAILURE_THRESHOLD` | Circuit breaker failure threshold | 5 |
|
|
230
255
|
| `AI_LIB_BREAKER_COOLDOWN_SECS` | Circuit breaker cooldown seconds | 30 |
|
|
256
|
+
| `MOCK_HTTP_URL` | Mock server URL for testing (ai-protocol-mock) | - |
|
|
257
|
+
| `MOCK_MCP_URL` | Mock MCP endpoint for testing | - |
|
|
231
258
|
|
|
232
259
|
### Provider API Keys
|
|
233
260
|
|
|
@@ -106,6 +106,25 @@ COMPLIANCE_DIR=../ai-protocol/tests/compliance pytest tests/compliance/ -v
|
|
|
106
106
|
|
|
107
107
|
For details, see [CROSS_RUNTIME.md](https://github.com/hiddenpath/ai-protocol/blob/main/docs/CROSS_RUNTIME.md).
|
|
108
108
|
|
|
109
|
+
### Testing with ai-protocol-mock
|
|
110
|
+
|
|
111
|
+
For integration and MCP e2e tests without real API calls, use [ai-protocol-mock](https://github.com/hiddenpath/ai-protocol-mock):
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Start mock server (from ai-protocol-mock repo)
|
|
115
|
+
docker-compose up -d
|
|
116
|
+
|
|
117
|
+
# Run tests with mock
|
|
118
|
+
MOCK_HTTP_URL=http://localhost:4010 MOCK_MCP_URL=http://localhost:4010/mcp pytest tests/ -v
|
|
119
|
+
|
|
120
|
+
# Run only mock E2E tests (chat, streaming, tools, MCP)
|
|
121
|
+
MOCK_HTTP_URL=http://localhost:4010 pytest tests/integration/test_mock_chat_e2e.py tests/integration/test_mcp_bridge_e2e.py -v
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
With proxy: set `NO_PROXY` to include the mock server IP (e.g. `NO_PROXY=192.168.2.13,localhost,127.0.0.1`).
|
|
125
|
+
|
|
126
|
+
Or in code: `AiClient.create("openai/gpt-4o", base_url="http://localhost:4010")`
|
|
127
|
+
|
|
109
128
|
## 📦 Installation
|
|
110
129
|
|
|
111
130
|
```bash
|
|
@@ -125,6 +144,9 @@ pip install ai-lib-python[embeddings] # Embedding generation
|
|
|
125
144
|
pip install ai-lib-python[structured] # Structured output / JSON mode
|
|
126
145
|
pip install ai-lib-python[batch] # Batch processing
|
|
127
146
|
pip install ai-lib-python[agentic] # Agent workflow support
|
|
147
|
+
pip install ai-lib-python[stt] # Speech-to-Text (STT)
|
|
148
|
+
pip install ai-lib-python[tts] # Text-to-Speech (TTS)
|
|
149
|
+
pip install ai-lib-python[reranking] # Document reranking
|
|
128
150
|
|
|
129
151
|
# Infrastructure extras
|
|
130
152
|
pip install ai-lib-python[telemetry] # OpenTelemetry integration
|
|
@@ -158,6 +180,8 @@ Provider manifests are resolved in a backward-compatible order:
|
|
|
158
180
|
| `AI_LIB_RPS` | Rate limit (requests per second) | - |
|
|
159
181
|
| `AI_LIB_BREAKER_FAILURE_THRESHOLD` | Circuit breaker failure threshold | 5 |
|
|
160
182
|
| `AI_LIB_BREAKER_COOLDOWN_SECS` | Circuit breaker cooldown seconds | 30 |
|
|
183
|
+
| `MOCK_HTTP_URL` | Mock server URL for testing (ai-protocol-mock) | - |
|
|
184
|
+
| `MOCK_MCP_URL` | Mock MCP endpoint for testing | - |
|
|
161
185
|
|
|
162
186
|
### Provider API Keys
|
|
163
187
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ai-lib-python"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.2"
|
|
8
8
|
description = "Official Python Runtime for AI-Protocol - The canonical Pythonic implementation for unified AI model interaction"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT OR Apache-2.0"
|
|
@@ -43,6 +43,9 @@ embeddings = []
|
|
|
43
43
|
structured = []
|
|
44
44
|
batch = []
|
|
45
45
|
agentic = []
|
|
46
|
+
stt = []
|
|
47
|
+
tts = []
|
|
48
|
+
reranking = []
|
|
46
49
|
|
|
47
50
|
# Infrastructure extras
|
|
48
51
|
telemetry = [
|
|
@@ -56,7 +59,7 @@ tokenizer = ["tiktoken>=0.5"]
|
|
|
56
59
|
full = [
|
|
57
60
|
"watchdog>=3.0",
|
|
58
61
|
"keyring>=24.0",
|
|
59
|
-
"ai-lib-python[vision,audio,embeddings,structured,batch,agentic,telemetry,tokenizer]",
|
|
62
|
+
"ai-lib-python[vision,audio,embeddings,structured,batch,agentic,stt,tts,reranking,telemetry,tokenizer]",
|
|
60
63
|
]
|
|
61
64
|
jupyter = ["ipywidgets>=8.0"]
|
|
62
65
|
dev = [
|
|
@@ -143,6 +146,10 @@ ignore = [
|
|
|
143
146
|
"SIM105", # contextlib.suppress doesn't work with yield
|
|
144
147
|
"ARG001", # unused function argument (reserved for future)
|
|
145
148
|
"ARG003", # unused class method argument (reserved for future)
|
|
149
|
+
"RUF002", # allow bilingual module headers with full-width punctuation
|
|
150
|
+
"RUF012", # defer mutable class-default cleanup to focused refactor
|
|
151
|
+
"RUF022", # keep curated __all__ export order by domain grouping
|
|
152
|
+
"TC001", # keep runtime imports explicit in legacy driver modules
|
|
146
153
|
]
|
|
147
154
|
|
|
148
155
|
[tool.ruff.lint.isort]
|
|
@@ -167,3 +174,4 @@ exclude_lines = [
|
|
|
167
174
|
"if __name__ == .__main__.:",
|
|
168
175
|
"@abstractmethod",
|
|
169
176
|
]
|
|
177
|
+
|
|
@@ -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):
|
|
@@ -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",
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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:
|
|
@@ -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):
|
|
@@ -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
|
|
|
@@ -6,7 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
8
|
from enum import Enum
|
|
9
|
-
from typing import TYPE_CHECKING
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
12
|
from collections.abc import Callable
|
|
@@ -29,14 +29,14 @@ class GuardrailViolation:
|
|
|
29
29
|
message: str
|
|
30
30
|
severity: GuardrailSeverity
|
|
31
31
|
matched_text: str | None = None
|
|
32
|
-
metadata: dict = field(default_factory=dict)
|
|
32
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
33
33
|
|
|
34
34
|
def __post_init__(self) -> None:
|
|
35
35
|
"""Validate severity."""
|
|
36
36
|
if not isinstance(self.severity, GuardrailSeverity):
|
|
37
37
|
self.severity = GuardrailSeverity(self.severity)
|
|
38
38
|
|
|
39
|
-
def to_dict(self) -> dict:
|
|
39
|
+
def to_dict(self) -> dict[str, Any]:
|
|
40
40
|
"""Convert violation to dictionary."""
|
|
41
41
|
return {
|
|
42
42
|
"rule_id": self.rule_id,
|
|
@@ -54,10 +54,10 @@ class GuardrailResult:
|
|
|
54
54
|
is_safe: bool
|
|
55
55
|
violations: list[GuardrailViolation] = field(default_factory=list)
|
|
56
56
|
filtered_content: str | None = None
|
|
57
|
-
metadata: dict = field(default_factory=dict)
|
|
57
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
58
58
|
|
|
59
59
|
@classmethod
|
|
60
|
-
def safe(cls, content: str | None = None) ->
|
|
60
|
+
def safe(cls, content: str | None = None) -> GuardrailResult:
|
|
61
61
|
"""Create a safe result."""
|
|
62
62
|
return cls(is_safe=True, violations=[], filtered_content=content)
|
|
63
63
|
|
|
@@ -66,7 +66,7 @@ class GuardrailResult:
|
|
|
66
66
|
cls,
|
|
67
67
|
violations: list[GuardrailViolation],
|
|
68
68
|
filtered_content: str | None = None,
|
|
69
|
-
) ->
|
|
69
|
+
) -> GuardrailResult:
|
|
70
70
|
"""Create a violated result."""
|
|
71
71
|
return cls(
|
|
72
72
|
is_safe=False,
|
|
@@ -74,7 +74,7 @@ class GuardrailResult:
|
|
|
74
74
|
filtered_content=filtered_content,
|
|
75
75
|
)
|
|
76
76
|
|
|
77
|
-
def to_dict(self) -> dict:
|
|
77
|
+
def to_dict(self) -> dict[str, Any]:
|
|
78
78
|
"""Convert result to dictionary."""
|
|
79
79
|
return {
|
|
80
80
|
"is_safe": self.is_safe,
|
|
@@ -303,7 +303,7 @@ class ConditionalGuardrail(Guardrail):
|
|
|
303
303
|
self,
|
|
304
304
|
rule_id: str,
|
|
305
305
|
guardrail: Guardrail,
|
|
306
|
-
condition: Callable[[dict], bool],
|
|
306
|
+
condition: Callable[[dict[str, Any]], bool],
|
|
307
307
|
severity: GuardrailSeverity = GuardrailSeverity.WARNING,
|
|
308
308
|
) -> None:
|
|
309
309
|
"""Initialize conditional guardrail.
|
|
@@ -317,9 +317,9 @@ class ConditionalGuardrail(Guardrail):
|
|
|
317
317
|
super().__init__(rule_id, severity)
|
|
318
318
|
self._guardrail = guardrail
|
|
319
319
|
self._condition = condition
|
|
320
|
-
self._context: dict = {}
|
|
320
|
+
self._context: dict[str, Any] = {}
|
|
321
321
|
|
|
322
|
-
def set_context(self, context: dict) -> None:
|
|
322
|
+
def set_context(self, context: dict[str, Any]) -> None:
|
|
323
323
|
"""Set the context for condition evaluation."""
|
|
324
324
|
self._context = context
|
|
325
325
|
|