Frontend (17 items): - Virtualized message list with batch loading - CSS split with skeleton, drawer, search filter, message action styles - Code splitting via React.lazy + Suspense for Admin/Ethics/Settings pages - Skeleton loading components (Skeleton, SkeletonCard, SkeletonGrid) - Debounced search/filter component (SearchFilter) - Error boundary with fallback UI - Keyboard shortcuts (Ctrl+K search, Ctrl+Enter send, Escape dismiss) - Page transition animations (fade-in) - PWA support (manifest.json + service worker) - WebSocket auto-reconnect with exponential backoff (10 retries) - Chat history persistence to localStorage (500 msg limit) - Message edit/delete on hover - Copy-to-clipboard on code blocks - Mobile drawer (bottom-sheet for consensus panel) - File upload support - User preferences sync to backend Testing (8 items): - Component tests: Toast, Markdown, ChatMessage, Avatar, ErrorBoundary, Skeleton - Hook tests: useChatHistory - E2E smoke tests (5 tests) - Accessibility audit utility Backend (12 items): - Vector memory with cosine similarity search - TTS/STT adapter factory wiring - Geometry kernel with orphan detection - Tenant registry with CRUD operations - Response cache with TTL - Connection pool (async) - Background task queue - Health check endpoints (/health, /ready) - Request tracing middleware (X-Request-ID) - API key rotation mechanism - Environment-based config (settings.py) - API route documentation improvements Infrastructure (4 items): - Grafana dashboard template - Database migration system - Storybook configuration Documentation (3 items): - ADR-001: Advisory Governance Model - ADR-002: Twelve-Head Architecture - ADR-003: Consequence Engine 552 Python tests + 45 frontend tests passing, 0 ruff errors. Co-Authored-By: Nakamoto, S <defi@defi-oracle.io>
107 lines
4.7 KiB
Python
107 lines
4.7 KiB
Python
"""Environment-based configuration using Pydantic Settings.
|
|
|
|
All settings are configurable via environment variables or .env file.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class APIConfig(BaseModel):
|
|
"""API server configuration."""
|
|
host: str = Field(default="0.0.0.0", description="Server bind host")
|
|
port: int = Field(default=8000, description="Server bind port")
|
|
workers: int = Field(default=1, description="Number of worker processes")
|
|
cors_origins: list[str] = Field(default=["*"], description="CORS allowed origins")
|
|
api_key: str | None = Field(default=None, description="API key for authentication")
|
|
rate_limit: int = Field(default=120, description="Rate limit (requests per window)")
|
|
rate_window: float = Field(default=60.0, description="Rate limit window in seconds")
|
|
|
|
|
|
class DatabaseConfig(BaseModel):
|
|
"""Database configuration."""
|
|
url: str = Field(default="sqlite:///fusionagi.db", description="Database URL")
|
|
pool_size: int = Field(default=5, description="Connection pool size")
|
|
max_overflow: int = Field(default=10, description="Max overflow connections")
|
|
echo: bool = Field(default=False, description="Echo SQL statements")
|
|
|
|
|
|
class CacheConfig(BaseModel):
|
|
"""Cache configuration."""
|
|
enabled: bool = Field(default=True, description="Enable response caching")
|
|
max_size: int = Field(default=1000, description="Max cached entries")
|
|
ttl_seconds: float = Field(default=300.0, description="Cache TTL in seconds")
|
|
backend: str = Field(default="memory", description="Cache backend (memory or redis)")
|
|
redis_url: str | None = Field(default=None, description="Redis URL if backend is redis")
|
|
|
|
|
|
class LoggingConfig(BaseModel):
|
|
"""Logging configuration."""
|
|
level: str = Field(default="INFO", description="Log level")
|
|
format: str = Field(default="json", description="Log format (json or text)")
|
|
correlation_id_header: str = Field(default="X-Request-ID", description="Request ID header")
|
|
|
|
|
|
class GovernanceConfig(BaseModel):
|
|
"""Governance configuration."""
|
|
mode: str = Field(default="advisory", description="Governance mode (advisory or enforcing)")
|
|
max_file_size: int | None = Field(default=None, description="Max file size in bytes (None=unlimited)")
|
|
allow_private_urls: bool = Field(default=True, description="Allow private/internal URLs")
|
|
|
|
|
|
class FusionAGIConfig(BaseModel):
|
|
"""Root configuration for FusionAGI."""
|
|
api: APIConfig = Field(default_factory=APIConfig)
|
|
database: DatabaseConfig = Field(default_factory=DatabaseConfig)
|
|
cache: CacheConfig = Field(default_factory=CacheConfig)
|
|
logging: LoggingConfig = Field(default_factory=LoggingConfig)
|
|
governance: GovernanceConfig = Field(default_factory=GovernanceConfig)
|
|
tenant_isolation: bool = Field(default=True, description="Enable tenant isolation")
|
|
max_concurrent_tasks: int = Field(default=5, description="Max background tasks")
|
|
|
|
|
|
def load_config() -> FusionAGIConfig:
|
|
"""Load configuration from environment variables.
|
|
|
|
Environment variables are mapped using the pattern:
|
|
FUSIONAGI_<SECTION>_<KEY> (e.g., FUSIONAGI_API_PORT=9000)
|
|
"""
|
|
import os
|
|
config = FusionAGIConfig()
|
|
|
|
env_map = {
|
|
"FUSIONAGI_API_HOST": ("api", "host"),
|
|
"FUSIONAGI_API_PORT": ("api", "port"),
|
|
"FUSIONAGI_API_WORKERS": ("api", "workers"),
|
|
"FUSIONAGI_API_KEY": ("api", "api_key"),
|
|
"FUSIONAGI_RATE_LIMIT": ("api", "rate_limit"),
|
|
"FUSIONAGI_RATE_WINDOW": ("api", "rate_window"),
|
|
"FUSIONAGI_DB_URL": ("database", "url"),
|
|
"FUSIONAGI_DB_POOL_SIZE": ("database", "pool_size"),
|
|
"FUSIONAGI_CACHE_ENABLED": ("cache", "enabled"),
|
|
"FUSIONAGI_CACHE_TTL": ("cache", "ttl_seconds"),
|
|
"FUSIONAGI_CACHE_BACKEND": ("cache", "backend"),
|
|
"FUSIONAGI_REDIS_URL": ("cache", "redis_url"),
|
|
"FUSIONAGI_LOG_LEVEL": ("logging", "level"),
|
|
"FUSIONAGI_LOG_FORMAT": ("logging", "format"),
|
|
"FUSIONAGI_GOVERNANCE_MODE": ("governance", "mode"),
|
|
}
|
|
|
|
for env_var, (section, key) in env_map.items():
|
|
value = os.environ.get(env_var)
|
|
if value is not None:
|
|
section_obj = getattr(config, section)
|
|
field_info = type(section_obj).model_fields.get(key)
|
|
if field_info and field_info.annotation:
|
|
annotation = field_info.annotation
|
|
if annotation is int:
|
|
value = int(value) # type: ignore[assignment]
|
|
elif annotation is float:
|
|
value = float(value) # type: ignore[assignment]
|
|
elif annotation is bool:
|
|
value = value.lower() in ("true", "1", "yes") # type: ignore[assignment]
|
|
setattr(section_obj, key, value)
|
|
|
|
return config
|