Files
rpa_vision_v3/visual_workflow_builder/backend/models.py
Dom a27b74cf22 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>
2026-01-29 11:23:51 +01:00

269 lines
9.1 KiB
Python

"""backend/models.py
Modèles "visuels" minimalistes pour le Visual Workflow Builder.
Auteur : Dom, Alice, Kiro - 08 janvier 2026
Objectif du Patch #1:
- Fournir des structures de données stables (to_dict/from_dict)
- Permettre la persistance disque (JSON/YAML) via services.serialization
- Permettre au backend de démarrer même si le reste du core RPA n'est pas branché
NB: Ces modèles sont volontairement permissifs (ils conservent les champs inconnus).
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Dict, List, Optional
from uuid import uuid4
def generate_id(prefix: str = "wf") -> str:
"""Génère un identifiant court et lisible."""
return f"{prefix}_{uuid4().hex[:12]}"
@dataclass
class WorkflowSettings:
"""Sac de paramètres pour un workflow."""
data: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls, d: Any) -> "WorkflowSettings":
if isinstance(d, WorkflowSettings):
return d
if not isinstance(d, dict):
return cls({})
return cls(dict(d))
def to_dict(self) -> Dict[str, Any]:
return dict(self.data)
@dataclass
class VisualNode:
"""Représentation d'un nœud dans le canvas visuel."""
id: str
type: str = "unknown"
position: Dict[str, Any] = field(default_factory=dict)
data: Dict[str, Any] = field(default_factory=dict)
style: Dict[str, Any] = field(default_factory=dict)
extra: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "VisualNode":
if not isinstance(d, dict):
raise ValueError("Le nœud doit être un dictionnaire")
node_id = d.get("id") or generate_id("node")
node_type = d.get("type") or d.get("node_type") or "unknown"
known = {"id", "type", "node_type", "position", "data", "style"}
extra = {k: v for k, v in d.items() if k not in known}
return cls(
id=str(node_id),
type=str(node_type),
position=dict(d.get("position") or {}),
data=dict(d.get("data") or {}),
style=dict(d.get("style") or {}),
extra=extra,
)
def to_dict(self) -> Dict[str, Any]:
out = {
"id": self.id,
"type": self.type,
"position": self.position,
"data": self.data,
"style": self.style,
}
out.update(self.extra)
return out
@dataclass
class VisualEdge:
"""Représentation d'une connexion entre nœuds."""
id: str
source: str
target: str
type: str = "default"
label: Optional[str] = None
data: Dict[str, Any] = field(default_factory=dict)
extra: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "VisualEdge":
if not isinstance(d, dict):
raise ValueError("La connexion doit être un dictionnaire")
edge_id = d.get("id") or generate_id("edge")
source = d.get("source")
target = d.get("target")
if not source or not target:
raise ValueError("La connexion nécessite 'source' et 'target'")
known = {"id", "source", "target", "type", "label", "data"}
extra = {k: v for k, v in d.items() if k not in known}
return cls(
id=str(edge_id),
source=str(source),
target=str(target),
type=str(d.get("type") or "default"),
label=d.get("label"),
data=dict(d.get("data") or {}),
extra=extra,
)
def to_dict(self) -> Dict[str, Any]:
out = {
"id": self.id,
"source": self.source,
"target": self.target,
"type": self.type,
"label": self.label,
"data": self.data,
}
out.update(self.extra)
return out
@dataclass
class Variable:
"""Variable de workflow."""
name: str
value: Any = None
var_type: str = "any"
scope: str = "workflow"
description: str = ""
required: bool = False
extra: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "Variable":
if not isinstance(d, dict):
raise ValueError("La variable doit être un dictionnaire")
name = d.get("name") or d.get("key")
if not name:
raise ValueError("La variable nécessite un 'name'")
known = {"name", "key", "value", "type", "var_type", "scope", "description", "required"}
extra = {k: v for k, v in d.items() if k not in known}
return cls(
name=str(name),
value=d.get("value"),
var_type=str(d.get("type") or d.get("var_type") or "any"),
scope=str(d.get("scope") or "workflow"),
description=str(d.get("description") or ""),
required=bool(d.get("required") or False),
extra=extra,
)
def to_dict(self) -> Dict[str, Any]:
out = {
"name": self.name,
"value": self.value,
"type": self.var_type,
"scope": self.scope,
"description": self.description,
"required": self.required,
}
out.update(self.extra)
return out
@dataclass
class VisualWorkflow:
"""Workflow visuel complet."""
id: str
name: str
description: str = ""
created_by: str = "unknown"
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
nodes: List[VisualNode] = field(default_factory=list)
edges: List[VisualEdge] = field(default_factory=list)
variables: List[Variable] = field(default_factory=list)
settings: WorkflowSettings = field(default_factory=WorkflowSettings)
tags: List[str] = field(default_factory=list)
category: str = "default"
is_template: bool = False
extra: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "VisualWorkflow":
if not isinstance(d, dict):
raise ValueError("Le workflow doit être un dictionnaire")
wf_id = d.get("id") or generate_id("wf")
name = d.get("name") or "Sans titre"
known = {
"id", "name", "description", "created_by", "created_at", "updated_at",
"nodes", "edges", "variables", "settings", "tags", "category", "is_template"
}
extra = {k: v for k, v in d.items() if k not in known}
nodes = [VisualNode.from_dict(n) for n in (d.get("nodes") or [])]
edges = [VisualEdge.from_dict(e) for e in (d.get("edges") or [])]
variables = [Variable.from_dict(v) for v in (d.get("variables") or [])]
settings = WorkflowSettings.from_dict(d.get("settings") or {})
return cls(
id=str(wf_id),
name=str(name),
description=str(d.get("description") or ""),
created_by=str(d.get("created_by") or "unknown"),
created_at=str(d.get("created_at") or datetime.now().isoformat()),
updated_at=str(d.get("updated_at") or datetime.now().isoformat()),
nodes=nodes,
edges=edges,
variables=variables,
settings=settings,
tags=list(d.get("tags") or []),
category=str(d.get("category") or "default"),
is_template=bool(d.get("is_template") or False),
extra=extra,
)
def to_dict(self) -> Dict[str, Any]:
out = {
"id": self.id,
"name": self.name,
"description": self.description,
"created_by": self.created_by,
"created_at": self.created_at,
"updated_at": self.updated_at,
"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,
}
out.update(self.extra)
return out
def validate(self) -> List[str]:
"""Valide le workflow et retourne la liste des erreurs."""
errors: List[str] = []
if not self.name or not str(self.name).strip():
errors.append("le nom est requis")
node_ids = [n.id for n in self.nodes]
if len(node_ids) != len(set(node_ids)):
errors.append("identifiants de nœuds dupliqués")
# Les connexions doivent référencer des nœuds existants
nodes_set = set(node_ids)
for e in self.edges:
if e.source not in nodes_set:
errors.append(f"connexion {e.id} source '{e.source}' n'existe pas")
if e.target not in nodes_set:
errors.append(f"connexion {e.id} target '{e.target}' n'existe pas")
# Les variables doivent être uniques par nom
var_names = [v.name for v in self.variables]
if len(var_names) != len(set(var_names)):
errors.append("noms de variables dupliqués")
return errors