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:
517
visual_workflow_builder/backend/actions/registry.py
Normal file
517
visual_workflow_builder/backend/actions/registry.py
Normal file
@@ -0,0 +1,517 @@
|
||||
"""
|
||||
Registry Actions VWB - Gestionnaire d'Actions VisionOnly
|
||||
|
||||
Auteur : Dom, Alice, Kiro - 09 janvier 2026
|
||||
|
||||
Ce module implémente le registry des actions VisionOnly pour le Visual Workflow Builder.
|
||||
Il permet l'enregistrement, la recherche et la gestion des actions disponibles.
|
||||
|
||||
Fonctionnalités :
|
||||
- Enregistrement automatique des actions
|
||||
- Recherche par catégorie et type
|
||||
- Thread-safety pour accès concurrent
|
||||
- Chargement dynamique des actions
|
||||
"""
|
||||
|
||||
import threading
|
||||
from typing import Dict, List, Optional, Type, Any, Set
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import importlib
|
||||
import inspect
|
||||
|
||||
from .base_action import BaseVWBAction
|
||||
|
||||
|
||||
class VWBActionRegistry:
|
||||
"""
|
||||
Registry thread-safe pour les actions VWB.
|
||||
|
||||
Ce registry maintient un catalogue des actions disponibles et permet
|
||||
leur recherche et instanciation dynamique.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialise le registry."""
|
||||
self._actions: Dict[str, Type[BaseVWBAction]] = {}
|
||||
self._categories: Dict[str, Set[str]] = {}
|
||||
self._metadata: Dict[str, Dict[str, Any]] = {}
|
||||
self._lock = threading.RLock()
|
||||
self._initialized = False
|
||||
|
||||
print("📋 Registry Actions VWB initialisé")
|
||||
|
||||
def register_action(self,
|
||||
action_class: Type[BaseVWBAction],
|
||||
action_id: Optional[str] = None,
|
||||
category: str = "default",
|
||||
metadata: Optional[Dict[str, Any]] = None) -> bool:
|
||||
"""
|
||||
Enregistre une action dans le registry.
|
||||
|
||||
Args:
|
||||
action_class: Classe de l'action à enregistrer
|
||||
action_id: Identifiant unique (auto-généré si None)
|
||||
category: Catégorie de l'action
|
||||
metadata: Métadonnées additionnelles
|
||||
|
||||
Returns:
|
||||
True si l'enregistrement a réussi
|
||||
"""
|
||||
with self._lock:
|
||||
try:
|
||||
# Générer l'ID si non fourni
|
||||
if action_id is None:
|
||||
action_id = self._generate_action_id(action_class)
|
||||
|
||||
# Vérifier que l'action hérite de BaseVWBAction
|
||||
if not issubclass(action_class, BaseVWBAction):
|
||||
print(f"⚠️ {action_class.__name__} n'hérite pas de BaseVWBAction")
|
||||
return False
|
||||
|
||||
# Vérifier l'unicité de l'ID
|
||||
if action_id in self._actions:
|
||||
print(f"⚠️ Action '{action_id}' déjà enregistrée")
|
||||
return False
|
||||
|
||||
# Enregistrer l'action
|
||||
self._actions[action_id] = action_class
|
||||
|
||||
# Gérer les catégories
|
||||
if category not in self._categories:
|
||||
self._categories[category] = set()
|
||||
self._categories[category].add(action_id)
|
||||
|
||||
# Stocker les métadonnées
|
||||
self._metadata[action_id] = {
|
||||
'class_name': action_class.__name__,
|
||||
'module': action_class.__module__,
|
||||
'category': category,
|
||||
'registered_at': datetime.now().isoformat(),
|
||||
'metadata': metadata or {}
|
||||
}
|
||||
|
||||
print(f"✅ Action '{action_id}' enregistrée (catégorie: {category})")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur enregistrement action '{action_id}': {e}")
|
||||
return False
|
||||
|
||||
def get_action_class(self, action_id: str) -> Optional[Type[BaseVWBAction]]:
|
||||
"""
|
||||
Récupère la classe d'une action par son ID.
|
||||
|
||||
Args:
|
||||
action_id: Identifiant de l'action
|
||||
|
||||
Returns:
|
||||
Classe de l'action ou None si non trouvée
|
||||
"""
|
||||
with self._lock:
|
||||
return self._actions.get(action_id)
|
||||
|
||||
def create_action(self,
|
||||
action_id: str,
|
||||
parameters: Optional[Dict[str, Any]] = None,
|
||||
**kwargs) -> Optional[BaseVWBAction]:
|
||||
"""
|
||||
Crée une instance d'action.
|
||||
|
||||
Args:
|
||||
action_id: Identifiant de l'action
|
||||
parameters: Paramètres de l'action
|
||||
**kwargs: Arguments additionnels pour le constructeur
|
||||
|
||||
Returns:
|
||||
Instance de l'action ou None si erreur
|
||||
"""
|
||||
with self._lock:
|
||||
action_class = self._actions.get(action_id)
|
||||
if action_class is None:
|
||||
print(f"⚠️ Action '{action_id}' non trouvée dans le registry")
|
||||
return None
|
||||
|
||||
try:
|
||||
# Préparer les arguments du constructeur
|
||||
constructor_args = {
|
||||
'action_id': f"{action_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
|
||||
'parameters': parameters or {}
|
||||
}
|
||||
constructor_args.update(kwargs)
|
||||
|
||||
# Créer l'instance
|
||||
instance = action_class(**constructor_args)
|
||||
print(f"✅ Instance d'action '{action_id}' créée")
|
||||
return instance
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur création instance '{action_id}': {e}")
|
||||
return None
|
||||
|
||||
def list_actions(self, category: Optional[str] = None) -> List[str]:
|
||||
"""
|
||||
Liste les actions disponibles.
|
||||
|
||||
Args:
|
||||
category: Filtrer par catégorie (optionnel)
|
||||
|
||||
Returns:
|
||||
Liste des IDs d'actions
|
||||
"""
|
||||
with self._lock:
|
||||
if category is None:
|
||||
return list(self._actions.keys())
|
||||
else:
|
||||
return list(self._categories.get(category, set()))
|
||||
|
||||
def list_categories(self) -> List[str]:
|
||||
"""
|
||||
Liste les catégories disponibles.
|
||||
|
||||
Returns:
|
||||
Liste des catégories
|
||||
"""
|
||||
with self._lock:
|
||||
return list(self._categories.keys())
|
||||
|
||||
def get_action_metadata(self, action_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Récupère les métadonnées d'une action.
|
||||
|
||||
Args:
|
||||
action_id: Identifiant de l'action
|
||||
|
||||
Returns:
|
||||
Métadonnées ou None si non trouvée
|
||||
"""
|
||||
with self._lock:
|
||||
return self._metadata.get(action_id)
|
||||
|
||||
def search_actions(self,
|
||||
query: str,
|
||||
category: Optional[str] = None) -> List[str]:
|
||||
"""
|
||||
Recherche des actions par nom ou description.
|
||||
|
||||
Args:
|
||||
query: Terme de recherche
|
||||
category: Filtrer par catégorie (optionnel)
|
||||
|
||||
Returns:
|
||||
Liste des IDs d'actions correspondantes
|
||||
"""
|
||||
with self._lock:
|
||||
query_lower = query.lower()
|
||||
results = []
|
||||
|
||||
actions_to_search = self.list_actions(category)
|
||||
|
||||
for action_id in actions_to_search:
|
||||
metadata = self._metadata.get(action_id, {})
|
||||
class_name = metadata.get('class_name', '').lower()
|
||||
|
||||
# Rechercher dans l'ID et le nom de classe
|
||||
if (query_lower in action_id.lower() or
|
||||
query_lower in class_name):
|
||||
results.append(action_id)
|
||||
|
||||
return results
|
||||
|
||||
def get_registry_stats(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Obtient les statistiques du registry.
|
||||
|
||||
Returns:
|
||||
Dictionnaire avec les statistiques
|
||||
"""
|
||||
with self._lock:
|
||||
return {
|
||||
'total_actions': len(self._actions),
|
||||
'categories': {
|
||||
cat: len(actions)
|
||||
for cat, actions in self._categories.items()
|
||||
},
|
||||
'initialized': self._initialized,
|
||||
'last_update': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def auto_discover_actions(self, base_path: Optional[Path] = None) -> int:
|
||||
"""
|
||||
Découvre et enregistre automatiquement les actions.
|
||||
|
||||
Args:
|
||||
base_path: Chemin de base pour la découverte (optionnel)
|
||||
|
||||
Returns:
|
||||
Nombre d'actions découvertes
|
||||
"""
|
||||
with self._lock:
|
||||
if base_path is None:
|
||||
base_path = Path(__file__).parent
|
||||
|
||||
discovered_count = 0
|
||||
|
||||
try:
|
||||
# Découvrir les actions dans vision_ui
|
||||
vision_ui_path = base_path / "vision_ui"
|
||||
if vision_ui_path.exists():
|
||||
discovered_count += self._discover_in_directory(
|
||||
vision_ui_path, "vision_ui"
|
||||
)
|
||||
|
||||
# Découvrir les actions dans d'autres catégories
|
||||
for category_dir in base_path.iterdir():
|
||||
if (category_dir.is_dir() and
|
||||
category_dir.name not in ["__pycache__", "vision_ui"] and
|
||||
not category_dir.name.startswith(".")):
|
||||
|
||||
discovered_count += self._discover_in_directory(
|
||||
category_dir, category_dir.name
|
||||
)
|
||||
|
||||
self._initialized = True
|
||||
print(f"🔍 Découverte automatique terminée : {discovered_count} actions trouvées")
|
||||
return discovered_count
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur découverte automatique : {e}")
|
||||
return discovered_count
|
||||
|
||||
def _discover_in_directory(self, directory: Path, category: str) -> int:
|
||||
"""
|
||||
Découvre les actions dans un répertoire.
|
||||
|
||||
Args:
|
||||
directory: Répertoire à explorer
|
||||
category: Catégorie des actions
|
||||
|
||||
Returns:
|
||||
Nombre d'actions découvertes
|
||||
"""
|
||||
discovered_count = 0
|
||||
|
||||
for py_file in directory.glob("*.py"):
|
||||
if py_file.name.startswith("__"):
|
||||
continue
|
||||
|
||||
try:
|
||||
# Construire le nom du module
|
||||
module_name = f"visual_workflow_builder.backend.actions.{category}.{py_file.stem}"
|
||||
|
||||
# Importer le module
|
||||
module = importlib.import_module(module_name)
|
||||
|
||||
# Chercher les classes d'actions
|
||||
for name, obj in inspect.getmembers(module, inspect.isclass):
|
||||
if (obj != BaseVWBAction and
|
||||
issubclass(obj, BaseVWBAction) and
|
||||
obj.__module__ == module.__name__):
|
||||
|
||||
# Générer l'ID de l'action
|
||||
action_id = self._generate_action_id(obj)
|
||||
|
||||
# Enregistrer l'action
|
||||
if self.register_action(obj, action_id, category):
|
||||
discovered_count += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Erreur import {py_file}: {e}")
|
||||
|
||||
return discovered_count
|
||||
|
||||
def _generate_action_id(self, action_class: Type[BaseVWBAction]) -> str:
|
||||
"""
|
||||
Génère un ID d'action à partir de la classe.
|
||||
|
||||
Args:
|
||||
action_class: Classe de l'action
|
||||
|
||||
Returns:
|
||||
ID généré
|
||||
"""
|
||||
class_name = action_class.__name__
|
||||
|
||||
# Convertir VWBClickAnchorAction -> click_anchor
|
||||
if class_name.startswith("VWB") and class_name.endswith("Action"):
|
||||
# Enlever VWB et Action
|
||||
core_name = class_name[3:-6]
|
||||
|
||||
# Convertir CamelCase en snake_case
|
||||
import re
|
||||
snake_case = re.sub('([A-Z]+)', r'_\1', core_name).lower()
|
||||
return snake_case.lstrip('_')
|
||||
|
||||
# Fallback : utiliser le nom de classe en minuscules
|
||||
return class_name.lower()
|
||||
|
||||
def clear(self):
|
||||
"""Vide le registry."""
|
||||
with self._lock:
|
||||
self._actions.clear()
|
||||
self._categories.clear()
|
||||
self._metadata.clear()
|
||||
self._initialized = False
|
||||
print("🗑️ Registry vidé")
|
||||
|
||||
|
||||
# Instance globale du registry
|
||||
_global_registry: Optional[VWBActionRegistry] = None
|
||||
_registry_lock = threading.Lock()
|
||||
|
||||
|
||||
def get_global_registry() -> VWBActionRegistry:
|
||||
"""
|
||||
Obtient l'instance globale du registry (singleton thread-safe).
|
||||
|
||||
Returns:
|
||||
Instance du registry
|
||||
"""
|
||||
global _global_registry
|
||||
|
||||
if _global_registry is None:
|
||||
with _registry_lock:
|
||||
if _global_registry is None:
|
||||
_global_registry = VWBActionRegistry()
|
||||
|
||||
# Auto-découverte des actions au premier accès
|
||||
try:
|
||||
_global_registry.auto_discover_actions()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Erreur auto-découverte : {e}")
|
||||
|
||||
return _global_registry
|
||||
|
||||
|
||||
def register_action(action_class: Type[BaseVWBAction],
|
||||
action_id: Optional[str] = None,
|
||||
category: str = "default",
|
||||
metadata: Optional[Dict[str, Any]] = None) -> bool:
|
||||
"""
|
||||
Enregistre une action dans le registry global.
|
||||
|
||||
Args:
|
||||
action_class: Classe de l'action
|
||||
action_id: Identifiant unique (optionnel)
|
||||
category: Catégorie de l'action
|
||||
metadata: Métadonnées additionnelles
|
||||
|
||||
Returns:
|
||||
True si l'enregistrement a réussi
|
||||
"""
|
||||
return get_global_registry().register_action(
|
||||
action_class, action_id, category, metadata
|
||||
)
|
||||
|
||||
|
||||
def get_action_class(action_id: str) -> Optional[Type[BaseVWBAction]]:
|
||||
"""
|
||||
Récupère une classe d'action du registry global.
|
||||
|
||||
Args:
|
||||
action_id: Identifiant de l'action
|
||||
|
||||
Returns:
|
||||
Classe de l'action ou None
|
||||
"""
|
||||
return get_global_registry().get_action_class(action_id)
|
||||
|
||||
|
||||
def create_action(action_id: str,
|
||||
parameters: Optional[Dict[str, Any]] = None,
|
||||
**kwargs) -> Optional[BaseVWBAction]:
|
||||
"""
|
||||
Crée une instance d'action depuis le registry global.
|
||||
|
||||
Args:
|
||||
action_id: Identifiant de l'action
|
||||
parameters: Paramètres de l'action
|
||||
**kwargs: Arguments additionnels
|
||||
|
||||
Returns:
|
||||
Instance de l'action ou None
|
||||
"""
|
||||
return get_global_registry().create_action(action_id, parameters, **kwargs)
|
||||
|
||||
|
||||
def list_available_actions(category: Optional[str] = None) -> List[str]:
|
||||
"""
|
||||
Liste les actions disponibles dans le registry global.
|
||||
|
||||
Args:
|
||||
category: Filtrer par catégorie (optionnel)
|
||||
|
||||
Returns:
|
||||
Liste des IDs d'actions
|
||||
"""
|
||||
return get_global_registry().list_actions(category)
|
||||
|
||||
|
||||
def get_registry_info() -> Dict[str, Any]:
|
||||
"""
|
||||
Obtient les informations du registry global.
|
||||
|
||||
Returns:
|
||||
Informations du registry
|
||||
"""
|
||||
registry = get_global_registry()
|
||||
stats = registry.get_registry_stats()
|
||||
|
||||
return {
|
||||
'stats': stats,
|
||||
'actions': {
|
||||
action_id: registry.get_action_metadata(action_id)
|
||||
for action_id in registry.list_actions()
|
||||
},
|
||||
'categories': registry.list_categories()
|
||||
}
|
||||
|
||||
|
||||
# Décorateur pour l'enregistrement automatique
|
||||
def vwb_action(action_id: Optional[str] = None,
|
||||
category: str = "default",
|
||||
metadata: Optional[Dict[str, Any]] = None):
|
||||
"""
|
||||
Décorateur pour l'enregistrement automatique d'actions VWB.
|
||||
|
||||
Args:
|
||||
action_id: Identifiant unique (optionnel)
|
||||
category: Catégorie de l'action
|
||||
metadata: Métadonnées additionnelles
|
||||
|
||||
Returns:
|
||||
Décorateur
|
||||
"""
|
||||
def decorator(action_class: Type[BaseVWBAction]):
|
||||
register_action(action_class, action_id, category, metadata)
|
||||
return action_class
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test du registry
|
||||
print("🧪 Test du Registry Actions VWB")
|
||||
|
||||
registry = VWBActionRegistry()
|
||||
|
||||
# Test de découverte automatique
|
||||
discovered = registry.auto_discover_actions()
|
||||
print(f"Actions découvertes : {discovered}")
|
||||
|
||||
# Afficher les statistiques
|
||||
stats = registry.get_registry_stats()
|
||||
print(f"Statistiques : {stats}")
|
||||
|
||||
# Lister les actions
|
||||
actions = registry.list_actions()
|
||||
print(f"Actions disponibles : {actions}")
|
||||
|
||||
# Test de création d'action
|
||||
if actions:
|
||||
test_action_id = actions[0]
|
||||
instance = registry.create_action(test_action_id)
|
||||
if instance:
|
||||
print(f"✅ Instance créée pour '{test_action_id}': {type(instance).__name__}")
|
||||
else:
|
||||
print(f"❌ Échec création instance pour '{test_action_id}'")
|
||||
Reference in New Issue
Block a user