Files
igny8/backend/igny8_core/business/automation/views.py
IGNY8 VPS (Salman) 316cafab1b automation fixes
2025-12-03 12:24:59 +00:00

428 lines
14 KiB
Python

"""
Automation API Views
REST API endpoints for automation management
"""
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django.shortcuts import get_object_or_404
from django.utils import timezone
from igny8_core.business.automation.models import AutomationConfig, AutomationRun
from igny8_core.business.automation.services import AutomationService
from igny8_core.auth.models import Account, Site
class AutomationViewSet(viewsets.ViewSet):
"""API endpoints for automation"""
permission_classes = [IsAuthenticated]
def _get_site(self, request):
"""Get site from request"""
site_id = request.query_params.get('site_id')
if not site_id:
return None, Response(
{'error': 'site_id required'},
status=status.HTTP_400_BAD_REQUEST
)
site = get_object_or_404(Site, id=site_id, account=request.user.account)
return site, None
@action(detail=False, methods=['get'])
def config(self, request):
"""
GET /api/v1/automation/config/?site_id=123
Get automation configuration for site
"""
site, error_response = self._get_site(request)
if error_response:
return error_response
config, _ = AutomationConfig.objects.get_or_create(
account=site.account,
site=site,
defaults={
'is_enabled': False,
'frequency': 'daily',
'scheduled_time': '02:00',
}
)
return Response({
'is_enabled': config.is_enabled,
'frequency': config.frequency,
'scheduled_time': str(config.scheduled_time),
'stage_1_batch_size': config.stage_1_batch_size,
'stage_2_batch_size': config.stage_2_batch_size,
'stage_3_batch_size': config.stage_3_batch_size,
'stage_4_batch_size': config.stage_4_batch_size,
'stage_5_batch_size': config.stage_5_batch_size,
'stage_6_batch_size': config.stage_6_batch_size,
'last_run_at': config.last_run_at,
'next_run_at': config.next_run_at,
})
@action(detail=False, methods=['put'])
def update_config(self, request):
"""
PUT /api/v1/automation/update_config/?site_id=123
Update automation configuration
Body:
{
"is_enabled": true,
"frequency": "daily",
"scheduled_time": "02:00",
"stage_1_batch_size": 20,
...
}
"""
site, error_response = self._get_site(request)
if error_response:
return error_response
config, _ = AutomationConfig.objects.get_or_create(
account=site.account,
site=site
)
# Update fields
if 'is_enabled' in request.data:
config.is_enabled = request.data['is_enabled']
if 'frequency' in request.data:
config.frequency = request.data['frequency']
if 'scheduled_time' in request.data:
config.scheduled_time = request.data['scheduled_time']
if 'stage_1_batch_size' in request.data:
config.stage_1_batch_size = request.data['stage_1_batch_size']
if 'stage_2_batch_size' in request.data:
config.stage_2_batch_size = request.data['stage_2_batch_size']
if 'stage_3_batch_size' in request.data:
config.stage_3_batch_size = request.data['stage_3_batch_size']
if 'stage_4_batch_size' in request.data:
config.stage_4_batch_size = request.data['stage_4_batch_size']
if 'stage_5_batch_size' in request.data:
config.stage_5_batch_size = request.data['stage_5_batch_size']
if 'stage_6_batch_size' in request.data:
config.stage_6_batch_size = request.data['stage_6_batch_size']
config.save()
return Response({'message': 'Config updated'})
@action(detail=False, methods=['post'])
def run_now(self, request):
"""
POST /api/v1/automation/run_now/?site_id=123
Trigger automation run immediately
"""
site, error_response = self._get_site(request)
if error_response:
return error_response
try:
service = AutomationService(site.account, site)
run_id = service.start_automation(trigger_type='manual')
# Start async processing
from igny8_core.business.automation.tasks import run_automation_task
run_automation_task.delay(run_id)
return Response({
'run_id': run_id,
'message': 'Automation started'
})
except ValueError as e:
return Response(
{'error': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
return Response(
{'error': f'Failed to start automation: {str(e)}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@action(detail=False, methods=['get'])
def current_run(self, request):
"""
GET /api/v1/automation/current_run/?site_id=123
Get current automation run status
"""
site, error_response = self._get_site(request)
if error_response:
return error_response
run = AutomationRun.objects.filter(
site=site,
status__in=['running', 'paused']
).order_by('-started_at').first()
if not run:
return Response({'run': None})
return Response({
'run': {
'run_id': run.run_id,
'status': run.status,
'current_stage': run.current_stage,
'trigger_type': run.trigger_type,
'started_at': run.started_at,
'total_credits_used': run.total_credits_used,
'stage_1_result': run.stage_1_result,
'stage_2_result': run.stage_2_result,
'stage_3_result': run.stage_3_result,
'stage_4_result': run.stage_4_result,
'stage_5_result': run.stage_5_result,
'stage_6_result': run.stage_6_result,
'stage_7_result': run.stage_7_result,
}
})
@action(detail=False, methods=['post'])
def pause(self, request):
"""
POST /api/v1/automation/pause/?run_id=abc123
Pause automation run
"""
run_id = request.query_params.get('run_id')
if not run_id:
return Response(
{'error': 'run_id required'},
status=status.HTTP_400_BAD_REQUEST
)
try:
service = AutomationService.from_run_id(run_id)
service.pause_automation()
return Response({'message': 'Automation paused'})
except AutomationRun.DoesNotExist:
return Response(
{'error': 'Run not found'},
status=status.HTTP_404_NOT_FOUND
)
@action(detail=False, methods=['post'])
def resume(self, request):
"""
POST /api/v1/automation/resume/?run_id=abc123
Resume paused automation run
"""
run_id = request.query_params.get('run_id')
if not run_id:
return Response(
{'error': 'run_id required'},
status=status.HTTP_400_BAD_REQUEST
)
try:
service = AutomationService.from_run_id(run_id)
service.resume_automation()
# Resume async processing
from igny8_core.business.automation.tasks import resume_automation_task
resume_automation_task.delay(run_id)
return Response({'message': 'Automation resumed'})
except AutomationRun.DoesNotExist:
return Response(
{'error': 'Run not found'},
status=status.HTTP_404_NOT_FOUND
)
@action(detail=False, methods=['get'])
def history(self, request):
"""
GET /api/v1/automation/history/?site_id=123
Get automation run history
"""
site, error_response = self._get_site(request)
if error_response:
return error_response
runs = AutomationRun.objects.filter(
site=site
).order_by('-started_at')[:20]
return Response({
'runs': [
{
'run_id': run.run_id,
'status': run.status,
'trigger_type': run.trigger_type,
'started_at': run.started_at,
'completed_at': run.completed_at,
'total_credits_used': run.total_credits_used,
'current_stage': run.current_stage,
}
for run in runs
]
})
@action(detail=False, methods=['get'])
def logs(self, request):
"""
GET /api/v1/automation/logs/?run_id=abc123&lines=100
Get automation run logs
"""
run_id = request.query_params.get('run_id')
if not run_id:
return Response(
{'error': 'run_id required'},
status=status.HTTP_400_BAD_REQUEST
)
try:
run = AutomationRun.objects.get(run_id=run_id)
service = AutomationService(run.account, run.site)
lines = int(request.query_params.get('lines', 100))
log_text = service.logger.get_activity_log(
run_id, run.account.id, run.site.id, lines
)
return Response({
'run_id': run_id,
'log': log_text
})
except AutomationRun.DoesNotExist:
return Response(
{'error': 'Run not found'},
status=status.HTTP_404_NOT_FOUND
)
@action(detail=False, methods=['get'])
def estimate(self, request):
"""
GET /api/v1/automation/estimate/?site_id=123
Estimate credits needed for automation
"""
site, error_response = self._get_site(request)
if error_response:
return error_response
service = AutomationService(site.account, site)
estimated_credits = service.estimate_credits()
return Response({
'estimated_credits': estimated_credits,
'current_balance': site.account.credits,
'sufficient': site.account.credits >= (estimated_credits * 1.2)
})
@action(detail=False, methods=['get'])
def pipeline_overview(self, request):
"""
GET /api/v1/automation/pipeline_overview/?site_id=123
Get pipeline overview with pending counts for all stages
"""
site, error_response = self._get_site(request)
if error_response:
return error_response
from igny8_core.business.planning.models import Keywords, Clusters, ContentIdeas
from igny8_core.business.content.models import Tasks, Content, Images
from django.db.models import Count
# Stage 1: Keywords pending clustering
stage_1_pending = Keywords.objects.filter(
site=site,
status='new',
cluster__isnull=True,
disabled=False
).count()
# Stage 2: Clusters needing ideas
stage_2_pending = Clusters.objects.filter(
site=site,
status='new',
disabled=False
).exclude(
ideas__isnull=False
).count()
# Stage 3: Ideas ready to queue
stage_3_pending = ContentIdeas.objects.filter(
site=site,
status='new'
).count()
# Stage 4: Tasks ready for content generation
# Tasks don't have content FK - check if content exists via task title matching
stage_4_pending = Tasks.objects.filter(
site=site,
status='queued'
).count()
# Stage 5: Content ready for image prompts
stage_5_pending = Content.objects.filter(
site=site,
status='draft'
).annotate(
images_count=Count('images')
).filter(
images_count=0
).count()
# Stage 6: Image prompts ready for generation
stage_6_pending = Images.objects.filter(
site=site,
status='pending'
).count()
# Stage 7: Content ready for review
stage_7_ready = Content.objects.filter(
site=site,
status='review'
).count()
return Response({
'stages': [
{
'number': 1,
'name': 'Keywords → Clusters',
'pending': stage_1_pending,
'type': 'AI'
},
{
'number': 2,
'name': 'Clusters → Ideas',
'pending': stage_2_pending,
'type': 'AI'
},
{
'number': 3,
'name': 'Ideas → Tasks',
'pending': stage_3_pending,
'type': 'Local'
},
{
'number': 4,
'name': 'Tasks → Content',
'pending': stage_4_pending,
'type': 'AI'
},
{
'number': 5,
'name': 'Content → Image Prompts',
'pending': stage_5_pending,
'type': 'AI'
},
{
'number': 6,
'name': 'Image Prompts → Images',
'pending': stage_6_pending,
'type': 'AI'
},
{
'number': 7,
'name': 'Manual Review Gate',
'pending': stage_7_ready,
'type': 'Manual'
}
]
})