ai-lib-python 0.7.0__py3-none-any.whl → 0.8.2__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 (54) hide show
  1. ai_lib_python/__init__.py +1 -1
  2. ai_lib_python/batch/collector.py +4 -1
  3. ai_lib_python/cache/manager.py +1 -0
  4. ai_lib_python/client/__init__.py +2 -2
  5. ai_lib_python/client/builder.py +33 -0
  6. ai_lib_python/client/cancel.py +7 -3
  7. ai_lib_python/client/core.py +97 -14
  8. ai_lib_python/computer_use/__init__.py +0 -1
  9. ai_lib_python/drivers/anthropic.py +10 -0
  10. ai_lib_python/drivers/gemini.py +11 -1
  11. ai_lib_python/drivers/openai.py +16 -12
  12. ai_lib_python/embeddings/client.py +9 -5
  13. ai_lib_python/guardrails/__init__.py +3 -3
  14. ai_lib_python/guardrails/base.py +10 -10
  15. ai_lib_python/guardrails/filters.py +28 -50
  16. ai_lib_python/guardrails/validators.py +12 -12
  17. ai_lib_python/multimodal/__init__.py +5 -0
  18. ai_lib_python/pipeline/accumulate.py +2 -2
  19. ai_lib_python/pipeline/base.py +3 -3
  20. ai_lib_python/pipeline/event_map.py +1 -1
  21. ai_lib_python/pipeline/fan_out.py +3 -3
  22. ai_lib_python/pipeline/select.py +1 -1
  23. ai_lib_python/protocol/loader.py +18 -6
  24. ai_lib_python/protocol/manifest.py +3 -1
  25. ai_lib_python/protocol/v2/capabilities.py +27 -1
  26. ai_lib_python/protocol/v2/manifest.py +45 -9
  27. ai_lib_python/registry/__init__.py +1 -2
  28. ai_lib_python/rerank/__init__.py +20 -0
  29. ai_lib_python/rerank/client.py +144 -0
  30. ai_lib_python/resilience/executor.py +9 -5
  31. ai_lib_python/resilience/fallback.py +3 -3
  32. ai_lib_python/resilience/preflight.py +15 -27
  33. ai_lib_python/resilience/retry.py +5 -4
  34. ai_lib_python/resilience/signals.py +7 -7
  35. ai_lib_python/structured/json_mode.py +6 -2
  36. ai_lib_python/structured/schema.py +10 -4
  37. ai_lib_python/structured/validator.py +18 -8
  38. ai_lib_python/stt/__init__.py +22 -0
  39. ai_lib_python/stt/client.py +171 -0
  40. ai_lib_python/telemetry/health.py +1 -1
  41. ai_lib_python/telemetry/logger.py +4 -1
  42. ai_lib_python/transport/auth.py +2 -2
  43. ai_lib_python/transport/http.py +22 -3
  44. ai_lib_python/transport/pool.py +1 -0
  45. ai_lib_python/tts/__init__.py +22 -0
  46. ai_lib_python/tts/client.py +164 -0
  47. ai_lib_python/types/message.py +25 -5
  48. ai_lib_python/types/tool.py +19 -1
  49. {ai_lib_python-0.7.0.dist-info → ai_lib_python-0.8.2.dist-info}/METADATA +28 -1
  50. ai_lib_python-0.8.2.dist-info/RECORD +103 -0
  51. ai_lib_python-0.7.0.dist-info/RECORD +0 -97
  52. {ai_lib_python-0.7.0.dist-info → ai_lib_python-0.8.2.dist-info}/WHEEL +0 -0
  53. {ai_lib_python-0.7.0.dist-info → ai_lib_python-0.8.2.dist-info}/licenses/LICENSE-APACHE +0 -0
  54. {ai_lib_python-0.7.0.dist-info → ai_lib_python-0.8.2.dist-info}/licenses/LICENSE-MIT +0 -0
ai_lib_python/__init__.py CHANGED
@@ -27,7 +27,7 @@ from ai_lib_python.types.message import (
27
27
  )
