feat: replay E2E fonctionnel — 25/25 actions, 0 retries, SomEngine via serveur

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>
This commit is contained in:
Dom
2026-03-31 14:04:41 +02:00
parent 5e0b53cfd1
commit a7de6a488b
79542 changed files with 6091757 additions and 1 deletions

View File

@@ -0,0 +1,615 @@
# Design Document - Workflow Composition
## Overview
Ce document décrit l'architecture et la conception pour la composition et le contrôle de flux des workflows dans RPA Vision V3. Le système permet de chaîner des workflows, créer des sous-workflows réutilisables, fusionner des workflows similaires, et utiliser des structures de contrôle (boucles, conditions, déclencheurs).
## Architecture
```mermaid
graph TB
subgraph "Composition Layer"
WC[WorkflowChainer]
SR[SubWorkflowRegistry]
WM[WorkflowMerger]
SE[SequenceExtractor]
end
subgraph "Control Flow Layer"
LE[LoopExecutor]
CE[ConditionalEvaluator]
TM[TriggerManager]
end
subgraph "Data Layer"
VM[VariableManager]
DG[DependencyGraph]
EL[ExecutionLogger]
end
subgraph "Existing Core"
WG[WorkflowGraph]
EX[ExecutionLoop]
TR[TargetResolver]
end
WC --> VM
WC --> EL
SR --> DG
WM --> WG
SE --> SR
LE --> EX
CE --> TR
TM --> EX
VM --> EL
DG --> SR
```
## Components and Interfaces
### 1. WorkflowChainer
Gère l'exécution séquentielle de workflows chaînés.
```python
@dataclass
class ChainConfig:
"""Configuration de chaînage entre workflows."""
source_workflow_id: str
target_workflow_id: str
variable_mapping: Dict[str, str] # source_var -> target_var
on_failure: Literal["retry", "skip", "abort"]
max_retries: int = 3
class WorkflowChainer:
def __init__(self, variable_manager: VariableManager, logger: ExecutionLogger):
self.variable_manager = variable_manager
self.logger = logger
self.chains: Dict[str, List[ChainConfig]] = {}
def add_chain(self, config: ChainConfig) -> None:
"""Ajoute une configuration de chaînage."""
def validate_chain(self, source_id: str, target_id: str) -> ValidationResult:
"""Valide la compatibilité entre deux workflows."""
def execute_chain(self, start_workflow_id: str, context: ExecutionContext) -> ChainResult:
"""Exécute une chaîne de workflows."""
def to_dict(self) -> Dict[str, Any]:
"""Sérialise la configuration de chaînage."""
@classmethod
def from_dict(cls, data: Dict[str, Any], ...) -> 'WorkflowChainer':
"""Désérialise la configuration de chaînage."""
```
### 2. SubWorkflowRegistry
Gère les sous-workflows réutilisables et leurs références.
```python
@dataclass
class SubWorkflowDefinition:
"""Définition d'un sous-workflow."""
workflow_id: str
name: str
input_parameters: List[ParameterDef]
output_values: List[ParameterDef]
@dataclass
class ReferenceNode:
"""Node qui référence un sous-workflow."""
node_id: str
sub_workflow_id: str
input_bindings: Dict[str, str] # param_name -> variable_name
output_bindings: Dict[str, str]
class SubWorkflowRegistry:
def __init__(self, dependency_graph: DependencyGraph):
self.dependency_graph = dependency_graph
self.definitions: Dict[str, SubWorkflowDefinition] = {}
self.references: Dict[str, List[ReferenceNode]] = {}
def register(self, definition: SubWorkflowDefinition) -> None:
"""Enregistre un sous-workflow."""
def create_reference(self, parent_workflow_id: str, ref: ReferenceNode) -> None:
"""Crée une référence vers un sous-workflow."""
def execute_reference(self, ref: ReferenceNode, context: ExecutionContext) -> ExecutionResult:
"""Exécute un sous-workflow via sa référence."""
def get_dependents(self, workflow_id: str) -> List[str]:
"""Retourne les workflows qui dépendent de ce sous-workflow."""
def to_dict(self) -> Dict[str, Any]:
"""Sérialise le registre."""
@classmethod
def from_dict(cls, data: Dict[str, Any], ...) -> 'SubWorkflowRegistry':
"""Désérialise le registre."""
```
### 3. WorkflowMerger
Détecte et fusionne les workflows similaires.
```python
@dataclass
class MergeCandidate:
"""Candidat à la fusion."""
workflow_a_id: str
workflow_b_id: str
similarity_score: float
shared_nodes: List[str]
conflicts: List[NodeConflict]
@dataclass
class NodeConflict:
"""Conflit entre deux nodes."""
node_id: str
action_a: str
action_b: str
class WorkflowMerger:
def __init__(self, similarity_threshold: float = 0.9):
self.similarity_threshold = similarity_threshold
def calculate_similarity(self, workflow_a: WorkflowGraph, workflow_b: WorkflowGraph) -> float:
"""Calcule la similarité entre deux workflows."""
def find_merge_candidates(self, workflows: List[WorkflowGraph]) -> List[MergeCandidate]:
"""Trouve les paires de workflows candidats à la fusion."""
def merge(self, workflow_a: WorkflowGraph, workflow_b: WorkflowGraph,
conflict_resolutions: Dict[str, str]) -> WorkflowGraph:
"""Fusionne deux workflows en préservant tous les chemins uniques."""
def get_unique_paths(self, workflow: WorkflowGraph) -> List[List[str]]:
"""Extrait tous les chemins uniques d'un workflow."""
```
### 4. SequenceExtractor
Détecte et extrait les séquences communes.
```python
@dataclass
class CommonSequence:
"""Séquence commune détectée."""
nodes: List[str]
occurrences: List[SequenceOccurrence]
priority: Literal["high", "medium", "low"]
@dataclass
class SequenceOccurrence:
"""Occurrence d'une séquence dans un workflow."""
workflow_id: str
start_index: int
end_index: int
class SequenceExtractor:
def __init__(self, registry: SubWorkflowRegistry, min_sequence_length: int = 3):
self.registry = registry
self.min_sequence_length = min_sequence_length
def find_common_sequences(self, workflows: List[WorkflowGraph]) -> List[CommonSequence]:
"""Trouve les séquences communes entre workflows."""
def extract_as_subworkflow(self, sequence: CommonSequence, name: str) -> SubWorkflowDefinition:
"""Extrait une séquence comme sous-workflow."""
def replace_with_references(self, sequence: CommonSequence, sub_workflow_id: str) -> None:
"""Remplace les occurrences par des références."""
```
### 5. LoopExecutor
Gère l'exécution des boucles.
```python
@dataclass
class LoopConfig:
"""Configuration d'une boucle."""
loop_id: str
loop_type: Literal["count", "condition"]
max_iterations: Optional[int] # Pour type "count"
exit_condition: Optional[VisualCondition] # Pour type "condition"
body_nodes: List[str]
safety_limit: int = 1000
@dataclass
class LoopState:
"""État d'exécution d'une boucle."""
loop_id: str
current_iteration: int
started_at: datetime
last_condition_result: Optional[bool]
class LoopExecutor:
def __init__(self, execution_loop: ExecutionLoop, safety_limit: int = 1000):
self.execution_loop = execution_loop
self.safety_limit = safety_limit
self.active_loops: Dict[str, LoopState] = {}
def start_loop(self, config: LoopConfig, context: ExecutionContext) -> LoopState:
"""Démarre une nouvelle boucle."""
def execute_iteration(self, loop_id: str, context: ExecutionContext) -> IterationResult:
"""Exécute une itération de la boucle."""
def should_continue(self, loop_id: str, context: ExecutionContext) -> bool:
"""Évalue si la boucle doit continuer."""
def increment_counter(self, loop_id: str) -> int:
"""Incrémente le compteur et retourne la nouvelle valeur."""
def to_dict(self) -> Dict[str, Any]:
"""Sérialise l'état des boucles."""
@classmethod
def from_dict(cls, data: Dict[str, Any], ...) -> 'LoopExecutor':
"""Désérialise l'état des boucles."""
```
### 6. ConditionalEvaluator
Évalue les conditions et détermine les branches à exécuter.
```python
@dataclass
class BranchConfig:
"""Configuration d'une branche conditionnelle."""
branch_id: str
condition: VisualCondition
target_node: str
priority: int # Ordre d'évaluation
@dataclass
class ConditionalNode:
"""Node conditionnel avec plusieurs branches."""
node_id: str
branches: List[BranchConfig]
default_branch: Optional[str]
@dataclass
class VisualCondition:
"""Condition basée sur l'état visuel."""
condition_type: Literal["element_present", "element_absent", "text_equals", "text_contains"]
target_element: Optional[str] # ID ou template
expected_text: Optional[str]
class ConditionalEvaluator:
def __init__(self, target_resolver: TargetResolver):
self.target_resolver = target_resolver
def evaluate_condition(self, condition: VisualCondition, screenshot: np.ndarray) -> bool:
"""Évalue une condition visuelle."""
def evaluate_node(self, node: ConditionalNode, screenshot: np.ndarray) -> str:
"""Évalue un node conditionnel et retourne l'ID de la branche à exécuter."""
def to_dict(self) -> Dict[str, Any]:
"""Sérialise la configuration."""
@classmethod
def from_dict(cls, data: Dict[str, Any], ...) -> 'ConditionalEvaluator':
"""Désérialise la configuration."""
```
### 7. TriggerManager
Gère les déclencheurs automatiques.
```python
@dataclass
class ScheduleTrigger:
"""Déclencheur basé sur un horaire."""
trigger_id: str
workflow_id: str
cron_expression: Optional[str]
interval_seconds: Optional[int]
@dataclass
class FileTrigger:
"""Déclencheur basé sur l'apparition d'un fichier."""
trigger_id: str
workflow_id: str
watch_directory: str
file_pattern: str
@dataclass
class VisualTrigger:
"""Déclencheur basé sur la détection visuelle."""
trigger_id: str
workflow_id: str
target_element: str
check_interval_seconds: int
@dataclass
class TriggerContext:
"""Contexte passé au workflow lors du déclenchement."""
trigger_id: str
trigger_type: str
fired_at: datetime
file_path: Optional[str]
detected_element: Optional[Dict[str, Any]]
class TriggerManager:
def __init__(self, execution_loop: ExecutionLoop, concurrency_mode: Literal["concurrent", "queue"]):
self.execution_loop = execution_loop
self.concurrency_mode = concurrency_mode
self.triggers: Dict[str, Union[ScheduleTrigger, FileTrigger, VisualTrigger]] = {}
self.execution_queue: List[Tuple[str, TriggerContext]] = []
def register_trigger(self, trigger: Union[ScheduleTrigger, FileTrigger, VisualTrigger]) -> None:
"""Enregistre un déclencheur."""
def fire_trigger(self, trigger_id: str) -> TriggerContext:
"""Déclenche manuellement un trigger et retourne le contexte."""
def handle_concurrent_triggers(self, workflow_id: str, contexts: List[TriggerContext]) -> None:
"""Gère les déclenchements concurrents."""
def to_dict(self) -> Dict[str, Any]:
"""Sérialise la configuration des triggers."""
@classmethod
def from_dict(cls, data: Dict[str, Any], ...) -> 'TriggerManager':
"""Désérialise la configuration des triggers."""
```
### 8. DependencyGraph
Gère le graphe de dépendances entre workflows.
```python
class DependencyGraph:
def __init__(self):
self.dependencies: Dict[str, Set[str]] = {} # workflow -> sous-workflows utilisés
self.dependents: Dict[str, Set[str]] = {} # sous-workflow -> workflows qui l'utilisent
def add_dependency(self, workflow_id: str, sub_workflow_id: str) -> None:
"""Ajoute une dépendance."""
def remove_dependency(self, workflow_id: str, sub_workflow_id: str) -> None:
"""Supprime une dépendance."""
def get_dependencies(self, workflow_id: str) -> Set[str]:
"""Retourne les dépendances d'un workflow."""
def get_dependents(self, workflow_id: str) -> Set[str]:
"""Retourne les workflows qui dépendent de celui-ci."""
def has_circular_dependency(self, workflow_id: str, sub_workflow_id: str) -> bool:
"""Vérifie si l'ajout créerait une dépendance circulaire."""
def get_usage_stats(self, workflow_id: str) -> Dict[str, int]:
"""Retourne les statistiques d'utilisation."""
```
### 9. VariableManager (Extension)
Extension du VariableManager existant pour les variables globales.
```python
class GlobalVariableManager:
def __init__(self):
self.global_vars: Dict[str, Any] = {}
self.override_log: List[VariableOverride] = []
def set_global(self, name: str, value: Any, source_workflow: str) -> None:
"""Définit une variable globale."""
def get_global(self, name: str, default: Any = None) -> Any:
"""Récupère une variable globale."""
def transfer_to_workflow(self, target_workflow_id: str) -> Dict[str, Any]:
"""Transfère les variables au workflow suivant."""
def get_final_state(self) -> Dict[str, Any]:
"""Retourne l'état final des variables."""
def to_dict(self) -> Dict[str, Any]:
"""Sérialise le contexte d'exécution."""
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'GlobalVariableManager':
"""Désérialise le contexte d'exécution."""
```
## Data Models
```python
@dataclass
class WorkflowCompositionConfig:
"""Configuration complète de composition."""
chains: List[ChainConfig]
sub_workflows: Dict[str, SubWorkflowDefinition]
references: Dict[str, List[ReferenceNode]]
loops: Dict[str, LoopConfig]
conditionals: Dict[str, ConditionalNode]
triggers: List[Union[ScheduleTrigger, FileTrigger, VisualTrigger]]
@dataclass
class ExecutionContext:
"""Contexte d'exécution partagé."""
chain_id: str
current_workflow_id: str
global_variables: Dict[str, Any]
execution_log: List[LogEntry]
trigger_context: Optional[TriggerContext]
@dataclass
class LogEntry:
"""Entrée du log d'exécution unifié."""
timestamp: datetime
workflow_id: str
node_id: str
event_type: str
details: Dict[str, Any]
@dataclass
class ValidationResult:
"""Résultat de validation."""
is_valid: bool
errors: List[str]
warnings: List[str]
```
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Property Reflection
Après analyse des critères d'acceptation, voici les propriétés consolidées en éliminant les redondances :
- Les propriétés de round-trip (1.6, 2.6, 6.6, 7.6, 8.6, 9.4) peuvent être combinées en une seule propriété générique de sérialisation
- Les propriétés de propagation de variables (1.2, 9.1, 9.5) peuvent être combinées
- Les propriétés de validation de dépendances (5.2, 5.3) peuvent être combinées
### Properties
**Property 1: Round-trip de sérialisation**
*Pour tout* objet de configuration (ChainConfig, LoopConfig, ConditionalNode, Trigger, ExecutionContext), la sérialisation suivie de la désérialisation doit produire un objet équivalent.
**Validates: Requirements 1.6, 2.6, 6.6, 7.6, 8.6, 9.4**
**Property 2: Propagation des variables dans les chaînes**
*Pour tout* workflow A avec des variables de contexte chaîné vers un workflow B, toutes les variables mappées doivent être accessibles dans B avec leurs valeurs correctes, et en cas de conflit de noms, la valeur la plus récente prévaut.
**Validates: Requirements 1.2, 9.1, 9.5**
**Property 3: Validation de compatibilité de chaînage**
*Pour toute* paire de workflows (A, B) avec une configuration de chaînage, la validation doit détecter les incompatibilités entre l'état final de A et l'état initial de B.
**Validates: Requirements 1.4**
**Property 4: Log unifié complet**
*Pour toute* chaîne de workflows exécutée, le log unifié doit contenir exactement tous les événements de tous les workflows dans l'ordre chronologique.
**Validates: Requirements 1.5**
**Property 5: Retour de contrôle après sous-workflow**
*Pour tout* workflow parent appelant un sous-workflow via une référence, après complétion du sous-workflow, l'exécution doit reprendre au node suivant la référence dans le parent.
**Validates: Requirements 2.1, 2.2**
**Property 6: Propagation d'erreur de sous-workflow**
*Pour toute* erreur survenant dans un sous-workflow, l'erreur doit être propagée au workflow parent avec le contexte complet (stack trace, état des variables, node en échec).
**Validates: Requirements 2.5**
**Property 7: Détection de similarité pour fusion**
*Pour toute* paire de workflows avec un score de similarité supérieur à 0.9, le système doit générer une suggestion de fusion.
**Validates: Requirements 3.1**
**Property 8: Préservation des chemins lors de la fusion**
*Pour tout* merge de workflows A et B, le workflow résultant doit contenir tous les chemins uniques de A et tous les chemins uniques de B.
**Validates: Requirements 3.2**
**Property 9: Combinaison des statistiques lors de la fusion**
*Pour tout* merge de workflows A et B, les statistiques du résultat doivent être la combinaison (somme ou union selon le type) des statistiques de A et B.
**Validates: Requirements 3.3**
**Property 10: Détection de séquences communes**
*Pour toute* séquence de nodes identique apparaissant dans N workflows (N >= 2), le système doit la détecter et suggérer son extraction.
**Validates: Requirements 4.1**
**Property 11: Remplacement par références après extraction**
*Pour toute* extraction de séquence en sous-workflow, toutes les occurrences originales doivent être remplacées par des nodes de référence pointant vers le nouveau sous-workflow.
**Validates: Requirements 4.2**
**Property 12: Priorisation des séquences fréquentes**
*Pour toute* séquence apparaissant dans 3 workflows ou plus, elle doit être marquée comme candidat haute priorité.
**Validates: Requirements 4.3**
**Property 13: Validation des dépendances**
*Pour toute* tentative de suppression d'un sous-workflow avec des dépendants, un avertissement doit être généré listant tous les workflows affectés. *Pour toute* configuration créant une dépendance circulaire, elle doit être rejetée avec une erreur.
**Validates: Requirements 5.2, 5.3**
**Property 14: Incrémentation du compteur de boucle**
*Pour toute* boucle, après chaque itération complète, le compteur doit être incrémenté exactement de 1.
**Validates: Requirements 6.2**
**Property 15: Terminaison de boucle à la limite**
*Pour toute* boucle avec une limite de N itérations, elle doit terminer après exactement N itérations (ou moins si la condition de sortie est atteinte).
**Validates: Requirements 6.3**
**Property 16: Garde de sécurité des boucles**
*Pour toute* boucle, le nombre d'itérations ne doit jamais dépasser 1000, et un warning doit être loggé si cette limite est atteinte.
**Validates: Requirements 6.5**
**Property 17: Ordre d'évaluation des branches conditionnelles**
*Pour tout* node conditionnel avec plusieurs branches dont les conditions sont vraies, seule la première branche (par ordre de priorité) doit être exécutée.
**Validates: Requirements 7.2**
**Property 18: Branche par défaut ou erreur**
*Pour tout* node conditionnel où aucune condition n'est satisfaite, la branche par défaut doit être exécutée si elle existe, sinon une erreur doit être levée.
**Validates: Requirements 7.3**
**Property 19: Passage du contexte de déclenchement**
*Pour tout* déclenchement de trigger, le contexte (timestamp, chemin fichier ou élément détecté) doit être passé au workflow.
**Validates: Requirements 8.4**
**Property 20: Gestion des déclenchements concurrents**
*Pour tout* workflow avec plusieurs triggers configurés, les exécutions concurrentes doivent être gérées selon la configuration (concurrent ou queue).
**Validates: Requirements 8.5**
**Property 21: Lecture de variable avec défaut**
*Pour toute* lecture d'une variable globale non définie, la valeur par défaut spécifiée doit être retournée.
**Validates: Requirements 9.2**
**Property 22: Préservation de l'état final des variables**
*Pour toute* chaîne de workflows complétée, le log d'exécution doit contenir l'état final de toutes les variables globales.
**Validates: Requirements 9.3**
## Error Handling
### Erreurs de chaînage
- **ChainValidationError** : Incompatibilité entre workflows
- **ChainExecutionError** : Échec pendant l'exécution d'une chaîne
- **VariableTransferError** : Échec du transfert de variables
### Erreurs de sous-workflows
- **SubWorkflowNotFoundError** : Référence vers un sous-workflow inexistant
- **CircularDependencyError** : Dépendance circulaire détectée
- **SubWorkflowExecutionError** : Échec dans un sous-workflow
### Erreurs de boucles
- **LoopSafetyLimitError** : Limite de sécurité atteinte (1000 itérations)
- **LoopConditionError** : Erreur lors de l'évaluation de la condition
### Erreurs de conditions
- **NoMatchingBranchError** : Aucune branche ne correspond et pas de défaut
- **ConditionEvaluationError** : Erreur lors de l'évaluation d'une condition
### Erreurs de triggers
- **TriggerConfigurationError** : Configuration de trigger invalide
- **ConcurrentExecutionError** : Erreur de gestion des exécutions concurrentes
## Testing Strategy
### Bibliothèque de Property-Based Testing
Le projet utilise **Hypothesis** pour Python, déjà configuré dans le projet.
### Tests unitaires
- Tests des composants individuels (ChainConfig, LoopConfig, etc.)
- Tests des validations (compatibilité, dépendances circulaires)
- Tests des cas limites (boucle vide, chaîne d'un seul workflow)
### Tests property-based
Chaque propriété de correction sera implémentée comme un test property-based avec :
- Minimum 100 itérations par test
- Annotation explicite référençant la propriété du design
- Format : `**Feature: workflow-composition, Property {number}: {property_text}**`
### Stratégie de génération
- Générateurs de workflows aléatoires avec nodes, transitions, et configurations
- Générateurs de configurations de chaînage valides et invalides
- Générateurs de conditions visuelles simulées
- Générateurs de contextes d'exécution avec variables
### Tests d'intégration
- Exécution de chaînes complètes avec plusieurs workflows
- Exécution de boucles avec conditions visuelles simulées
- Déclenchement de triggers et vérification du contexte