| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369 |
- """
- Action System for Trixy Scheduler
- This module provides various action types that define what happens when schedule
- entries are executed. Actions integrate with the Trixy event system, ML training
- components, and support custom function execution.
- Action Types:
- - EventAction: Trigger events through the event system
- - MLTrainingAction: Start ML model training processes
- - FunctionAction: Execute custom functions or methods
- - MultiAction: Execute multiple actions in sequence or parallel
- - ConditionalAction: Execute actions based on conditions
- Key Features:
- - Thread-safe implementations
- - Comprehensive error handling and validation
- - Integration with Trixy event system and ML training
- - Serialization support for persistence
- - Factory pattern for dynamic creation
- - Conditional and multi-action support
- - Retry mechanisms and timeout handling
- Usage:
- from trixy_core.scheduler.actions import EventAction, MLTrainingAction, FunctionAction
-
- # Event action - trigger system events
- event_action = EventAction("system_backup", {"type": "full", "location": "/backup"})
-
- # ML training action - start training process
- ml_action = MLTrainingAction("voice_recognition", {"epochs": 100, "batch_size": 32})
-
- # Function action - execute custom function
- func_action = FunctionAction(my_custom_function, param1="value1", param2="value2")
-
- # Multi action - execute multiple actions
- multi_action = MultiAction([event_action, ml_action], parallel=False)
- """
- import time
- import threading
- import inspect
- import importlib
- import traceback
- from abc import ABC, abstractmethod
- from concurrent.futures import ThreadPoolExecutor, as_completed, Future
- from typing import Dict, Any, Optional, List, Union, Callable, Tuple
- from dataclasses import dataclass
- from enum import Enum
- import json
- def pprint(message: str) -> None:
- """
- Action logging function that adapts based on mode.
- Uses the same pattern as specified in CLAUDE.md.
- """
- print(f"[SCHEDULER.ACTIONS] {message}")
- class ActionError(Exception):
- """Base exception for action-related errors."""
- pass
- class ActionValidationError(ActionError):
- """Raised when action validation fails."""
- pass
- class ActionExecutionError(ActionError):
- """Raised when action execution fails."""
- pass
- class ActionTimeoutError(ActionError):
- """Raised when action execution times out."""
- pass
- class ActionStatus(Enum):
- """Status of action execution."""
- PENDING = "pending"
- RUNNING = "running"
- COMPLETED = "completed"
- FAILED = "failed"
- TIMEOUT = "timeout"
- CANCELLED = "cancelled"
- @dataclass
- class ActionResult:
- """Result of action execution."""
- status: ActionStatus
- start_time: float
- end_time: Optional[float] = None
- result_data: Optional[Any] = None
- error_message: Optional[str] = None
- exception: Optional[Exception] = None
-
- @property
- def duration_seconds(self) -> float:
- """Get execution duration in seconds."""
- if self.end_time is None:
- return time.time() - self.start_time
- return self.end_time - self.start_time
-
- def to_dict(self) -> Dict[str, Any]:
- """Convert result to dictionary."""
- return {
- 'status': self.status.value,
- 'start_time': self.start_time,
- 'end_time': self.end_time,
- 'duration_seconds': self.duration_seconds,
- 'result_data': self.result_data,
- 'error_message': self.error_message,
- 'exception_type': type(self.exception).__name__ if self.exception else None
- }
- class BaseAction(ABC):
- """
- Abstract base class for all actions.
-
- All action implementations must inherit from this class and implement
- the required methods for execution and validation.
- """
-
- def __init__(
- self,
- name: Optional[str] = None,
- enabled: bool = True,
- timeout_seconds: Optional[float] = None,
- retry_count: int = 0,
- retry_delay_seconds: float = 1.0,
- is_critical: bool = False,
- **kwargs
- ):
- """
- Initialize base action.
-
- Args:
- name: Optional name for the action
- enabled: Whether the action is enabled
- timeout_seconds: Maximum execution time (None for no timeout)
- retry_count: Number of retries on failure
- retry_delay_seconds: Delay between retries
- is_critical: Whether action failure should stop schedule execution
- **kwargs: Additional action-specific parameters
- """
- self.name = name or f"{self.__class__.__name__}_{id(self)}"
- self.enabled = enabled
- self.timeout_seconds = timeout_seconds
- self.retry_count = retry_count
- self.retry_delay_seconds = retry_delay_seconds
- self.is_critical = is_critical
-
- # Execution tracking
- self.execution_count = 0
- self.success_count = 0
- self.failure_count = 0
- self.last_executed = None
- self.last_result = None
-
- # Thread safety
- self._lock = threading.RLock()
-
- # Store additional parameters
- self.parameters = kwargs
-
- pprint(f"Created action: {self.name} ({self.__class__.__name__})")
-
- @abstractmethod
- def _execute_action(self, context: Dict[str, Any]) -> Any:
- """
- Execute the actual action logic.
-
- Args:
- context: Execution context from schedule
-
- Returns:
- Any: Action result data
-
- Raises:
- ActionExecutionError: If execution fails
- """
- pass
-
- def execute(self, context: Optional[Dict[str, Any]] = None) -> ActionResult:
- """
- Execute the action with retry logic and error handling.
-
- Args:
- context: Optional execution context
-
- Returns:
- ActionResult: Result of action execution
- """
- if not self.enabled:
- result = ActionResult(
- status=ActionStatus.CANCELLED,
- start_time=time.time(),
- end_time=time.time(),
- error_message="Action is disabled"
- )
- return result
-
- context = context or {}
- start_time = time.time()
-
- with self._lock:
- self.execution_count += 1
- self.last_executed = start_time
-
- pprint(f"Executing action '{self.name}'")
-
- # Execute with retries
- last_exception = None
- for attempt in range(self.retry_count + 1):
- if attempt > 0:
- pprint(f"Retrying action '{self.name}' (attempt {attempt + 1}/{self.retry_count + 1})")
- time.sleep(self.retry_delay_seconds)
-
- result = self._execute_with_timeout(context, start_time)
-
- if result.status == ActionStatus.COMPLETED:
- with self._lock:
- self.success_count += 1
- self.last_result = result
- pprint(f"Action '{self.name}' completed successfully")
- return result
-
- last_exception = result.exception
-
- if result.status == ActionStatus.TIMEOUT:
- pprint(f"Action '{self.name}' timed out")
- break # Don't retry timeouts
-
- # All attempts failed
- with self._lock:
- self.failure_count += 1
- self.last_result = result
-
- pprint(f"Action '{self.name}' failed after {self.retry_count + 1} attempts")
- return result
-
- def _execute_with_timeout(self, context: Dict[str, Any], start_time: float) -> ActionResult:
- """
- Execute action with timeout handling.
-
- Args:
- context: Execution context
- start_time: Start time of execution
-
- Returns:
- ActionResult: Execution result
- """
- if self.timeout_seconds is None:
- # No timeout - execute directly
- try:
- result_data = self._execute_action(context)
- return ActionResult(
- status=ActionStatus.COMPLETED,
- start_time=start_time,
- end_time=time.time(),
- result_data=result_data
- )
- except Exception as e:
- return ActionResult(
- status=ActionStatus.FAILED,
- start_time=start_time,
- end_time=time.time(),
- error_message=str(e),
- exception=e
- )
-
- # Execute with timeout using ThreadPoolExecutor
- with ThreadPoolExecutor(max_workers=1) as executor:
- future = executor.submit(self._execute_action, context)
-
- try:
- result_data = future.result(timeout=self.timeout_seconds)
- return ActionResult(
- status=ActionStatus.COMPLETED,
- start_time=start_time,
- end_time=time.time(),
- result_data=result_data
- )
- except Exception as e:
- if isinstance(e, TimeoutError):
- future.cancel()
- return ActionResult(
- status=ActionStatus.TIMEOUT,
- start_time=start_time,
- end_time=time.time(),
- error_message=f"Action timed out after {self.timeout_seconds} seconds",
- exception=ActionTimeoutError(f"Timeout after {self.timeout_seconds} seconds")
- )
- else:
- return ActionResult(
- status=ActionStatus.FAILED,
- start_time=start_time,
- end_time=time.time(),
- error_message=str(e),
- exception=e
- )
-
- def enable(self) -> None:
- """Enable the action."""
- with self._lock:
- self.enabled = True
- pprint(f"Action '{self.name}' enabled")
-
- def disable(self) -> None:
- """Disable the action."""
- with self._lock:
- self.enabled = False
- pprint(f"Action '{self.name}' disabled")
-
- def reset_statistics(self) -> None:
- """Reset execution statistics."""
- with self._lock:
- self.execution_count = 0
- self.success_count = 0
- self.failure_count = 0
- self.last_executed = None
- self.last_result = None
- pprint(f"Action '{self.name}' statistics reset")
-
- def validate(self) -> List[str]:
- """
- Validate the action configuration.
-
- Returns:
- List[str]: List of validation errors (empty if valid)
- """
- errors = []
-
- if not self.name:
- errors.append("Action name is required")
-
- if self.timeout_seconds is not None and self.timeout_seconds <= 0:
- errors.append("Timeout seconds must be positive")
-
- if self.retry_count < 0:
- errors.append("Retry count cannot be negative")
-
- if self.retry_delay_seconds < 0:
- errors.append("Retry delay cannot be negative")
-
- return errors
-
- def get_info(self) -> Dict[str, Any]:
- """
- Get information about the action.
-
- Returns:
- Dict[str, Any]: Action information
- """
- with self._lock:
- success_rate = 0.0
- if self.execution_count > 0:
- success_rate = (self.success_count / self.execution_count) * 100
-
- return {
- 'name': self.name,
- 'type': self.__class__.__name__,
- 'enabled': self.enabled,
- 'is_critical': self.is_critical,
- 'timeout_seconds': self.timeout_seconds,
- 'retry_count': self.retry_count,
- 'execution_count': self.execution_count,
- 'success_count': self.success_count,
- 'failure_count': self.failure_count,
- 'success_rate_percent': round(success_rate, 2),
- 'last_executed': self.last_executed,
- 'last_status': self.last_result.status.value if self.last_result else None,
- 'parameters': self.parameters.copy()
- }
-
- def to_dict(self) -> Dict[str, Any]:
- """
- Convert action to dictionary for serialization.
-
- Returns:
- Dict[str, Any]: Dictionary representation
- """
- base_dict = {
- 'type': self.__class__.__name__,
- 'name': self.name,
- 'enabled': self.enabled,
- 'timeout_seconds': self.timeout_seconds,
- 'retry_count': self.retry_count,
- 'retry_delay_seconds': self.retry_delay_seconds,
- 'is_critical': self.is_critical,
- 'execution_count': self.execution_count,
- 'success_count': self.success_count,
- 'failure_count': self.failure_count,
- 'last_executed': self.last_executed
- }
-
- # Add action-specific data
- base_dict.update(self._get_specific_dict())
-
- return base_dict
-
- @abstractmethod
- def _get_specific_dict(self) -> Dict[str, Any]:
- """
- Get action-specific dictionary data.
-
- Returns:
- Dict[str, Any]: Action-specific data
- """
- pass
-
- @classmethod
- @abstractmethod
- def from_dict(cls, data: Dict[str, Any]) -> 'BaseAction':
- """
- Create action from dictionary data.
-
- Args:
- data: Dictionary data
-
- Returns:
- BaseAction: Created action instance
- """
- pass
-
- def __str__(self) -> str:
- """String representation of the action."""
- return f"{self.__class__.__name__}(name='{self.name}', enabled={self.enabled})"
-
- def __repr__(self) -> str:
- """Detailed representation of the action."""
- return (
- f"{self.__class__.__name__}(name='{self.name}', enabled={self.enabled}, "
- f"executions={self.execution_count}, success_rate={self.success_count}/{self.execution_count})"
- )
- class EventAction(BaseAction):
- """
- Action that triggers events through the Trixy event system.
-
- Integrates with the application's event handler to trigger
- events with specified data.
- """
-
- def __init__(
- self,
- event_name: str,
- event_data: Optional[Dict[str, Any]] = None,
- wait_for_completion: bool = False,
- completion_timeout: float = 30.0,
- **kwargs
- ):
- """
- Initialize event action.
-
- Args:
- event_name: Name of event to trigger
- event_data: Optional data to pass with event
- wait_for_completion: Whether to wait for event processing completion
- completion_timeout: Timeout for waiting for completion
- **kwargs: Additional parameters
- """
- super().__init__(**kwargs)
-
- self.event_name = event_name
- self.event_data = event_data or {}
- self.wait_for_completion = wait_for_completion
- self.completion_timeout = completion_timeout
-
- # Event system integration
- self._event_handler = None
-
- self._validate_parameters()
-
- def _validate_parameters(self) -> None:
- """Validate event action parameters."""
- if not self.event_name or not isinstance(self.event_name, str):
- raise ActionValidationError("Event name must be a non-empty string")
-
- if not isinstance(self.event_data, dict):
- raise ActionValidationError("Event data must be a dictionary")
-
- if self.completion_timeout <= 0:
- raise ActionValidationError("Completion timeout must be positive")
-
- def set_event_handler(self, event_handler) -> None:
- """Set the event handler for integration."""
- self._event_handler = event_handler
- pprint(f"EventAction '{self.name}' connected to event system")
-
- def _execute_action(self, context: Dict[str, Any]) -> Any:
- """Execute the event action."""
- if not self._event_handler:
- raise ActionExecutionError("Event handler not available")
-
- # Merge context data with event data
- combined_data = self.event_data.copy()
- combined_data.update(context)
-
- pprint(f"Triggering event '{self.event_name}' from action '{self.name}'")
-
- if self.wait_for_completion:
- # Trigger event and wait for completion
- event_id = self._event_handler.trigger_event(self.event_name, **combined_data)
-
- if event_id:
- # Wait for event completion
- history_entry = self._event_handler.wait_for_event(event_id, self.completion_timeout)
- if history_entry:
- if history_entry.errors:
- raise ActionExecutionError(f"Event processing failed: {'; '.join(history_entry.errors)}")
- return {
- 'event_id': event_id,
- 'event_name': self.event_name,
- 'execution_time_ms': history_entry.execution_time_ms,
- 'handlers_called': len(history_entry.handlers_called)
- }
- else:
- raise ActionExecutionError(f"Event processing timed out after {self.completion_timeout} seconds")
- else:
- raise ActionExecutionError("Failed to trigger event")
- else:
- # Fire and forget
- event_id = self._event_handler.trigger_event(self.event_name, **combined_data)
- return {
- 'event_id': event_id,
- 'event_name': self.event_name,
- 'wait_for_completion': False
- }
-
- def _get_specific_dict(self) -> Dict[str, Any]:
- """Get event action specific data."""
- return {
- 'event_name': self.event_name,
- 'event_data': self.event_data,
- 'wait_for_completion': self.wait_for_completion,
- 'completion_timeout': self.completion_timeout
- }
-
- @classmethod
- def from_dict(cls, data: Dict[str, Any]) -> 'EventAction':
- """Create EventAction from dictionary."""
- action = cls(
- event_name=data['event_name'],
- event_data=data.get('event_data', {}),
- wait_for_completion=data.get('wait_for_completion', False),
- completion_timeout=data.get('completion_timeout', 30.0),
- name=data.get('name'),
- enabled=data.get('enabled', True),
- timeout_seconds=data.get('timeout_seconds'),
- retry_count=data.get('retry_count', 0),
- retry_delay_seconds=data.get('retry_delay_seconds', 1.0),
- is_critical=data.get('is_critical', False)
- )
-
- # Restore state
- action.execution_count = data.get('execution_count', 0)
- action.success_count = data.get('success_count', 0)
- action.failure_count = data.get('failure_count', 0)
- action.last_executed = data.get('last_executed')
-
- return action
- class MLTrainingAction(BaseAction):
- """
- Action that starts ML model training processes.
-
- Integrates with the Trixy ML training system to start
- training for voice recognition, wakeword detection, etc.
- """
-
- def __init__(
- self,
- training_type: str,
- training_config: Optional[Dict[str, Any]] = None,
- wait_for_completion: bool = True,
- **kwargs
- ):
- """
- Initialize ML training action.
-
- Args:
- training_type: Type of training (e.g., 'voice_recognition', 'wakeword')
- training_config: Configuration for training process
- wait_for_completion: Whether to wait for training completion
- **kwargs: Additional parameters
- """
- super().__init__(**kwargs)
-
- self.training_type = training_type
- self.training_config = training_config or {}
- self.wait_for_completion = wait_for_completion
-
- # Training system integration
- self._training_manager = None
-
- self._validate_parameters()
-
- def _validate_parameters(self) -> None:
- """Validate ML training action parameters."""
- if not self.training_type or not isinstance(self.training_type, str):
- raise ActionValidationError("Training type must be a non-empty string")
-
- if not isinstance(self.training_config, dict):
- raise ActionValidationError("Training config must be a dictionary")
-
- valid_training_types = ['voice_recognition', 'wakeword', 'custom']
- if self.training_type not in valid_training_types:
- pprint(f"Warning: Unknown training type '{self.training_type}'. Valid types: {valid_training_types}")
-
- def set_training_manager(self, training_manager) -> None:
- """Set the training manager for integration."""
- self._training_manager = training_manager
- pprint(f"MLTrainingAction '{self.name}' connected to training system")
-
- def _execute_action(self, context: Dict[str, Any]) -> Any:
- """Execute the ML training action."""
- # Note: This is a placeholder implementation
- # In a real system, this would integrate with the actual ML training pipeline
-
- pprint(f"Starting ML training: {self.training_type}")
-
- # Merge context with training config
- combined_config = self.training_config.copy()
- combined_config.update(context)
-
- # Simulate training process
- if self.training_type == 'voice_recognition':
- return self._start_voice_recognition_training(combined_config)
- elif self.training_type == 'wakeword':
- return self._start_wakeword_training(combined_config)
- else:
- return self._start_custom_training(combined_config)
-
- def _start_voice_recognition_training(self, config: Dict[str, Any]) -> Dict[str, Any]:
- """Start voice recognition training."""
- pprint(f"Starting voice recognition training with config: {config}")
-
- # Placeholder implementation
- # In reality, this would:
- # 1. Load training data
- # 2. Initialize model
- # 3. Start training process
- # 4. Monitor progress
- # 5. Save trained model
-
- if self.wait_for_completion:
- # Simulate training time
- time.sleep(1) # Placeholder for actual training
-
- return {
- 'training_type': 'voice_recognition',
- 'status': 'completed',
- 'model_path': f'/models/voice_recognition/model_{int(time.time())}.pth',
- 'accuracy': 0.95,
- 'training_time_seconds': 1,
- 'epochs_completed': config.get('epochs', 100)
- }
- else:
- # Start training in background
- return {
- 'training_type': 'voice_recognition',
- 'status': 'started',
- 'training_id': f'vr_{int(time.time())}',
- 'wait_for_completion': False
- }
-
- def _start_wakeword_training(self, config: Dict[str, Any]) -> Dict[str, Any]:
- """Start wakeword training."""
- pprint(f"Starting wakeword training with config: {config}")
-
- # Placeholder implementation
- if self.wait_for_completion:
- time.sleep(0.5) # Placeholder for actual training
-
- return {
- 'training_type': 'wakeword',
- 'status': 'completed',
- 'model_path': f'/models/wakeword/model_{int(time.time())}.pth',
- 'accuracy': 0.92,
- 'training_time_seconds': 0.5,
- 'epochs_completed': config.get('epochs', 50)
- }
- else:
- return {
- 'training_type': 'wakeword',
- 'status': 'started',
- 'training_id': f'ww_{int(time.time())}',
- 'wait_for_completion': False
- }
-
- def _start_custom_training(self, config: Dict[str, Any]) -> Dict[str, Any]:
- """Start custom training."""
- pprint(f"Starting custom training with config: {config}")
-
- # Placeholder implementation
- return {
- 'training_type': 'custom',
- 'status': 'started',
- 'config': config,
- 'message': 'Custom training would be implemented based on specific requirements'
- }
-
- def _get_specific_dict(self) -> Dict[str, Any]:
- """Get ML training action specific data."""
- return {
- 'training_type': self.training_type,
- 'training_config': self.training_config,
- 'wait_for_completion': self.wait_for_completion
- }
-
- @classmethod
- def from_dict(cls, data: Dict[str, Any]) -> 'MLTrainingAction':
- """Create MLTrainingAction from dictionary."""
- action = cls(
- training_type=data['training_type'],
- training_config=data.get('training_config', {}),
- wait_for_completion=data.get('wait_for_completion', True),
- name=data.get('name'),
- enabled=data.get('enabled', True),
- timeout_seconds=data.get('timeout_seconds'),
- retry_count=data.get('retry_count', 0),
- retry_delay_seconds=data.get('retry_delay_seconds', 1.0),
- is_critical=data.get('is_critical', False)
- )
-
- # Restore state
- action.execution_count = data.get('execution_count', 0)
- action.success_count = data.get('success_count', 0)
- action.failure_count = data.get('failure_count', 0)
- action.last_executed = data.get('last_executed')
-
- return action
- class FunctionAction(BaseAction):
- """
- Action that executes custom functions or methods.
-
- Supports calling Python functions, methods, or importable callables
- with specified parameters.
- """
-
- def __init__(
- self,
- function: Union[Callable, str],
- function_args: Optional[List[Any]] = None,
- function_kwargs: Optional[Dict[str, Any]] = None,
- import_path: Optional[str] = None,
- **kwargs
- ):
- """
- Initialize function action.
-
- Args:
- function: Function to call or string name for import
- function_args: Positional arguments for function
- function_kwargs: Keyword arguments for function
- import_path: Import path for function (e.g., 'module.submodule')
- **kwargs: Additional parameters
- """
- super().__init__(**kwargs)
-
- self.function_args = function_args or []
- self.function_kwargs = function_kwargs or {}
- self.import_path = import_path
-
- # Store function reference or name
- if callable(function):
- self.function = function
- self.function_name = getattr(function, '__name__', str(function))
- elif isinstance(function, str):
- self.function = None
- self.function_name = function
- else:
- raise ActionValidationError("Function must be callable or string name")
-
- self._validate_parameters()
-
- def _validate_parameters(self) -> None:
- """Validate function action parameters."""
- if not isinstance(self.function_args, list):
- raise ActionValidationError("Function args must be a list")
-
- if not isinstance(self.function_kwargs, dict):
- raise ActionValidationError("Function kwargs must be a dictionary")
-
- if self.function is None and not self.import_path:
- raise ActionValidationError("Import path required when function is specified as string")
-
- def _resolve_function(self) -> Callable:
- """Resolve function reference for execution."""
- if self.function is not None:
- return self.function
-
- # Import the function
- try:
- if '.' in self.import_path:
- module_path, function_name = self.import_path.rsplit('.', 1)
- module = importlib.import_module(module_path)
- function = getattr(module, function_name)
- else:
- # Function in current module
- module = importlib.import_module(self.import_path)
- function = getattr(module, self.function_name)
-
- if not callable(function):
- raise ActionExecutionError(f"'{self.function_name}' is not callable")
-
- return function
-
- except ImportError as e:
- raise ActionExecutionError(f"Failed to import function: {e}")
- except AttributeError as e:
- raise ActionExecutionError(f"Function not found: {e}")
-
- def _execute_action(self, context: Dict[str, Any]) -> Any:
- """Execute the function action."""
- function = self._resolve_function()
-
- # Merge context with function kwargs
- combined_kwargs = self.function_kwargs.copy()
- combined_kwargs.update(context)
-
- pprint(f"Executing function '{self.function_name}' from action '{self.name}'")
-
- try:
- # Call the function
- result = function(*self.function_args, **combined_kwargs)
-
- return {
- 'function_name': self.function_name,
- 'result': result,
- 'args_count': len(self.function_args),
- 'kwargs_count': len(combined_kwargs)
- }
-
- except Exception as e:
- raise ActionExecutionError(f"Function execution failed: {e}")
-
- def _get_specific_dict(self) -> Dict[str, Any]:
- """Get function action specific data."""
- return {
- 'function_name': self.function_name,
- 'function_args': self.function_args,
- 'function_kwargs': self.function_kwargs,
- 'import_path': self.import_path
- }
-
- @classmethod
- def from_dict(cls, data: Dict[str, Any]) -> 'FunctionAction':
- """Create FunctionAction from dictionary."""
- action = cls(
- function=data['function_name'],
- function_args=data.get('function_args', []),
- function_kwargs=data.get('function_kwargs', {}),
- import_path=data.get('import_path'),
- name=data.get('name'),
- enabled=data.get('enabled', True),
- timeout_seconds=data.get('timeout_seconds'),
- retry_count=data.get('retry_count', 0),
- retry_delay_seconds=data.get('retry_delay_seconds', 1.0),
- is_critical=data.get('is_critical', False)
- )
-
- # Restore state
- action.execution_count = data.get('execution_count', 0)
- action.success_count = data.get('success_count', 0)
- action.failure_count = data.get('failure_count', 0)
- action.last_executed = data.get('last_executed')
-
- return action
- class MultiAction(BaseAction):
- """
- Action that executes multiple actions in sequence or parallel.
-
- Supports executing a list of actions either sequentially or in parallel,
- with configurable failure handling.
- """
-
- def __init__(
- self,
- actions: List[BaseAction],
- parallel: bool = False,
- stop_on_first_failure: bool = True,
- max_parallel_workers: int = 5,
- **kwargs
- ):
- """
- Initialize multi action.
-
- Args:
- actions: List of actions to execute
- parallel: Whether to execute actions in parallel
- stop_on_first_failure: Whether to stop on first action failure
- max_parallel_workers: Maximum parallel workers for parallel execution
- **kwargs: Additional parameters
- """
- super().__init__(**kwargs)
-
- self.actions = actions
- self.parallel = parallel
- self.stop_on_first_failure = stop_on_first_failure
- self.max_parallel_workers = max_parallel_workers
-
- self._validate_parameters()
-
- def _validate_parameters(self) -> None:
- """Validate multi action parameters."""
- if not self.actions:
- raise ActionValidationError("At least one action must be specified")
-
- for i, action in enumerate(self.actions):
- if not isinstance(action, BaseAction):
- raise ActionValidationError(f"Action {i} is not a BaseAction instance")
-
- if self.max_parallel_workers <= 0:
- raise ActionValidationError("Max parallel workers must be positive")
-
- def _execute_action(self, context: Dict[str, Any]) -> Any:
- """Execute the multi action."""
- if self.parallel:
- return self._execute_parallel(context)
- else:
- return self._execute_sequential(context)
-
- def _execute_sequential(self, context: Dict[str, Any]) -> Dict[str, Any]:
- """Execute actions sequentially."""
- results = []
- successful_count = 0
- failed_count = 0
-
- for i, action in enumerate(self.actions):
- pprint(f"Executing action {i+1}/{len(self.actions)}: {action.name}")
-
- try:
- result = action.execute(context)
- results.append({
- 'action_name': action.name,
- 'action_index': i,
- 'result': result.to_dict()
- })
-
- if result.status == ActionStatus.COMPLETED:
- successful_count += 1
- else:
- failed_count += 1
- if self.stop_on_first_failure:
- pprint(f"Stopping multi-action due to failure in action '{action.name}'")
- break
-
- except Exception as e:
- failed_count += 1
- results.append({
- 'action_name': action.name,
- 'action_index': i,
- 'error': str(e)
- })
-
- if self.stop_on_first_failure:
- pprint(f"Stopping multi-action due to exception in action '{action.name}': {e}")
- break
-
- return {
- 'execution_type': 'sequential',
- 'total_actions': len(self.actions),
- 'executed_actions': len(results),
- 'successful_count': successful_count,
- 'failed_count': failed_count,
- 'results': results
- }
-
- def _execute_parallel(self, context: Dict[str, Any]) -> Dict[str, Any]:
- """Execute actions in parallel."""
- results = []
- successful_count = 0
- failed_count = 0
-
- with ThreadPoolExecutor(max_workers=self.max_parallel_workers) as executor:
- # Submit all actions
- future_to_action = {
- executor.submit(action.execute, context): (i, action)
- for i, action in enumerate(self.actions)
- }
-
- # Collect results
- for future in as_completed(future_to_action):
- action_index, action = future_to_action[future]
-
- try:
- result = future.result()
- results.append({
- 'action_name': action.name,
- 'action_index': action_index,
- 'result': result.to_dict()
- })
-
- if result.status == ActionStatus.COMPLETED:
- successful_count += 1
- else:
- failed_count += 1
-
- except Exception as e:
- failed_count += 1
- results.append({
- 'action_name': action.name,
- 'action_index': action_index,
- 'error': str(e)
- })
-
- # Sort results by action index to maintain order
- results.sort(key=lambda x: x['action_index'])
-
- return {
- 'execution_type': 'parallel',
- 'total_actions': len(self.actions),
- 'executed_actions': len(results),
- 'successful_count': successful_count,
- 'failed_count': failed_count,
- 'max_workers': self.max_parallel_workers,
- 'results': results
- }
-
- def _get_specific_dict(self) -> Dict[str, Any]:
- """Get multi action specific data."""
- return {
- 'actions': [action.to_dict() for action in self.actions],
- 'parallel': self.parallel,
- 'stop_on_first_failure': self.stop_on_first_failure,
- 'max_parallel_workers': self.max_parallel_workers
- }
-
- @classmethod
- def from_dict(cls, data: Dict[str, Any]) -> 'MultiAction':
- """Create MultiAction from dictionary."""
- # Recreate actions from their dictionaries
- actions = []
- for action_data in data.get('actions', []):
- action = ActionFactory.from_dict(action_data)
- actions.append(action)
-
- action = cls(
- actions=actions,
- parallel=data.get('parallel', False),
- stop_on_first_failure=data.get('stop_on_first_failure', True),
- max_parallel_workers=data.get('max_parallel_workers', 5),
- name=data.get('name'),
- enabled=data.get('enabled', True),
- timeout_seconds=data.get('timeout_seconds'),
- retry_count=data.get('retry_count', 0),
- retry_delay_seconds=data.get('retry_delay_seconds', 1.0),
- is_critical=data.get('is_critical', False)
- )
-
- # Restore state
- action.execution_count = data.get('execution_count', 0)
- action.success_count = data.get('success_count', 0)
- action.failure_count = data.get('failure_count', 0)
- action.last_executed = data.get('last_executed')
-
- return action
- class ConditionalAction(BaseAction):
- """
- Action that executes other actions based on conditions.
-
- Supports conditional execution based on context data,
- with support for multiple condition types.
- """
-
- def __init__(
- self,
- condition: Union[str, Callable],
- true_action: BaseAction,
- false_action: Optional[BaseAction] = None,
- condition_params: Optional[Dict[str, Any]] = None,
- **kwargs
- ):
- """
- Initialize conditional action.
-
- Args:
- condition: Condition to evaluate (string or callable)
- true_action: Action to execute if condition is true
- false_action: Action to execute if condition is false (optional)
- condition_params: Parameters for condition evaluation
- **kwargs: Additional parameters
- """
- super().__init__(**kwargs)
-
- self.condition = condition
- self.true_action = true_action
- self.false_action = false_action
- self.condition_params = condition_params or {}
-
- self._validate_parameters()
-
- def _validate_parameters(self) -> None:
- """Validate conditional action parameters."""
- if not isinstance(self.true_action, BaseAction):
- raise ActionValidationError("True action must be a BaseAction instance")
-
- if self.false_action is not None and not isinstance(self.false_action, BaseAction):
- raise ActionValidationError("False action must be a BaseAction instance")
-
- if not (isinstance(self.condition, str) or callable(self.condition)):
- raise ActionValidationError("Condition must be a string or callable")
-
- def _evaluate_condition(self, context: Dict[str, Any]) -> bool:
- """
- Evaluate the condition.
-
- Args:
- context: Execution context
-
- Returns:
- bool: True if condition is met
- """
- if callable(self.condition):
- # Call the condition function
- try:
- return bool(self.condition(context, **self.condition_params))
- except Exception as e:
- pprint(f"Error evaluating condition function: {e}")
- return False
-
- elif isinstance(self.condition, str):
- # Simple string-based conditions
- return self._evaluate_string_condition(context)
-
- return False
-
- def _evaluate_string_condition(self, context: Dict[str, Any]) -> bool:
- """
- Evaluate string-based condition.
-
- Args:
- context: Execution context
-
- Returns:
- bool: True if condition is met
- """
- condition = self.condition.strip().lower()
-
- # Simple key existence check
- if condition.startswith('has_'):
- key = condition[4:] # Remove 'has_' prefix
- return key in context
-
- # Simple value equality check
- if '=' in condition:
- key, value = condition.split('=', 1)
- key = key.strip()
- value = value.strip()
-
- # Try to convert value to appropriate type
- if value.lower() in ['true', 'false']:
- value = value.lower() == 'true'
- elif value.isdigit():
- value = int(value)
- elif value.replace('.', '', 1).isdigit():
- value = float(value)
-
- return context.get(key) == value
-
- # Default: check if condition string is a key with truthy value
- return bool(context.get(condition))
-
- def _execute_action(self, context: Dict[str, Any]) -> Any:
- """Execute the conditional action."""
- condition_result = self._evaluate_condition(context)
-
- pprint(f"Condition '{self.condition}' evaluated to: {condition_result}")
-
- if condition_result:
- if self.true_action:
- pprint(f"Executing true action: {self.true_action.name}")
- result = self.true_action.execute(context)
- return {
- 'condition_result': True,
- 'executed_action': 'true',
- 'action_name': self.true_action.name,
- 'action_result': result.to_dict()
- }
- else:
- if self.false_action:
- pprint(f"Executing false action: {self.false_action.name}")
- result = self.false_action.execute(context)
- return {
- 'condition_result': False,
- 'executed_action': 'false',
- 'action_name': self.false_action.name,
- 'action_result': result.to_dict()
- }
-
- return {
- 'condition_result': condition_result,
- 'executed_action': None,
- 'message': 'No action executed based on condition result'
- }
-
- def _get_specific_dict(self) -> Dict[str, Any]:
- """Get conditional action specific data."""
- return {
- 'condition': self.condition if isinstance(self.condition, str) else 'callable',
- 'condition_params': self.condition_params,
- 'true_action': self.true_action.to_dict(),
- 'false_action': self.false_action.to_dict() if self.false_action else None
- }
-
- @classmethod
- def from_dict(cls, data: Dict[str, Any]) -> 'ConditionalAction':
- """Create ConditionalAction from dictionary."""
- # Recreate actions from their dictionaries
- true_action = ActionFactory.from_dict(data['true_action'])
- false_action = None
- if data.get('false_action'):
- false_action = ActionFactory.from_dict(data['false_action'])
-
- action = cls(
- condition=data['condition'],
- true_action=true_action,
- false_action=false_action,
- condition_params=data.get('condition_params', {}),
- name=data.get('name'),
- enabled=data.get('enabled', True),
- timeout_seconds=data.get('timeout_seconds'),
- retry_count=data.get('retry_count', 0),
- retry_delay_seconds=data.get('retry_delay_seconds', 1.0),
- is_critical=data.get('is_critical', False)
- )
-
- # Restore state
- action.execution_count = data.get('execution_count', 0)
- action.success_count = data.get('success_count', 0)
- action.failure_count = data.get('failure_count', 0)
- action.last_executed = data.get('last_executed')
-
- return action
- # Action factory for dynamic creation
- class ActionFactory:
- """Factory class for creating actions from configuration."""
-
- _action_classes = {
- 'EventAction': EventAction,
- 'MLTrainingAction': MLTrainingAction,
- 'FunctionAction': FunctionAction,
- 'MultiAction': MultiAction,
- 'ConditionalAction': ConditionalAction
- }
-
- @classmethod
- def create_action(cls, action_type: str, **kwargs) -> BaseAction:
- """
- Create an action instance.
-
- Args:
- action_type: Type of action to create
- **kwargs: Action-specific parameters
-
- Returns:
- BaseAction: Created action instance
-
- Raises:
- ActionValidationError: If action type is unknown or parameters are invalid
- """
- if action_type not in cls._action_classes:
- raise ActionValidationError(f"Unknown action type: {action_type}")
-
- action_class = cls._action_classes[action_type]
- return action_class(**kwargs)
-
- @classmethod
- def from_dict(cls, data: Dict[str, Any]) -> BaseAction:
- """
- Create action from dictionary data.
-
- Args:
- data: Dictionary containing action configuration
-
- Returns:
- BaseAction: Created action instance
- """
- action_type = data.get('type')
- if not action_type:
- raise ActionValidationError("Action type not specified in data")
-
- if action_type not in cls._action_classes:
- raise ActionValidationError(f"Unknown action type: {action_type}")
-
- action_class = cls._action_classes[action_type]
- return action_class.from_dict(data)
-
- @classmethod
- def get_supported_types(cls) -> List[str]:
- """Get list of supported action types."""
- return list(cls._action_classes.keys())
- # Convenience functions
- def create_action_from_dict(data: Dict[str, Any]) -> BaseAction:
- """
- Create action from dictionary data.
-
- Args:
- data: Dictionary containing action configuration
-
- Returns:
- BaseAction: Created action instance
- """
- return ActionFactory.from_dict(data)
- def get_supported_action_types() -> List[str]:
- """Get list of supported action types."""
- return ActionFactory.get_supported_types()
- def validate_action_config(config: Dict[str, Any]) -> List[str]:
- """
- Validate action configuration.
-
- Args:
- config: Action configuration dictionary
-
- Returns:
- List[str]: List of validation errors (empty if valid)
- """
- try:
- action = create_action_from_dict(config)
- return action.validate()
- except Exception as e:
- return [str(e)]
- # Module exports
- __all__ = [
- 'BaseAction',
- 'ActionError',
- 'ActionValidationError',
- 'ActionExecutionError',
- 'ActionTimeoutError',
- 'ActionStatus',
- 'ActionResult',
- 'EventAction',
- 'MLTrainingAction',
- 'FunctionAction',
- 'MultiAction',
- 'ConditionalAction',
- 'ActionFactory',
- 'create_action_from_dict',
- 'get_supported_action_types',
- 'validate_action_config',
- 'pprint'
- ]
|