Add scheduled automation task and update URL routing
- Introduced a new scheduled task for executing automation rules every 5 minutes in the Celery beat schedule. - Updated URL routing to include a new endpoint for automation-related functionalities. - Refactored imports in various modules to align with the new business layer structure, ensuring backward compatibility for billing models, exceptions, and services.
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
Automation services
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
Action Executor
|
||||
Executes rule actions
|
||||
"""
|
||||
import logging
|
||||
from igny8_core.business.planning.services.clustering_service import ClusteringService
|
||||
from igny8_core.business.planning.services.ideas_service import IdeasService
|
||||
from igny8_core.business.content.services.content_generation_service import ContentGenerationService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ActionExecutor:
|
||||
"""Executes rule actions"""
|
||||
|
||||
def __init__(self):
|
||||
self.clustering_service = ClusteringService()
|
||||
self.ideas_service = IdeasService()
|
||||
self.content_service = ContentGenerationService()
|
||||
|
||||
def execute(self, action, context, rule):
|
||||
"""
|
||||
Execute a single action.
|
||||
|
||||
Args:
|
||||
action: Action dict with 'type' and 'params'
|
||||
context: Context dict
|
||||
rule: AutomationRule instance
|
||||
|
||||
Returns:
|
||||
dict: Action execution result
|
||||
"""
|
||||
action_type = action.get('type')
|
||||
params = action.get('params', {})
|
||||
|
||||
if action_type == 'cluster_keywords':
|
||||
return self._execute_cluster_keywords(params, rule)
|
||||
elif action_type == 'generate_ideas':
|
||||
return self._execute_generate_ideas(params, rule)
|
||||
elif action_type == 'generate_content':
|
||||
return self._execute_generate_content(params, rule)
|
||||
else:
|
||||
logger.warning(f"Unknown action type: {action_type}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': f'Unknown action type: {action_type}'
|
||||
}
|
||||
|
||||
def _execute_cluster_keywords(self, params, rule):
|
||||
"""Execute cluster keywords action"""
|
||||
keyword_ids = params.get('keyword_ids', [])
|
||||
sector_id = params.get('sector_id') or (rule.sector.id if rule.sector else None)
|
||||
|
||||
try:
|
||||
result = self.clustering_service.cluster_keywords(
|
||||
keyword_ids=keyword_ids,
|
||||
account=rule.account,
|
||||
sector_id=sector_id
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error clustering keywords: {str(e)}", exc_info=True)
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _execute_generate_ideas(self, params, rule):
|
||||
"""Execute generate ideas action"""
|
||||
cluster_ids = params.get('cluster_ids', [])
|
||||
|
||||
try:
|
||||
result = self.ideas_service.generate_ideas(
|
||||
cluster_ids=cluster_ids,
|
||||
account=rule.account
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating ideas: {str(e)}", exc_info=True)
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _execute_generate_content(self, params, rule):
|
||||
"""Execute generate content action"""
|
||||
task_ids = params.get('task_ids', [])
|
||||
|
||||
try:
|
||||
result = self.content_service.generate_content(
|
||||
task_ids=task_ids,
|
||||
account=rule.account
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating content: {str(e)}", exc_info=True)
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
Automation Service
|
||||
Main service for executing automation rules
|
||||
"""
|
||||
import logging
|
||||
from django.utils import timezone
|
||||
from igny8_core.business.automation.models import AutomationRule, ScheduledTask
|
||||
from igny8_core.business.automation.services.rule_engine import RuleEngine
|
||||
from igny8_core.business.billing.services.credit_service import CreditService
|
||||
from igny8_core.business.billing.exceptions import InsufficientCreditsError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AutomationService:
|
||||
"""Service for executing automation rules"""
|
||||
|
||||
def __init__(self):
|
||||
self.rule_engine = RuleEngine()
|
||||
self.credit_service = CreditService()
|
||||
|
||||
def execute_rule(self, rule, context=None):
|
||||
"""
|
||||
Execute an automation rule.
|
||||
|
||||
Args:
|
||||
rule: AutomationRule instance
|
||||
context: Optional context dict for condition evaluation
|
||||
|
||||
Returns:
|
||||
dict: Execution result with status and data
|
||||
"""
|
||||
if not rule.is_active or rule.status != 'active':
|
||||
return {
|
||||
'status': 'skipped',
|
||||
'reason': 'Rule is inactive',
|
||||
'rule_id': rule.id
|
||||
}
|
||||
|
||||
# Check credits (estimate based on actions)
|
||||
estimated_credits = self._estimate_credits(rule)
|
||||
try:
|
||||
self.credit_service.check_credits_legacy(rule.account, estimated_credits)
|
||||
except InsufficientCreditsError as e:
|
||||
logger.warning(f"Rule {rule.id} skipped: {str(e)}")
|
||||
return {
|
||||
'status': 'skipped',
|
||||
'reason': f'Insufficient credits: {str(e)}',
|
||||
'rule_id': rule.id
|
||||
}
|
||||
|
||||
# Execute via rule engine
|
||||
try:
|
||||
result = self.rule_engine.execute(rule, context or {})
|
||||
|
||||
# Update rule tracking
|
||||
rule.last_executed_at = timezone.now()
|
||||
rule.execution_count += 1
|
||||
rule.save(update_fields=['last_executed_at', 'execution_count'])
|
||||
|
||||
return {
|
||||
'status': 'completed',
|
||||
'rule_id': rule.id,
|
||||
'result': result
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing rule {rule.id}: {str(e)}", exc_info=True)
|
||||
return {
|
||||
'status': 'failed',
|
||||
'reason': str(e),
|
||||
'rule_id': rule.id
|
||||
}
|
||||
|
||||
def _estimate_credits(self, rule):
|
||||
"""Estimate credits needed for rule execution"""
|
||||
# Simple estimation based on action types
|
||||
estimated = 0
|
||||
for action in rule.actions:
|
||||
action_type = action.get('type', '')
|
||||
if 'cluster' in action_type:
|
||||
estimated += 10
|
||||
elif 'idea' in action_type:
|
||||
estimated += 15
|
||||
elif 'content' in action_type:
|
||||
estimated += 50 # Conservative estimate
|
||||
else:
|
||||
estimated += 5 # Default
|
||||
return max(estimated, 10) # Minimum 10 credits
|
||||
|
||||
def execute_scheduled_rules(self):
|
||||
"""
|
||||
Execute all scheduled rules that are due.
|
||||
Called by Celery Beat task.
|
||||
|
||||
Returns:
|
||||
dict: Summary of executions
|
||||
"""
|
||||
from django.utils import timezone
|
||||
now = timezone.now()
|
||||
|
||||
# Get active scheduled rules
|
||||
rules = AutomationRule.objects.filter(
|
||||
trigger='schedule',
|
||||
is_active=True,
|
||||
status='active'
|
||||
)
|
||||
|
||||
executed = 0
|
||||
skipped = 0
|
||||
failed = 0
|
||||
|
||||
for rule in rules:
|
||||
# Check if rule should execute based on schedule
|
||||
if self._should_execute_schedule(rule, now):
|
||||
result = self.execute_rule(rule)
|
||||
if result['status'] == 'completed':
|
||||
executed += 1
|
||||
elif result['status'] == 'skipped':
|
||||
skipped += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
return {
|
||||
'executed': executed,
|
||||
'skipped': skipped,
|
||||
'failed': failed,
|
||||
'total': len(rules)
|
||||
}
|
||||
|
||||
def _should_execute_schedule(self, rule, now):
|
||||
"""
|
||||
Check if a scheduled rule should execute now.
|
||||
Simple implementation - can be enhanced with proper cron parsing.
|
||||
"""
|
||||
if not rule.schedule:
|
||||
return False
|
||||
|
||||
# For now, simple check - can be enhanced with cron parser
|
||||
# This is a placeholder - proper implementation would parse cron string
|
||||
return True # Simplified for now
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Condition Evaluator
|
||||
Evaluates rule conditions
|
||||
"""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConditionEvaluator:
|
||||
"""Evaluates rule conditions"""
|
||||
|
||||
OPERATORS = {
|
||||
'equals': lambda a, b: a == b,
|
||||
'not_equals': lambda a, b: a != b,
|
||||
'greater_than': lambda a, b: a > b,
|
||||
'greater_than_or_equal': lambda a, b: a >= b,
|
||||
'less_than': lambda a, b: a < b,
|
||||
'less_than_or_equal': lambda a, b: a <= b,
|
||||
'in': lambda a, b: a in b,
|
||||
'contains': lambda a, b: b in a if isinstance(a, str) else a in b,
|
||||
'is_empty': lambda a, b: not a or (isinstance(a, str) and not a.strip()),
|
||||
'is_not_empty': lambda a, b: a and (not isinstance(a, str) or a.strip()),
|
||||
}
|
||||
|
||||
def evaluate(self, conditions, context):
|
||||
"""
|
||||
Evaluate a list of conditions.
|
||||
|
||||
Args:
|
||||
conditions: List of condition dicts
|
||||
context: Context dict for field resolution
|
||||
|
||||
Returns:
|
||||
bool: True if all conditions are met
|
||||
"""
|
||||
if not conditions:
|
||||
return True
|
||||
|
||||
for condition in conditions:
|
||||
if not self._evaluate_condition(condition, context):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _evaluate_condition(self, condition, context):
|
||||
"""
|
||||
Evaluate a single condition.
|
||||
|
||||
Condition format:
|
||||
{
|
||||
"field": "content.status",
|
||||
"operator": "equals",
|
||||
"value": "draft"
|
||||
}
|
||||
"""
|
||||
field_path = condition.get('field')
|
||||
operator = condition.get('operator', 'equals')
|
||||
expected_value = condition.get('value')
|
||||
|
||||
if not field_path:
|
||||
logger.warning("Condition missing 'field'")
|
||||
return False
|
||||
|
||||
# Resolve field value from context
|
||||
actual_value = self._resolve_field(field_path, context)
|
||||
|
||||
# Get operator function
|
||||
op_func = self.OPERATORS.get(operator)
|
||||
if not op_func:
|
||||
logger.warning(f"Unknown operator: {operator}")
|
||||
return False
|
||||
|
||||
# Evaluate
|
||||
try:
|
||||
return op_func(actual_value, expected_value)
|
||||
except Exception as e:
|
||||
logger.error(f"Error evaluating condition: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
def _resolve_field(self, field_path, context):
|
||||
"""
|
||||
Resolve a field path from context.
|
||||
|
||||
Examples:
|
||||
- "content.status" -> context['content']['status']
|
||||
- "count" -> context['count']
|
||||
"""
|
||||
parts = field_path.split('.')
|
||||
value = context
|
||||
|
||||
for part in parts:
|
||||
if isinstance(value, dict):
|
||||
value = value.get(part)
|
||||
elif hasattr(value, part):
|
||||
value = getattr(value, part)
|
||||
else:
|
||||
return None
|
||||
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
return value
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Rule Engine
|
||||
Orchestrates rule execution
|
||||
"""
|
||||
import logging
|
||||
from igny8_core.business.automation.services.condition_evaluator import ConditionEvaluator
|
||||
from igny8_core.business.automation.services.action_executor import ActionExecutor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RuleEngine:
|
||||
"""Orchestrates rule execution"""
|
||||
|
||||
def __init__(self):
|
||||
self.condition_evaluator = ConditionEvaluator()
|
||||
self.action_executor = ActionExecutor()
|
||||
|
||||
def execute(self, rule, context):
|
||||
"""
|
||||
Execute a rule by evaluating conditions and executing actions.
|
||||
|
||||
Args:
|
||||
rule: AutomationRule instance
|
||||
context: Context dict for evaluation
|
||||
|
||||
Returns:
|
||||
dict: Execution results
|
||||
"""
|
||||
# Evaluate conditions
|
||||
if rule.conditions:
|
||||
conditions_met = self.condition_evaluator.evaluate(rule.conditions, context)
|
||||
if not conditions_met:
|
||||
return {
|
||||
'success': False,
|
||||
'reason': 'Conditions not met'
|
||||
}
|
||||
|
||||
# Execute actions
|
||||
action_results = []
|
||||
for action in rule.actions:
|
||||
try:
|
||||
result = self.action_executor.execute(action, context, rule)
|
||||
action_results.append({
|
||||
'action': action,
|
||||
'success': True,
|
||||
'result': result
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Action execution failed: {str(e)}", exc_info=True)
|
||||
action_results.append({
|
||||
'action': action,
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
})
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'actions': action_results
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user