# Design Document - Corrections Critiques RPA Vision V3 ## Vue d'Ensemble Ce document spécifie le design pour corriger les problèmes critiques identifiés dans RPA Vision V3. Les corrections visent à établir l'exécution automatique des workflows, la stabilité en production, et des performances acceptables tout en préservant la qualité et la logique des fonctionnalités existantes. ## Architecture L'architecture des corrections suit une approche de **refactoring progressif** qui préserve les fonctionnalités existantes tout en corrigeant les problèmes structurels : ``` ┌─────────────────────────────────────────────────────────────────┐ │ Architecture des Corrections Critiques │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │ │ │ WorkflowPipeline│ │ ActionExecutor │ │ TargetResolver│ │ │ │ Enhanced │ │ Integration │ │ Optimized │ │ │ │ │ │ │ │ │ │ │ │ • execute_ │◄──►│ • Unified Error │◄──►│ • Spatial │ │ │ │ workflow_step │ │ Handling │ │ Index │ │ │ │ • Error Recovery│ │ • Target │ │ Cache │ │ │ │ • State Mgmt │ │ Resolution │ │ • Memory │ │ │ └─────────────────┘ └─────────────────┘ │ Mgmt │ │ │ │ │ └─────────────┘ │ │ │ │ │ │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │ │ │ ErrorHandler │ │ DataContracts │ │ ConfigMgr │ │ │ │ Centralized │ │ Standardized │ │ Centralized │ │ │ │ │ │ │ │ │ │ │ │ • Recovery │ │ • BBox (x,y,w,h)│ │ • Validation│ │ │ │ Strategies │ │ • datetime │ │ • Security │ │ │ │ • Escalation │ │ • string IDs │ │ • Env Adapt │ │ │ │ • Audit Trail │ │ • Pydantic │ │ │ │ │ └─────────────────┘ └─────────────────┘ └─────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ## Composants et Interfaces ### 1. Intégration WorkflowPipeline-ActionExecutor **Problème :** WorkflowPipeline ne utilise pas ActionExecutor pour l'exécution automatique. **Solution :** Extension de WorkflowPipeline avec une méthode d'exécution intégrée : ```python class WorkflowPipeline: def __init__(self, target_resolver: TargetResolver, action_executor: ActionExecutor, error_handler: ErrorHandler): self.target_resolver = target_resolver self.action_executor = action_executor self.error_handler = error_handler def execute_workflow_step(self, workflow_id: str, current_state: ScreenState) -> ExecutionResult: """ Exécute une étape de workflow de bout en bout Processus: 1. Matcher l'état actuel avec le workflow 2. Obtenir la prochaine action à exécuter 3. Résoudre la cible avec TargetResolver 4. Exécuter l'action avec ActionExecutor 5. Gérer les erreurs avec ErrorHandler 6. Retourner le résultat détaillé """ try: # 1. Match current state match_result = self.match_current_state(current_state) if not match_result: return ExecutionResult.no_match() # 2. Get next action action = self.get_next_action(workflow_id, match_result.node_id) if not action: return ExecutionResult.workflow_complete() # 3. Execute with integrated error handling result = self.action_executor.execute_action(action, current_state) return ExecutionResult.success(result, match_result) except Exception as e: recovery = self.error_handler.handle_error(e, { 'workflow_id': workflow_id, 'current_state': current_state, 'action': action if 'action' in locals() else None }) if recovery.should_retry: return self.execute_workflow_step(workflow_id, current_state) else: return ExecutionResult.error(e, recovery) ``` ### 2. Gestion d'Erreurs Unifiée **Problème :** Gestion d'erreurs fragmentée, ErrorHandler non utilisé partout. **Solution :** Centralisation avec ErrorHandler et stratégies de récupération : ```python class ErrorHandler: def __init__(self): self.recovery_strategies = { TargetNotFoundError: SpatialFallbackStrategy(), UIElementChangedError: SemanticVariantStrategy(), NetworkError: RetryWithBackoffStrategy(), ValidationError: DataNormalizationStrategy() } def handle_error(self, error: Exception, context: Dict[str, Any]) -> RecoveryResult: """ Gère une erreur avec stratégie de récupération appropriée Processus: 1. Identifier le type d'erreur 2. Sélectionner la stratégie de récupération 3. Appliquer la stratégie avec le contexte 4. Logger la tentative de récupération 5. Retourner le résultat de récupération """ error_type = type(error) strategy = self.recovery_strategies.get(error_type) if not strategy: return RecoveryResult.escalate(error, "No recovery strategy available") try: recovery = strategy.recover(error, context) self._log_recovery_attempt(error, strategy, recovery, context) return recovery except Exception as recovery_error: return RecoveryResult.escalate( error, f"Recovery failed: {recovery_error}", original_error=error ) # Pattern d'utilisation dans tous les composants def risky_operation(): try: result = perform_operation() return result except Exception as e: recovery = error_handler.handle_error(e, get_context()) if recovery.should_retry: return risky_operation() # Retry with recovery else: raise ProcessingError(f"Operation failed after recovery: {e}") ``` ### 3. Résolution des Imports Circulaires **Problème :** Structure d'imports complexe avec risques de circularité. **Solution :** Refactoring avec imports lazy et interfaces abstraites : ```python # core/models/__init__.py - Version corrigée from typing import TYPE_CHECKING # Imports directs pour les types de base from .base_models import BaseModel, BBox, Timestamp from .ui_element import UIElement from .screen_state import ScreenState # Imports lazy pour éviter les cycles if TYPE_CHECKING: from .workflow_graph import WorkflowGraph, WorkflowNode from .resolved_target import ResolvedTarget def get_workflow_graph(): """Lazy import pour WorkflowGraph""" from .workflow_graph import WorkflowGraph return WorkflowGraph def get_resolved_target(): """Lazy import pour ResolvedTarget""" from .resolved_target import ResolvedTarget return ResolvedTarget # Interfaces abstraites pour découpler les composants from abc import ABC, abstractmethod class ITargetResolver(ABC): @abstractmethod def resolve_target(self, target_spec: 'TargetSpec', ui_elements: List['UIElement']) -> Optional['ResolvedTarget']: pass class IActionExecutor(ABC): @abstractmethod def execute_action(self, action: 'Action', screen_state: 'ScreenState') -> 'ExecutionResult': pass ``` ### 4. Standardisation des Contrats de Données **Problème :** Formats de données incohérents entre couches. **Solution :** Standardisation avec Pydantic et utilitaires de conversion : ```python from pydantic import BaseModel, validator, Field from typing import Tuple, Union from datetime import datetime class BBox(BaseModel): """Bounding box standardisée au format (x, y, width, height)""" x: int = Field(..., ge=0, description="Position X (coin supérieur gauche)") y: int = Field(..., ge=0, description="Position Y (coin supérieur gauche)") width: int = Field(..., gt=0, description="Largeur") height: int = Field(..., gt=0, description="Hauteur") @validator('*', pre=True) def validate_positive(cls, v): if isinstance(v, (int, float)) and v < 0: raise ValueError("Coordinates must be non-negative") return int(v) def to_tuple(self) -> Tuple[int, int, int, int]: """Conversion vers tuple (x, y, w, h)""" return (self.x, self.y, self.width, self.height) @classmethod def from_xyxy(cls, x1: int, y1: int, x2: int, y2: int) -> 'BBox': """Conversion depuis format (x1, y1, x2, y2)""" return cls( x=min(x1, x2), y=min(y1, y2), width=abs(x2 - x1), height=abs(y2 - y1) ) class UIElement(BaseModel): """Élément UI avec contrats de données standardisés""" element_id: str = Field(..., description="ID unique de l'élément") bbox: BBox = Field(..., description="Boîte englobante") timestamp: datetime = Field(default_factory=datetime.now, description="Timestamp de création") confidence: float = Field(default=1.0, ge=0.0, le=1.0, description="Confiance de détection") @validator('element_id') def validate_element_id(cls, v): if not isinstance(v, str) or not v.strip(): raise ValueError("element_id must be a non-empty string") return v.strip() # Utilitaires de conversion sûrs class DataConverter: @staticmethod def ensure_bbox_xywh(bbox: Union[BBox, Tuple, List, Dict]) -> BBox: """Assure que bbox est au format (x, y, w, h)""" if isinstance(bbox, BBox): return bbox elif isinstance(bbox, (tuple, list)) and len(bbox) == 4: return BBox(x=bbox[0], y=bbox[1], width=bbox[2], height=bbox[3]) elif isinstance(bbox, dict): return BBox(**bbox) else: raise ValueError(f"Cannot convert {type(bbox)} to BBox") @staticmethod def ensure_datetime(timestamp: Union[datetime, str, int, float]) -> datetime: """Assure que timestamp est un objet datetime""" if isinstance(timestamp, datetime): return timestamp elif isinstance(timestamp, str): return datetime.fromisoformat(timestamp) elif isinstance(timestamp, (int, float)): return datetime.fromtimestamp(timestamp) else: raise ValueError(f"Cannot convert {type(timestamp)} to datetime") ``` ### 5. Optimisation des Performances **Problème :** Goulots d'étranglement dans TargetResolver et gestion des embeddings. **Solution :** Cache intelligent et réutilisation des ressources : ```python from functools import lru_cache from typing import Optional, Dict, Any import weakref class OptimizedTargetResolver: def __init__(self): self._spatial_index_cache: Dict[str, Any] = {} self._embedding_cache = weakref.WeakValueDictionary() self._model_cache: Dict[str, Any] = {} @lru_cache(maxsize=100) def get_spatial_index(self, screen_signature: str) -> 'SpatialIndex': """Cache de l'index spatial par signature d'écran""" if screen_signature not in self._spatial_index_cache: self._spatial_index_cache[screen_signature] = self._build_spatial_index() return self._spatial_index_cache[screen_signature] def get_embedding(self, element_id: str, force_reload: bool = False) -> Optional[np.ndarray]: """Lazy loading des embeddings avec cache""" if not force_reload and element_id in self._embedding_cache: return self._embedding_cache[element_id] embedding = self._load_embedding_from_disk(element_id) if embedding is not None: self._embedding_cache[element_id] = embedding return embedding def get_model(self, model_name: str) -> Any: """Cache des modèles ML chargés""" if model_name not in self._model_cache: self._model_cache[model_name] = self._load_model(model_name) return self._model_cache[model_name] def resolve_target(self, target_spec: TargetSpec, ui_elements: List[UIElement]) -> Optional[ResolvedTarget]: """Résolution optimisée avec réutilisation des ressources""" # Réutiliser l'index spatial screen_sig = self._compute_screen_signature(ui_elements) spatial_index = self.get_spatial_index(screen_sig) # Éviter les calculs redondants cache_key = self._compute_resolution_cache_key(target_spec, screen_sig) if cache_key in self._resolution_cache: return self._resolution_cache[cache_key] # Résolution normale avec cache result = self._resolve_with_cache(target_spec, ui_elements, spatial_index) self._resolution_cache[cache_key] = result return result ``` ### 6. Gestion Mémoire Robuste **Problème :** Fuites mémoire et consommation croissante. **Solution :** Nettoyage automatique et limites effectives : ```python import gc import psutil from threading import Timer from typing import Dict, Any class MemoryManager: def __init__(self, max_memory_mb: int = 1024): self.max_memory_mb = max_memory_mb self.cleanup_timer: Optional[Timer] = None self.resource_registry: Dict[str, Any] = {} def register_resource(self, resource_id: str, resource: Any, cleanup_func: callable = None): """Enregistre une ressource pour nettoyage automatique""" self.resource_registry[resource_id] = { 'resource': resource, 'cleanup_func': cleanup_func or (lambda x: None), 'created_at': datetime.now() } def check_memory_usage(self) -> float: """Vérifie l'utilisation mémoire actuelle""" process = psutil.Process() memory_mb = process.memory_info().rss / 1024 / 1024 return memory_mb def cleanup_if_needed(self): """Nettoyage préventif si nécessaire""" current_memory = self.check_memory_usage() if current_memory > self.max_memory_mb * 0.8: # 80% threshold self._perform_cleanup() def _perform_cleanup(self): """Effectue le nettoyage des ressources""" # 1. Nettoyer les embeddings anciens self._cleanup_old_embeddings() # 2. Libérer les ressources GPU self._cleanup_gpu_resources() # 3. Vider les caches LRU self._clear_lru_caches() # 4. Force garbage collection gc.collect() def _cleanup_old_embeddings(self): """Nettoie les embeddings anciens""" cutoff_time = datetime.now() - timedelta(hours=1) to_remove = [] for resource_id, info in self.resource_registry.items(): if (info['created_at'] < cutoff_time and 'embedding' in resource_id.lower()): info['cleanup_func'](info['resource']) to_remove.append(resource_id) for resource_id in to_remove: del self.resource_registry[resource_id] def shutdown(self): """Nettoyage complet à l'arrêt""" for info in self.resource_registry.values(): try: info['cleanup_func'](info['resource']) except Exception as e: logger.warning(f"Error during resource cleanup: {e}") self.resource_registry.clear() gc.collect() # Cache LRU avec limite effective class EffectiveLRUCache: def __init__(self, maxsize: int = 128, memory_limit_mb: int = 100): self.maxsize = maxsize self.memory_limit_mb = memory_limit_mb self.cache: Dict[Any, Any] = {} self.access_order: List[Any] = [] def get(self, key: Any) -> Any: if key in self.cache: self.access_order.remove(key) self.access_order.append(key) return self.cache[key] return None def put(self, key: Any, value: Any): # Vérifier les limites avant d'ajouter self._enforce_limits() if key in self.cache: self.access_order.remove(key) elif len(self.cache) >= self.maxsize: self._evict_lru() self.cache[key] = value self.access_order.append(key) def _enforce_limits(self): """Applique les limites de taille et mémoire""" current_memory = self._estimate_memory_usage() while (len(self.cache) >= self.maxsize or current_memory > self.memory_limit_mb): if not self.access_order: break self._evict_lru() current_memory = self._estimate_memory_usage() ``` ## Modèles de Données ### ExecutionResult Étendu ```python @dataclass class ExecutionResult: """Résultat d'exécution avec métadonnées complètes""" success: bool action_executed: Optional[str] = None target_resolved: Optional[ResolvedTarget] = None error: Optional[Exception] = None recovery_applied: Optional[RecoveryResult] = None performance_metrics: Dict[str, float] = field(default_factory=dict) correlation_id: str = field(default_factory=lambda: str(uuid.uuid4())) @classmethod def success(cls, result: Any, match_result: Any) -> 'ExecutionResult': return cls( success=True, action_executed=str(result.action), target_resolved=result.target, performance_metrics=result.metrics ) @classmethod def error(cls, error: Exception, recovery: RecoveryResult) -> 'ExecutionResult': return cls( success=False, error=error, recovery_applied=recovery ) @dataclass class RecoveryResult: """Résultat d'une tentative de récupération""" should_retry: bool strategy_used: str recovery_data: Dict[str, Any] = field(default_factory=dict) escalation_reason: Optional[str] = None @classmethod def retry(cls, strategy: str, data: Dict[str, Any] = None) -> 'RecoveryResult': return cls(should_retry=True, strategy_used=strategy, recovery_data=data or {}) @classmethod def escalate(cls, error: Exception, reason: str, **kwargs) -> 'RecoveryResult': return cls( should_retry=False, strategy_used="escalation", escalation_reason=reason, recovery_data=kwargs ) ``` ## Propriétés de Correction *Une propriété est une caractéristique ou un comportement qui doit être vrai dans toutes les exécutions valides d'un système - essentiellement, une déclaration formelle sur ce que le système doit faire. Les propriétés servent de pont entre les spécifications lisibles par l'homme et les garanties de correction vérifiables par machine.* ### Propriété 1: Intégration WorkflowPipeline-ActionExecutor *Pour tout* workflow traité par WorkflowPipeline, ActionExecutor doit être utilisé pour l'exécution des actions **Valide: Exigences 1.1, 1.2** ### Propriété 2: Gestion d'erreurs unifiée *Pour toute* erreur survenant dans le système, ErrorHandler doit être utilisé pour appliquer une stratégie de récupération appropriée **Valide: Exigences 2.1, 2.2** ### Propriété 3: Absence d'imports circulaires *Pour tout* module du système, l'importation ne doit pas créer de dépendances cycliques **Valide: Exigences 3.1, 3.3** ### Propriété 4: Cohérence des contrats de données BBox *Pour toute* coordonnée BBox utilisée dans le système, le format doit être exclusivement (x,y,w,h) **Valide: Exigences 4.1** ### Propriété 5: Cohérence des timestamps *Pour tout* timestamp manipulé dans le système, il doit être un objet datetime **Valide: Exigences 4.2** ### Propriété 6: Réutilisation de l'index spatial *Pour toute* résolution de TargetResolver avec la même signature d'écran, l'index spatial doit être réutilisé **Valide: Exigences 5.1** ### Propriété 7: Limites effectives des caches LRU *Pour tout* cache LRU utilisé, les limites de taille configurées doivent être respectées **Valide: Exigences 6.1** ### Propriété 8: Libération des ressources GPU *Pour toute* ressource GPU allouée, elle doit être libérée après utilisation **Valide: Exigences 6.3** ### Propriété 9: Validation des inputs de sécurité *Pour tout* input utilisateur reçu, il doit être validé contre les injections malveillantes **Valide: Exigences 7.2** ### Propriété 10: Interfaces abstraites pour le découplage *Pour toute* interaction entre composants, elle doit passer par des interfaces abstraites **Valide: Exigences 8.1** ### Propriété 11: Correlation IDs uniques *Pour toute* opération effectuée, un correlation ID unique doit être assigné **Valide: Exigences 9.1** ### Propriété 12: Centralisation des constantes *Pour toute* constante nécessaire au système, elle doit être centralisée dans core/config.py **Valide: Exigences 10.1** ## Gestion d'Erreurs ### Stratégies de Récupération 1. **SpatialFallbackStrategy** : Utilise des critères spatiaux alternatifs 2. **SemanticVariantStrategy** : Essaie des variantes sémantiques du texte 3. **RetryWithBackoffStrategy** : Retry avec délai exponentiel 4. **DataNormalizationStrategy** : Normalise et convertit les données 5. **GracefulDegradationStrategy** : Continue avec fonctionnalité réduite ### Escalade d'Erreurs ```python class ErrorEscalation: def escalate_error(self, error: Exception, context: Dict[str, Any], recovery_attempts: List[RecoveryResult]) -> None: """ Escalade une erreur après échec des récupérations Processus: 1. Collecter le contexte complet 2. Documenter les tentatives de récupération 3. Créer un rapport d'erreur détaillé 4. Notifier les systèmes de monitoring 5. Préserver l'état pour diagnostic """ ``` ## Stratégie de Test ### Tests Unitaires - Test d'intégration WorkflowPipeline-ActionExecutor - Test des stratégies de récupération d'erreurs - Test de résolution des imports circulaires - Test de conversion des contrats de données - Test des optimisations de performance - Test de gestion mémoire ### Tests de Propriétés Utilisation du framework Hypothesis avec 100+ itérations par propriété : - **Propriété 1** : Intégration WorkflowPipeline-ActionExecutor - **Propriété 2** : Gestion d'erreurs unifiée - **Propriété 3** : Absence d'imports circulaires - **Propriété 4** : Cohérence des contrats de données BBox - **Propriété 5** : Cohérence des timestamps - **Propriété 6** : Réutilisation de l'index spatial - **Propriété 7** : Limites effectives des caches LRU - **Propriété 8** : Libération des ressources GPU - **Propriété 9** : Validation des inputs de sécurité - **Propriété 10** : Interfaces abstraites pour le découplage - **Propriété 11** : Correlation IDs uniques - **Propriété 12** : Centralisation des constantes ### Tests d'Intégration - Exécution de bout en bout des workflows - Test de charge avec gestion mémoire - Test de sécurité en environnement de production - Test de performance avec métriques détaillées - Test de récupération d'erreurs en conditions réelles ### Validation de Non-Régression **Principe Critique :** Chaque correction doit préserver la qualité et la logique des fonctionnalités existantes. ```python class RegressionValidator: def validate_no_regression(self, before_state: SystemState, after_state: SystemState) -> ValidationResult: """ Valide qu'aucune régression n'a été introduite Vérifications: 1. Toutes les fonctionnalités existantes fonctionnent 2. Les performances ne se dégradent pas 3. Les contrats d'API sont préservés 4. Les tests existants continuent de passer 5. La qualité du code est maintenue ou améliorée """ ```