403 lines
16 KiB
Python
403 lines
16 KiB
Python
"""
|
|
Conflict resolution system for content scheduling.
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, List, Any, Optional, Tuple
|
|
from dataclasses import dataclass
|
|
|
|
# Use unified database models
|
|
from lib.database.models import ContentItem, Schedule, ScheduleStatus
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
@dataclass
|
|
class ConflictInfo:
|
|
"""Information about a scheduling conflict."""
|
|
schedule_1: Schedule
|
|
schedule_2: Schedule
|
|
conflict_type: str
|
|
severity: str
|
|
description: str
|
|
suggested_resolution: str
|
|
|
|
class ConflictResolver:
|
|
"""Resolve scheduling conflicts automatically."""
|
|
|
|
def __init__(self):
|
|
"""Initialize the conflict resolver."""
|
|
self.logger = logger
|
|
self.resolution_strategies = {
|
|
'time_overlap': self._resolve_time_overlap,
|
|
'platform_conflict': self._resolve_platform_conflict,
|
|
'resource_conflict': self._resolve_resource_conflict,
|
|
'priority_conflict': self._resolve_priority_conflict
|
|
}
|
|
|
|
def detect_conflicts(self, schedules: List[Schedule]) -> List[ConflictInfo]:
|
|
"""Detect conflicts between schedules.
|
|
|
|
Args:
|
|
schedules: List of Schedule objects to check
|
|
|
|
Returns:
|
|
List of detected conflicts
|
|
"""
|
|
try:
|
|
conflicts = []
|
|
|
|
# Sort schedules by time
|
|
sorted_schedules = sorted(schedules, key=lambda x: x.scheduled_time)
|
|
|
|
for i in range(len(sorted_schedules)):
|
|
for j in range(i + 1, len(sorted_schedules)):
|
|
schedule_1 = sorted_schedules[i]
|
|
schedule_2 = sorted_schedules[j]
|
|
|
|
# Check for time overlap conflicts
|
|
time_conflicts = self._check_time_overlap(schedule_1, schedule_2)
|
|
conflicts.extend(time_conflicts)
|
|
|
|
# Check for platform conflicts
|
|
platform_conflicts = self._check_platform_conflict(schedule_1, schedule_2)
|
|
conflicts.extend(platform_conflicts)
|
|
|
|
# Check for priority conflicts
|
|
priority_conflicts = self._check_priority_conflict(schedule_1, schedule_2)
|
|
conflicts.extend(priority_conflicts)
|
|
|
|
return conflicts
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error detecting conflicts: {str(e)}")
|
|
return []
|
|
|
|
def _check_time_overlap(self, schedule_1: Schedule, schedule_2: Schedule) -> List[ConflictInfo]:
|
|
"""Check for time overlap conflicts."""
|
|
conflicts = []
|
|
|
|
try:
|
|
# Assume each schedule takes 1 hour (can be made configurable)
|
|
duration = timedelta(hours=1)
|
|
|
|
end_1 = schedule_1.scheduled_time + duration
|
|
end_2 = schedule_2.scheduled_time + duration
|
|
|
|
# Check for overlap
|
|
if (schedule_1.scheduled_time < end_2 and end_1 > schedule_2.scheduled_time):
|
|
time_diff = abs((schedule_2.scheduled_time - schedule_1.scheduled_time).total_seconds() / 60)
|
|
|
|
severity = 'high' if time_diff < 30 else 'medium'
|
|
|
|
conflicts.append(ConflictInfo(
|
|
schedule_1=schedule_1,
|
|
schedule_2=schedule_2,
|
|
conflict_type='time_overlap',
|
|
severity=severity,
|
|
description=f"Schedules overlap by {60 - time_diff:.0f} minutes",
|
|
suggested_resolution=f"Move one schedule by at least {60 - time_diff + 15:.0f} minutes"
|
|
))
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error checking time overlap: {str(e)}")
|
|
|
|
return conflicts
|
|
|
|
def _check_platform_conflict(self, schedule_1: Schedule, schedule_2: Schedule) -> List[ConflictInfo]:
|
|
"""Check for platform conflicts."""
|
|
conflicts = []
|
|
|
|
try:
|
|
# This is a placeholder - platform conflicts would depend on specific platform limitations
|
|
# For now, we'll check if schedules are too close on the same platform
|
|
|
|
time_diff = abs((schedule_2.scheduled_time - schedule_1.scheduled_time).total_seconds() / 60)
|
|
|
|
# If schedules are within 15 minutes, it might be a platform conflict
|
|
if time_diff < 15:
|
|
conflicts.append(ConflictInfo(
|
|
schedule_1=schedule_1,
|
|
schedule_2=schedule_2,
|
|
conflict_type='platform_conflict',
|
|
severity='medium',
|
|
description=f"Schedules too close for optimal platform performance",
|
|
suggested_resolution="Space schedules at least 15 minutes apart"
|
|
))
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error checking platform conflict: {str(e)}")
|
|
|
|
return conflicts
|
|
|
|
def _check_priority_conflict(self, schedule_1: Schedule, schedule_2: Schedule) -> List[ConflictInfo]:
|
|
"""Check for priority conflicts."""
|
|
conflicts = []
|
|
|
|
try:
|
|
# Check if high priority items are scheduled too close to low priority items
|
|
if schedule_1.priority > 7 and schedule_2.priority < 4:
|
|
time_diff = abs((schedule_2.scheduled_time - schedule_1.scheduled_time).total_seconds() / 60)
|
|
|
|
if time_diff < 60: # Within 1 hour
|
|
conflicts.append(ConflictInfo(
|
|
schedule_1=schedule_1,
|
|
schedule_2=schedule_2,
|
|
conflict_type='priority_conflict',
|
|
severity='low',
|
|
description="High priority content scheduled close to low priority content",
|
|
suggested_resolution="Consider spacing high and low priority content further apart"
|
|
))
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error checking priority conflict: {str(e)}")
|
|
|
|
return conflicts
|
|
|
|
def resolve_conflicts(self, conflicts: List[ConflictInfo]) -> Dict[str, Any]:
|
|
"""Resolve detected conflicts automatically.
|
|
|
|
Args:
|
|
conflicts: List of conflicts to resolve
|
|
|
|
Returns:
|
|
Dictionary containing resolution results
|
|
"""
|
|
try:
|
|
resolved_conflicts = []
|
|
unresolved_conflicts = []
|
|
schedule_adjustments = {}
|
|
|
|
for conflict in conflicts:
|
|
try:
|
|
# Get resolution strategy
|
|
strategy = self.resolution_strategies.get(conflict.conflict_type)
|
|
|
|
if strategy:
|
|
resolution = strategy(conflict)
|
|
|
|
if resolution['success']:
|
|
resolved_conflicts.append({
|
|
'conflict': conflict,
|
|
'resolution': resolution
|
|
})
|
|
|
|
# Track schedule adjustments
|
|
for schedule_id, adjustments in resolution.get('adjustments', {}).items():
|
|
if schedule_id not in schedule_adjustments:
|
|
schedule_adjustments[schedule_id] = {}
|
|
schedule_adjustments[schedule_id].update(adjustments)
|
|
else:
|
|
unresolved_conflicts.append(conflict)
|
|
else:
|
|
unresolved_conflicts.append(conflict)
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error resolving conflict: {str(e)}")
|
|
unresolved_conflicts.append(conflict)
|
|
|
|
return {
|
|
'resolved_conflicts': resolved_conflicts,
|
|
'unresolved_conflicts': unresolved_conflicts,
|
|
'schedule_adjustments': schedule_adjustments,
|
|
'success_rate': len(resolved_conflicts) / len(conflicts) if conflicts else 1.0
|
|
}
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error resolving conflicts: {str(e)}")
|
|
return {
|
|
'resolved_conflicts': [],
|
|
'unresolved_conflicts': conflicts,
|
|
'schedule_adjustments': {},
|
|
'success_rate': 0.0
|
|
}
|
|
|
|
def _resolve_time_overlap(self, conflict: ConflictInfo) -> Dict[str, Any]:
|
|
"""Resolve time overlap conflicts."""
|
|
try:
|
|
# Strategy: Move the lower priority schedule
|
|
schedule_1 = conflict.schedule_1
|
|
schedule_2 = conflict.schedule_2
|
|
|
|
# Determine which schedule to move
|
|
if schedule_1.priority >= schedule_2.priority:
|
|
schedule_to_move = schedule_2
|
|
anchor_schedule = schedule_1
|
|
else:
|
|
schedule_to_move = schedule_1
|
|
anchor_schedule = schedule_2
|
|
|
|
# Calculate new time (move 1.5 hours after anchor)
|
|
new_time = anchor_schedule.scheduled_time + timedelta(hours=1.5)
|
|
|
|
return {
|
|
'success': True,
|
|
'strategy': 'move_lower_priority',
|
|
'adjustments': {
|
|
str(schedule_to_move.id): {
|
|
'new_scheduled_time': new_time,
|
|
'reason': 'Resolved time overlap conflict'
|
|
}
|
|
},
|
|
'description': f"Moved schedule {schedule_to_move.id} to {new_time}"
|
|
}
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error resolving time overlap: {str(e)}")
|
|
return {'success': False, 'error': str(e)}
|
|
|
|
def _resolve_platform_conflict(self, conflict: ConflictInfo) -> Dict[str, Any]:
|
|
"""Resolve platform conflicts."""
|
|
try:
|
|
# Strategy: Space schedules 20 minutes apart
|
|
schedule_1 = conflict.schedule_1
|
|
schedule_2 = conflict.schedule_2
|
|
|
|
# Move the later schedule
|
|
if schedule_1.scheduled_time < schedule_2.scheduled_time:
|
|
schedule_to_move = schedule_2
|
|
anchor_time = schedule_1.scheduled_time
|
|
else:
|
|
schedule_to_move = schedule_1
|
|
anchor_time = schedule_2.scheduled_time
|
|
|
|
new_time = anchor_time + timedelta(minutes=20)
|
|
|
|
return {
|
|
'success': True,
|
|
'strategy': 'space_schedules',
|
|
'adjustments': {
|
|
str(schedule_to_move.id): {
|
|
'new_scheduled_time': new_time,
|
|
'reason': 'Resolved platform conflict'
|
|
}
|
|
},
|
|
'description': f"Spaced schedule {schedule_to_move.id} to {new_time}"
|
|
}
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error resolving platform conflict: {str(e)}")
|
|
return {'success': False, 'error': str(e)}
|
|
|
|
def _resolve_resource_conflict(self, conflict: ConflictInfo) -> Dict[str, Any]:
|
|
"""Resolve resource conflicts."""
|
|
try:
|
|
# This is a placeholder for resource conflict resolution
|
|
return {
|
|
'success': False,
|
|
'reason': 'Resource conflict resolution not implemented'
|
|
}
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error resolving resource conflict: {str(e)}")
|
|
return {'success': False, 'error': str(e)}
|
|
|
|
def _resolve_priority_conflict(self, conflict: ConflictInfo) -> Dict[str, Any]:
|
|
"""Resolve priority conflicts."""
|
|
try:
|
|
# Strategy: Move low priority content away from high priority content
|
|
schedule_1 = conflict.schedule_1
|
|
schedule_2 = conflict.schedule_2
|
|
|
|
# Identify high and low priority schedules
|
|
if schedule_1.priority > schedule_2.priority:
|
|
high_priority = schedule_1
|
|
low_priority = schedule_2
|
|
else:
|
|
high_priority = schedule_2
|
|
low_priority = schedule_1
|
|
|
|
# Move low priority content 2 hours away
|
|
new_time = high_priority.scheduled_time + timedelta(hours=2)
|
|
|
|
return {
|
|
'success': True,
|
|
'strategy': 'separate_priorities',
|
|
'adjustments': {
|
|
str(low_priority.id): {
|
|
'new_scheduled_time': new_time,
|
|
'reason': 'Resolved priority conflict'
|
|
}
|
|
},
|
|
'description': f"Moved low priority schedule {low_priority.id} to {new_time}"
|
|
}
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error resolving priority conflict: {str(e)}")
|
|
return {'success': False, 'error': str(e)}
|
|
|
|
def suggest_optimal_schedule(
|
|
self,
|
|
new_schedule: Schedule,
|
|
existing_schedules: List[Schedule]
|
|
) -> Dict[str, Any]:
|
|
"""Suggest optimal scheduling for new content.
|
|
|
|
Args:
|
|
new_schedule: New schedule to optimize
|
|
existing_schedules: List of existing schedules
|
|
|
|
Returns:
|
|
Dictionary containing optimization suggestions
|
|
"""
|
|
try:
|
|
suggestions = []
|
|
|
|
# Check for conflicts with proposed time
|
|
all_schedules = existing_schedules + [new_schedule]
|
|
conflicts = self.detect_conflicts(all_schedules)
|
|
|
|
if not conflicts:
|
|
return {
|
|
'optimal_time': new_schedule.scheduled_time,
|
|
'conflicts': [],
|
|
'suggestions': ['Current time is optimal']
|
|
}
|
|
|
|
# Generate alternative times
|
|
base_time = new_schedule.scheduled_time
|
|
alternative_times = []
|
|
|
|
# Try different time slots
|
|
for hours_offset in [1, 2, 3, -1, -2, -3]:
|
|
alt_time = base_time + timedelta(hours=hours_offset)
|
|
alt_schedule = Schedule(
|
|
content_item_id=new_schedule.content_item_id,
|
|
scheduled_time=alt_time,
|
|
status=new_schedule.status,
|
|
recurrence=new_schedule.recurrence,
|
|
priority=new_schedule.priority
|
|
)
|
|
|
|
# Check conflicts for this alternative
|
|
alt_conflicts = self.detect_conflicts(existing_schedules + [alt_schedule])
|
|
|
|
alternative_times.append({
|
|
'time': alt_time,
|
|
'conflicts': len(alt_conflicts),
|
|
'severity': max([c.severity for c in alt_conflicts], default='none')
|
|
})
|
|
|
|
# Sort by number of conflicts and severity
|
|
alternative_times.sort(key=lambda x: (x['conflicts'], x['severity']))
|
|
|
|
optimal_time = alternative_times[0]['time'] if alternative_times else new_schedule.scheduled_time
|
|
|
|
return {
|
|
'optimal_time': optimal_time,
|
|
'conflicts': conflicts,
|
|
'alternatives': alternative_times[:3], # Top 3 alternatives
|
|
'suggestions': [
|
|
f"Consider scheduling at {optimal_time}",
|
|
f"Current time has {len(conflicts)} conflicts",
|
|
"Review alternative times for better optimization"
|
|
]
|
|
}
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error suggesting optimal schedule: {str(e)}")
|
|
return {
|
|
'optimal_time': new_schedule.scheduled_time,
|
|
'conflicts': [],
|
|
'suggestions': ['Error occurred during optimization']
|
|
} |