# 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