28
28
  from ai_lib_python.types.tool import ToolCall, ToolDefinition
29
29
 
30
- __version__ = "0.7.0"
30
+ __version__ = "0.7.5"
31
31
 
32
32
  __all__ = [
33
33
  # Client
@@ -191,7 +191,10 @@ class BatchCollector(Generic[T, R]):
191
191
 
192
192
  try:
193
193
  # Execute batch
194
- results = await self._executor(data_list)
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):
@@ -130,6 +130,7 @@ class CacheManager:
130
130
  self._key_generator = CacheKeyGenerator()
131
131
  self._stats = CacheStats()
132
132
 
133
+ self._backend: CacheBackend
133
134
  if not self._config.enabled:
134
135
  self._backend = NullCache()
135
136
  else:
@@ -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(self._timeout) # type: ignore
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) # type: ignore # noqa: RUF006
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) # type: ignore # noqa: RUF006
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=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
- async def do_request() -> ChatResponse:
215
- payload = builder.build_payload()
216
- endpoint = self._manifest.get_chat_endpoint()
227
+ models_to_try = [self._model_id, *self._fallbacks]
228
+ last_error = None
217
229
 
218
- response = await self._transport.post(endpoint, json=payload)
219
- data = response.json()
220
-
221
- return self._parse_response(data)
222
-
223
- # Use executor if available for resilience
224
- if self._executor:
225
- return await self._executor.execute(do_request)
226
- return await do_request()
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
@@ -14,7 +14,6 @@ from enum import Enum
14
14
  from typing import Any
15
15
  from urllib.parse import urlparse
16
16
 
17
-
18
17
  # ─── Normalized Action Types ────────────────────────────────────────────────
19
18
 
20
19
 
@@ -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
- model: str,
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 ContentBlock, Message
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
- return {"role": role, "content": m.content}
124
- # list[ContentBlock] → OpenAI content array
125
- blocks = []
126
- for b in m.content:
127
- if b.type == "text":
128
- blocks.append({"type": "text", "text": b.text})
129
- elif b.type == "image":
130
- blocks.append(b.model_dump(by_alias=True))
131
- else:
132
- blocks.append(b.model_dump(by_alias=True))
133
- return {"role": role, "content": blocks}
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
- # Try to get from manifest, default to OpenAI-style
197
- if hasattr(self._manifest, "embedding_endpoint"):
198
- return self._manifest.embedding_endpoint
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.from_manifest(
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, GuardrailViolation, GuardrailResult
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) -> "GuardrailResult":
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
- ) -> "GuardrailResult":
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
 
@@ -5,14 +5,13 @@ Concrete filter implementations for common guardrail use cases.
5
5
  from __future__ import annotations
6
6
 
7
7
  import re
8
- import string
9
8
  from typing import TYPE_CHECKING
10
9
 
11
10
  from ai_lib_python.guardrails.base import (
12
- CompositeGuardrail,
13
11
  Guardrail,
14
12
  GuardrailResult,
15
13
  GuardrailSeverity,
14
+ GuardrailViolation,
16
15
  )
17
16
 
18
17
  if TYPE_CHECKING:
@@ -121,19 +120,13 @@ class KeywordFilter(Guardrail):
121
120
  self,
122
121
  message: str,
123
122
  matched_text: str,
124
- ) -> "GuardrailResult":
125
- """Create a violation result."""
126
- from ai_lib_python.guardrails.base import GuardrailViolation
127
-
128
- return GuardrailResult.violated(
129
- [
130
- GuardrailViolation(
131
- rule_id=self._rule_id,
132
- message=message,
133
- severity=self._severity,
134
- matched_text=matched_text,
135
- )
136
- ]
123
+ ) -> GuardrailViolation:
124
+ """Create a violation."""
125
+ return GuardrailViolation(
126
+ rule_id=self._rule_id,
127
+ message=message,
128
+ severity=self._severity,
129
+ matched_text=matched_text,
137
130
  )
138
131
 
139
132
 
