alwrity chatbot assistant, content scheduler, and content repurposing

This commit is contained in:
ajaysi
2025-06-02 00:00:18 +05:30
parent 889021c078
commit 5ca2fd5977
69 changed files with 13952 additions and 3279 deletions

View File

@@ -0,0 +1,651 @@
"""
Calendar integration for content scheduling.
"""
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Tuple
from dataclasses import dataclass
import json
# Use unified database models
from lib.database.models import ContentItem, Schedule, ScheduleStatus, ContentType, Platform, get_session
logger = logging.getLogger(__name__)
@dataclass
class CalendarEvent:
"""Calendar event representation."""
id: str
title: str
description: str
start_time: datetime
end_time: datetime
location: Optional[str] = None
attendees: List[str] = None
event_type: str = "content_schedule"
metadata: Dict[str, Any] = None
class CalendarIntegration:
"""Integration with calendar systems for content scheduling."""
def __init__(self, calendar_provider: str = "google"):
"""Initialize calendar integration.
Args:
calendar_provider: Calendar provider (google, outlook, etc.)
"""
self.logger = logger
self.session = get_session()
self.calendar_provider = calendar_provider
# Calendar provider configurations
self.provider_configs = {
'google': {
'api_endpoint': 'https://www.googleapis.com/calendar/v3',
'scopes': ['https://www.googleapis.com/auth/calendar'],
'event_duration_minutes': 30
},
'outlook': {
'api_endpoint': 'https://graph.microsoft.com/v1.0',
'scopes': ['https://graph.microsoft.com/calendars.readwrite'],
'event_duration_minutes': 30
},
'apple': {
'api_endpoint': 'https://caldav.icloud.com',
'scopes': ['calendar'],
'event_duration_minutes': 30
}
}
# Event templates for different content types
self.event_templates = {
ContentType.ARTICLE: {
'title_prefix': '📝 Publish Article:',
'description_template': 'Publish article "{title}" to {platforms}',
'duration_minutes': 15
},
ContentType.VIDEO: {
'title_prefix': '🎥 Publish Video:',
'description_template': 'Publish video "{title}" to {platforms}',
'duration_minutes': 30
},
ContentType.IMAGE: {
'title_prefix': '📸 Publish Image:',
'description_template': 'Publish image "{title}" to {platforms}',
'duration_minutes': 10
},
ContentType.SOCIAL_POST: {
'title_prefix': '📱 Social Post:',
'description_template': 'Publish social post "{title}" to {platforms}',
'duration_minutes': 5
}
}
def sync_schedules_to_calendar(self, schedules: List[Schedule] = None) -> Dict[str, Any]:
"""Sync content schedules to calendar.
Args:
schedules: List of schedules to sync (if None, sync all pending schedules)
Returns:
Dictionary with sync results
"""
try:
if schedules is None:
schedules = self.session.query(Schedule).filter(
Schedule.status == ScheduleStatus.PENDING
).all()
sync_results = {
'total_schedules': len(schedules),
'synced_successfully': 0,
'failed_syncs': 0,
'errors': [],
'created_events': []
}
for schedule in schedules:
try:
# Get content item details
content_item = self.session.query(ContentItem).filter(
ContentItem.id == schedule.content_item_id
).first()
if not content_item:
sync_results['errors'].append(f"Content item not found for schedule {schedule.id}")
sync_results['failed_syncs'] += 1
continue
# Create calendar event
event = self._create_calendar_event(schedule, content_item)
# Sync to calendar provider
event_id = self._sync_event_to_provider(event)
if event_id:
# Update schedule with calendar event ID
schedule.metadata = schedule.metadata or {}
schedule.metadata['calendar_event_id'] = event_id
self.session.commit()
sync_results['synced_successfully'] += 1
sync_results['created_events'].append({
'schedule_id': schedule.id,
'event_id': event_id,
'title': event.title
})
else:
sync_results['failed_syncs'] += 1
sync_results['errors'].append(f"Failed to create calendar event for schedule {schedule.id}")
except Exception as e:
self.logger.error(f"Error syncing schedule {schedule.id}: {str(e)}")
sync_results['failed_syncs'] += 1
sync_results['errors'].append(f"Schedule {schedule.id}: {str(e)}")
return sync_results
except Exception as e:
self.logger.error(f"Error syncing schedules to calendar: {str(e)}")
return {
'total_schedules': 0,
'synced_successfully': 0,
'failed_syncs': 0,
'errors': [f"Sync error: {str(e)}"],
'created_events': []
}
def import_calendar_events(self, calendar_id: str = None, date_range: Tuple[datetime, datetime] = None) -> Dict[str, Any]:
"""Import events from calendar and suggest content schedules.
Args:
calendar_id: Calendar ID to import from
date_range: Date range to import events from
Returns:
Dictionary with import results and suggestions
"""
try:
if date_range is None:
start_date = datetime.now()
end_date = start_date + timedelta(days=30)
date_range = (start_date, end_date)
# Get events from calendar provider
events = self._get_events_from_provider(calendar_id, date_range)
import_results = {
'total_events': len(events),
'content_suggestions': [],
'scheduling_gaps': [],
'optimal_times': []
}
# Analyze events for content scheduling opportunities
for event in events:
suggestions = self._analyze_event_for_content_opportunities(event)
import_results['content_suggestions'].extend(suggestions)
# Find scheduling gaps
gaps = self._find_scheduling_gaps(events, date_range)
import_results['scheduling_gaps'] = gaps
# Suggest optimal posting times
optimal_times = self._suggest_optimal_posting_times(events, date_range)
import_results['optimal_times'] = optimal_times
return import_results
except Exception as e:
self.logger.error(f"Error importing calendar events: {str(e)}")
return {
'total_events': 0,
'content_suggestions': [],
'scheduling_gaps': [],
'optimal_times': [],
'error': str(e)
}
def create_content_schedule_from_event(self, event: CalendarEvent, content_item_id: int) -> Optional[Schedule]:
"""Create a content schedule from a calendar event.
Args:
event: Calendar event
content_item_id: ID of content item to schedule
Returns:
Created schedule or None if failed
"""
try:
# Get content item
content_item = self.session.query(ContentItem).filter(
ContentItem.id == content_item_id
).first()
if not content_item:
self.logger.error(f"Content item {content_item_id} not found")
return None
# Create schedule
schedule = Schedule(
content_item_id=content_item_id,
scheduled_time=event.start_time,
status=ScheduleStatus.PENDING,
priority=5, # Default priority
metadata={
'calendar_event_id': event.id,
'created_from_calendar': True,
'original_event_title': event.title
}
)
self.session.add(schedule)
self.session.commit()
self.logger.info(f"Created schedule {schedule.id} from calendar event {event.id}")
return schedule
except Exception as e:
self.logger.error(f"Error creating schedule from event: {str(e)}")
self.session.rollback()
return None
def update_calendar_event_from_schedule(self, schedule: Schedule) -> bool:
"""Update calendar event when schedule changes.
Args:
schedule: Updated schedule
Returns:
True if successful, False otherwise
"""
try:
# Check if schedule has associated calendar event
if not schedule.metadata or 'calendar_event_id' not in schedule.metadata:
return False
event_id = schedule.metadata['calendar_event_id']
# Get content item
content_item = self.session.query(ContentItem).filter(
ContentItem.id == schedule.content_item_id
).first()
if not content_item:
return False
# Create updated event
updated_event = self._create_calendar_event(schedule, content_item)
updated_event.id = event_id
# Update event in calendar provider
success = self._update_event_in_provider(updated_event)
if success:
self.logger.info(f"Updated calendar event {event_id} for schedule {schedule.id}")
else:
self.logger.error(f"Failed to update calendar event {event_id}")
return success
except Exception as e:
self.logger.error(f"Error updating calendar event: {str(e)}")
return False
def delete_calendar_event_from_schedule(self, schedule: Schedule) -> bool:
"""Delete calendar event when schedule is deleted.
Args:
schedule: Schedule being deleted
Returns:
True if successful, False otherwise
"""
try:
# Check if schedule has associated calendar event
if not schedule.metadata or 'calendar_event_id' not in schedule.metadata:
return True # No event to delete
event_id = schedule.metadata['calendar_event_id']
# Delete event from calendar provider
success = self._delete_event_from_provider(event_id)
if success:
self.logger.info(f"Deleted calendar event {event_id} for schedule {schedule.id}")
else:
self.logger.error(f"Failed to delete calendar event {event_id}")
return success
except Exception as e:
self.logger.error(f"Error deleting calendar event: {str(e)}")
return False
def get_calendar_view(self, date_range: Tuple[datetime, datetime] = None) -> Dict[str, Any]:
"""Get calendar view of scheduled content.
Args:
date_range: Date range for calendar view
Returns:
Dictionary with calendar view data
"""
try:
if date_range is None:
start_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
end_date = start_date + timedelta(days=30)
date_range = (start_date, end_date)
# Get schedules in date range
schedules = self.session.query(Schedule).filter(
Schedule.scheduled_time >= date_range[0],
Schedule.scheduled_time <= date_range[1]
).all()
calendar_events = []
for schedule in schedules:
content_item = self.session.query(ContentItem).filter(
ContentItem.id == schedule.content_item_id
).first()
if content_item:
event = self._create_calendar_event(schedule, content_item)
calendar_events.append({
'id': str(schedule.id),
'title': event.title,
'description': event.description,
'start': event.start_time.isoformat(),
'end': event.end_time.isoformat(),
'status': schedule.status.value,
'priority': schedule.priority,
'content_type': content_item.content_type.value if content_item.content_type else 'unknown',
'platforms': schedule.platforms or []
})
# Group events by day
events_by_day = {}
for event in calendar_events:
day = datetime.fromisoformat(event['start']).date()
if day not in events_by_day:
events_by_day[day] = []
events_by_day[day].append(event)
return {
'date_range': {
'start': date_range[0].isoformat(),
'end': date_range[1].isoformat()
},
'total_events': len(calendar_events),
'events': calendar_events,
'events_by_day': {day.isoformat(): events for day, events in events_by_day.items()},
'summary': self._generate_calendar_summary(calendar_events)
}
except Exception as e:
self.logger.error(f"Error getting calendar view: {str(e)}")
return {
'date_range': None,
'total_events': 0,
'events': [],
'events_by_day': {},
'summary': {},
'error': str(e)
}
def _create_calendar_event(self, schedule: Schedule, content_item: ContentItem) -> CalendarEvent:
"""Create calendar event from schedule and content item."""
try:
# Get event template based on content type
template = self.event_templates.get(
content_item.content_type,
self.event_templates[ContentType.SOCIAL_POST]
)
# Create event title
title = f"{template['title_prefix']} {content_item.title}"
# Create event description
platforms_str = ', '.join(schedule.platforms) if schedule.platforms else 'Default platforms'
description = template['description_template'].format(
title=content_item.title,
platforms=platforms_str
)
# Add content summary if available
if content_item.summary:
description += f"\n\nSummary: {content_item.summary}"
# Calculate end time
duration = timedelta(minutes=template['duration_minutes'])
end_time = schedule.scheduled_time + duration
# Create metadata
metadata = {
'schedule_id': schedule.id,
'content_item_id': content_item.id,
'content_type': content_item.content_type.value if content_item.content_type else 'unknown',
'platforms': schedule.platforms or [],
'priority': schedule.priority,
'status': schedule.status.value
}
return CalendarEvent(
id=f"schedule_{schedule.id}",
title=title,
description=description,
start_time=schedule.scheduled_time,
end_time=end_time,
metadata=metadata
)
except Exception as e:
self.logger.error(f"Error creating calendar event: {str(e)}")
# Return a basic event as fallback
return CalendarEvent(
id=f"schedule_{schedule.id}",
title=f"Content Schedule: {content_item.title}",
description="Content publishing schedule",
start_time=schedule.scheduled_time,
end_time=schedule.scheduled_time + timedelta(minutes=30)
)
def _sync_event_to_provider(self, event: CalendarEvent) -> Optional[str]:
"""Sync event to calendar provider (mock implementation)."""
try:
# This is a mock implementation
# In a real system, you would integrate with actual calendar APIs
self.logger.info(f"Syncing event to {self.calendar_provider}: {event.title}")
# Simulate API call
event_id = f"{self.calendar_provider}_{event.id}_{int(datetime.now().timestamp())}"
return event_id
except Exception as e:
self.logger.error(f"Error syncing event to provider: {str(e)}")
return None
def _get_events_from_provider(self, calendar_id: str, date_range: Tuple[datetime, datetime]) -> List[CalendarEvent]:
"""Get events from calendar provider (mock implementation)."""
try:
# This is a mock implementation
# In a real system, you would fetch from actual calendar APIs
self.logger.info(f"Fetching events from {self.calendar_provider} calendar {calendar_id}")
# Return empty list for mock
return []
except Exception as e:
self.logger.error(f"Error fetching events from provider: {str(e)}")
return []
def _update_event_in_provider(self, event: CalendarEvent) -> bool:
"""Update event in calendar provider (mock implementation)."""
try:
# This is a mock implementation
self.logger.info(f"Updating event in {self.calendar_provider}: {event.id}")
return True
except Exception as e:
self.logger.error(f"Error updating event in provider: {str(e)}")
return False
def _delete_event_from_provider(self, event_id: str) -> bool:
"""Delete event from calendar provider (mock implementation)."""
try:
# This is a mock implementation
self.logger.info(f"Deleting event from {self.calendar_provider}: {event_id}")
return True
except Exception as e:
self.logger.error(f"Error deleting event from provider: {str(e)}")
return False
def _analyze_event_for_content_opportunities(self, event: CalendarEvent) -> List[Dict[str, Any]]:
"""Analyze calendar event for content opportunities."""
suggestions = []
try:
# Look for keywords that suggest content opportunities
content_keywords = ['meeting', 'conference', 'launch', 'announcement', 'webinar', 'presentation']
event_text = f"{event.title} {event.description}".lower()
for keyword in content_keywords:
if keyword in event_text:
suggestions.append({
'type': 'content_opportunity',
'keyword': keyword,
'suggested_time': event.end_time, # Suggest posting after the event
'content_type': self._suggest_content_type_for_keyword(keyword),
'description': f"Consider creating content about the {keyword}"
})
except Exception as e:
self.logger.error(f"Error analyzing event for opportunities: {str(e)}")
return suggestions
def _find_scheduling_gaps(self, events: List[CalendarEvent], date_range: Tuple[datetime, datetime]) -> List[Dict[str, Any]]:
"""Find gaps in schedule that could be used for content posting."""
gaps = []
try:
# Sort events by start time
sorted_events = sorted(events, key=lambda x: x.start_time)
current_time = date_range[0]
for event in sorted_events:
# Check if there's a gap before this event
if event.start_time > current_time + timedelta(hours=2):
gaps.append({
'start': current_time.isoformat(),
'end': event.start_time.isoformat(),
'duration_hours': (event.start_time - current_time).total_seconds() / 3600,
'suggested_use': 'Content posting opportunity'
})
current_time = max(current_time, event.end_time)
# Check for gap after last event
if current_time < date_range[1] - timedelta(hours=2):
gaps.append({
'start': current_time.isoformat(),
'end': date_range[1].isoformat(),
'duration_hours': (date_range[1] - current_time).total_seconds() / 3600,
'suggested_use': 'Content posting opportunity'
})
except Exception as e:
self.logger.error(f"Error finding scheduling gaps: {str(e)}")
return gaps
def _suggest_optimal_posting_times(self, events: List[CalendarEvent], date_range: Tuple[datetime, datetime]) -> List[Dict[str, Any]]:
"""Suggest optimal times for content posting based on calendar."""
optimal_times = []
try:
# Define optimal posting hours (9 AM, 1 PM, 5 PM)
optimal_hours = [9, 13, 17]
current_date = date_range[0].date()
end_date = date_range[1].date()
while current_date <= end_date:
for hour in optimal_hours:
suggested_time = datetime.combine(current_date, datetime.min.time().replace(hour=hour))
# Check if this time conflicts with any events
conflicts = any(
event.start_time <= suggested_time <= event.end_time
for event in events
)
if not conflicts:
optimal_times.append({
'time': suggested_time.isoformat(),
'reason': f'Optimal posting time ({hour}:00) with no calendar conflicts',
'confidence': 0.8
})
current_date += timedelta(days=1)
except Exception as e:
self.logger.error(f"Error suggesting optimal posting times: {str(e)}")
return optimal_times
def _suggest_content_type_for_keyword(self, keyword: str) -> str:
"""Suggest content type based on keyword."""
keyword_mapping = {
'meeting': 'social_post',
'conference': 'article',
'launch': 'video',
'announcement': 'social_post',
'webinar': 'video',
'presentation': 'article'
}
return keyword_mapping.get(keyword, 'social_post')
def _generate_calendar_summary(self, events: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Generate summary statistics for calendar events."""
try:
if not events:
return {}
# Count by status
status_counts = {}
for event in events:
status = event.get('status', 'unknown')
status_counts[status] = status_counts.get(status, 0) + 1
# Count by content type
type_counts = {}
for event in events:
content_type = event.get('content_type', 'unknown')
type_counts[content_type] = type_counts.get(content_type, 0) + 1
# Count by day
daily_counts = {}
for event in events:
day = datetime.fromisoformat(event['start']).date().isoformat()
daily_counts[day] = daily_counts.get(day, 0) + 1
return {
'total_events': len(events),
'by_status': status_counts,
'by_content_type': type_counts,
'by_day': daily_counts,
'busiest_day': max(daily_counts.items(), key=lambda x: x[1]) if daily_counts else None
}
except Exception as e:
self.logger.error(f"Error generating calendar summary: {str(e)}")
return {}