quick commit
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Failing after 6m9s
CI/CD Pipeline / Security Scanning (push) Successful in 26s
CI/CD Pipeline / Tests (3.11) (push) Failing after 5m24s
CI/CD Pipeline / Tests (3.12) (push) Failing after 5m23s
CI/CD Pipeline / Build Docker Image (push) Has been skipped
CI/CD Pipeline / Deploy to Staging (push) Has been skipped
CI/CD Pipeline / Deploy to Production (push) Has been skipped
CI/CD Pipeline / Notification (push) Successful in 1s
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Failing after 6m9s
CI/CD Pipeline / Security Scanning (push) Successful in 26s
CI/CD Pipeline / Tests (3.11) (push) Failing after 5m24s
CI/CD Pipeline / Tests (3.12) (push) Failing after 5m23s
CI/CD Pipeline / Build Docker Image (push) Has been skipped
CI/CD Pipeline / Deploy to Staging (push) Has been skipped
CI/CD Pipeline / Deploy to Production (push) Has been skipped
CI/CD Pipeline / Notification (push) Successful in 1s
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
"""Base classes for AI providers."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Literal
|
||||
from typing import Literal, TypeVar
|
||||
|
||||
|
||||
class ContentCategory(str, Enum):
|
||||
@@ -20,6 +23,64 @@ class ContentCategory(str, Enum):
|
||||
MISINFORMATION = "misinformation"
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RetryConfig:
|
||||
"""Retry configuration for AI calls."""
|
||||
|
||||
retries: int = 3
|
||||
base_delay: float = 0.25
|
||||
max_delay: float = 2.0
|
||||
|
||||
|
||||
def parse_categories(values: list[str]) -> list[ContentCategory]:
|
||||
"""Parse category values into ContentCategory enums."""
|
||||
categories: list[ContentCategory] = []
|
||||
for value in values:
|
||||
try:
|
||||
categories.append(ContentCategory(value))
|
||||
except ValueError:
|
||||
continue
|
||||
return categories
|
||||
|
||||
|
||||
async def run_with_retries(
|
||||
operation: Callable[[], Awaitable[_T]],
|
||||
*,
|
||||
config: RetryConfig | None = None,
|
||||
logger: logging.Logger | None = None,
|
||||
operation_name: str = "AI call",
|
||||
) -> _T:
|
||||
"""Run an async operation with retries and backoff."""
|
||||
retry_config = config or RetryConfig()
|
||||
delay = retry_config.base_delay
|
||||
last_error: Exception | None = None
|
||||
|
||||
for attempt in range(1, retry_config.retries + 1):
|
||||
try:
|
||||
return await operation()
|
||||
except Exception as error: # noqa: BLE001 - we re-raise after retries
|
||||
last_error = error
|
||||
if attempt >= retry_config.retries:
|
||||
raise
|
||||
if logger:
|
||||
logger.warning(
|
||||
"%s failed (attempt %s/%s): %s",
|
||||
operation_name,
|
||||
attempt,
|
||||
retry_config.retries,
|
||||
error,
|
||||
)
|
||||
await asyncio.sleep(delay)
|
||||
delay = min(retry_config.max_delay, delay * 2)
|
||||
|
||||
if last_error:
|
||||
raise last_error
|
||||
raise RuntimeError("Retry loop exited unexpectedly")
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModerationResult:
|
||||
"""Result of AI content moderation."""
|
||||
|
||||
Reference in New Issue
Block a user