@@ -176,7 +169,7 @@ class RegexFilter(Guardrail):
176
169
  self._message = message
177
170
 
178
171
  @property
179
- def pattern(self) -> re.Pattern:
172
+ def pattern(self) -> re.Pattern[str]:
180
173
  """Get the compiled pattern."""
181
174
  return self._pattern
182
175
 
@@ -253,6 +246,7 @@ class LengthFilter(Guardrail):
253
246
  self._min_length = min_length
254
247
  self._max_length = max_length
255
248
  self._count_mode = count_mode
249
+ self._counter: Callable[[str], int]
256
250
 
257
251
  if count_mode == "chars":
258
252
  self._counter = len
@@ -273,8 +267,6 @@ class LengthFilter(Guardrail):
273
267
  violations = []
274
268
 
275
269
  if self._min_length is not None and length < self._min_length:
276
- from ai_lib_python.guardrails.base import GuardrailViolation
277
-
278
270
  violations.append(
279
271
  GuardrailViolation(
280
272
  rule_id=self._rule_id,
@@ -284,8 +276,6 @@ class LengthFilter(Guardrail):
284
276
  )
285
277
 
286
278
  if self._max_length is not None and length > self._max_length:
287
- from ai_lib_python.guardrails.base import GuardrailViolation
288
-
289
279
  violations.append(
290
280
  GuardrailViolation(
291
281
  rule_id=self._rule_id,
@@ -352,12 +342,12 @@ class ProfanityFilter(Guardrail):
352
342
  # Find the actual matched text (preserving case)
353
343
  pattern = re.escape(keyword)
354
344
  if not self._case_sensitive:
355
- pattern = re.compile(pattern, re.IGNORECASE)
345
+ pattern_obj = re.compile(pattern, re.IGNORECASE)
346
+ else:
347
+ pattern_obj = re.compile(pattern)
356
348
 
357
- match = pattern.search(content)
349
+ match = pattern_obj.search(content)
358
350
  if match:
359
- from ai_lib_python.guardrails.base import GuardrailViolation
360
-
361
351
  violations.append(
362
352
  GuardrailViolation(
363
353
  rule_id=self._rule_id,
@@ -463,19 +453,13 @@ class UrlFilter(Guardrail):
463
453
 
464
454
  return GuardrailResult.safe(content=content)
465
455
 
466
- def _create_url_violation(self, message: str, url: str) -> "GuardrailResult":
467
- """Create a URL violation result."""
468
- from ai_lib_python.guardrails.base import GuardrailViolation
469
-
470
- return GuardrailResult.violated(
471
- [
472
- GuardrailViolation(
473
- rule_id=self._rule_id,
474
- message=message,
475
- severity=self._severity,
476
- matched_text=url,
477
- )
478
- ]
456
+ def _create_url_violation(self, message: str, url: str) -> GuardrailViolation:
457
+ """Create a URL violation."""
458
+ return GuardrailViolation(
459
+ rule_id=self._rule_id,
460
+ message=message,
461
+ severity=self._severity,
462
+ matched_text=url,
479
463
  )
480
464
 
481
465
 
@@ -567,17 +551,11 @@ class EmailFilter(Guardrail):
567
551
  """Replace email addresses."""
568
552
  return self._EMAIL_PATTERN.sub(self._replacement, content)
569
553
 
570
- def _create_email_violation(self, message: str, email: str) -> "GuardrailResult":
571
- """Create an email violation result."""
572
- from ai_lib_python.guardrails.base import GuardrailViolation
573
-
574
- return GuardrailResult.violated(
575
- [
576
- GuardrailViolation(
577
- rule_id=self._rule_id,
578
- message=message,
579
- severity=self._severity,
580
- matched_text=email,
581
- )
582
- ]
554
+ def _create_email_violation(self, message: str, email: str) -> GuardrailViolation:
555
+ """Create an email violation."""
556
+ return GuardrailViolation(
557
+ rule_id=self._rule_id,
558
+ message=message,
559
+ severity=self._severity,
560
+ matched_text=email,
583
561
  )