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:
65
visual_workflow_builder/backend/models/__init__.py
Normal file
65
visual_workflow_builder/backend/models/__init__.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Models package for Visual Workflow Builder
|
||||
|
||||
Contains data models and database schemas.
|
||||
"""
|
||||
|
||||
from .visual_workflow import (
|
||||
# Enums
|
||||
NodeCategory,
|
||||
NodeStatus,
|
||||
ParameterType,
|
||||
|
||||
# Basic types
|
||||
Position,
|
||||
Size,
|
||||
Port,
|
||||
ValidationRule,
|
||||
ParameterDefinition,
|
||||
|
||||
# Edge types
|
||||
EdgeStyle,
|
||||
EdgeCondition,
|
||||
VisualEdge,
|
||||
|
||||
# Node types
|
||||
VisualNode,
|
||||
|
||||
# Workflow types
|
||||
Variable,
|
||||
WorkflowSettings,
|
||||
VisualWorkflow,
|
||||
|
||||
# Utilities
|
||||
generate_id
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Enums
|
||||
'NodeCategory',
|
||||
'NodeStatus',
|
||||
'ParameterType',
|
||||
|
||||
# Basic types
|
||||
'Position',
|
||||
'Size',
|
||||
'Port',
|
||||
'ValidationRule',
|
||||
'ParameterDefinition',
|
||||
|
||||
# Edge types
|
||||
'EdgeStyle',
|
||||
'EdgeCondition',
|
||||
'VisualEdge',
|
||||
|
||||
# Node types
|
||||
'VisualNode',
|
||||
|
||||
# Workflow types
|
||||
'Variable',
|
||||
'WorkflowSettings',
|
||||
'VisualWorkflow',
|
||||
|
||||
# Utilities
|
||||
'generate_id'
|
||||
]
|
||||
180
visual_workflow_builder/backend/models/self_healing_config.py
Normal file
180
visual_workflow_builder/backend/models/self_healing_config.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""Self-healing configuration models for Visual Workflow Builder."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Any, List, Optional
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class RecoveryStrategy(Enum):
|
||||
"""Available recovery strategies."""
|
||||
SEMANTIC_VARIANT = "semantic_variant"
|
||||
SPATIAL_FALLBACK = "spatial_fallback"
|
||||
TIMING_ADAPTATION = "timing_adaptation"
|
||||
FORMAT_TRANSFORMATION = "format_transformation"
|
||||
ALL = "all"
|
||||
|
||||
|
||||
class RecoveryMode(Enum):
|
||||
"""Recovery modes for different scenarios."""
|
||||
DISABLED = "disabled"
|
||||
CONSERVATIVE = "conservative" # Only high-confidence recoveries
|
||||
BALANCED = "balanced" # Default mode
|
||||
AGGRESSIVE = "aggressive" # Try all strategies
|
||||
|
||||
|
||||
@dataclass
|
||||
class SelfHealingConfig:
|
||||
"""Configuration for self-healing behavior of a node."""
|
||||
|
||||
# Basic settings
|
||||
enabled: bool = True
|
||||
recovery_mode: RecoveryMode = RecoveryMode.BALANCED
|
||||
max_attempts: int = 3
|
||||
confidence_threshold: float = 0.7
|
||||
|
||||
# Strategy configuration
|
||||
enabled_strategies: List[RecoveryStrategy] = field(
|
||||
default_factory=lambda: [RecoveryStrategy.ALL]
|
||||
)
|
||||
strategy_timeout: float = 30.0 # seconds
|
||||
|
||||
# Advanced settings
|
||||
learn_from_success: bool = True
|
||||
require_user_confirmation: bool = False
|
||||
stop_on_failure: bool = False
|
||||
|
||||
# Notification settings
|
||||
notify_on_recovery: bool = True
|
||||
notify_on_failure: bool = True
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for serialization."""
|
||||
return {
|
||||
'enabled': self.enabled,
|
||||
'recovery_mode': self.recovery_mode.value,
|
||||
'max_attempts': self.max_attempts,
|
||||
'confidence_threshold': self.confidence_threshold,
|
||||
'enabled_strategies': [s.value for s in self.enabled_strategies],
|
||||
'strategy_timeout': self.strategy_timeout,
|
||||
'learn_from_success': self.learn_from_success,
|
||||
'require_user_confirmation': self.require_user_confirmation,
|
||||
'stop_on_failure': self.stop_on_failure,
|
||||
'notify_on_recovery': self.notify_on_recovery,
|
||||
'notify_on_failure': self.notify_on_failure
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'SelfHealingConfig':
|
||||
"""Create from dictionary."""
|
||||
return cls(
|
||||
enabled=data.get('enabled', True),
|
||||
recovery_mode=RecoveryMode(data.get('recovery_mode', 'balanced')),
|
||||
max_attempts=data.get('max_attempts', 3),
|
||||
confidence_threshold=data.get('confidence_threshold', 0.7),
|
||||
enabled_strategies=[
|
||||
RecoveryStrategy(s) for s in data.get('enabled_strategies', ['all'])
|
||||
],
|
||||
strategy_timeout=data.get('strategy_timeout', 30.0),
|
||||
learn_from_success=data.get('learn_from_success', True),
|
||||
require_user_confirmation=data.get('require_user_confirmation', False),
|
||||
stop_on_failure=data.get('stop_on_failure', False),
|
||||
notify_on_recovery=data.get('notify_on_recovery', True),
|
||||
notify_on_failure=data.get('notify_on_failure', True)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_default_for_action(cls, action_type: str) -> 'SelfHealingConfig':
|
||||
"""Get default configuration for specific action types."""
|
||||
if action_type in ['click', 'hover']:
|
||||
# More aggressive for UI interactions
|
||||
return cls(
|
||||
recovery_mode=RecoveryMode.BALANCED,
|
||||
enabled_strategies=[
|
||||
RecoveryStrategy.SEMANTIC_VARIANT,
|
||||
RecoveryStrategy.SPATIAL_FALLBACK
|
||||
],
|
||||
max_attempts=3,
|
||||
confidence_threshold=0.6
|
||||
)
|
||||
elif action_type in ['type', 'input']:
|
||||
# Conservative for data input
|
||||
return cls(
|
||||
recovery_mode=RecoveryMode.CONSERVATIVE,
|
||||
enabled_strategies=[
|
||||
RecoveryStrategy.FORMAT_TRANSFORMATION,
|
||||
RecoveryStrategy.TIMING_ADAPTATION
|
||||
],
|
||||
max_attempts=2,
|
||||
confidence_threshold=0.8,
|
||||
require_user_confirmation=True
|
||||
)
|
||||
elif action_type in ['wait', 'navigate']:
|
||||
# Timing-focused for navigation
|
||||
return cls(
|
||||
recovery_mode=RecoveryMode.BALANCED,
|
||||
enabled_strategies=[
|
||||
RecoveryStrategy.TIMING_ADAPTATION
|
||||
],
|
||||
max_attempts=2,
|
||||
confidence_threshold=0.7
|
||||
)
|
||||
else:
|
||||
# Default configuration
|
||||
return cls()
|
||||
|
||||
|
||||
@dataclass
|
||||
class RecoveryNotification:
|
||||
"""Notification about recovery attempt."""
|
||||
|
||||
node_id: str
|
||||
strategy_used: str
|
||||
success: bool
|
||||
confidence: float
|
||||
execution_time: float
|
||||
message: str
|
||||
timestamp: str
|
||||
requires_attention: bool = False
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for serialization."""
|
||||
return {
|
||||
'node_id': self.node_id,
|
||||
'strategy_used': self.strategy_used,
|
||||
'success': self.success,
|
||||
'confidence': self.confidence,
|
||||
'execution_time': self.execution_time,
|
||||
'message': self.message,
|
||||
'timestamp': self.timestamp,
|
||||
'requires_attention': self.requires_attention
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class RecoveryStatistics:
|
||||
"""Statistics about recovery attempts."""
|
||||
|
||||
total_attempts: int = 0
|
||||
successful_recoveries: int = 0
|
||||
failed_recoveries: int = 0
|
||||
average_confidence: float = 0.0
|
||||
most_used_strategy: Optional[str] = None
|
||||
total_time_saved: float = 0.0 # seconds
|
||||
|
||||
@property
|
||||
def success_rate(self) -> float:
|
||||
"""Calculate success rate."""
|
||||
return (self.successful_recoveries / self.total_attempts
|
||||
if self.total_attempts > 0 else 0.0)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for serialization."""
|
||||
return {
|
||||
'total_attempts': self.total_attempts,
|
||||
'successful_recoveries': self.successful_recoveries,
|
||||
'failed_recoveries': self.failed_recoveries,
|
||||
'success_rate': self.success_rate,
|
||||
'average_confidence': self.average_confidence,
|
||||
'most_used_strategy': self.most_used_strategy,
|
||||
'total_time_saved': self.total_time_saved
|
||||
}
|
||||
200
visual_workflow_builder/backend/models/template.py
Normal file
200
visual_workflow_builder/backend/models/template.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
Template Data Models
|
||||
|
||||
Contains data models for workflow templates and template parameters.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from .visual_workflow import VisualWorkflow, ParameterType
|
||||
|
||||
|
||||
class TemplateDifficulty(Enum):
|
||||
"""Difficulty levels for templates"""
|
||||
BEGINNER = 'beginner'
|
||||
INTERMEDIATE = 'intermediate'
|
||||
ADVANCED = 'advanced'
|
||||
|
||||
|
||||
@dataclass
|
||||
class TemplateParameter:
|
||||
"""Configurable parameter for a template"""
|
||||
name: str
|
||||
type: ParameterType
|
||||
description: str
|
||||
default_value: Optional[Any] = None
|
||||
|
||||
# Mapping to workflow nodes
|
||||
node_id: str = ""
|
||||
parameter_name: str = ""
|
||||
|
||||
# UI hints
|
||||
label: str = ""
|
||||
placeholder: Optional[str] = None
|
||||
required: bool = True
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'name': self.name,
|
||||
'type': self.type.value,
|
||||
'description': self.description,
|
||||
'default_value': self.default_value,
|
||||
'node_id': self.node_id,
|
||||
'parameter_name': self.parameter_name,
|
||||
'label': self.label,
|
||||
'placeholder': self.placeholder,
|
||||
'required': self.required
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'TemplateParameter':
|
||||
return cls(
|
||||
name=data['name'],
|
||||
type=ParameterType(data['type']),
|
||||
description=data['description'],
|
||||
default_value=data.get('default_value'),
|
||||
node_id=data.get('node_id', ''),
|
||||
parameter_name=data.get('parameter_name', ''),
|
||||
label=data.get('label', ''),
|
||||
placeholder=data.get('placeholder'),
|
||||
required=data.get('required', True)
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkflowTemplate:
|
||||
"""Template for creating workflows"""
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
category: str
|
||||
|
||||
# Template workflow structure
|
||||
workflow: VisualWorkflow
|
||||
|
||||
# Configurable parameters
|
||||
parameters: List[TemplateParameter] = field(default_factory=list)
|
||||
|
||||
# Metadata
|
||||
tags: List[str] = field(default_factory=list)
|
||||
difficulty: TemplateDifficulty = TemplateDifficulty.BEGINNER
|
||||
estimated_time: int = 5 # minutes
|
||||
|
||||
# Usage statistics
|
||||
usage_count: int = 0
|
||||
rating: float = 0.0
|
||||
|
||||
# Timestamps
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
updated_at: datetime = field(default_factory=datetime.now)
|
||||
created_by: str = "system"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'category': self.category,
|
||||
'workflow': self.workflow.to_dict(),
|
||||
'parameters': [p.to_dict() for p in self.parameters],
|
||||
'tags': self.tags,
|
||||
'difficulty': self.difficulty.value,
|
||||
'estimated_time': self.estimated_time,
|
||||
'usage_count': self.usage_count,
|
||||
'rating': self.rating,
|
||||
'created_at': self.created_at.isoformat(),
|
||||
'updated_at': self.updated_at.isoformat(),
|
||||
'created_by': self.created_by
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'WorkflowTemplate':
|
||||
return cls(
|
||||
id=data['id'],
|
||||
name=data['name'],
|
||||
description=data['description'],
|
||||
category=data['category'],
|
||||
workflow=VisualWorkflow.from_dict(data['workflow']),
|
||||
parameters=[TemplateParameter.from_dict(p) for p in data.get('parameters', [])],
|
||||
tags=data.get('tags', []),
|
||||
difficulty=TemplateDifficulty(data.get('difficulty', 'beginner')),
|
||||
estimated_time=data.get('estimated_time', 5),
|
||||
usage_count=data.get('usage_count', 0),
|
||||
rating=data.get('rating', 0.0),
|
||||
created_at=datetime.fromisoformat(data.get('created_at', datetime.now().isoformat())),
|
||||
updated_at=datetime.fromisoformat(data.get('updated_at', datetime.now().isoformat())),
|
||||
created_by=data.get('created_by', 'system')
|
||||
)
|
||||
|
||||
def instantiate(self, parameters: Dict[str, Any], workflow_name: str, created_by: str = "user") -> VisualWorkflow:
|
||||
"""Create a new workflow instance from this template"""
|
||||
# Create a copy of the template workflow
|
||||
workflow_data = self.workflow.to_dict()
|
||||
|
||||
# Generate new IDs
|
||||
workflow_data['id'] = str(uuid4())
|
||||
workflow_data['name'] = workflow_name
|
||||
workflow_data['created_at'] = datetime.now().isoformat()
|
||||
workflow_data['updated_at'] = datetime.now().isoformat()
|
||||
workflow_data['created_by'] = created_by
|
||||
workflow_data['is_template'] = False
|
||||
|
||||
# Apply parameter substitutions
|
||||
for param in self.parameters:
|
||||
if param.name in parameters:
|
||||
value = parameters[param.name]
|
||||
|
||||
# Find the target node and update its parameter
|
||||
for node_data in workflow_data['nodes']:
|
||||
if node_data['id'] == param.node_id:
|
||||
node_data['parameters'][param.parameter_name] = value
|
||||
break
|
||||
|
||||
# Generate new node and edge IDs to avoid conflicts
|
||||
node_id_mapping = {}
|
||||
for i, node_data in enumerate(workflow_data['nodes']):
|
||||
old_id = node_data['id']
|
||||
new_id = f"{workflow_data['id']}_node_{i}"
|
||||
node_id_mapping[old_id] = new_id
|
||||
node_data['id'] = new_id
|
||||
|
||||
# Update edge references
|
||||
for edge_data in workflow_data['edges']:
|
||||
edge_data['id'] = str(uuid4())
|
||||
edge_data['source'] = node_id_mapping.get(edge_data['source'], edge_data['source'])
|
||||
edge_data['target'] = node_id_mapping.get(edge_data['target'], edge_data['target'])
|
||||
|
||||
return VisualWorkflow.from_dict(workflow_data)
|
||||
|
||||
def validate(self) -> List[str]:
|
||||
"""Validate template structure"""
|
||||
errors = []
|
||||
|
||||
# Basic validation
|
||||
if not self.id:
|
||||
errors.append("Template ID is required")
|
||||
if not self.name:
|
||||
errors.append("Template name is required")
|
||||
if not self.category:
|
||||
errors.append("Template category is required")
|
||||
|
||||
# Validate workflow
|
||||
workflow_errors = self.workflow.validate()
|
||||
errors.extend([f"Workflow: {err}" for err in workflow_errors])
|
||||
|
||||
# Validate parameters
|
||||
node_ids = {node.id for node in self.workflow.nodes}
|
||||
for param in self.parameters:
|
||||
if param.node_id and param.node_id not in node_ids:
|
||||
errors.append(f"Parameter {param.name} references non-existent node {param.node_id}")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def generate_template_id() -> str:
|
||||
"""Generate a unique template ID"""
|
||||
return f"template_{str(uuid4())[:8]}"
|
||||
540
visual_workflow_builder/backend/models/visual_workflow.py
Normal file
540
visual_workflow_builder/backend/models/visual_workflow.py
Normal file
@@ -0,0 +1,540 @@
|
||||
"""
|
||||
Visual Workflow Data Models
|
||||
|
||||
Contains the core data models for visual workflows, nodes, edges, and related structures.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional
|
||||
from uuid import uuid4
|
||||
from .self_healing_config import SelfHealingConfig
|
||||
|
||||
|
||||
class NodeCategory(Enum):
|
||||
"""Categories for organizing node types"""
|
||||
ACTION = 'action'
|
||||
LOGIC = 'logic'
|
||||
DATA = 'data'
|
||||
FLOW = 'flow'
|
||||
INTEGRATION = 'integration'
|
||||
|
||||
|
||||
class NodeStatus(Enum):
|
||||
"""Execution status of a node"""
|
||||
IDLE = 'idle'
|
||||
RUNNING = 'running'
|
||||
SUCCESS = 'success'
|
||||
FAILED = 'failed'
|
||||
SKIPPED = 'skipped'
|
||||
|
||||
|
||||
class ParameterType(Enum):
|
||||
"""Types of parameters supported"""
|
||||
STRING = 'string'
|
||||
NUMBER = 'number'
|
||||
BOOLEAN = 'boolean'
|
||||
SELECT = 'select'
|
||||
TARGET = 'target'
|
||||
VARIABLE = 'variable'
|
||||
EXPRESSION = 'expression'
|
||||
FILE = 'file'
|
||||
|
||||
|
||||
@dataclass
|
||||
class Position:
|
||||
"""2D position in the canvas"""
|
||||
x: float
|
||||
y: float
|
||||
|
||||
def to_dict(self) -> Dict[str, float]:
|
||||
return {'x': self.x, 'y': self.y}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, float]) -> 'Position':
|
||||
return cls(x=data['x'], y=data['y'])
|
||||
|
||||
|
||||
@dataclass
|
||||
class Size:
|
||||
"""2D size dimensions"""
|
||||
width: float
|
||||
height: float
|
||||
|
||||
def to_dict(self) -> Dict[str, float]:
|
||||
return {'width': self.width, 'height': self.height}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, float]) -> 'Size':
|
||||
return cls(width=data['width'], height=data['height'])
|
||||
|
||||
|
||||
@dataclass
|
||||
class Port:
|
||||
"""Input or output port on a node"""
|
||||
id: str
|
||||
name: str
|
||||
type: str # 'input' or 'output'
|
||||
data_type: Optional[str] = None # Type of data flowing through
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'type': self.type,
|
||||
'data_type': self.data_type
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'Port':
|
||||
return cls(
|
||||
id=data['id'],
|
||||
name=data['name'],
|
||||
type=data['type'],
|
||||
data_type=data.get('data_type')
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationRule:
|
||||
"""Validation rule for parameters"""
|
||||
type: str # 'required', 'min', 'max', 'pattern', 'custom'
|
||||
value: Any
|
||||
message: str
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'type': self.type,
|
||||
'value': self.value,
|
||||
'message': self.message
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'ValidationRule':
|
||||
return cls(
|
||||
type=data['type'],
|
||||
value=data['value'],
|
||||
message=data['message']
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParameterDefinition:
|
||||
"""Definition of a node parameter"""
|
||||
name: str
|
||||
type: ParameterType
|
||||
required: bool
|
||||
default_value: Optional[Any] = None
|
||||
|
||||
# Validation
|
||||
validation: Optional[List[ValidationRule]] = None
|
||||
|
||||
# UI
|
||||
label: str = ""
|
||||
description: Optional[str] = None
|
||||
placeholder: Optional[str] = None
|
||||
|
||||
# Special behavior
|
||||
is_target: bool = False
|
||||
is_variable: bool = False
|
||||
is_expression: bool = False
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'name': self.name,
|
||||
'type': self.type.value,
|
||||
'required': self.required,
|
||||
'default_value': self.default_value,
|
||||
'validation': [v.to_dict() for v in self.validation] if self.validation else None,
|
||||
'label': self.label,
|
||||
'description': self.description,
|
||||
'placeholder': self.placeholder,
|
||||
'is_target': self.is_target,
|
||||
'is_variable': self.is_variable,
|
||||
'is_expression': self.is_expression
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'ParameterDefinition':
|
||||
validation = None
|
||||
if data.get('validation'):
|
||||
validation = [ValidationRule.from_dict(v) for v in data['validation']]
|
||||
|
||||
return cls(
|
||||
name=data['name'],
|
||||
type=ParameterType(data['type']),
|
||||
required=data['required'],
|
||||
default_value=data.get('default_value'),
|
||||
validation=validation,
|
||||
label=data.get('label', ''),
|
||||
description=data.get('description'),
|
||||
placeholder=data.get('placeholder'),
|
||||
is_target=data.get('is_target', False),
|
||||
is_variable=data.get('is_variable', False),
|
||||
is_expression=data.get('is_expression', False)
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class EdgeStyle:
|
||||
"""Visual style for an edge"""
|
||||
color: Optional[str] = None
|
||||
width: Optional[float] = None
|
||||
dashed: bool = False
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'color': self.color,
|
||||
'width': self.width,
|
||||
'dashed': self.dashed
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'EdgeStyle':
|
||||
return cls(
|
||||
color=data.get('color'),
|
||||
width=data.get('width'),
|
||||
dashed=data.get('dashed', False)
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class EdgeCondition:
|
||||
"""Condition for edge execution"""
|
||||
type: str # 'always', 'success', 'failure', 'expression'
|
||||
expression: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'type': self.type,
|
||||
'expression': self.expression
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'EdgeCondition':
|
||||
return cls(
|
||||
type=data['type'],
|
||||
expression=data.get('expression')
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class VisualEdge:
|
||||
"""Connection between nodes"""
|
||||
id: str
|
||||
source: str # node ID
|
||||
target: str # node ID
|
||||
source_port: str
|
||||
target_port: str
|
||||
|
||||
# Condition
|
||||
condition: Optional[EdgeCondition] = None
|
||||
|
||||
# Visual style
|
||||
style: Optional[EdgeStyle] = None
|
||||
|
||||
# State
|
||||
selected: bool = False
|
||||
animated: bool = False
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'id': self.id,
|
||||
'source': self.source,
|
||||
'target': self.target,
|
||||
'source_port': self.source_port,
|
||||
'target_port': self.target_port,
|
||||
'condition': self.condition.to_dict() if self.condition else None,
|
||||
'style': self.style.to_dict() if self.style else None,
|
||||
'selected': self.selected,
|
||||
'animated': self.animated
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'VisualEdge':
|
||||
condition = None
|
||||
if data.get('condition'):
|
||||
condition = EdgeCondition.from_dict(data['condition'])
|
||||
|
||||
style = None
|
||||
if data.get('style'):
|
||||
style = EdgeStyle.from_dict(data['style'])
|
||||
|
||||
# Gérer les ports manquants (format VWB vs standard)
|
||||
# VWB utilise sourceHandle/targetHandle, standard utilise source_port/target_port
|
||||
source_port = data.get('source_port') or data.get('sourceHandle', 'out')
|
||||
target_port = data.get('target_port') or data.get('targetHandle', 'in')
|
||||
|
||||
return cls(
|
||||
id=data['id'],
|
||||
source=data['source'],
|
||||
target=data['target'],
|
||||
source_port=source_port,
|
||||
target_port=target_port,
|
||||
condition=condition,
|
||||
style=style,
|
||||
selected=data.get('selected', False),
|
||||
animated=data.get('animated', False)
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class VisualNode:
|
||||
"""Visual representation of a workflow node"""
|
||||
id: str
|
||||
type: str # 'click', 'type', 'wait', 'if', 'loop', etc.
|
||||
|
||||
# Visual position
|
||||
position: Position
|
||||
size: Size
|
||||
|
||||
# Configuration
|
||||
parameters: Dict[str, Any]
|
||||
|
||||
# Connections
|
||||
input_ports: List[Port]
|
||||
output_ports: List[Port]
|
||||
|
||||
# Self-healing configuration
|
||||
self_healing: Optional[SelfHealingConfig] = None
|
||||
|
||||
# Visual state
|
||||
selected: bool = False
|
||||
highlighted: bool = False
|
||||
status: Optional[NodeStatus] = None
|
||||
|
||||
# Metadata
|
||||
label: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
|
||||
# Données VWB complètes (préserve visualSelection, isVWBCatalogAction, etc.)
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
result = {
|
||||
'id': self.id,
|
||||
'type': self.type,
|
||||
'position': self.position.to_dict(),
|
||||
'size': self.size.to_dict(),
|
||||
'parameters': self.parameters,
|
||||
'input_ports': [p.to_dict() for p in self.input_ports],
|
||||
'output_ports': [p.to_dict() for p in self.output_ports],
|
||||
'self_healing': self.self_healing.to_dict() if self.self_healing else None,
|
||||
'selected': self.selected,
|
||||
'highlighted': self.highlighted,
|
||||
'status': self.status.value if self.status else None,
|
||||
'label': self.label,
|
||||
'description': self.description,
|
||||
'color': self.color
|
||||
}
|
||||
# Inclure les données VWB complètes si présentes
|
||||
if self.data:
|
||||
result['data'] = self.data
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'VisualNode':
|
||||
status = None
|
||||
if data.get('status'):
|
||||
status = NodeStatus(data['status'])
|
||||
|
||||
self_healing = None
|
||||
if data.get('self_healing'):
|
||||
self_healing = SelfHealingConfig.from_dict(data['self_healing'])
|
||||
|
||||
# Gérer les différents formats de données (VWB vs standard)
|
||||
# Format VWB: data.parameters contient les paramètres
|
||||
# Format standard: parameters directement au niveau du node
|
||||
if 'data' in data and isinstance(data['data'], dict):
|
||||
parameters = data['data'].get('parameters', data.get('parameters', {}))
|
||||
else:
|
||||
parameters = data.get('parameters', {})
|
||||
|
||||
# Gérer size manquant (défaut: 200x80)
|
||||
if 'size' in data:
|
||||
size = Size.from_dict(data['size'])
|
||||
else:
|
||||
size = Size(width=200, height=80)
|
||||
|
||||
# Gérer ports manquants (défaut: ports vides)
|
||||
input_ports = [Port.from_dict(p) for p in data.get('input_ports', [])]
|
||||
output_ports = [Port.from_dict(p) for p in data.get('output_ports', [])]
|
||||
|
||||
# Préserver les données VWB complètes
|
||||
vwb_data = data.get('data') if isinstance(data.get('data'), dict) else None
|
||||
|
||||
return cls(
|
||||
id=data['id'],
|
||||
type=data['type'],
|
||||
position=Position.from_dict(data['position']),
|
||||
size=size,
|
||||
parameters=parameters,
|
||||
input_ports=input_ports,
|
||||
output_ports=output_ports,
|
||||
self_healing=self_healing,
|
||||
selected=data.get('selected', False),
|
||||
highlighted=data.get('highlighted', False),
|
||||
status=status,
|
||||
label=data.get('label') or data.get('name'),
|
||||
description=data.get('description'),
|
||||
color=data.get('color'),
|
||||
data=vwb_data
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Variable:
|
||||
"""Workflow variable"""
|
||||
name: str
|
||||
type: str # 'string', 'number', 'boolean', 'object'
|
||||
value: Any
|
||||
description: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'name': self.name,
|
||||
'type': self.type,
|
||||
'value': self.value,
|
||||
'description': self.description
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'Variable':
|
||||
return cls(
|
||||
name=data['name'],
|
||||
type=data['type'],
|
||||
value=data['value'],
|
||||
description=data.get('description')
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkflowSettings:
|
||||
"""Workflow execution settings"""
|
||||
timeout: int = 300000 # 5 minutes default
|
||||
retry_on_failure: bool = True
|
||||
max_retries: int = 3
|
||||
enable_self_healing: bool = True
|
||||
enable_analytics: bool = True
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'timeout': self.timeout,
|
||||
'retry_on_failure': self.retry_on_failure,
|
||||
'max_retries': self.max_retries,
|
||||
'enable_self_healing': self.enable_self_healing,
|
||||
'enable_analytics': self.enable_analytics
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'WorkflowSettings':
|
||||
return cls(
|
||||
timeout=data.get('timeout', 300000),
|
||||
retry_on_failure=data.get('retry_on_failure', True),
|
||||
max_retries=data.get('max_retries', 3),
|
||||
enable_self_healing=data.get('enable_self_healing', True),
|
||||
enable_analytics=data.get('enable_analytics', True)
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class VisualWorkflow:
|
||||
"""Complete visual workflow representation"""
|
||||
id: str
|
||||
name: str
|
||||
description: Optional[str]
|
||||
version: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
created_by: str
|
||||
|
||||
# Visual structure
|
||||
nodes: List[VisualNode]
|
||||
edges: List[VisualEdge]
|
||||
|
||||
# Configuration
|
||||
variables: List[Variable]
|
||||
settings: WorkflowSettings
|
||||
|
||||
# Metadata
|
||||
tags: List[str] = field(default_factory=list)
|
||||
category: Optional[str] = None
|
||||
is_template: bool = False
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'version': self.version,
|
||||
'created_at': self.created_at.isoformat(),
|
||||
'updated_at': self.updated_at.isoformat(),
|
||||
'created_by': self.created_by,
|
||||
'nodes': [n.to_dict() for n in self.nodes],
|
||||
'edges': [e.to_dict() for e in self.edges],
|
||||
'variables': [v.to_dict() for v in self.variables],
|
||||
'settings': self.settings.to_dict(),
|
||||
'tags': self.tags,
|
||||
'category': self.category,
|
||||
'is_template': self.is_template
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'VisualWorkflow':
|
||||
return cls(
|
||||
id=data['id'],
|
||||
name=data['name'],
|
||||
description=data.get('description'),
|
||||
version=data['version'],
|
||||
created_at=datetime.fromisoformat(data['created_at']),
|
||||
updated_at=datetime.fromisoformat(data['updated_at']),
|
||||
created_by=data['created_by'],
|
||||
nodes=[VisualNode.from_dict(n) for n in data['nodes']],
|
||||
edges=[VisualEdge.from_dict(e) for e in data['edges']],
|
||||
variables=[Variable.from_dict(v) for v in data['variables']],
|
||||
settings=WorkflowSettings.from_dict(data['settings']),
|
||||
tags=data.get('tags', []),
|
||||
category=data.get('category'),
|
||||
is_template=data.get('is_template', False)
|
||||
)
|
||||
|
||||
def validate(self) -> List[str]:
|
||||
"""Validate workflow structure and return list of errors"""
|
||||
errors = []
|
||||
|
||||
# Check required fields
|
||||
if not self.id:
|
||||
errors.append("Workflow ID is required")
|
||||
if not self.name:
|
||||
errors.append("Workflow name is required")
|
||||
if not self.version:
|
||||
errors.append("Workflow version is required")
|
||||
|
||||
# Validate nodes
|
||||
node_ids = {node.id for node in self.nodes}
|
||||
for node in self.nodes:
|
||||
if not node.id:
|
||||
errors.append(f"Node missing ID")
|
||||
if not node.type:
|
||||
errors.append(f"Node {node.id} missing type")
|
||||
|
||||
# Validate edges
|
||||
for edge in self.edges:
|
||||
if edge.source not in node_ids:
|
||||
errors.append(f"Edge {edge.id} references non-existent source node {edge.source}")
|
||||
if edge.target not in node_ids:
|
||||
errors.append(f"Edge {edge.id} references non-existent target node {edge.target}")
|
||||
|
||||
# Validate variables
|
||||
variable_names = {var.name for var in self.variables}
|
||||
if len(variable_names) != len(self.variables):
|
||||
errors.append("Duplicate variable names found")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def generate_id(prefix: str = "wf") -> str:
|
||||
"""Generate a unique ID"""
|
||||
return str(uuid4())
|
||||
Reference in New Issue
Block a user