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:
Dom
2026-01-29 11:23:51 +01:00
parent 21bfa3b337
commit a27b74cf22
1595 changed files with 412691 additions and 400 deletions

View 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'
]

View 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
}

View 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]}"

View 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())