Base code
This commit is contained in:
349
backend/services/active_strategy_service.py
Normal file
349
backend/services/active_strategy_service.py
Normal file
@@ -0,0 +1,349 @@
|
||||
"""
|
||||
Active Strategy Service
|
||||
|
||||
Manages active content strategies with 3-tier caching for optimal performance
|
||||
in content calendar generation. Ensures Phase 1 and Phase 2 use the correct
|
||||
active strategy from the database.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, desc
|
||||
from loguru import logger
|
||||
|
||||
# Import database models
|
||||
from models.enhanced_strategy_models import EnhancedContentStrategy
|
||||
from models.monitoring_models import StrategyActivationStatus
|
||||
|
||||
class ActiveStrategyService:
|
||||
"""
|
||||
Service for managing active content strategies with 3-tier caching.
|
||||
|
||||
Tier 1: Memory cache (fastest)
|
||||
Tier 2: Database query with activation status
|
||||
Tier 3: Fallback to most recent strategy
|
||||
"""
|
||||
|
||||
def __init__(self, db_session: Optional[Session] = None):
|
||||
self.db_session = db_session
|
||||
self._memory_cache = {} # Tier 1: Memory cache
|
||||
self._cache_ttl = 300 # 5 minutes cache TTL
|
||||
self._last_cache_update = {}
|
||||
|
||||
logger.info("🚀 ActiveStrategyService initialized with 3-tier caching")
|
||||
|
||||
async def get_active_strategy(self, user_id: int, force_refresh: bool = False) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get the active content strategy for a user with 3-tier caching.
|
||||
|
||||
Args:
|
||||
user_id: User ID
|
||||
force_refresh: Force refresh cache
|
||||
|
||||
Returns:
|
||||
Active strategy data or None if not found
|
||||
"""
|
||||
try:
|
||||
cache_key = f"active_strategy_{user_id}"
|
||||
|
||||
# Tier 1: Memory Cache Check
|
||||
if not force_refresh and self._is_cache_valid(cache_key):
|
||||
cached_strategy = self._memory_cache.get(cache_key)
|
||||
if cached_strategy:
|
||||
logger.info(f"✅ Tier 1 Cache HIT: Active strategy for user {user_id}")
|
||||
return cached_strategy
|
||||
|
||||
# Tier 2: Database Query with Activation Status
|
||||
active_strategy = await self._get_active_strategy_from_db(user_id)
|
||||
if active_strategy:
|
||||
# Cache the result
|
||||
self._cache_strategy(cache_key, active_strategy)
|
||||
logger.info(f"✅ Tier 2 Database HIT: Active strategy {active_strategy.get('id')} for user {user_id}")
|
||||
return active_strategy
|
||||
|
||||
# Tier 3: Fallback to Most Recent Strategy
|
||||
fallback_strategy = await self._get_most_recent_strategy(user_id)
|
||||
if fallback_strategy:
|
||||
# Cache the fallback result
|
||||
self._cache_strategy(cache_key, fallback_strategy)
|
||||
logger.warning(f"⚠️ Tier 3 Fallback: Using most recent strategy {fallback_strategy.get('id')} for user {user_id}")
|
||||
return fallback_strategy
|
||||
|
||||
logger.error(f"❌ No strategy found for user {user_id}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error getting active strategy for user {user_id}: {str(e)}")
|
||||
return None
|
||||
|
||||
async def _get_active_strategy_from_db(self, user_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get active strategy from database using activation status.
|
||||
|
||||
Args:
|
||||
user_id: User ID
|
||||
|
||||
Returns:
|
||||
Active strategy data or None
|
||||
"""
|
||||
try:
|
||||
if not self.db_session:
|
||||
logger.warning("Database session not available")
|
||||
return None
|
||||
|
||||
# Query for active strategy using activation status
|
||||
active_status = self.db_session.query(StrategyActivationStatus).filter(
|
||||
and_(
|
||||
StrategyActivationStatus.user_id == user_id,
|
||||
StrategyActivationStatus.status == 'active'
|
||||
)
|
||||
).order_by(desc(StrategyActivationStatus.activation_date)).first()
|
||||
|
||||
if not active_status:
|
||||
logger.info(f"No active strategy status found for user {user_id}")
|
||||
return None
|
||||
|
||||
# Get the strategy details
|
||||
strategy = self.db_session.query(EnhancedContentStrategy).filter(
|
||||
EnhancedContentStrategy.id == active_status.strategy_id
|
||||
).first()
|
||||
|
||||
if not strategy:
|
||||
logger.warning(f"Active strategy {active_status.strategy_id} not found in database")
|
||||
return None
|
||||
|
||||
# Convert to dictionary
|
||||
strategy_data = self._convert_strategy_to_dict(strategy)
|
||||
strategy_data['activation_status'] = {
|
||||
'activation_date': active_status.activation_date.isoformat() if active_status.activation_date else None,
|
||||
'performance_score': active_status.performance_score,
|
||||
'last_updated': active_status.last_updated.isoformat() if active_status.last_updated else None
|
||||
}
|
||||
|
||||
logger.info(f"✅ Found active strategy {strategy.id} for user {user_id}")
|
||||
return strategy_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error querying active strategy from database: {str(e)}")
|
||||
return None
|
||||
|
||||
async def _get_most_recent_strategy(self, user_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get the most recent strategy as fallback.
|
||||
|
||||
Args:
|
||||
user_id: User ID
|
||||
|
||||
Returns:
|
||||
Most recent strategy data or None
|
||||
"""
|
||||
try:
|
||||
if not self.db_session:
|
||||
logger.warning("Database session not available")
|
||||
return None
|
||||
|
||||
# Get the most recent strategy with comprehensive AI analysis
|
||||
strategy = self.db_session.query(EnhancedContentStrategy).filter(
|
||||
and_(
|
||||
EnhancedContentStrategy.user_id == user_id,
|
||||
EnhancedContentStrategy.comprehensive_ai_analysis.isnot(None)
|
||||
)
|
||||
).order_by(desc(EnhancedContentStrategy.created_at)).first()
|
||||
|
||||
if not strategy:
|
||||
# Fallback to any strategy
|
||||
strategy = self.db_session.query(EnhancedContentStrategy).filter(
|
||||
EnhancedContentStrategy.user_id == user_id
|
||||
).order_by(desc(EnhancedContentStrategy.created_at)).first()
|
||||
|
||||
if strategy:
|
||||
strategy_data = self._convert_strategy_to_dict(strategy)
|
||||
strategy_data['activation_status'] = {
|
||||
'activation_date': None,
|
||||
'performance_score': None,
|
||||
'last_updated': None,
|
||||
'note': 'Fallback to most recent strategy'
|
||||
}
|
||||
|
||||
logger.info(f"✅ Found fallback strategy {strategy.id} for user {user_id}")
|
||||
return strategy_data
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error getting most recent strategy: {str(e)}")
|
||||
return None
|
||||
|
||||
def _convert_strategy_to_dict(self, strategy: EnhancedContentStrategy) -> Dict[str, Any]:
|
||||
"""
|
||||
Convert strategy model to dictionary.
|
||||
|
||||
Args:
|
||||
strategy: EnhancedContentStrategy model
|
||||
|
||||
Returns:
|
||||
Strategy dictionary
|
||||
"""
|
||||
try:
|
||||
strategy_dict = {
|
||||
'id': strategy.id,
|
||||
'user_id': strategy.user_id,
|
||||
'name': strategy.name,
|
||||
'industry': strategy.industry,
|
||||
'target_audience': strategy.target_audience,
|
||||
'content_pillars': strategy.content_pillars,
|
||||
'business_objectives': strategy.business_objectives,
|
||||
'brand_voice': strategy.brand_voice,
|
||||
'editorial_guidelines': strategy.editorial_guidelines,
|
||||
'content_frequency': strategy.content_frequency,
|
||||
'preferred_formats': strategy.preferred_formats,
|
||||
'content_mix': strategy.content_mix,
|
||||
'competitive_analysis': strategy.competitive_analysis,
|
||||
'market_positioning': strategy.market_positioning,
|
||||
'kpi_targets': strategy.kpi_targets,
|
||||
'success_metrics': strategy.success_metrics,
|
||||
'audience_segments': strategy.audience_segments,
|
||||
'content_themes': strategy.content_themes,
|
||||
'seasonal_focus': strategy.seasonal_focus,
|
||||
'campaign_integration': strategy.campaign_integration,
|
||||
'platform_strategy': strategy.platform_strategy,
|
||||
'engagement_goals': strategy.engagement_goals,
|
||||
'conversion_objectives': strategy.conversion_objectives,
|
||||
'brand_guidelines': strategy.brand_guidelines,
|
||||
'content_standards': strategy.content_standards,
|
||||
'quality_thresholds': strategy.quality_thresholds,
|
||||
'performance_benchmarks': strategy.performance_benchmarks,
|
||||
'optimization_focus': strategy.optimization_focus,
|
||||
'trend_alignment': strategy.trend_alignment,
|
||||
'innovation_areas': strategy.innovation_areas,
|
||||
'risk_mitigation': strategy.risk_mitigation,
|
||||
'scalability_plans': strategy.scalability_plans,
|
||||
'measurement_framework': strategy.measurement_framework,
|
||||
'continuous_improvement': strategy.continuous_improvement,
|
||||
'ai_recommendations': strategy.ai_recommendations,
|
||||
'comprehensive_ai_analysis': strategy.comprehensive_ai_analysis,
|
||||
'created_at': strategy.created_at.isoformat() if strategy.created_at else None,
|
||||
'updated_at': strategy.updated_at.isoformat() if strategy.updated_at else None,
|
||||
'completion_percentage': getattr(strategy, 'completion_percentage', 0)
|
||||
}
|
||||
|
||||
return strategy_dict
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error converting strategy to dictionary: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _is_cache_valid(self, cache_key: str) -> bool:
|
||||
"""
|
||||
Check if cache is still valid.
|
||||
|
||||
Args:
|
||||
cache_key: Cache key
|
||||
|
||||
Returns:
|
||||
True if cache is valid, False otherwise
|
||||
"""
|
||||
if cache_key not in self._last_cache_update:
|
||||
return False
|
||||
|
||||
last_update = self._last_cache_update[cache_key]
|
||||
return (datetime.now() - last_update).total_seconds() < self._cache_ttl
|
||||
|
||||
def _cache_strategy(self, cache_key: str, strategy_data: Dict[str, Any]):
|
||||
"""
|
||||
Cache strategy data.
|
||||
|
||||
Args:
|
||||
cache_key: Cache key
|
||||
strategy_data: Strategy data to cache
|
||||
"""
|
||||
self._memory_cache[cache_key] = strategy_data
|
||||
self._last_cache_update[cache_key] = datetime.now()
|
||||
logger.debug(f"📦 Cached strategy data for key: {cache_key}")
|
||||
|
||||
async def clear_cache(self, user_id: Optional[int] = None):
|
||||
"""
|
||||
Clear cache for specific user or all users.
|
||||
|
||||
Args:
|
||||
user_id: User ID to clear cache for, or None for all users
|
||||
"""
|
||||
if user_id:
|
||||
cache_key = f"active_strategy_{user_id}"
|
||||
if cache_key in self._memory_cache:
|
||||
del self._memory_cache[cache_key]
|
||||
if cache_key in self._last_cache_update:
|
||||
del self._last_cache_update[cache_key]
|
||||
logger.info(f"🗑️ Cleared cache for user {user_id}")
|
||||
else:
|
||||
self._memory_cache.clear()
|
||||
self._last_cache_update.clear()
|
||||
logger.info("🗑️ Cleared all cache")
|
||||
|
||||
async def get_cache_stats(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get cache statistics.
|
||||
|
||||
Returns:
|
||||
Cache statistics
|
||||
"""
|
||||
return {
|
||||
'total_cached_items': len(self._memory_cache),
|
||||
'cache_ttl_seconds': self._cache_ttl,
|
||||
'cached_users': list(self._memory_cache.keys()),
|
||||
'last_updates': {k: v.isoformat() for k, v in self._last_cache_update.items()}
|
||||
}
|
||||
|
||||
def count_active_strategies_with_tasks(self) -> int:
|
||||
"""
|
||||
Count how many active strategies have monitoring tasks.
|
||||
|
||||
This is used for intelligent scheduling - if there are no active strategies
|
||||
with tasks, the scheduler can check less frequently.
|
||||
|
||||
Returns:
|
||||
Number of active strategies that have at least one active monitoring task
|
||||
"""
|
||||
try:
|
||||
if not self.db_session:
|
||||
logger.warning("Database session not available")
|
||||
return 0
|
||||
|
||||
from sqlalchemy import func, and_
|
||||
from models.monitoring_models import MonitoringTask
|
||||
|
||||
# Count distinct strategies that:
|
||||
# 1. Have activation status = 'active'
|
||||
# 2. Have at least one active monitoring task
|
||||
count = self.db_session.query(
|
||||
func.count(func.distinct(EnhancedContentStrategy.id))
|
||||
).join(
|
||||
StrategyActivationStatus,
|
||||
EnhancedContentStrategy.id == StrategyActivationStatus.strategy_id
|
||||
).join(
|
||||
MonitoringTask,
|
||||
EnhancedContentStrategy.id == MonitoringTask.strategy_id
|
||||
).filter(
|
||||
and_(
|
||||
StrategyActivationStatus.status == 'active',
|
||||
MonitoringTask.status == 'active'
|
||||
)
|
||||
).scalar()
|
||||
|
||||
return count or 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error counting active strategies with tasks: {e}")
|
||||
# On error, assume there are active strategies (safer to check more frequently)
|
||||
return 1
|
||||
|
||||
def has_active_strategies_with_tasks(self) -> bool:
|
||||
"""
|
||||
Check if there are any active strategies with monitoring tasks.
|
||||
|
||||
Returns:
|
||||
True if there are active strategies with tasks, False otherwise
|
||||
"""
|
||||
return self.count_active_strategies_with_tasks() > 0
|
||||
Reference in New Issue
Block a user