v1.0 - Version stable: multi-PC, détection UI-DETR-1, 3 modes exécution

- Frontend v4 accessible sur réseau local (192.168.1.40)
- Ports ouverts: 3002 (frontend), 5001 (backend), 5004 (dashboard)
- Ollama GPU fonctionnel
- Self-healing interactif
- Dashboard confiance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Dom
2026-01-29 11:23:51 +01:00
parent 21bfa3b337
commit a27b74cf22
1595 changed files with 412691 additions and 400 deletions

652
core/config.py Normal file
View File

@@ -0,0 +1,652 @@
"""
Configuration centralisée pour RPA Vision V3
Gestionnaire de configuration unifié qui élimine les incohérences entre composants.
Utilise les variables d'environnement avec des valeurs par défaut sensées.
En production, définir ENVIRONMENT=production pour forcer la configuration.
"""
import os
import logging
from pathlib import Path
from dataclasses import dataclass, field
from typing import Optional, List, Dict, Any, Callable
import json
from datetime import datetime
logger = logging.getLogger(__name__)
@dataclass
class ValidationError:
"""Erreur de validation de configuration"""
field: str
value: Any
message: str
severity: str = "error" # error, warning
@dataclass
class SystemConfig:
"""Configuration système unifiée - Point central de toute configuration"""
# Chemins unifiés
base_path: Path = field(default_factory=lambda: Path.cwd())
data_path: Path = field(default_factory=lambda: Path("data"))
logs_path: Path = field(default_factory=lambda: Path("logs"))
# Services
api_host: str = "0.0.0.0"
api_port: int = 8000
dashboard_host: str = "0.0.0.0"
dashboard_port: int = 5001
worker_threads: int = 4
# Base de données
sessions_path: Path = field(default_factory=lambda: Path("data/sessions"))
workflows_path: Path = field(default_factory=lambda: Path("data/workflows"))
embeddings_path: Path = field(default_factory=lambda: Path("data/embeddings"))
faiss_index_path: Path = field(default_factory=lambda: Path("data/faiss_index"))
screenshots_path: Path = field(default_factory=lambda: Path("data/screenshots"))
training_path: Path = field(default_factory=lambda: Path("data/training"))
uploads_path: Path = field(default_factory=lambda: Path("data/training/uploads"))
# Sécurité
secret_key: str = "dev_secret_key_not_for_production"
encryption_password: str = "dev_default_key_not_for_production"
auth_enabled: bool = True
allowed_origins: List[str] = field(default_factory=lambda: ["*"])
# Monitoring
health_check_interval: int = 30
metrics_enabled: bool = True
# Environment
environment: str = "development"
debug: bool = False
# Models
clip_model: str = "ViT-B-32"
clip_pretrained: str = "openai"
clip_device: str = "cpu"
vlm_model: str = "qwen3-vl:8b"
vlm_endpoint: str = "http://localhost:11434"
owl_model: str = "google/owlv2-base-patch16-ensemble"
owl_confidence_threshold: float = 0.1
# FAISS
faiss_dimensions: int = 512
faiss_index_type: str = "Flat"
faiss_metric: str = "cosine"
faiss_nprobe: int = 8
faiss_auto_optimize: bool = True
faiss_migration_threshold: int = 10000
# GPU
gpu_idle_timeout_seconds: int = 300
gpu_vram_threshold_mb: int = 1024
gpu_max_load_retries: int = 3
gpu_load_timeout_seconds: int = 30
gpu_unload_timeout_seconds: int = 5
def __post_init__(self):
"""Normalise les chemins après initialisation"""
# Convertir tous les chemins relatifs en absolus basés sur base_path
for field_name in self.__dataclass_fields__:
value = getattr(self, field_name)
if isinstance(value, Path) and not value.is_absolute():
if field_name != 'base_path':
setattr(self, field_name, self.base_path / value)
def ensure_directories(self):
"""Crée tous les répertoires nécessaires avec les bonnes permissions"""
directories = [
self.data_path, self.logs_path, self.sessions_path,
self.workflows_path, self.embeddings_path, self.faiss_index_path,
self.screenshots_path, self.training_path, self.uploads_path
]
for directory in directories:
try:
directory.mkdir(parents=True, exist_ok=True)
# Vérifier les permissions d'écriture
test_file = directory / ".write_test"
test_file.touch()
test_file.unlink()
logger.debug(f"Directory created/verified: {directory}")
except Exception as e:
logger.error(f"Failed to create/verify directory {directory}: {e}")
raise
class ConfigurationManager:
"""Gestionnaire centralisé de configuration - Point unique de vérité"""
def __init__(self):
self._config: Optional[SystemConfig] = None
self._config_watchers: List[Callable[[SystemConfig], None]] = []
self._last_loaded: Optional[datetime] = None
def load_config(self) -> SystemConfig:
"""Charge la configuration depuis les variables d'environnement et fichiers"""
try:
# Charger depuis les variables d'environnement
config = self._load_from_env()
# Charger depuis le fichier de config si présent
config_file = Path(".env")
if config_file.exists():
config = self._merge_from_file(config, config_file)
# Valider la configuration
validation_errors = self.validate_config(config)
if any(error.severity == "error" for error in validation_errors):
error_messages = [f"{error.field}: {error.message}"
for error in validation_errors if error.severity == "error"]
raise ValueError(f"Configuration validation failed: {'; '.join(error_messages)}")
# Afficher les warnings
warnings = [error for error in validation_errors if error.severity == "warning"]
for warning in warnings:
logger.warning(f"Configuration warning - {warning.field}: {warning.message}")
# Créer les répertoires
config.ensure_directories()
self._config = config
self._last_loaded = datetime.now()
# Notifier les watchers
for watcher in self._config_watchers:
try:
watcher(config)
except Exception as e:
logger.error(f"Configuration watcher failed: {e}")
logger.info(f"Configuration loaded successfully (environment: {config.environment})")
return config
except Exception as e:
logger.error(f"Failed to load configuration: {e}")
raise
def _load_from_env(self) -> SystemConfig:
"""Charge la configuration depuis les variables d'environnement"""
base_path = Path(os.getenv("BASE_PATH", Path.cwd()))
return SystemConfig(
# Chemins
base_path=base_path,
data_path=Path(os.getenv("DATA_PATH", "data")),
logs_path=Path(os.getenv("LOGS_PATH", "logs")),
sessions_path=Path(os.getenv("SESSIONS_PATH", "data/sessions")),
workflows_path=Path(os.getenv("WORKFLOWS_PATH", "data/workflows")),
embeddings_path=Path(os.getenv("EMBEDDINGS_PATH", "data/embeddings")),
faiss_index_path=Path(os.getenv("FAISS_INDEX_PATH", "data/faiss_index")),
screenshots_path=Path(os.getenv("SCREENSHOTS_PATH", "data/screenshots")),
training_path=Path(os.getenv("TRAINING_PATH", "data/training")),
uploads_path=Path(os.getenv("UPLOADS_PATH", "data/training/uploads")),
# Services
api_host=os.getenv("API_HOST", "0.0.0.0"),
api_port=int(os.getenv("API_PORT", "8000")),
dashboard_host=os.getenv("DASHBOARD_HOST", "0.0.0.0"),
dashboard_port=int(os.getenv("DASHBOARD_PORT", "5001")),
worker_threads=int(os.getenv("WORKER_THREADS", "4")),
# Sécurité
secret_key=os.getenv("SECRET_KEY", "dev_secret_key_not_for_production"),
encryption_password=os.getenv("ENCRYPTION_PASSWORD", "dev_default_key_not_for_production"),
auth_enabled=os.getenv("AUTH_ENABLED", "true").lower() == "true",
allowed_origins=os.getenv("ALLOWED_ORIGINS", "*").split(","),
# Monitoring
health_check_interval=int(os.getenv("HEALTH_CHECK_INTERVAL", "30")),
metrics_enabled=os.getenv("METRICS_ENABLED", "true").lower() == "true",
# Environment
environment=os.getenv("ENVIRONMENT", "development"),
debug=os.getenv("DEBUG", "false").lower() == "true",
# Models
clip_model=os.getenv("CLIP_MODEL", "ViT-B-32"),
clip_pretrained=os.getenv("CLIP_PRETRAINED", "openai"),
clip_device=os.getenv("CLIP_DEVICE", "cpu"),
vlm_model=os.getenv("VLM_MODEL", "qwen3-vl:8b"),
vlm_endpoint=os.getenv("VLM_ENDPOINT", "http://localhost:11434"),
owl_model=os.getenv("OWL_MODEL", "google/owlv2-base-patch16-ensemble"),
owl_confidence_threshold=float(os.getenv("OWL_CONFIDENCE_THRESHOLD", "0.1")),
# FAISS
faiss_dimensions=int(os.getenv("FAISS_DIMENSIONS", "512")),
faiss_index_type=os.getenv("FAISS_INDEX_TYPE", "Flat"),
faiss_metric=os.getenv("FAISS_METRIC", "cosine"),
faiss_nprobe=int(os.getenv("FAISS_NPROBE", "8")),
faiss_auto_optimize=os.getenv("FAISS_AUTO_OPTIMIZE", "true").lower() == "true",
faiss_migration_threshold=int(os.getenv("FAISS_MIGRATION_THRESHOLD", "10000")),
# GPU
gpu_idle_timeout_seconds=int(os.getenv("GPU_IDLE_TIMEOUT", "300")),
gpu_vram_threshold_mb=int(os.getenv("GPU_VRAM_THRESHOLD_MB", "1024")),
gpu_max_load_retries=int(os.getenv("GPU_MAX_LOAD_RETRIES", "3")),
gpu_load_timeout_seconds=int(os.getenv("GPU_LOAD_TIMEOUT", "30")),
gpu_unload_timeout_seconds=int(os.getenv("GPU_UNLOAD_TIMEOUT", "5"))
)
def _merge_from_file(self, config: SystemConfig, config_file: Path) -> SystemConfig:
"""Merge configuration from file (future enhancement)"""
# Pour l'instant, on utilise seulement les variables d'environnement
# Cette méthode peut être étendue pour supporter les fichiers JSON/YAML
return config
def validate_config(self, config: SystemConfig) -> List[ValidationError]:
"""Valide la cohérence de la configuration"""
errors = []
# Validation de l'environnement de production
if config.environment == "production":
if config.secret_key == "dev_secret_key_not_for_production":
errors.append(ValidationError(
"secret_key", config.secret_key,
"SECRET_KEY must be set in production environment"
))
if config.encryption_password == "dev_default_key_not_for_production":
errors.append(ValidationError(
"encryption_password", config.encryption_password,
"ENCRYPTION_PASSWORD must be set in production environment"
))
if config.debug:
errors.append(ValidationError(
"debug", config.debug,
"DEBUG should be false in production environment",
"warning"
))
# Validation des ports
if config.api_port == config.dashboard_port:
errors.append(ValidationError(
"ports", f"api:{config.api_port}, dashboard:{config.dashboard_port}",
"API and Dashboard ports must be different"
))
if not (1024 <= config.api_port <= 65535):
errors.append(ValidationError(
"api_port", config.api_port,
"API port must be between 1024 and 65535"
))
if not (1024 <= config.dashboard_port <= 65535):
errors.append(ValidationError(
"dashboard_port", config.dashboard_port,
"Dashboard port must be between 1024 and 65535"
))
# Validation des chemins
try:
config.base_path.resolve()
except Exception as e:
errors.append(ValidationError(
"base_path", config.base_path,
f"Invalid base path: {e}"
))
# Validation des modèles
if config.faiss_dimensions <= 0:
errors.append(ValidationError(
"faiss_dimensions", config.faiss_dimensions,
"FAISS dimensions must be positive"
))
if config.worker_threads <= 0:
errors.append(ValidationError(
"worker_threads", config.worker_threads,
"Worker threads must be positive"
))
# Validation des timeouts
if config.health_check_interval <= 0:
errors.append(ValidationError(
"health_check_interval", config.health_check_interval,
"Health check interval must be positive"
))
return errors
def apply_config(self, config: SystemConfig):
"""Applique une nouvelle configuration"""
# Valider d'abord
validation_errors = self.validate_config(config)
if any(error.severity == "error" for error in validation_errors):
error_messages = [f"{error.field}: {error.message}"
for error in validation_errors if error.severity == "error"]
raise ValueError(f"Configuration validation failed: {'; '.join(error_messages)}")
# Créer les répertoires
config.ensure_directories()
# Appliquer la configuration
old_config = self._config
self._config = config
self._last_loaded = datetime.now()
# Notifier les watchers du changement
for watcher in self._config_watchers:
try:
watcher(config)
except Exception as e:
logger.error(f"Configuration watcher failed during apply: {e}")
# En cas d'erreur, restaurer l'ancienne configuration
self._config = old_config
raise
logger.info("Configuration applied successfully")
def watch_config_changes(self, callback: Callable[[SystemConfig], None]):
"""Enregistre un callback pour les changements de configuration"""
self._config_watchers.append(callback)
# Si on a déjà une configuration, appeler immédiatement le callback
if self._config:
try:
callback(self._config)
except Exception as e:
logger.error(f"Configuration watcher failed during registration: {e}")
def get_config(self) -> SystemConfig:
"""Récupère la configuration actuelle"""
if self._config is None:
return self.load_config()
return self._config
def reload_config(self) -> SystemConfig:
"""Recharge la configuration depuis les sources"""
return self.load_config()
# Instance globale du gestionnaire de configuration
_config_manager: Optional[ConfigurationManager] = None
def get_configuration_manager() -> ConfigurationManager:
"""Récupère le gestionnaire de configuration global (singleton)"""
global _config_manager
if _config_manager is None:
_config_manager = ConfigurationManager()
return _config_manager
def get_config() -> SystemConfig:
"""Récupère la configuration système unifiée"""
return get_configuration_manager().get_config()
def reload_config() -> SystemConfig:
"""Recharge la configuration système"""
return get_configuration_manager().reload_config()
# Backward compatibility - Keep old classes for gradual migration
@dataclass
class ServerConfig:
"""Configuration du serveur - DEPRECATED: Use SystemConfig instead"""
api_host: str = "0.0.0.0"
api_port: int = 8000
dashboard_host: str = "0.0.0.0"
dashboard_port: int = 5001
environment: str = "development"
debug: bool = False
@classmethod
def from_env(cls) -> 'ServerConfig':
logger.warning("ServerConfig is deprecated. Use SystemConfig via get_config() instead.")
config = get_config()
return cls(
api_host=config.api_host,
api_port=config.api_port,
dashboard_host=config.dashboard_host,
dashboard_port=config.dashboard_port,
environment=config.environment,
debug=config.debug
)
@dataclass
class SecurityConfig:
"""Configuration de sécurité - DEPRECATED: Use SystemConfig instead"""
encryption_password: Optional[str] = None
secret_key: Optional[str] = None
allowed_origins: List[str] = field(default_factory=lambda: ["*"])
@classmethod
def from_env(cls, environment: str = "development") -> 'SecurityConfig':
logger.warning("SecurityConfig is deprecated. Use SystemConfig via get_config() instead.")
config = get_config()
return cls(
encryption_password=config.encryption_password,
secret_key=config.secret_key,
allowed_origins=config.allowed_origins
)
@dataclass
class ModelConfig:
"""Configuration des modèles ML - DEPRECATED: Use SystemConfig instead"""
clip_model: str = "ViT-B-32"
clip_pretrained: str = "openai"
clip_device: str = "cpu"
vlm_model: str = "qwen3-vl:8b"
vlm_endpoint: str = "http://localhost:11434"
owl_model: str = "google/owlv2-base-patch16-ensemble"
owl_confidence_threshold: float = 0.1
@classmethod
def from_env(cls) -> 'ModelConfig':
logger.warning("ModelConfig is deprecated. Use SystemConfig via get_config() instead.")
config = get_config()
return cls(
clip_model=config.clip_model,
clip_pretrained=config.clip_pretrained,
clip_device=config.clip_device,
vlm_model=config.vlm_model,
vlm_endpoint=config.vlm_endpoint,
owl_model=config.owl_model,
owl_confidence_threshold=config.owl_confidence_threshold
)
@dataclass
class PathConfig:
"""Configuration des chemins - DEPRECATED: Use SystemConfig instead"""
data_path: Path = field(default_factory=lambda: Path("data"))
models_path: Path = field(default_factory=lambda: Path("models"))
logs_path: Path = field(default_factory=lambda: Path("logs"))
uploads_path: Path = field(default_factory=lambda: Path("data/training/uploads"))
sessions_path: Path = field(default_factory=lambda: Path("data/training/sessions"))
@classmethod
def from_env(cls) -> 'PathConfig':
logger.warning("PathConfig is deprecated. Use SystemConfig via get_config() instead.")
config = get_config()
return cls(
data_path=config.data_path,
models_path=Path("models"), # Not in SystemConfig yet
logs_path=config.logs_path,
uploads_path=config.uploads_path,
sessions_path=config.sessions_path
)
def ensure_directories(self):
"""Crée tous les répertoires nécessaires"""
config = get_config()
config.ensure_directories()
@dataclass
class FAISSConfig:
"""Configuration FAISS - DEPRECATED: Use SystemConfig instead"""
dimensions: int = 512
index_type: str = "Flat"
metric: str = "cosine"
nprobe: int = 8
auto_optimize: bool = True
migration_threshold: int = 10000
@classmethod
def from_env(cls) -> 'FAISSConfig':
logger.warning("FAISSConfig is deprecated. Use SystemConfig via get_config() instead.")
config = get_config()
return cls(
dimensions=config.faiss_dimensions,
index_type=config.faiss_index_type,
metric=config.faiss_metric,
nprobe=config.faiss_nprobe,
auto_optimize=config.faiss_auto_optimize,
migration_threshold=config.faiss_migration_threshold
)
@dataclass
class GPUResourceConfig:
"""Configuration for GPU resource management - DEPRECATED: Use SystemConfig instead"""
ollama_endpoint: str = "http://localhost:11434"
vlm_model: str = "qwen3-vl:8b"
clip_model: str = "ViT-B-32"
idle_timeout_seconds: int = 300
vram_threshold_for_clip_gpu_mb: int = 1024
max_load_retries: int = 3
load_timeout_seconds: int = 30
unload_timeout_seconds: int = 5
@classmethod
def from_env(cls) -> 'GPUResourceConfig':
logger.warning("GPUResourceConfig is deprecated. Use SystemConfig via get_config() instead.")
config = get_config()
return cls(
ollama_endpoint=config.vlm_endpoint,
vlm_model=config.vlm_model,
clip_model=config.clip_model,
idle_timeout_seconds=config.gpu_idle_timeout_seconds,
vram_threshold_for_clip_gpu_mb=config.gpu_vram_threshold_mb,
max_load_retries=config.gpu_max_load_retries,
load_timeout_seconds=config.gpu_load_timeout_seconds,
unload_timeout_seconds=config.gpu_unload_timeout_seconds
)
@dataclass
class AppConfig:
"""Configuration globale de l'application - DEPRECATED: Use SystemConfig instead"""
server: ServerConfig
security: SecurityConfig
models: ModelConfig
paths: PathConfig
faiss: FAISSConfig
gpu: GPUResourceConfig = field(default_factory=GPUResourceConfig)
@classmethod
def from_env(cls) -> 'AppConfig':
"""Charge la configuration depuis les variables d'environnement"""
logger.warning("AppConfig is deprecated. Use SystemConfig via get_config() instead.")
return cls(
server=ServerConfig.from_env(),
security=SecurityConfig.from_env(),
models=ModelConfig.from_env(),
paths=PathConfig.from_env(),
faiss=FAISSConfig.from_env(),
gpu=GPUResourceConfig.from_env()
)
# Exemple de fichier .env
# Exemple de fichier .env
ENV_TEMPLATE = """
# RPA Vision V3 Configuration Unifiée
# Copier ce fichier en .env et modifier les valeurs
# Environment
ENVIRONMENT=development # development, staging, production
DEBUG=false
# Base Path
BASE_PATH=/opt/rpa_vision_v3 # Production path, use . for development
# Server
API_HOST=0.0.0.0
API_PORT=8000
DASHBOARD_HOST=0.0.0.0
DASHBOARD_PORT=5001
WORKER_THREADS=4
# Security (REQUIRED in production)
# SECRET_KEY=your_secure_secret_key_here
# ENCRYPTION_PASSWORD=your_secure_password_here
AUTH_ENABLED=true
# ALLOWED_ORIGINS=https://yourdomain.com,https://api.yourdomain.com
# Data Paths (relative to BASE_PATH)
DATA_PATH=data
LOGS_PATH=logs
SESSIONS_PATH=data/sessions
WORKFLOWS_PATH=data/workflows
EMBEDDINGS_PATH=data/embeddings
FAISS_INDEX_PATH=data/faiss_index
SCREENSHOTS_PATH=data/screenshots
TRAINING_PATH=data/training
UPLOADS_PATH=data/training/uploads
# Models
CLIP_MODEL=ViT-B-32
CLIP_PRETRAINED=openai
CLIP_DEVICE=cpu
VLM_MODEL=qwen3-vl:8b
VLM_ENDPOINT=http://localhost:11434
OWL_MODEL=google/owlv2-base-patch16-ensemble
OWL_CONFIDENCE_THRESHOLD=0.1
# FAISS
FAISS_DIMENSIONS=512
FAISS_INDEX_TYPE=Flat
FAISS_METRIC=cosine
FAISS_NPROBE=8
FAISS_AUTO_OPTIMIZE=true
FAISS_MIGRATION_THRESHOLD=10000
# GPU
GPU_IDLE_TIMEOUT=300
GPU_VRAM_THRESHOLD_MB=1024
GPU_MAX_LOAD_RETRIES=3
GPU_LOAD_TIMEOUT=30
GPU_UNLOAD_TIMEOUT=5
# Monitoring
HEALTH_CHECK_INTERVAL=30
METRICS_ENABLED=true
"""
if __name__ == "__main__":
# Test de la configuration unifiée
config_manager = get_configuration_manager()
config = config_manager.load_config()
print("=== Configuration Système Unifiée ===")
print(f"Environment: {config.environment}")
print(f"Base Path: {config.base_path}")
print(f"Data Path: {config.data_path}")
print(f"API Port: {config.api_port}")
print(f"Dashboard Port: {config.dashboard_port}")
print(f"Sessions Path: {config.sessions_path}")
print(f"CLIP Model: {config.clip_model}")
print(f"Auth Enabled: {config.auth_enabled}")
# Test de validation
validation_errors = config_manager.validate_config(config)
if validation_errors:
print("\n=== Validation Errors/Warnings ===")
for error in validation_errors:
print(f"[{error.severity.upper()}] {error.field}: {error.message}")
else:
print("\n✅ Configuration validation passed")
print(f"\n✅ Configuration Manager initialized successfully")