Validé sur PC Windows (DESKTOP-58D5CAC, 2560x1600) : - 8 clics résolus visuellement (1 anchor_template, 1 som_text_match, 6 som_vlm) - Score moyen 0.75, temps moyen 1.6s - Texte tapé correctement (bonjour, test word, date, email) - 0 retries, 2 actions non vérifiées (OK) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
25 KiB
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 :
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 :
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 :
# 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 :
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 :
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 :
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
@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
- SpatialFallbackStrategy : Utilise des critères spatiaux alternatifs
- SemanticVariantStrategy : Essaie des variantes sémantiques du texte
- RetryWithBackoffStrategy : Retry avec délai exponentiel
- DataNormalizationStrategy : Normalise et convertit les données
- GracefulDegradationStrategy : Continue avec fonctionnalité réduite
Escalade d'Erreurs
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.
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
"""