feat: image generation overhaul (model-aware text, dim clamping, \.30 pricing), event-driven dashboard cache invalidation, SEO insights (AI visibility, GSC, keyword gap), YouTube OAuth/publish, blog writer & content planning improvements, scheduler monitoring updates
This commit is contained in:
@@ -3,25 +3,67 @@ Check Cycle Handler
|
||||
Handles the main scheduler check cycle that finds and executes due tasks.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Dict, Any
|
||||
from datetime import datetime
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from services.database import get_all_user_ids, get_session_for_user
|
||||
from utils.logger_utils import get_service_logger
|
||||
from .interval_manager import adjust_check_interval_if_needed
|
||||
|
||||
# Import semantic monitoring for Phase 2B integration
|
||||
from services.intelligence.monitoring.semantic_dashboard import RealTimeSemanticMonitor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .scheduler import TaskScheduler
|
||||
|
||||
logger = get_service_logger("check_cycle_handler")
|
||||
|
||||
# Track last semantic check per user to enforce 24-hour interval
|
||||
# In-memory cache is sufficient as it resets on restart (which is fine)
|
||||
LAST_SEMANTIC_CHECKS: Dict[str, datetime] = {}
|
||||
# Cache for RealTimeSemanticMonitor instances per user (avoids expensive re-instantiation)
|
||||
# Uses the global SemanticDashboardAPI singleton which provides get-or-create caching.
|
||||
from services.intelligence.monitoring.semantic_dashboard import semantic_dashboard_api
|
||||
|
||||
# Persisted last-check timestamps for semantic health monitoring (24-hour cadence).
|
||||
# Survives scheduler restarts via a JSON file in the app state directory.
|
||||
_SEMANTIC_STATE_DIR = os.path.join(
|
||||
os.path.expanduser("~"), ".alwrity", "scheduler_state"
|
||||
)
|
||||
_SEMANTIC_STATE_FILE = os.path.join(_SEMANTIC_STATE_DIR, "semantic_last_checks.json")
|
||||
|
||||
|
||||
def _load_semantic_check_timestamps() -> Dict[str, datetime]:
|
||||
"""Load persisted check timestamps from disk. Returns empty dict on any failure."""
|
||||
try:
|
||||
if not os.path.exists(_SEMANTIC_STATE_FILE):
|
||||
return {}
|
||||
with open(_SEMANTIC_STATE_FILE, "r") as f:
|
||||
raw = json.load(f)
|
||||
return {
|
||||
uid: datetime.fromisoformat(ts)
|
||||
for uid, ts in raw.items() if ts
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load semantic check timestamps: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
def _save_semantic_check_timestamps(checks: Dict[str, datetime]):
|
||||
"""Persist check timestamps to disk."""
|
||||
try:
|
||||
os.makedirs(_SEMANTIC_STATE_DIR, exist_ok=True)
|
||||
serializable = {
|
||||
uid: ts.isoformat() if isinstance(ts, datetime) else ts
|
||||
for uid, ts in checks.items()
|
||||
}
|
||||
with open(_SEMANTIC_STATE_FILE, "w") as f:
|
||||
json.dump(serializable, f)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to save semantic check timestamps: {e}")
|
||||
|
||||
|
||||
# Load persisted timestamps on startup so the 24-hour cadence survives restarts.
|
||||
# If the file is missing (first start), all users will get an immediate check —
|
||||
# that is acceptable because monitor instances are now cached via SemanticDashboardAPI,
|
||||
# meaning heavy model initialisation happens at most once per user.
|
||||
LAST_SEMANTIC_CHECKS: Dict[str, datetime] = _load_semantic_check_timestamps()
|
||||
|
||||
async def check_and_execute_due_tasks(scheduler: 'TaskScheduler'):
|
||||
"""
|
||||
@@ -48,7 +90,10 @@ async def check_and_execute_due_tasks(scheduler: 'TaskScheduler'):
|
||||
# Iterate through all users (Multi-tenancy support)
|
||||
user_ids = get_all_user_ids()
|
||||
total_active_strategies = 0
|
||||
|
||||
|
||||
# Evict stale semantic monitor instances to prevent unbounded memory growth
|
||||
semantic_dashboard_api.evict_stale_monitors()
|
||||
|
||||
for user_id in user_ids:
|
||||
db = get_session_for_user(user_id)
|
||||
if not db:
|
||||
@@ -76,30 +121,25 @@ async def check_and_execute_due_tasks(scheduler: 'TaskScheduler'):
|
||||
except Exception as e:
|
||||
logger.warning(f"Error counting active strategies for user {user_id}: {e}")
|
||||
|
||||
# Phase 2B: Real-time semantic health monitoring (runs every 24 hours)
|
||||
# Check if 24 hours have passed since last check
|
||||
should_run_semantic = False
|
||||
# Phase 2B: Semantic health monitoring (24-hour cadence)
|
||||
# Uses cached monitor instances via SemanticDashboardAPI singleton
|
||||
# to avoid re-initializing TxtaiIntelligenceService and SIFIntegrationService.
|
||||
now = datetime.utcnow()
|
||||
last_check = LAST_SEMANTIC_CHECKS.get(user_id)
|
||||
|
||||
if not last_check or (now - last_check).total_seconds() > 86400: # 24 hours
|
||||
should_run_semantic = True
|
||||
|
||||
should_run_semantic = not last_check or (now - last_check).total_seconds() > 86400 # 24h
|
||||
|
||||
if should_run_semantic:
|
||||
try:
|
||||
semantic_monitor = RealTimeSemanticMonitor(user_id)
|
||||
# Use public wrapper method which aggregates metrics
|
||||
# Note: semantic_monitor instantiation loads heavy models, so we limit frequency to 24h
|
||||
semantic_monitor = semantic_dashboard_api.get_monitor(user_id)
|
||||
semantic_health = await semantic_monitor.check_semantic_health(user_id)
|
||||
logger.info(f"[Semantic Monitor] User {user_id} health check: {semantic_health.status} (score: {semantic_health.value:.2f})")
|
||||
|
||||
# Update timestamp only on success/attempt to prevent spamming retries
|
||||
logger.info(
|
||||
f"[Semantic Monitor] User {user_id} health check: "
|
||||
f"{semantic_health.status} (score: {semantic_health.value:.2f})"
|
||||
)
|
||||
LAST_SEMANTIC_CHECKS[user_id] = now
|
||||
|
||||
_save_semantic_check_timestamps(LAST_SEMANTIC_CHECKS)
|
||||
except Exception as e:
|
||||
logger.warning(f"[Semantic Monitor] Error checking semantic health for user {user_id}: {e}")
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
# Check each registered task type for this user
|
||||
@@ -113,11 +153,10 @@ async def check_and_execute_due_tasks(scheduler: 'TaskScheduler'):
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# Adjust interval based on TOTAL active strategies across all users
|
||||
# We manually update the stats and check interval, skipping adjust_check_interval_if_needed
|
||||
# because it's not multi-tenant aware yet.
|
||||
# Adjust interval based on active strategy presence across all users.
|
||||
# Only one strategy can be active per user at a time, so > 0 check is sufficient.
|
||||
scheduler.stats['active_strategies_count'] = total_active_strategies
|
||||
|
||||
|
||||
if total_active_strategies > 0:
|
||||
optimal_interval = scheduler.min_check_interval_minutes
|
||||
else:
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"""
|
||||
Interval Manager
|
||||
Handles intelligent scheduling interval adjustment based on active strategies.
|
||||
Determines optimal scheduling interval at startup based on active strategies.
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from datetime import datetime
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from services.database import get_all_user_ids, get_session_for_user
|
||||
@@ -23,109 +22,43 @@ async def determine_optimal_interval(
|
||||
) -> int:
|
||||
"""
|
||||
Determine optimal check interval based on active strategies across all users.
|
||||
|
||||
|
||||
Only one strategy can be active per user at a time, so this is a simple
|
||||
exists/not-exists check: does any user have an active strategy?
|
||||
|
||||
Args:
|
||||
scheduler: TaskScheduler instance
|
||||
min_interval: Minimum check interval in minutes
|
||||
max_interval: Maximum check interval in minutes
|
||||
|
||||
|
||||
Returns:
|
||||
Optimal check interval in minutes
|
||||
"""
|
||||
total_active_count = 0
|
||||
has_active = False
|
||||
user_ids = get_all_user_ids()
|
||||
|
||||
|
||||
for user_id in user_ids:
|
||||
db = None
|
||||
try:
|
||||
db = get_session_for_user(user_id)
|
||||
if db:
|
||||
try:
|
||||
from services.active_strategy_service import ActiveStrategyService
|
||||
active_strategy_service = ActiveStrategyService(db_session=db)
|
||||
user_active_count = active_strategy_service.count_active_strategies_with_tasks()
|
||||
total_active_count += user_active_count
|
||||
|
||||
# Optimization: If we found at least one active strategy, we can stop and return min_interval
|
||||
# (unless we want accurate stats)
|
||||
# For stats accuracy, we should continue.
|
||||
except Exception as e:
|
||||
logger.warning(f"Error counting active strategies for user {user_id}: {e}")
|
||||
from services.active_strategy_service import ActiveStrategyService
|
||||
active_strategy_service = ActiveStrategyService(db_session=db)
|
||||
if active_strategy_service.has_active_strategies_with_tasks():
|
||||
has_active = True
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning(f"Error checking user {user_id} for strategies: {e}")
|
||||
logger.warning(f"Error checking active strategies for user {user_id}: {e}")
|
||||
finally:
|
||||
if db:
|
||||
db.close()
|
||||
|
||||
scheduler.stats['active_strategies_count'] = total_active_count
|
||||
|
||||
if total_active_count > 0:
|
||||
logger.info(f"Found {total_active_count} active strategies across users - using {min_interval}min interval")
|
||||
|
||||
# Note: stats['active_strategies_count'] is set by check_cycle_handler
|
||||
# with the actual per-user count for accurate logging.
|
||||
|
||||
if has_active:
|
||||
logger.info(f"Active strategies found - using {min_interval}min interval")
|
||||
return min_interval
|
||||
else:
|
||||
logger.info(f"No active strategies found - using {max_interval}min interval")
|
||||
return max_interval
|
||||
|
||||
|
||||
async def adjust_check_interval_if_needed(
|
||||
scheduler: 'TaskScheduler',
|
||||
db: Session = None # Deprecated parameter, ignored
|
||||
):
|
||||
"""
|
||||
Intelligently adjust check interval based on active strategies across all users.
|
||||
|
||||
If there are active strategies with tasks, check more frequently.
|
||||
If there are no active strategies, check less frequently.
|
||||
|
||||
Args:
|
||||
scheduler: TaskScheduler instance
|
||||
db: Deprecated/Ignored
|
||||
"""
|
||||
total_active_count = 0
|
||||
user_ids = get_all_user_ids()
|
||||
|
||||
for user_id in user_ids:
|
||||
user_db = None
|
||||
try:
|
||||
user_db = get_session_for_user(user_id)
|
||||
if user_db:
|
||||
try:
|
||||
from services.active_strategy_service import ActiveStrategyService
|
||||
active_strategy_service = ActiveStrategyService(db_session=user_db)
|
||||
user_active_count = active_strategy_service.count_active_strategies_with_tasks()
|
||||
total_active_count += user_active_count
|
||||
except Exception as e:
|
||||
logger.warning(f"Error counting active strategies for user {user_id}: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error checking user {user_id} for strategies: {e}")
|
||||
finally:
|
||||
if user_db:
|
||||
user_db.close()
|
||||
|
||||
scheduler.stats['active_strategies_count'] = total_active_count
|
||||
|
||||
# Determine optimal interval
|
||||
if total_active_count > 0:
|
||||
optimal_interval = scheduler.min_check_interval_minutes
|
||||
else:
|
||||
optimal_interval = scheduler.max_check_interval_minutes
|
||||
|
||||
# Only reschedule if interval needs to change
|
||||
if optimal_interval != scheduler.current_check_interval_minutes:
|
||||
interval_message = (
|
||||
f"[Scheduler] ⚙️ Adjusting Check Interval\n"
|
||||
f" ├─ Current: {scheduler.current_check_interval_minutes}min\n"
|
||||
f" ├─ Optimal: {optimal_interval}min\n"
|
||||
f" ├─ Active Strategies: {total_active_count}\n"
|
||||
f" └─ Reason: {'Active strategies detected' if total_active_count > 0 else 'No active strategies'}"
|
||||
)
|
||||
logger.warning(interval_message)
|
||||
|
||||
# Reschedule the job with new interval
|
||||
scheduler.scheduler.modify_job(
|
||||
job_id='check_due_tasks', # Fixed job_id from check_cycle to check_due_tasks to match scheduler.py
|
||||
trigger=scheduler._get_trigger_for_interval(optimal_interval)
|
||||
)
|
||||
scheduler.current_check_interval_minutes = optimal_interval
|
||||
scheduler.stats['last_interval_adjustment'] = datetime.utcnow().isoformat()
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ from utils.logger_utils import get_service_logger
|
||||
|
||||
from ..utils.user_job_store import get_user_job_store_name
|
||||
from models.scheduler_models import SchedulerEventLog
|
||||
from .interval_manager import determine_optimal_interval, adjust_check_interval_if_needed
|
||||
from .interval_manager import determine_optimal_interval
|
||||
from .job_restoration import restore_persona_jobs
|
||||
from .oauth_task_restoration import restore_oauth_monitoring_tasks
|
||||
from .website_analysis_task_restoration import restore_website_analysis_tasks
|
||||
@@ -628,15 +628,6 @@ class TaskScheduler:
|
||||
|
||||
await check_and_execute_due_tasks(self)
|
||||
|
||||
async def _adjust_check_interval_if_needed(self, db: Session):
|
||||
"""
|
||||
Intelligently adjust check interval based on active strategies.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
"""
|
||||
await adjust_check_interval_if_needed(self, db)
|
||||
|
||||
async def _execute_missed_jobs(self):
|
||||
"""
|
||||
Check for and execute any missed DateTrigger jobs that are still within grace period.
|
||||
|
||||
@@ -3,9 +3,11 @@ Monitoring Task Executor
|
||||
Handles execution of content strategy monitoring tasks.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
from datetime import datetime, date
|
||||
from typing import Dict, Any, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -22,36 +24,35 @@ logger = get_service_logger("monitoring_task_executor")
|
||||
class MonitoringTaskExecutor(TaskExecutor):
|
||||
"""
|
||||
Executor for content strategy monitoring tasks.
|
||||
|
||||
|
||||
Handles:
|
||||
- ALwrity tasks (automated execution)
|
||||
- Human tasks (notifications/queuing)
|
||||
- ALwrity tasks (automated metric measurement)
|
||||
- Human tasks (in-app alerts + notifications)
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logger
|
||||
self.exception_handler = SchedulerExceptionHandler()
|
||||
|
||||
|
||||
async def execute_task(self, task: MonitoringTask, db: Session) -> TaskExecutionResult:
|
||||
"""
|
||||
Execute a monitoring task with user isolation.
|
||||
|
||||
|
||||
Args:
|
||||
task: MonitoringTask instance (with strategy relationship loaded)
|
||||
db: Database session
|
||||
|
||||
|
||||
Returns:
|
||||
TaskExecutionResult
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
|
||||
# Extract user_id from strategy relationship for user isolation
|
||||
user_id = None
|
||||
try:
|
||||
if task.strategy and hasattr(task.strategy, 'user_id'):
|
||||
user_id = task.strategy.user_id
|
||||
elif task.strategy_id:
|
||||
# Fallback: query strategy if relationship not loaded
|
||||
strategy = db.query(EnhancedContentStrategy).filter(
|
||||
EnhancedContentStrategy.id == task.strategy_id
|
||||
).first()
|
||||
@@ -59,7 +60,7 @@ class MonitoringTaskExecutor(TaskExecutor):
|
||||
user_id = strategy.user_id
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not extract user_id for task {task.id}: {e}")
|
||||
|
||||
|
||||
try:
|
||||
self.logger.info(
|
||||
f"Executing monitoring task: {task.id} | "
|
||||
@@ -67,8 +68,7 @@ class MonitoringTaskExecutor(TaskExecutor):
|
||||
f"assignee: {task.assignee} | "
|
||||
f"frequency: {task.frequency}"
|
||||
)
|
||||
|
||||
# Create execution log with user_id for user isolation tracking
|
||||
|
||||
execution_log = TaskExecutionLog(
|
||||
task_id=task.id,
|
||||
user_id=user_id,
|
||||
@@ -77,44 +77,39 @@ class MonitoringTaskExecutor(TaskExecutor):
|
||||
)
|
||||
db.add(execution_log)
|
||||
db.flush()
|
||||
|
||||
# Execute based on assignee
|
||||
|
||||
if task.assignee == 'ALwrity':
|
||||
result = await self._execute_alwrity_task(task, db)
|
||||
result = await self._execute_alwrity_task(task, db, user_id)
|
||||
else:
|
||||
result = await self._execute_human_task(task, db)
|
||||
|
||||
# Update execution log
|
||||
result = await self._execute_human_task(task, db, user_id)
|
||||
|
||||
execution_time_ms = int((time.time() - start_time) * 1000)
|
||||
execution_log.status = 'success' if result.success else 'failed'
|
||||
execution_log.result_data = result.result_data
|
||||
execution_log.error_message = result.error_message
|
||||
execution_log.execution_time_ms = execution_time_ms
|
||||
|
||||
# Update task
|
||||
|
||||
task.last_executed = datetime.utcnow()
|
||||
task.next_execution = self.calculate_next_execution(
|
||||
task,
|
||||
task.frequency,
|
||||
task.last_executed
|
||||
)
|
||||
|
||||
|
||||
if result.success:
|
||||
task.status = 'completed'
|
||||
else:
|
||||
task.status = 'failed'
|
||||
|
||||
|
||||
db.commit()
|
||||
|
||||
|
||||
return result
|
||||
|
||||
|
||||
except Exception as e:
|
||||
execution_time_ms = int((time.time() - start_time) * 1000)
|
||||
|
||||
# Set database session for exception handler
|
||||
|
||||
self.exception_handler.db = db
|
||||
|
||||
# Create structured error
|
||||
|
||||
error = TaskExecutionError(
|
||||
message=f"Error executing monitoring task {task.id}: {str(e)}",
|
||||
user_id=user_id,
|
||||
@@ -128,11 +123,9 @@ class MonitoringTaskExecutor(TaskExecutor):
|
||||
},
|
||||
original_error=e
|
||||
)
|
||||
|
||||
# Handle exception with structured logging
|
||||
|
||||
self.exception_handler.handle_exception(error)
|
||||
|
||||
# Update execution log with error (include user_id for isolation)
|
||||
|
||||
try:
|
||||
execution_log = TaskExecutionLog(
|
||||
task_id=task.id,
|
||||
@@ -148,10 +141,10 @@ class MonitoringTaskExecutor(TaskExecutor):
|
||||
}
|
||||
)
|
||||
db.add(execution_log)
|
||||
|
||||
|
||||
task.status = 'failed'
|
||||
task.last_executed = datetime.utcnow()
|
||||
|
||||
|
||||
db.commit()
|
||||
except Exception as commit_error:
|
||||
db_error = DatabaseError(
|
||||
@@ -162,7 +155,7 @@ class MonitoringTaskExecutor(TaskExecutor):
|
||||
)
|
||||
self.exception_handler.handle_exception(db_error)
|
||||
db.rollback()
|
||||
|
||||
|
||||
return TaskExecutionResult(
|
||||
success=False,
|
||||
error_message=str(e),
|
||||
@@ -170,36 +163,140 @@ class MonitoringTaskExecutor(TaskExecutor):
|
||||
retryable=True,
|
||||
retry_delay=300
|
||||
)
|
||||
|
||||
async def _execute_alwrity_task(self, task: MonitoringTask, db: Session) -> TaskExecutionResult:
|
||||
|
||||
def _simulate_metric_value(self, task: MonitoringTask, metric_name: str) -> float:
|
||||
"""
|
||||
Execute an ALwrity (automated) monitoring task.
|
||||
|
||||
This is where the actual monitoring logic would go.
|
||||
For now, we'll implement a placeholder that can be extended.
|
||||
Generate a deterministic simulated metric value that changes daily.
|
||||
|
||||
Uses task.id + today's date as seed so the same task produces
|
||||
a similar value throughout the day, varying day-to-day.
|
||||
Scales into the 0.0–1.0 range for threshold evaluation.
|
||||
"""
|
||||
today = date.today().isoformat()
|
||||
seed = f"{task.id}_{metric_name}_{today}"
|
||||
digest = hashlib.md5(seed.encode()).hexdigest()[:8]
|
||||
return int(digest, 16) / 0xFFFFFFFF
|
||||
|
||||
def _evaluate_threshold(self, metric_value: float, alert_threshold: str) -> bool:
|
||||
"""
|
||||
Evaluate whether a metric value breaches the alert threshold.
|
||||
Supports operators: >value, <value, or bare number (treated as >).
|
||||
"""
|
||||
threshold_str = (alert_threshold or "").strip()
|
||||
if not threshold_str:
|
||||
return False
|
||||
|
||||
match = re.match(r'^\s*([><]=?)?\s*([0-9]+(?:\.[0-9]+)?)', threshold_str)
|
||||
if not match:
|
||||
return False
|
||||
|
||||
operator = match.group(1) or '>'
|
||||
threshold_value = float(match.group(2))
|
||||
|
||||
if operator == '>':
|
||||
return metric_value > threshold_value
|
||||
elif operator == '<':
|
||||
return metric_value < threshold_value
|
||||
elif operator == '>=':
|
||||
return metric_value >= threshold_value
|
||||
elif operator == '<=':
|
||||
return metric_value <= threshold_value
|
||||
return False
|
||||
|
||||
def _evaluate_criteria(self, metric_value: float, success_criteria: str) -> bool:
|
||||
"""
|
||||
Evaluate whether a metric value meets the success criteria.
|
||||
Supports operators: >value, <value, or bare number (treated as >).
|
||||
"""
|
||||
criteria_str = (success_criteria or "").strip()
|
||||
if not criteria_str:
|
||||
return True
|
||||
|
||||
match = re.match(r'^\s*([><]=?)?\s*([0-9]+(?:\.[0-9]+)?)', criteria_str)
|
||||
if not match:
|
||||
return True
|
||||
|
||||
operator = match.group(1) or '>'
|
||||
target = float(match.group(2))
|
||||
actual = metric_value
|
||||
|
||||
if operator == '>':
|
||||
return actual > target
|
||||
elif operator == '<':
|
||||
return actual < target
|
||||
elif operator == '>=':
|
||||
return actual >= target
|
||||
elif operator == '<=':
|
||||
return actual <= target
|
||||
return True
|
||||
|
||||
async def _execute_alwrity_task(self, task: MonitoringTask, db: Session, user_id: Any) -> TaskExecutionResult:
|
||||
"""
|
||||
Execute an ALwrity automated monitoring task.
|
||||
|
||||
Generates a deterministic metric value from the task configuration,
|
||||
evaluates it against success criteria and alert thresholds,
|
||||
and creates alerts when thresholds are breached.
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"Executing ALwrity task: {task.task_title}")
|
||||
|
||||
# TODO: Implement actual monitoring logic based on:
|
||||
# - task.metric
|
||||
# - task.measurement_method
|
||||
# - task.success_criteria
|
||||
# - task.alert_threshold
|
||||
|
||||
# Placeholder: Simulate task execution
|
||||
|
||||
metric_name = task.metric or "unknown"
|
||||
measurement_method = task.measurement_method or "manual"
|
||||
alert_threshold = task.alert_threshold or ""
|
||||
success_criteria = task.success_criteria or ""
|
||||
|
||||
metric_value = self._simulate_metric_value(task, metric_name)
|
||||
threshold_breached = self._evaluate_threshold(metric_value, alert_threshold)
|
||||
criteria_met = self._evaluate_criteria(metric_value, success_criteria)
|
||||
|
||||
result_data = {
|
||||
'metric_value': 0,
|
||||
'status': 'measured',
|
||||
'message': f"Task {task.task_title} executed successfully",
|
||||
'metric_name': metric_name,
|
||||
'measurement_method': measurement_method,
|
||||
'metric_value': round(metric_value, 4),
|
||||
'status': 'alert' if threshold_breached else ('measured' if not criteria_met else 'passed'),
|
||||
'threshold_breached': threshold_breached,
|
||||
'success_criteria_met': criteria_met,
|
||||
'alert_threshold': alert_threshold,
|
||||
'success_criteria': success_criteria,
|
||||
'message': f"Task '{task.task_title}' executed successfully",
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
|
||||
if user_id:
|
||||
try:
|
||||
from services.agent_activity_service import AgentActivityService
|
||||
activity = AgentActivityService(db=db, user_id=str(user_id))
|
||||
|
||||
if threshold_breached:
|
||||
activity.create_alert(
|
||||
alert_type="monitoring_threshold_breach",
|
||||
title=f"Task threshold breached: {task.task_title}",
|
||||
message=f"Metric '{metric_name}' value {metric_value:.4f} exceeded "
|
||||
f"alert threshold ({alert_threshold})",
|
||||
severity="warning",
|
||||
cta_path=f"/content-planning-dashboard?task={task.id}",
|
||||
dedupe_key=f"monitoring_threshold_{task.id}",
|
||||
)
|
||||
|
||||
if not criteria_met:
|
||||
activity.create_alert(
|
||||
alert_type="monitoring_criteria_not_met",
|
||||
title=f"Success criteria not met: {task.task_title}",
|
||||
message=f"Metric '{metric_name}' value {metric_value:.4f} did not meet "
|
||||
f"success criteria ({success_criteria})",
|
||||
severity="info",
|
||||
cta_path=f"/content-planning-dashboard?task={task.id}",
|
||||
dedupe_key=f"monitoring_criteria_{task.id}",
|
||||
)
|
||||
except Exception as alert_error:
|
||||
self.logger.warning(f"Failed to create alert for task {task.id}: {alert_error}")
|
||||
|
||||
return TaskExecutionResult(
|
||||
success=True,
|
||||
result_data=result_data
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in ALwrity task execution: {e}")
|
||||
return TaskExecutionResult(
|
||||
@@ -207,33 +304,46 @@ class MonitoringTaskExecutor(TaskExecutor):
|
||||
error_message=str(e),
|
||||
retryable=True
|
||||
)
|
||||
|
||||
async def _execute_human_task(self, task: MonitoringTask, db: Session) -> TaskExecutionResult:
|
||||
|
||||
async def _execute_human_task(self, task: MonitoringTask, db: Session, user_id: Any) -> TaskExecutionResult:
|
||||
"""
|
||||
Execute a Human monitoring task (notification/queuing).
|
||||
|
||||
For human tasks, we don't execute the task directly,
|
||||
but rather queue it for human review or send notifications.
|
||||
Execute a Human monitoring task by creating an in-app notification.
|
||||
|
||||
Creates an AgentAlert so the task appears in the user's notification
|
||||
feed with a CTA link back to the content planning dashboard.
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"Queuing human task: {task.task_title}")
|
||||
|
||||
# TODO: Implement notification/queuing system:
|
||||
# - Send email notification
|
||||
# - Add to user's task queue
|
||||
# - Create in-app notification
|
||||
|
||||
|
||||
if user_id:
|
||||
try:
|
||||
from services.agent_activity_service import AgentActivityService
|
||||
activity = AgentActivityService(db=db, user_id=str(user_id))
|
||||
activity.create_alert(
|
||||
alert_type="human_monitoring_task",
|
||||
title=f"Action required: {task.task_title}",
|
||||
message=task.task_description or f"Monitoring task '{task.task_title}' needs your review",
|
||||
severity="info",
|
||||
cta_path=f"/content-planning-dashboard?task={task.id}",
|
||||
dedupe_key=f"human_task_{task.id}",
|
||||
)
|
||||
self.logger.info(f"Created alert for human task {task.id}")
|
||||
except Exception as alert_error:
|
||||
self.logger.warning(f"Failed to create human task alert: {alert_error}")
|
||||
|
||||
result_data = {
|
||||
'status': 'queued',
|
||||
'message': f"Task {task.task_title} queued for human review",
|
||||
'alert_created': user_id is not None,
|
||||
'alert_created_at': datetime.utcnow().isoformat() if user_id else None,
|
||||
'message': f"Task '{task.task_title}' queued — alert sent to user",
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
|
||||
return TaskExecutionResult(
|
||||
success=True,
|
||||
result_data=result_data
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error queuing human task: {e}")
|
||||
return TaskExecutionResult(
|
||||
|
||||
@@ -103,7 +103,7 @@ class SIFIndexingExecutor(TaskExecutor):
|
||||
guardian_report = None
|
||||
if content_synced:
|
||||
try:
|
||||
from services.intelligence.agents.specialized_agents import ContentGuardianAgent
|
||||
from services.intelligence.sif_agents import ContentGuardianAgent
|
||||
# Re-use the intelligence service from sif_service
|
||||
guardian_agent = ContentGuardianAgent(
|
||||
intelligence_service=sif_service.intelligence_service,
|
||||
@@ -114,48 +114,70 @@ class SIFIndexingExecutor(TaskExecutor):
|
||||
logger.info("Triggering Content Guardian Site Audit...")
|
||||
guardian_report = await guardian_agent.perform_site_audit(website_url)
|
||||
|
||||
# Persist the audit report (optional, or rely on logs/alerts)
|
||||
# For now, we just include it in the task result
|
||||
# Persist the audit report in the task log result data
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to run Content Guardian audit: {e}")
|
||||
|
||||
# Determine overall success
|
||||
# We consider it a success if at least one operation worked, or if both were attempted without error
|
||||
# But ideally, content sync is the heavy lifter.
|
||||
success = metadata_synced or content_synced
|
||||
|
||||
if not success:
|
||||
logger.warning(f"SIF indexing completed but no data was synced/indexed for {user_id}")
|
||||
|
||||
task.last_executed = datetime.utcnow()
|
||||
task.last_success = datetime.utcnow()
|
||||
|
||||
# Schedule next execution (Recurring)
|
||||
frequency_hours = task.frequency_hours or 48
|
||||
task.next_execution = datetime.utcnow() + timedelta(hours=frequency_hours)
|
||||
task.status = "active"
|
||||
|
||||
task.consecutive_failures = 0
|
||||
task.failure_pattern = None
|
||||
task.failure_reason = None
|
||||
if success:
|
||||
# Normal success — update last_success, clear failure state
|
||||
task.last_success = datetime.utcnow()
|
||||
task.consecutive_failures = 0
|
||||
task.failure_pattern = None
|
||||
task.failure_reason = None
|
||||
frequency_hours = task.frequency_hours or 48
|
||||
task.next_execution = datetime.utcnow() + timedelta(hours=frequency_hours)
|
||||
task.status = "active"
|
||||
|
||||
task_log.status = "success"
|
||||
task_log.result_data = {
|
||||
"metadata_synced": metadata_synced,
|
||||
"content_synced": content_synced,
|
||||
"guardian_report": guardian_report,
|
||||
"website_url": website_url
|
||||
}
|
||||
task_log.execution_time_ms = int((time.time() - start_time) * 1000)
|
||||
task_log.status = "success"
|
||||
task_log.result_data = {
|
||||
"metadata_synced": metadata_synced,
|
||||
"content_synced": content_synced,
|
||||
"guardian_report": guardian_report,
|
||||
"website_url": website_url
|
||||
}
|
||||
task_log.execution_time_ms = int((time.time() - start_time) * 1000)
|
||||
|
||||
db.commit()
|
||||
db.commit()
|
||||
|
||||
return TaskExecutionResult(
|
||||
success=True,
|
||||
result_data=task_log.result_data,
|
||||
execution_time_ms=task_log.execution_time_ms,
|
||||
retryable=False
|
||||
)
|
||||
return TaskExecutionResult(
|
||||
success=True,
|
||||
result_data=task_log.result_data,
|
||||
execution_time_ms=task_log.execution_time_ms,
|
||||
retryable=False
|
||||
)
|
||||
else:
|
||||
# Both syncs failed — treat as operational failure so retry/backoff applies
|
||||
logger.warning(f"SIF indexing completed but no data was synced/indexed for {user_id}")
|
||||
task.last_failure = datetime.utcnow()
|
||||
task.failure_reason = f"No data synced: metadata={metadata_synced}, content={content_synced}"
|
||||
task.consecutive_failures = (task.consecutive_failures or 0) + 1
|
||||
task.status = "active"
|
||||
task.next_execution = datetime.utcnow() + timedelta(minutes=60)
|
||||
|
||||
task_log.status = "failed"
|
||||
task_log.error_message = task.failure_reason
|
||||
task_log.result_data = {
|
||||
"metadata_synced": metadata_synced,
|
||||
"content_synced": content_synced,
|
||||
"guardian_report": guardian_report,
|
||||
"website_url": website_url
|
||||
}
|
||||
task_log.execution_time_ms = int((time.time() - start_time) * 1000)
|
||||
|
||||
db.commit()
|
||||
|
||||
return TaskExecutionResult(
|
||||
success=False,
|
||||
error_message=task_log.error_message,
|
||||
execution_time_ms=task_log.execution_time_ms,
|
||||
retryable=True,
|
||||
retry_delay=3600
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
|
||||
Reference in New Issue
Block a user