old automation cleanup adn status feilds of planner udpate

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-03 05:13:53 +00:00
parent 7df6e190fc
commit c9f082cb12
40 changed files with 1832 additions and 2134 deletions

View File

@@ -1,4 +0,0 @@
"""
Automation business logic - AutomationRule, ScheduledTask models and services
"""

View File

@@ -1,143 +0,0 @@
"""
Automation Models
Phase 2: Automation System
"""
from django.db import models
from django.core.validators import MinValueValidator
from igny8_core.auth.models import SiteSectorBaseModel, AccountBaseModel
import json
class AutomationRule(SiteSectorBaseModel):
"""
Automation Rule model for defining automated workflows.
Rules can be triggered by:
- schedule: Time-based triggers (cron-like)
- event: Event-based triggers (content created, keyword added, etc.)
- manual: Manual execution only
"""
TRIGGER_CHOICES = [
('schedule', 'Schedule'),
('event', 'Event'),
('manual', 'Manual'),
]
STATUS_CHOICES = [
('active', 'Active'),
('inactive', 'Inactive'),
('paused', 'Paused'),
]
name = models.CharField(max_length=255, help_text="Rule name")
description = models.TextField(blank=True, null=True, help_text="Rule description")
# Trigger configuration
trigger = models.CharField(max_length=50, choices=TRIGGER_CHOICES, default='manual')
# Schedule configuration (for schedule triggers)
# Stored as cron-like string: "0 0 * * *" (daily at midnight)
schedule = models.CharField(
max_length=100,
blank=True,
null=True,
help_text="Cron-like schedule string (e.g., '0 0 * * *' for daily at midnight)"
)
# Conditions (JSON field)
# Format: [{"field": "content.status", "operator": "equals", "value": "draft"}, ...]
conditions = models.JSONField(
default=list,
help_text="List of conditions that must be met for rule to execute"
)
# Actions (JSON field)
# Format: [{"type": "generate_content", "params": {...}}, ...]
actions = models.JSONField(
default=list,
help_text="List of actions to execute when rule triggers"
)
# Status
is_active = models.BooleanField(default=True, help_text="Whether rule is active")
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='active')
# Execution tracking
last_executed_at = models.DateTimeField(null=True, blank=True)
execution_count = models.IntegerField(default=0, validators=[MinValueValidator(0)])
# Metadata
metadata = models.JSONField(default=dict, help_text="Additional metadata")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'automation'
db_table = 'igny8_automation_rules'
ordering = ['-created_at']
verbose_name = 'Automation Rule'
verbose_name_plural = 'Automation Rules'
indexes = [
models.Index(fields=['trigger', 'is_active']),
models.Index(fields=['status']),
models.Index(fields=['site', 'sector']),
models.Index(fields=['trigger', 'is_active', 'status']),
]
def __str__(self):
return f"{self.name} ({self.get_trigger_display()})"
class ScheduledTask(AccountBaseModel):
"""
Scheduled Task model for tracking scheduled automation rule executions.
"""
STATUS_CHOICES = [
('pending', 'Pending'),
('running', 'Running'),
('completed', 'Completed'),
('failed', 'Failed'),
('cancelled', 'Cancelled'),
]
automation_rule = models.ForeignKey(
AutomationRule,
on_delete=models.CASCADE,
related_name='scheduled_tasks',
help_text="The automation rule this task belongs to"
)
scheduled_at = models.DateTimeField(help_text="When the task is scheduled to run")
executed_at = models.DateTimeField(null=True, blank=True, help_text="When the task was actually executed")
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='pending')
# Execution results
result = models.JSONField(default=dict, help_text="Execution result data")
error_message = models.TextField(blank=True, null=True, help_text="Error message if execution failed")
# Metadata
metadata = models.JSONField(default=dict, help_text="Additional metadata")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'automation'
db_table = 'igny8_scheduled_tasks'
ordering = ['-scheduled_at']
verbose_name = 'Scheduled Task'
verbose_name_plural = 'Scheduled Tasks'
indexes = [
models.Index(fields=['automation_rule', 'status']),
models.Index(fields=['scheduled_at', 'status']),
models.Index(fields=['account', 'status']),
models.Index(fields=['status', 'scheduled_at']),
]
def __str__(self):
return f"Scheduled task for {self.automation_rule.name} at {self.scheduled_at}"

View File

@@ -1,4 +0,0 @@
"""
Automation services
"""

View File

@@ -1,101 +0,0 @@
"""
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)
}

View File

@@ -1,141 +0,0 @@
"""
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

View File

@@ -1,104 +0,0 @@
"""
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

View File

@@ -1,61 +0,0 @@
"""
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
}

View File

@@ -1,28 +0,0 @@
"""
Automation Celery Tasks
"""
from celery import shared_task
import logging
from igny8_core.business.automation.services.automation_service import AutomationService
logger = logging.getLogger(__name__)
@shared_task(name='igny8_core.business.automation.tasks.execute_scheduled_automation_rules')
def execute_scheduled_automation_rules():
"""
Execute all scheduled automation rules.
Called by Celery Beat.
"""
try:
service = AutomationService()
result = service.execute_scheduled_rules()
logger.info(f"Executed scheduled automation rules: {result}")
return result
except Exception as e:
logger.error(f"Error executing scheduled automation rules: {str(e)}", exc_info=True)
return {
'success': False,
'error': str(e)
}

View File

@@ -5,12 +5,18 @@ from igny8_core.auth.models import SiteSectorBaseModel, SeedKeyword
class Clusters(SiteSectorBaseModel):
"""Clusters model for keyword grouping - pure topic clusters"""
STATUS_CHOICES = [
('new', 'New'),
('mapped', 'Mapped'),
]
name = models.CharField(max_length=255, unique=True, db_index=True)
description = models.TextField(blank=True, null=True)
keywords_count = models.IntegerField(default=0)
volume = models.IntegerField(default=0)
mapped_pages = models.IntegerField(default=0)
status = models.CharField(max_length=50, default='active')
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='new')
disabled = models.BooleanField(default=False, help_text="Exclude from processes")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -37,9 +43,8 @@ class Keywords(SiteSectorBaseModel):
"""
STATUS_CHOICES = [
('active', 'Active'),
('pending', 'Pending'),
('archived', 'Archived'),
('new', 'New'),
('mapped', 'Mapped'),
]
# Required: Link to global SeedKeyword
@@ -75,7 +80,8 @@ class Keywords(SiteSectorBaseModel):
related_name='keywords',
limit_choices_to={'sector': models.F('sector')} # Cluster must be in same sector
)
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='pending')
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='new')
disabled = models.BooleanField(default=False, help_text="Exclude from processes")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -142,8 +148,8 @@ class ContentIdeas(SiteSectorBaseModel):
STATUS_CHOICES = [
('new', 'New'),
('scheduled', 'Scheduled'),
('published', 'Published'),
('queued', 'Queued'),
('completed', 'Completed'),
]
CONTENT_TYPE_CHOICES = [
@@ -193,6 +199,7 @@ class ContentIdeas(SiteSectorBaseModel):
)
# REMOVED: taxonomy FK to SiteBlueprintTaxonomy (legacy blueprint functionality)
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='new')
disabled = models.BooleanField(default=False, help_text="Exclude from processes")
estimated_word_count = models.IntegerField(default=1000)
content_type = models.CharField(
max_length=50,