AI Utilities follows a layered architecture with clear separation of concerns:
┌─────────────────────────────────────────┐
│ User API │
│ AiClient, AsyncAiClient, convenience │
├─────────────────────────────────────────┤
│ Configuration │
│ AiSettings, config resolver, env vars │
├─────────────────────────────────────────┤
│ Provider Layer │
│ OpenAI, Groq, Ollama, etc. │
├─────────────────────────────────────────┤
│ Infrastructure │
│ Caching, rate limiting, error handling │
├─────────────────────────────────────────┤
│ Utilities │
│ Audio processing, files, monitoring │
└─────────────────────────────────────────┘
Purpose: Main user-facing API
Key Classes:
AiClient- Synchronous clientAsyncAiClient- Asynchronous clientcreate_client()- Convenience function
Responsibilities:
- Provide simple API for AI interactions
- Handle configuration loading
- Coordinate provider selection
- Manage error handling and retries
Purpose: Manage all configuration and settings
Key Classes:
AiSettings- Pydantic-based settings modelConfigurationResolver- Resolve and validate configuration
Features:
- Environment variable loading
.envfile support- Validation and type checking
- Provider-specific defaults
Purpose: Abstract different AI providers behind common interface
Key Classes:
BaseProvider- Abstract base classOpenAIProvider- OpenAI implementationGroqProvider- Groq implementationOllamaProvider- Local Ollama support
Interface:
class BaseProvider:
def ask(self, prompt: str, **kwargs) -> AskResult
def ask_batch(self, prompts: List[str], **kwargs) -> List[AskResult]
def ask_stream(self, prompt: str, **kwargs) -> Iterator[str]
def list_models(self) -> List[str]Purpose: Reduce API costs and improve response times
Backends:
MemoryCache- In-memory cachingSQLiteCache- Persistent file-based cachingRedisCache- Distributed caching
Features:
- TTL support
- Namespace isolation
- Cache statistics
- Automatic cleanup
Purpose: Interactive configuration wizard
Components:
SetupWizard- Main wizard logic- CLI interface (
cli.py) - Provider-specific setup guidance
1. User calls client.ask("question")
2. Client loads/validates configuration
3. Provider factory creates appropriate provider
4. Cache layer checks for cached response
5. If cache miss: Provider makes API call
6. Response is cached (if enabled)
7. Response is returned to user
1. AiSettings() is called
2. Pydantic loads environment variables
3. .env file is loaded (if exists)
4. Defaults are applied
5. Validation occurs
6. Settings are ready for use
Used for provider creation:
def create_provider(settings: AiSettings, provider: str = None) -> BaseProvider:
"""Create provider instance based on settings."""
provider_name = provider or settings.provider
if provider_name == "openai":
return OpenAIProvider(settings)
elif provider_name == "groq":
return GroqProvider(settings)
# ... other providersUsed for different caching backends:
class CacheBackend:
def get(self, key: str) -> Optional[Any]: pass
def set(self, key: str, value: Any, ttl: int) -> None: pass
def delete(self, key: str) -> None: pass
class MemoryCache(CacheBackend): ...
class SQLiteCache(CacheBackend): ...
class RedisCache(CacheBackend): ...Used for usage tracking:
class UsageTracker:
def track_request(self, usage: UsageData) -> None:
# Track usage statistics
pass
class AiClient:
def __init__(self, usage_tracker: UsageTracker = None):
self.usage_tracker = usage_tracker or create_usage_tracker()
def ask(self, prompt: str, **kwargs):
result = self.provider.ask(prompt, **kwargs)
self.usage_tracker.track_request(result.usage)
return resultclass AIUtilitiesError(Exception):
"""Base exception for all AI Utilities errors."""
class ProviderError(AIUtilitiesError):
"""Base class for provider-related errors."""
class ProviderConfigurationError(ProviderError):
"""Raised when provider configuration is invalid."""
class ProviderAPIError(ProviderError):
"""Raised when provider API returns an error."""
class ProviderRateLimitError(ProviderAPIError):
"""Raised when rate limit is exceeded."""- Fail fast - Validate configuration early
- Clear messages - Provide actionable error information
- Recovery guidance - Suggest solutions in error messages
- Consistent types - Use specific exception types
class AiSettings(BaseSettings):
"""Configuration settings using Pydantic."""
# Core settings
provider: str = Field(default="openai")
api_key: Optional[str] = None
model: Optional[str] = Field(
default=None,
validation_alias=AliasChoices("AI_MODEL", "OPENAI_MODEL")
)
# Behavior settings
temperature: float = Field(default=0.7, ge=0.0, le=2.0)
max_tokens: Optional[int] = Field(default=None, gt=0)
# Infrastructure settings
cache_enabled: bool = False
cache_backend: str = "memory"
cache_ttl_s: int = 3600
class Config:
env_prefix = "AI_"
case_sensitive = False- Priority Order: Explicit params → Environment → .env → Defaults
- Validation: Pydantic validates all values
- Type Coercion: Automatic type conversion
- Error Messages: Clear validation errors
All providers implement the same interface:
from abc import ABC, abstractmethod
from typing import List, Iterator, Optional
class BaseProvider(ABC):
def __init__(self, settings: AiSettings):
self.settings = settings
self._configure_client()
@abstractmethod
def ask(self, prompt: str, **kwargs) -> AskResult:
"""Make a synchronous request."""
pass
def ask_stream(self, prompt: str, **kwargs) -> Iterator[str]:
"""Stream response (optional implementation)."""
raise NotImplementedError("Streaming not supported")
def list_models(self) -> List[str]:
"""List available models (optional implementation)."""
raise NotImplementedError("Model listing not supported")class OpenAIProvider(BaseProvider):
def __init__(self, settings: AiSettings):
super().__init__(settings)
self.client = openai.OpenAI(
api_key=settings.api_key,
base_url=settings.base_url
)
def ask(self, prompt: str, **kwargs) -> AskResult:
try:
response = self.client.chat.completions.create(
model=self.settings.model,
messages=[{"role": "user", "content": prompt}],
temperature=self.settings.temperature,
max_tokens=self.settings.max_tokens
)
return AskResult(
text=response.choices[0].message.content,
usage=UsageData(
prompt_tokens=response.usage.prompt_tokens,
completion_tokens=response.usage.completion_tokens,
total_tokens=response.usage.total_tokens
)
)
except openai.APIError as e:
raise ProviderAPIError(f"OpenAI API error: {e}")def _generate_cache_key(
prompt: str,
provider: str,
model: str,
temperature: float,
**kwargs
) -> str:
"""Generate deterministic cache key."""
import hashlib
key_data = {
"prompt": prompt,
"provider": provider,
"model": model,
"temperature": temperature,
"kwargs": sorted(kwargs.items())
}
key_string = json.dumps(key_data, sort_keys=True)
return hashlib.sha256(key_string.encode()).hexdigest()class CacheBackend(ABC):
def get(self, key: str) -> Optional[Any]: pass
def set(self, key: str, value: Any, ttl: int) -> None: pass
def clear(self) -> None: pass
def clear_namespace(self, namespace: str) -> None: pass
class SQLiteCache(CacheBackend):
def __init__(self, db_path: str, max_entries: int = 1000):
self.db_path = db_path
self.max_entries = max_entries
self._init_database()
def _init_database(self):
"""Initialize SQLite database with proper schema."""
with sqlite3.connect(self.db_path) as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS cache (
key TEXT PRIMARY KEY,
value TEXT,
namespace TEXT,
created_at TIMESTAMP,
expires_at TIMESTAMP
)
""")__init__.py- Public API exportsclient.py- Main client implementationasync_client.py- Async clientconfig_models.py- Configuration modelsconfig_resolver.py- Configuration resolutioncli.py- Command-line interface
__init__.py- Provider exports and factorybase_provider.py- Abstract base classopenai_provider.py- OpenAI implementationgroq_provider.py- Groq implementationollama_provider.py- Local Ollama support
cache/- Caching backendsusage/- Usage trackingaudio/- Audio processingfiles/- File operations
- Inherit from
BaseProvider - Implement required methods
- Register in provider factory
- Add configuration validation
- Add tests
- Inherit from
CacheBackend - Implement storage methods
- Add configuration options
- Add performance tests
- Follow existing patterns
- Add proper type hints
- Include comprehensive tests
- Update documentation
For testing details, see Testing Guide. For development setup, see Development Setup.