Recovered state: integrated TrendSurferAgent, restored frontend/backend files, and cleaned up recovery scripts

This commit is contained in:
ajaysi
2026-02-08 13:56:57 +05:30
parent 1db10ccd0f
commit e404a86502
333 changed files with 42223 additions and 10875 deletions

View File

@@ -0,0 +1,73 @@
"""
ALwrity Autonomous Marketing Agents Module
This module provides autonomous marketing agents built on txtai's native agent framework.
The agents work together to monitor market conditions, analyze competitor activities,
and execute coordinated marketing strategies without human intervention.
"""
# Core agent framework
from .core_agent_framework import (
BaseALwrityAgent,
AgentAction,
AgentPerformance,
StrategyOrchestratorAgent
)
# Market signal detection
from .market_signal_detector import (
MarketSignal,
MarketSignalDetector,
MarketTrendAnalyzer
)
# Performance monitoring
from .performance_monitor import (
PerformanceMonitor,
performance_monitor,
PerformanceMetric,
AgentPerformanceMetrics
)
# Specialized agents
from .specialized_agents import (
ContentGuardianAgent,
LinkGraphAgent,
StrategyArchitectAgent,
ContentStrategyAgent,
CompetitorResponseAgent,
SEOOptimizationAgent,
SocialAmplificationAgent
)
from .trend_surfer_agent import TrendSurferAgent
# Agent Orchestrator
from .agent_orchestrator import (
ALwrityAgentOrchestrator,
orchestration_service
)
__all__ = [
'BaseALwrityAgent',
'AgentAction',
'AgentPerformance',
'StrategyOrchestratorAgent',
'MarketSignal',
'MarketSignalDetector',
'MarketTrendAnalyzer',
'PerformanceMonitor',
'performance_monitor',
'PerformanceMetric',
'AgentPerformanceMetrics',
'ContentGuardianAgent',
'LinkGraphAgent',
'StrategyArchitectAgent',
'ContentStrategyAgent',
'CompetitorResponseAgent',
'SEOOptimizationAgent',
'SocialAmplificationAgent',
'TrendSurferAgent',
'ALwrityAgentOrchestrator',
'orchestration_service'
]

View File

@@ -0,0 +1,429 @@
"""
ALwrity Agent Orchestration System
Main orchestration system that coordinates all autonomous marketing agents
Built on txtai's native agent framework
"""
import asyncio
import json
import logging
from datetime import datetime
from typing import Dict, List, Any, Optional
from dataclasses import dataclass, asdict
# txtai imports for native agent framework
try:
from txtai import Agent, LLM
TXTAI_AVAILABLE = Agent.__module__ != "txtai.agent.placeholder"
except ImportError:
TXTAI_AVAILABLE = False
logging.warning("txtai not available, using fallback implementation")
from utils.logger_utils import get_service_logger
from services.intelligence.agents.core_agent_framework import (
BaseALwrityAgent, AgentAction, AgentPerformance, StrategyOrchestratorAgent
)
from services.intelligence.agents.specialized_agents import (
ContentStrategyAgent, CompetitorResponseAgent, SEOOptimizationAgent, SocialAmplificationAgent
)
from services.intelligence.agents.trend_surfer_agent import TrendSurferAgent
from services.intelligence.agents.market_signal_detector import (
MarketSignal, MarketSignalDetector
)
from services.intelligence.agents.safety_framework import (
SafetyConstraintManager, RollbackManager, UserApprovalSystem, get_safety_framework
)
from services.intelligence.agents.performance_monitor import (
PerformanceMetric, AgentStatus, AgentPerformanceMonitor, performance_service
)
logger = get_service_logger(__name__)
@dataclass
class AgentTeamConfiguration:
"""Configuration for the complete agent team"""
user_id: str
shared_llm: str = "Qwen/Qwen3-4B-Instruct-2507"
max_iterations: int = 15
enable_safety: bool = True
enable_performance_monitoring: bool = True
enable_market_signals: bool = True
created_at: str = None
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.utcnow().isoformat()
class ALwrityAgentOrchestrator:
"""Main orchestrator for ALwrity autonomous marketing agents"""
def __init__(self, config: AgentTeamConfiguration):
self.config = config
self.user_id = config.user_id
self.agents: Dict[str, BaseALwrityAgent] = {}
self.orchestrator_agent: Optional[Agent] = None
self.market_detector: Optional[MarketSignalDetector] = None
self.performance_monitor: Optional[AgentPerformanceMonitor] = None
self.safety_framework: Optional[Dict[str, Any]] = None
# Initialize components
self._initialize_components()
logger.info(f"Initialized ALwrityAgentOrchestrator for user: {self.user_id}")
def _initialize_components(self):
"""Initialize all agent system components"""
try:
# Initialize shared LLM
if TXTAI_AVAILABLE:
self.llm = LLM(self.config.shared_llm)
else:
self.llm = None
# Initialize market signal detector
if self.config.enable_market_signals:
self.market_detector = MarketSignalDetector(self.user_id)
# Initialize performance monitoring
if self.config.enable_performance_monitoring:
self.performance_monitor = AgentPerformanceMonitor(self.user_id)
# Initialize safety framework
if self.config.enable_safety:
self.safety_framework = get_safety_framework(self.user_id)
# Create specialized agents
self._create_specialized_agents()
# Create master orchestrator agent
self._create_orchestrator_agent()
except Exception as e:
logger.error(f"Error initializing components for user {self.user_id}: {e}")
raise e
def _create_specialized_agents(self):
"""Create specialized marketing agents"""
try:
enabled_by_key = {}
db = None
try:
from services.database import get_session_for_user
from models.agent_activity_models import AgentProfile
db = get_session_for_user(self.user_id)
if db:
profiles = db.query(AgentProfile).filter(AgentProfile.user_id == self.user_id).all()
enabled_by_key = {p.agent_key: bool(p.enabled) for p in profiles if p and p.agent_key and p.enabled is not None}
except Exception:
enabled_by_key = {}
finally:
try:
if db:
db.close()
except Exception:
pass
# Content Strategy Agent
if enabled_by_key.get("content_strategist", True):
self.content_agent = ContentStrategyAgent(self.user_id, self.config.shared_llm, llm=self.llm)
self.agents['content'] = self.content_agent
# Competitor Response Agent
if enabled_by_key.get("competitor_analyst", True):
self.competitor_agent = CompetitorResponseAgent(self.user_id, self.config.shared_llm, llm=self.llm)
self.agents['competitor'] = self.competitor_agent
# SEO Optimization Agent
if enabled_by_key.get("seo_specialist", True):
self.seo_agent = SEOOptimizationAgent(self.user_id, self.config.shared_llm, llm=self.llm)
self.agents['seo'] = self.seo_agent
# Social Amplification Agent
if enabled_by_key.get("social_media_manager", True):
self.social_agent = SocialAmplificationAgent(self.user_id, self.config.shared_llm, llm=self.llm)
self.agents['social'] = self.social_agent
# Trend Surfer Agent
if enabled_by_key.get("trend_surfer", True):
# TrendSurferAgent needs TxtaiIntelligenceService, which we might need to get from SIF or initialize
# For now, we assume SIF integration is handled elsewhere or we pass a mock/stub if needed
# But wait, TrendSurferAgent constructor is (intelligence_service, user_id)
# We need to get the intelligence service here.
# Since AgentOrchestrator doesn't hold TxtaiIntelligenceService directly (SIFIntegrationService does),
# this is tricky.
# However, SIFIntegrationService initializes AgentOrchestrator.
# Let's import TxtaiIntelligenceService and initialize it here for the agent
from services.intelligence.txtai_service import TxtaiIntelligenceService
intel_service = TxtaiIntelligenceService(self.user_id)
self.trend_surfer_agent = TrendSurferAgent(intel_service, self.user_id)
self.agents['trend'] = self.trend_surfer_agent
logger.info(f"Created {len(self.agents)} specialized agents for user {self.user_id}")
except Exception as e:
logger.error(f"Error creating specialized agents for user {self.user_id}: {e}")
raise e
# Specialized agent creation methods have been moved to specialized_agents.py
def _create_orchestrator_agent(self):
"""Create master orchestrator agent using txtai native framework"""
try:
self.orchestrator_agent = StrategyOrchestratorAgent(
user_id=self.user_id,
market_detector=self.market_detector,
performance_monitor=self.performance_monitor,
llm=self.llm
)
# Set sub-agents
self.orchestrator_agent.set_sub_agents(self.agents)
logger.info(f"Created StrategyOrchestratorAgent for user {self.user_id}")
except Exception as e:
logger.error(f"Error creating orchestrator agent: {e}")
# Fallback to simple agent if class instantiation fails
self.orchestrator_agent = Agent(llm=self.llm)
async def execute_marketing_strategy(self, market_context: Dict[str, Any]) -> Dict[str, Any]:
"""Execute coordinated marketing strategy using agent team"""
try:
logger.info(f"Executing marketing strategy for user {self.user_id}")
# Prepare comprehensive context
context = await self._prepare_orchestrator_context(market_context)
# Execute orchestrator with full team
# The StrategyOrchestratorAgent will autonomously delegate tasks to sub-agents
instruction = (
"Analyze current market conditions and coordinate our marketing team to respond effectively.\n\n"
"Please:\n"
"1. Analyze the market situation.\n"
"2. DELEGATE tasks to specific agents using the 'task_delegator' tool.\n"
"3. Synthesize their results into a unified strategy.\n"
"4. Provide specific action recommendations.\n\n"
"Return a comprehensive strategy with specific actions, priorities, and expected outcomes."
)
orchestrator_prompt = self.orchestrator_agent.build_task_prompt(instruction=instruction, task_context=context)
result = await self.orchestrator_agent.run(orchestrator_prompt)
# Record performance metrics for the orchestration itself
if self.config.enable_performance_monitoring:
# We assume the agent's internal tracking handles per-action metrics
pass
logger.info(f"Marketing strategy execution completed for user {self.user_id}")
return {
"success": True,
"strategy": result,
"timestamp": datetime.utcnow().isoformat(),
# In a real system, we might parse the result to extract structured data
}
except Exception as e:
logger.error(f"Agent team execution failed for user {self.user_id}: {e}")
return {
"success": False,
"error": str(e),
"timestamp": datetime.utcnow().isoformat()
}
async def process_market_signals(self) -> List[MarketSignal]:
"""Process market signals and generate agent responses"""
try:
if not self.market_detector:
return []
# Detect market signals
signals = await self.market_detector.detect_market_signals()
logger.info(f"Processed {len(signals)} market signals for user {self.user_id}")
return signals
except Exception as e:
logger.error(f"Error processing market signals for user {self.user_id}: {e}")
return []
async def get_agent_status(self) -> Dict[str, Any]:
"""Get status of all agents"""
try:
agent_statuses = {}
for agent_type, agent in self.agents.items():
if hasattr(agent, 'get_current_status'):
status = await agent.get_current_status()
agent_statuses[agent_type] = status
# Get performance metrics if available
performance_summary = {}
if self.performance_monitor:
all_performance = self.performance_monitor.get_all_agents_performance()
performance_summary = {perf['agent_id']: perf for perf in all_performance}
return {
"user_id": self.user_id,
"timestamp": datetime.utcnow().isoformat(),
"agent_statuses": agent_statuses,
"performance_summary": performance_summary,
"market_signals_active": self.config.enable_market_signals,
"safety_enabled": self.config.enable_safety,
"performance_monitoring_enabled": self.config.enable_performance_monitoring
}
except Exception as e:
logger.error(f"Error getting agent status for user {self.user_id}: {e}")
return {
"error": str(e),
"timestamp": datetime.utcnow().isoformat()
}
# Tool implementations for txtai agents have been moved to StrategyOrchestratorAgent class
# Specialized agent tools have been moved to specialized_agents.py
# Helper methods
async def _prepare_orchestrator_context(self, market_context: Dict[str, Any]) -> Dict[str, Any]:
"""Prepare comprehensive context for orchestrator"""
context = {
"user_id": self.user_id,
"market_conditions": market_context,
"timestamp": datetime.utcnow().isoformat(),
"available_agents": list(self.agents.keys()),
"agent_capabilities": self._get_agent_capabilities(),
"system_status": await self.get_agent_status()
}
return context
def _get_agent_capabilities(self) -> Dict[str, List[str]]:
"""Get capabilities of each agent type"""
return {
"content": ["Content analysis", "Gap detection", "Optimization", "Performance tracking"],
"competitor": ["Competitor monitoring", "Threat analysis", "Response generation", "Strategy execution"],
"seo": ["SEO auditing", "Issue prioritization", "Auto-fixing", "Strategy generation"],
"social": ["Social monitoring", "Content adaptation", "Engagement optimization", "Distribution management"],
"trend": ["Trend detection", "Opportunity analysis", "Content angle generation"]
}
# Service class for agent orchestration
class AgentOrchestrationService:
"""Service class for managing agent orchestration"""
def __init__(self):
self.orchestrators: Dict[str, ALwrityAgentOrchestrator] = {}
self.execution_history: List[Dict[str, Any]] = []
logger.info("Initialized AgentOrchestrationService")
async def get_or_create_orchestrator(self, user_id: str) -> ALwrityAgentOrchestrator:
"""Get or create an orchestrator for a user"""
if user_id not in self.orchestrators:
config = AgentTeamConfiguration(user_id=user_id)
self.orchestrators[user_id] = ALwrityAgentOrchestrator(config)
logger.info(f"Created new orchestrator for user: {user_id}")
return self.orchestrators[user_id]
async def execute_marketing_strategy(self, user_id: str, market_context: Dict[str, Any]) -> Dict[str, Any]:
"""Execute marketing strategy for a user"""
try:
orchestrator = await self.get_or_create_orchestrator(user_id)
result = await orchestrator.execute_marketing_strategy(market_context)
# Record in history
execution_record = {
"user_id": user_id,
"timestamp": datetime.utcnow().isoformat(),
"market_context": market_context,
"result": result,
"success": result.get("success", False)
}
self.execution_history.append(execution_record)
# Keep only recent history (last 1000)
if len(self.execution_history) > 1000:
self.execution_history = self.execution_history[-1000:]
return result
except Exception as e:
logger.error(f"Error executing marketing strategy for user {user_id}: {e}")
return {
"success": False,
"error": str(e),
"timestamp": datetime.utcnow().isoformat()
}
async def get_agent_status(self, user_id: str) -> Dict[str, Any]:
"""Get agent status for a user"""
try:
orchestrator = await self.get_or_create_orchestrator(user_id)
return await orchestrator.get_agent_status()
except Exception as e:
logger.error(f"Error getting agent status for user {user_id}: {e}")
return {
"success": False,
"error": str(e),
"timestamp": datetime.utcnow().isoformat()
}
async def process_market_signals(self, user_id: str) -> List[MarketSignal]:
"""Process market signals for a user"""
try:
orchestrator = await self.get_or_create_orchestrator(user_id)
return await orchestrator.process_market_signals()
except Exception as e:
logger.error(f"Error processing market signals for user {user_id}: {e}")
return []
def get_execution_history(self, user_id: str = None, limit: int = 100) -> List[Dict[str, Any]]:
"""Get execution history"""
if user_id:
return [record for record in self.execution_history if record["user_id"] == user_id][-limit:]
else:
return self.execution_history[-limit:]
def get_global_performance_stats(self) -> Dict[str, Any]:
"""Get global performance statistics"""
if not self.execution_history:
return {}
total_executions = len(self.execution_history)
successful_executions = len([r for r in self.execution_history if r.get("success", False)])
unique_users = len(set(r["user_id"] for r in self.execution_history))
return {
"total_executions": total_executions,
"successful_executions": successful_executions,
"success_rate": successful_executions / total_executions if total_executions > 0 else 0.0,
"unique_users": unique_users,
"timestamp": datetime.utcnow().isoformat()
}
# Global service instance
orchestration_service = AgentOrchestrationService()
# Convenience functions for external use
async def execute_marketing_strategy(user_id: str, market_context: Dict[str, Any]) -> Dict[str, Any]:
"""Execute marketing strategy for a user"""
return await orchestration_service.execute_marketing_strategy(user_id, market_context)
async def get_agent_system_status(user_id: str) -> Dict[str, Any]:
"""Get agent system status for a user"""
return await orchestration_service.get_agent_status(user_id)
async def process_market_signals_for_user(user_id: str) -> List[MarketSignal]:
"""Process market signals for a user"""
return await orchestration_service.process_market_signals(user_id)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,250 @@
"""
Market Signal Detection System for ALwrity Autonomous Agents
Built on txtai's semantic intelligence and existing monitoring infrastructure
"""
import asyncio
import json
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Set
from dataclasses import dataclass, asdict
from enum import Enum
# Integration with existing ALwrity services
from services.intelligence.monitoring.semantic_dashboard import RealTimeSemanticMonitor
from services.intelligence.semantic_cache import SemanticCacheManager
from services.seo_analyzer import ComprehensiveSEOAnalyzer
from utils.logger_utils import get_service_logger
logger = get_service_logger(__name__)
class SignalType(Enum):
"""Types of market signals that agents can detect"""
COMPETITOR_CHANGE = "competitor"
SERP_FLUCTUATION = "serp"
SOCIAL_TREND = "social"
INDUSTRY_NEWS = "industry"
PERFORMANCE_CHANGE = "performance"
CONTENT_GAP = "content_gap"
SEO_OPPORTUNITY = "seo_opportunity"
class UrgencyLevel(Enum):
"""Urgency levels for market signals"""
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
@dataclass
class MarketSignal:
"""Represents a detected market signal"""
signal_id: str
signal_type: SignalType
source: str
description: str
impact_score: float # 0.0 to 1.0
urgency_level: UrgencyLevel
confidence_score: float # 0.0 to 1.0
related_topics: List[str]
suggested_actions: List[str]
metadata: Dict[str, Any]
detected_at: str = None
expires_at: str = None
def __post_init__(self):
if self.detected_at is None:
self.detected_at = datetime.utcnow().isoformat()
if self.expires_at is None:
# Default expiration based on urgency
if self.urgency_level == UrgencyLevel.CRITICAL:
expires_hours = 1
elif self.urgency_level == UrgencyLevel.HIGH:
expires_hours = 6
elif self.urgency_level == UrgencyLevel.MEDIUM:
expires_hours = 24
else:
expires_hours = 72
expires = datetime.utcnow().timestamp() + (expires_hours * 60 * 60)
self.expires_at = datetime.fromtimestamp(expires).isoformat()
@dataclass
class SignalContext:
"""Context for signal detection"""
user_id: str
competitor_data: Dict[str, Any]
semantic_health: Dict[str, Any]
seo_performance: Dict[str, Any]
content_analysis: Dict[str, Any]
historical_data: Dict[str, Any]
timestamp: str = None
def __post_init__(self):
if self.timestamp is None:
self.timestamp = datetime.utcnow().isoformat()
class MarketSignalDetector:
"""Main market signal detection system"""
def __init__(self, user_id: str):
self.user_id = user_id
self.semantic_monitor = RealTimeSemanticMonitor(user_id)
self.cache_manager = SemanticCacheManager()
self.seo_analyzer = ComprehensiveSEOAnalyzer()
# Signal detection thresholds
self.thresholds = {
"competitor_change_threshold": 0.3, # 30% change in competitor metrics
"serp_fluctuation_threshold": 0.2, # 20% change in SERP positions
"social_trend_threshold": 0.15, # 15% change in social metrics
"performance_change_threshold": 0.25, # 25% change in performance metrics
"content_gap_threshold": 0.4, # 40% semantic gap
"seo_opportunity_threshold": 0.3 # 30% SEO improvement opportunity
}
# Historical data for trend analysis
self.signal_history: List[MarketSignal] = []
self.baseline_metrics: Dict[str, float] = {}
logger.info(f"Initialized MarketSignalDetector for user: {user_id}")
async def detect_market_signals(self) -> List[MarketSignal]:
"""Detect all current market signals"""
try:
logger.info(f"Starting market signal detection for user: {self.user_id}")
# Get current context
context = await self._get_signal_context()
# Check cache first
cache_key = f"market_signals_{self.user_id}"
cached_signals = self.cache_manager.get(cache_key)
if cached_signals and self._is_cache_valid(cached_signals):
logger.info(f"Using cached market signals for user: {self.user_id}")
return cached_signals
# Detect signals from multiple sources
signals = []
# Competitor signals
competitor_signals = await self._detect_competitor_signals(context)
signals.extend(competitor_signals)
# SERP signals
serp_signals = await self._detect_serp_signals(context)
signals.extend(serp_signals)
# Social signals
social_signals = await self._detect_social_signals(context)
signals.extend(social_signals)
# Industry signals
industry_signals = await self._detect_industry_signals(context)
signals.extend(industry_signals)
# Performance signals
performance_signals = await self._detect_performance_signals(context)
signals.extend(performance_signals)
# Content gap signals
content_signals = await self._detect_content_gap_signals(context)
signals.extend(content_signals)
# SEO opportunity signals
seo_signals = await self._detect_seo_opportunity_signals(context)
signals.extend(seo_signals)
# Filter and prioritize signals
filtered_signals = self._filter_signals(signals)
prioritized_signals = self._prioritize_signals(filtered_signals)
# Update history
self.signal_history.extend(prioritized_signals)
self._trim_signal_history()
# Cache results
self.cache_manager.set(cache_key, prioritized_signals, ttl=300) # 5 minute cache
logger.info(f"Detected {len(prioritized_signals)} market signals for user: {self.user_id}")
return prioritized_signals
except Exception as e:
logger.error(f"Error detecting market signals: {str(e)}")
return []
async def _get_signal_context(self) -> SignalContext:
"""Fetch current context for signal detection"""
# Placeholder implementation
return SignalContext(
user_id=self.user_id,
competitor_data={},
semantic_health={},
seo_performance={},
content_analysis={},
historical_data={}
)
def _is_cache_valid(self, signals: List[MarketSignal]) -> bool:
"""Check if cached signals are still valid"""
if not signals:
return False
# Basic check for now
return True
async def _detect_competitor_signals(self, context: SignalContext) -> List[MarketSignal]:
"""Detect signals from competitor activities"""
return []
async def _detect_serp_signals(self, context: SignalContext) -> List[MarketSignal]:
"""Detect signals from SERP changes"""
return []
async def _detect_social_signals(self, context: SignalContext) -> List[MarketSignal]:
"""Detect signals from social trends"""
return []
async def _detect_industry_signals(self, context: SignalContext) -> List[MarketSignal]:
"""Detect signals from industry news"""
return []
async def _detect_performance_signals(self, context: SignalContext) -> List[MarketSignal]:
"""Detect signals from site performance"""
return []
async def _detect_content_gap_signals(self, context: SignalContext) -> List[MarketSignal]:
"""Detect signals from content gaps"""
return []
async def _detect_seo_opportunity_signals(self, context: SignalContext) -> List[MarketSignal]:
"""Detect signals from SEO opportunities"""
return []
def _filter_signals(self, signals: List[MarketSignal]) -> List[MarketSignal]:
"""Filter out low-quality or duplicate signals"""
return signals
def _prioritize_signals(self, signals: List[MarketSignal]) -> List[MarketSignal]:
"""Prioritize signals based on impact and urgency"""
return sorted(signals, key=lambda x: (x.urgency_level.value, x.impact_score), reverse=True)
def _trim_signal_history(self):
"""Keep signal history within limits"""
if len(self.signal_history) > 1000:
self.signal_history = self.signal_history[-1000:]
class MarketTrendAnalyzer:
"""
Analyzer for detecting market trends from aggregated signals.
"""
def __init__(self, user_id: str):
self.user_id = user_id
self.detector = MarketSignalDetector(user_id)
async def analyze_trends(self, context: Optional[Dict[str, Any]] = None) -> List[MarketSignal]:
"""Analyze current market trends"""
# Placeholder implementation
logger.info(f"Analyzing market trends for user {self.user_id}")
return []

View File

@@ -0,0 +1,128 @@
"""
Agent Performance Monitoring Framework for ALwrity Autonomous Marketing Agents
Tracks agent performance, efficiency, and provides optimization recommendations
"""
import asyncio
import json
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Tuple
from dataclasses import dataclass, asdict
from enum import Enum
from collections import defaultdict, deque
from utils.logger_utils import get_service_logger
from services.database import get_session_for_user
logger = get_service_logger(__name__)
class AgentStatus(Enum):
IDLE = "idle"
BUSY = "busy"
ERROR = "error"
OFFLINE = "offline"
INITIALIZING = "initializing"
class PerformanceMetric(Enum):
RESPONSE_TIME = "response_time"
SUCCESS_RATE = "success_rate"
TOKEN_USAGE = "token_usage"
COST_PER_ACTION = "cost_per_action"
RESOURCE_UTILIZATION = "resource_utilization"
GOAL_COMPLETION_RATE = "goal_completion_rate"
@dataclass
class AgentPerformanceMetrics:
agent_id: str
timestamp: datetime
metrics: Dict[str, float]
context: Dict[str, Any]
class PerformanceMonitor:
"""
Monitors and analyzes agent performance metrics
"""
def __init__(self):
self.metrics_buffer = deque(maxlen=1000)
self.performance_history = defaultdict(list)
self.alert_thresholds = {
PerformanceMetric.SUCCESS_RATE: 0.8, # Alert if success rate < 80%
PerformanceMetric.RESPONSE_TIME: 30.0, # Alert if response time > 30s
PerformanceMetric.GOAL_COMPLETION_RATE: 0.7 # Alert if completion < 70%
}
async def record_metric(self,
agent_id: str,
metric_type: PerformanceMetric,
value: float,
context: Optional[Dict[str, Any]] = None):
"""Record a performance metric for an agent"""
metric_entry = AgentPerformanceMetrics(
agent_id=agent_id,
timestamp=datetime.utcnow(),
metrics={metric_type.value: value},
context=context or {}
)
self.metrics_buffer.append(metric_entry)
self.performance_history[agent_id].append(metric_entry)
# Check thresholds
await self._check_thresholds(agent_id, metric_type, value)
# Persist if needed (batching implemented in production)
# await self._persist_metric(metric_entry)
async def get_agent_performance(self, agent_id: str, time_window_minutes: int = 60) -> Dict[str, Any]:
"""Get aggregated performance metrics for an agent"""
cutoff_time = datetime.utcnow() - timedelta(minutes=time_window_minutes)
relevant_metrics = [
m for m in self.performance_history[agent_id]
if m.timestamp > cutoff_time
]
if not relevant_metrics:
return {}
aggregated = defaultdict(list)
for m in relevant_metrics:
for k, v in m.metrics.items():
aggregated[k].append(v)
result = {
"agent_id": agent_id,
"period_minutes": time_window_minutes,
"sample_size": len(relevant_metrics),
"metrics": {
k: sum(v) / len(v) for k, v in aggregated.items()
}
}
return result
async def _check_thresholds(self, agent_id: str, metric_type: PerformanceMetric, value: float):
"""Check if metric violates thresholds"""
threshold = self.alert_thresholds.get(metric_type)
if not threshold:
return
is_violation = False
if metric_type in [PerformanceMetric.SUCCESS_RATE, PerformanceMetric.GOAL_COMPLETION_RATE]:
if value < threshold:
is_violation = True
elif value > threshold:
is_violation = True
if is_violation:
logger.warning(
f"Performance alert for agent {agent_id}: "
f"{metric_type.value} = {value} (Threshold: {threshold})"
)
# Trigger alert notification (impl via notification service)
# Singleton instance
performance_monitor = PerformanceMonitor()
AgentPerformanceMonitor = PerformanceMonitor
performance_service = performance_monitor

View File

@@ -0,0 +1,899 @@
"""
Agent Safety Framework for ALwrity Autonomous Marketing Agents
Implements safety constraints, validation, and rollback mechanisms
"""
import asyncio
import json
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Set
from dataclasses import dataclass, asdict
from enum import Enum
from utils.logger_utils import get_service_logger
from services.database import get_session_for_user
logger = get_service_logger(__name__)
class RiskLevel(Enum):
"""Risk levels for agent actions"""
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class ActionCategory(Enum):
"""Categories of agent actions"""
CONTENT_MODIFICATION = "content_modification"
SEO_OPTIMIZATION = "seo_optimization"
COMPETITOR_RESPONSE = "competitor_response"
SOCIAL_AMPLIFICATION = "social_amplification"
STRATEGY_CHANGE = "strategy_change"
SYSTEM_CONFIGURATION = "system_configuration"
@dataclass
class SafetyConstraint:
"""Represents a safety constraint for agent actions"""
constraint_id: str
name: str
description: str
action_categories: List[ActionCategory]
risk_threshold: float # Maximum allowed risk level (0.0 to 1.0)
approval_required: bool
auto_approval_threshold: float # Risk level below which auto-approval is allowed
daily_limit: Optional[int] = None # Maximum actions per day
hourly_limit: Optional[int] = None # Maximum actions per hour
conditions: Dict[str, Any] = None # Additional conditions for validation
created_at: str = None
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.utcnow().isoformat()
if self.conditions is None:
self.conditions = {}
@dataclass
class ActionCheckpoint:
"""Represents a checkpoint for rollback purposes"""
checkpoint_id: str
action_id: str
agent_id: str
user_id: str
action_type: str
action_data: Dict[str, Any]
system_state: Dict[str, Any]
created_at: str = None
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.utcnow().isoformat()
@dataclass
class SafetyValidation:
"""Result of safety validation"""
is_valid: bool
risk_level: RiskLevel
violations: List[str]
recommendations: List[str]
requires_approval: bool
confidence_score: float # 0.0 to 1.0
validation_timestamp: str = None
def __post_init__(self):
if self.validation_timestamp is None:
self.validation_timestamp = datetime.utcnow().isoformat()
class SafetyConstraintManager:
"""Manages safety constraints for agent actions"""
def __init__(self, user_id: str):
self.user_id = user_id
self.constraints: Dict[str, SafetyConstraint] = {}
self.action_history: List[Dict[str, Any]] = []
self.violation_history: List[Dict[str, Any]] = []
# Initialize default constraints
self._initialize_default_constraints()
logger.info(f"Initialized SafetyConstraintManager for user: {user_id}")
def _initialize_default_constraints(self):
"""Initialize default safety constraints"""
default_constraints = [
SafetyConstraint(
constraint_id="content_modification_limit",
name="Content Modification Daily Limit",
description="Limit the number of content modifications per day",
action_categories=[ActionCategory.CONTENT_MODIFICATION],
risk_threshold=0.7,
approval_required=False,
auto_approval_threshold=0.3,
daily_limit=50,
hourly_limit=10
),
SafetyConstraint(
constraint_id="high_risk_approval_required",
name="High Risk Action Approval",
description="Require approval for high-risk actions",
action_categories=[ActionCategory.STRATEGY_CHANGE, ActionCategory.SYSTEM_CONFIGURATION],
risk_threshold=0.8,
approval_required=True,
auto_approval_threshold=0.2
),
SafetyConstraint(
constraint_id="competitor_response_cooldown",
name="Competitor Response Cooldown",
description="Prevent excessive competitor responses",
action_categories=[ActionCategory.COMPETITOR_RESPONSE],
risk_threshold=0.6,
approval_required=False,
auto_approval_threshold=0.4,
daily_limit=20,
hourly_limit=5
),
SafetyConstraint(
constraint_id="seo_optimization_safety",
name="SEO Optimization Safety",
description="Ensure SEO optimizations don't harm rankings",
action_categories=[ActionCategory.SEO_OPTIMIZATION],
risk_threshold=0.5,
approval_required=False,
auto_approval_threshold=0.3,
daily_limit=30,
hourly_limit=8
),
SafetyConstraint(
constraint_id="social_amplification_limits",
name="Social Amplification Limits",
description="Limit social media amplification to prevent spam",
action_categories=[ActionCategory.SOCIAL_AMPLIFICATION],
risk_threshold=0.6,
approval_required=False,
auto_approval_threshold=0.4,
daily_limit=25,
hourly_limit=6
)
]
for constraint in default_constraints:
self.constraints[constraint.constraint_id] = constraint
async def validate_action(self, action_data: Dict[str, Any]) -> SafetyValidation:
"""Validate an action against safety constraints"""
try:
logger.info(f"Validating action for user {self.user_id}: {action_data.get('action_type', 'unknown')}")
violations = []
recommendations = []
requires_approval = False
confidence_score = 1.0
# Extract action details
action_type = action_data.get('action_type', 'unknown')
action_category = self._determine_action_category(action_type)
risk_score = action_data.get('risk_score', 0.5)
impact_score = action_data.get('impact_score', 0.5)
# Determine risk level
risk_level = self._calculate_risk_level(risk_score, impact_score)
# Check against all relevant constraints
for constraint in self.constraints.values():
if action_category in constraint.action_categories:
constraint_result = await self._check_constraint(constraint, action_data, risk_level)
if not constraint_result['is_valid']:
violations.extend(constraint_result['violations'])
confidence_score *= 0.9 # Reduce confidence for violations
if constraint_result['requires_approval']:
requires_approval = True
recommendations.extend(constraint_result['recommendations'])
# Check rate limits
rate_limit_result = await self._check_rate_limits(action_category, action_data)
if not rate_limit_result['is_valid']:
violations.extend(rate_limit_result['violations'])
confidence_score *= 0.8
# Check for suspicious patterns
pattern_result = await self._check_suspicious_patterns(action_data)
if not pattern_result['is_valid']:
violations.extend(pattern_result['violations'])
confidence_score *= 0.7
requires_approval = True # Suspicious patterns always require approval
# Final validation
is_valid = len(violations) == 0 and not requires_approval
logger.info(f"Action validation completed for user {self.user_id}. Valid: {is_valid}, Risk: {risk_level.value}, Violations: {len(violations)}")
# Record in history
await self._record_validation_history(action_data, is_valid, violations)
return SafetyValidation(
is_valid=is_valid,
risk_level=risk_level,
violations=violations,
recommendations=recommendations,
requires_approval=requires_approval,
confidence_score=max(0.0, min(1.0, confidence_score))
)
except Exception as e:
logger.error(f"Error validating action for user {self.user_id}: {e}")
# Return safe default on error
return SafetyValidation(
is_valid=False,
risk_level=RiskLevel.CRITICAL,
violations=["Validation system error"],
recommendations=["Manual review required"],
requires_approval=True,
confidence_score=0.0
)
def _determine_action_category(self, action_type: str) -> ActionCategory:
"""Determine the category of an action"""
action_type_lower = action_type.lower()
if any(keyword in action_type_lower for keyword in ['content', 'blog', 'article', 'post']):
return ActionCategory.CONTENT_MODIFICATION
elif any(keyword in action_type_lower for keyword in ['seo', 'meta', 'keyword', 'optimization']):
return ActionCategory.SEO_OPTIMIZATION
elif any(keyword in action_type_lower for keyword in ['competitor', 'competitive', 'response']):
return ActionCategory.COMPETITOR_RESPONSE
elif any(keyword in action_type_lower for keyword in ['social', 'share', 'amplify', 'distribute']):
return ActionCategory.SOCIAL_AMPLIFICATION
elif any(keyword in action_type_lower for keyword in ['strategy', 'plan', 'approach']):
return ActionCategory.STRATEGY_CHANGE
elif any(keyword in action_type_lower for keyword in ['config', 'setting', 'system']):
return ActionCategory.SYSTEM_CONFIGURATION
else:
return ActionCategory.CONTENT_MODIFICATION # Default category
def _calculate_risk_level(self, risk_score: float, impact_score: float) -> RiskLevel:
"""Calculate overall risk level"""
# Weighted combination of risk and impact
combined_score = (risk_score * 0.6) + (impact_score * 0.4)
if combined_score >= 0.8:
return RiskLevel.CRITICAL
elif combined_score >= 0.6:
return RiskLevel.HIGH
elif combined_score >= 0.3:
return RiskLevel.MEDIUM
else:
return RiskLevel.LOW
async def _check_constraint(self, constraint: SafetyConstraint, action_data: Dict[str, Any], risk_level: RiskLevel) -> Dict[str, Any]:
"""Check an action against a specific constraint"""
violations = []
recommendations = []
requires_approval = False
# Check risk threshold
if risk_level.value in ['high', 'critical'] and constraint.risk_threshold < 0.8:
violations.append(f"Risk level {risk_level.value} exceeds constraint threshold")
requires_approval = True
# Check rate limits
if constraint.daily_limit:
daily_count = await self._get_daily_action_count(constraint.constraint_id)
if daily_count >= constraint.daily_limit:
violations.append(f"Daily limit exceeded: {daily_count}/{constraint.daily_limit}")
if constraint.hourly_limit:
hourly_count = await self._get_hourly_action_count(constraint.constraint_id)
if hourly_count >= constraint.hourly_limit:
violations.append(f"Hourly limit exceeded: {hourly_count}/{constraint.hourly_limit}")
# Check approval requirement
if constraint.approval_required:
requires_approval = True
recommendations.append("Action requires manual approval due to safety constraints")
# Check auto-approval threshold
risk_score = action_data.get('risk_score', 0.5)
if risk_score > constraint.auto_approval_threshold:
requires_approval = True
# Custom condition checks
if constraint.conditions:
condition_result = await self._check_custom_conditions(constraint.conditions, action_data)
if not condition_result['is_valid']:
violations.extend(condition_result['violations'])
is_valid = len(violations) == 0 and not requires_approval
return {
"is_valid": is_valid,
"violations": violations,
"recommendations": recommendations,
"requires_approval": requires_approval
}
async def _check_rate_limits(self, action_category: ActionCategory, action_data: Dict[str, Any]) -> Dict[str, Any]:
"""Check rate limits for actions"""
violations = []
# Get current time window counts
recent_actions = await self._get_recent_actions(hours=1)
category_actions = [action for action in recent_actions if self._determine_action_category(action.get('action_type', '')) == action_category]
# Check hourly limits
if len(category_actions) > 50: # Default hourly limit
violations.append(f"Hourly action limit exceeded for {action_category.value}")
# Check daily limits
daily_actions = await self._get_recent_actions(hours=24)
daily_category_actions = [action for action in daily_actions if self._determine_action_category(action.get('action_type', '')) == action_category]
if len(daily_category_actions) > 200: # Default daily limit
violations.append(f"Daily action limit exceeded for {action_category.value}")
return {
"is_valid": len(violations) == 0,
"violations": violations
}
async def _check_suspicious_patterns(self, action_data: Dict[str, Any]) -> Dict[str, Any]:
"""Check for suspicious patterns in actions"""
violations = []
# Get recent action patterns
recent_actions = await self._get_recent_actions(hours=24)
# Check for rapid repetitive actions
action_type = action_data.get('action_type', '')
similar_actions = [action for action in recent_actions if action.get('action_type') == action_type]
if len(similar_actions) > 10: # More than 10 similar actions in 24 hours
violations.append(f"Suspicious pattern: {len(similar_actions)} similar actions in 24 hours")
# Check for unusual timing patterns
if len(recent_actions) > 100: # More than 100 actions in 1 hour
violations.append("Suspicious pattern: Unusually high action frequency")
# Check for conflicting actions
conflicting_actions = await self._detect_conflicting_actions(action_data, recent_actions)
if conflicting_actions:
violations.append(f"Conflicting actions detected: {len(conflicting_actions)}")
return {
"is_valid": len(violations) == 0,
"violations": violations
}
async def _detect_conflicting_actions(self, current_action: Dict[str, Any], recent_actions: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Detect actions that conflict with recent actions"""
conflicts = []
# Simple conflict detection based on action types
conflicting_pairs = [
("optimize_content", "delete_content"),
("increase_keywords", "decrease_keywords"),
("enable_feature", "disable_feature")
]
current_action_type = current_action.get('action_type', '')
for pair in conflicting_pairs:
if current_action_type == pair[0]:
# Check for recent opposite action
for action in recent_actions:
if action.get('action_type') == pair[1]:
conflicts.append(action)
break
elif current_action_type == pair[1]:
# Check for recent opposite action
for action in recent_actions:
if action.get('action_type') == pair[0]:
conflicts.append(action)
break
return conflicts
async def _check_custom_conditions(self, conditions: Dict[str, Any], action_data: Dict[str, Any]) -> Dict[str, Any]:
"""Check custom conditions for constraints"""
violations = []
# Example custom conditions (can be extended)
if conditions.get('max_content_length'):
content_length = len(action_data.get('content', ''))
if content_length > conditions['max_content_length']:
violations.append(f"Content length {content_length} exceeds maximum {conditions['max_content_length']}")
if conditions.get('allowed_keywords'):
content = action_data.get('content', '').lower()
allowed_keywords = [kw.lower() for kw in conditions['allowed_keywords']]
if not any(keyword in content for keyword in allowed_keywords):
violations.append("Content does not contain required keywords")
return {
"is_valid": len(violations) == 0,
"violations": violations
}
async def _get_recent_actions(self, hours: int = 24) -> List[Dict[str, Any]]:
"""Get recent actions from history"""
cutoff_time = datetime.utcnow() - timedelta(hours=hours)
return [
action for action in self.action_history
if datetime.fromisoformat(action.get('timestamp', datetime.utcnow().isoformat())) > cutoff_time
]
async def _get_daily_action_count(self, constraint_id: str) -> int:
"""Get daily action count for a specific constraint"""
daily_actions = await self._get_recent_actions(hours=24)
return len(daily_actions)
async def _get_hourly_action_count(self, constraint_id: str) -> int:
"""Get hourly action count for a specific constraint"""
hourly_actions = await self._get_recent_actions(hours=1)
return len(hourly_actions)
async def _record_validation_history(self, action_data: Dict[str, Any], is_valid: bool, violations: List[str]):
"""Record validation in history"""
validation_record = {
"timestamp": datetime.utcnow().isoformat(),
"action_type": action_data.get('action_type', 'unknown'),
"is_valid": is_valid,
"violations": violations,
"action_data": action_data
}
self.action_history.append(validation_record)
# Keep only recent history (last 1000 records)
if len(self.action_history) > 1000:
self.action_history = self.action_history[-1000:]
# Record violations separately
if violations:
violation_record = {
"timestamp": datetime.utcnow().isoformat(),
"action_type": action_data.get('action_type', 'unknown'),
"violations": violations,
"severity": "high" if len(violations) > 2 else "medium"
}
self.violation_history.append(violation_record)
# Keep only recent violations (last 500 records)
if len(self.violation_history) > 500:
self.violation_history = self.violation_history[-500:]
def add_custom_constraint(self, constraint: SafetyConstraint):
"""Add a custom safety constraint"""
self.constraints[constraint.constraint_id] = constraint
logger.info(f"Added custom constraint for user {self.user_id}: {constraint.constraint_id}")
def remove_constraint(self, constraint_id: str):
"""Remove a safety constraint"""
if constraint_id in self.constraints:
del self.constraints[constraint_id]
logger.info(f"Removed constraint for user {self.user_id}: {constraint_id}")
def get_constraints(self) -> Dict[str, SafetyConstraint]:
"""Get all safety constraints"""
return self.constraints.copy()
def get_validation_history(self, limit: int = 100) -> List[Dict[str, Any]]:
"""Get recent validation history"""
return self.action_history[-limit:] if self.action_history else []
def get_violation_history(self, limit: int = 50) -> List[Dict[str, Any]]:
"""Get recent violation history"""
return self.violation_history[-limit:] if self.violation_history else []
class RollbackManager:
"""Manages rollback operations for agent actions"""
def __init__(self, user_id: str):
self.user_id = user_id
self.checkpoints: List[ActionCheckpoint] = []
self.rollback_history: List[Dict[str, Any]] = []
logger.info(f"Initialized RollbackManager for user: {user_id}")
async def create_checkpoint(self, action_data: Dict[str, Any], system_state: Dict[str, Any]) -> str:
"""Create a checkpoint before executing an action"""
try:
checkpoint_id = f"checkpoint_{self.user_id}_{datetime.utcnow().strftime('%Y%m%d%H%M%S')}"
checkpoint = ActionCheckpoint(
checkpoint_id=checkpoint_id,
action_id=action_data.get('action_id', 'unknown'),
agent_id=action_data.get('agent_id', 'unknown'),
user_id=self.user_id,
action_type=action_data.get('action_type', 'unknown'),
action_data=action_data,
system_state=system_state
)
self.checkpoints.append(checkpoint)
# Keep only recent checkpoints (last 100)
if len(self.checkpoints) > 100:
self.checkpoints = self.checkpoints[-100:]
logger.info(f"Created checkpoint for user {self.user_id}: {checkpoint_id}")
return checkpoint_id
except Exception as e:
logger.error(f"Error creating checkpoint for user {self.user_id}: {e}")
raise e
async def rollback_to_checkpoint(self, checkpoint_id: str) -> Dict[str, Any]:
"""Rollback to a specific checkpoint"""
try:
# Find checkpoint
checkpoint = next((cp for cp in self.checkpoints if cp.checkpoint_id == checkpoint_id), None)
if not checkpoint:
return {
"success": False,
"error": f"Checkpoint not found: {checkpoint_id}"
}
logger.info(f"Rolling back to checkpoint for user {self.user_id}: {checkpoint_id}")
# Execute rollback (implementation depends on action type)
rollback_result = await self._execute_rollback(checkpoint)
# Record in history
rollback_record = {
"timestamp": datetime.utcnow().isoformat(),
"checkpoint_id": checkpoint_id,
"action_type": checkpoint.action_type,
"success": rollback_result["success"],
"details": rollback_result
}
self.rollback_history.append(rollback_record)
# Keep only recent rollback history (last 50)
if len(self.rollback_history) > 50:
self.rollback_history = self.rollback_history[-50:]
return rollback_result
except Exception as e:
logger.error(f"Error rolling back to checkpoint {checkpoint_id} for user {self.user_id}: {e}")
return {
"success": False,
"error": str(e)
}
async def _execute_rollback(self, checkpoint: ActionCheckpoint) -> Dict[str, Any]:
"""Execute the rollback operation based on action type"""
try:
action_type = checkpoint.action_type
action_data = checkpoint.action_data
system_state = checkpoint.system_state
# Implement rollback logic for different action types
if action_type == "content_modification":
return await self._rollback_content_modification(action_data, system_state)
elif action_type == "seo_optimization":
return await self._rollback_seo_optimization(action_data, system_state)
elif action_type == "competitor_response":
return await self._rollback_competitor_response(action_data, system_state)
elif action_type == "social_amplification":
return await self._rollback_social_amplification(action_data, system_state)
else:
# Generic rollback
return await self._rollback_generic(action_data, system_state)
except Exception as e:
logger.error(f"Error executing rollback for action {action_type}: {e}")
return {
"success": False,
"error": str(e)
}
async def _rollback_content_modification(self, action_data: Dict[str, Any], system_state: Dict[str, Any]) -> Dict[str, Any]:
"""Rollback content modification"""
try:
# Implementation would depend on how content is stored and managed
# For now, return a placeholder implementation
original_content = system_state.get('original_content', {})
modified_content = action_data.get('content', {})
logger.info(f"Rolling back content modification: {action_data.get('content_id', 'unknown')}")
return {
"success": True,
"message": "Content modification rolled back successfully",
"details": {
"content_id": action_data.get('content_id'),
"rollback_type": "content_modification",
"original_state_restored": bool(original_content)
}
}
except Exception as e:
return {
"success": False,
"error": f"Failed to rollback content modification: {str(e)}"
}
async def _rollback_seo_optimization(self, action_data: Dict[str, Any], system_state: Dict[str, Any]) -> Dict[str, Any]:
"""Rollback SEO optimization"""
try:
original_seo_state = system_state.get('seo_state', {})
logger.info(f"Rolling back SEO optimization: {action_data.get('optimization_type', 'unknown')}")
return {
"success": True,
"message": "SEO optimization rolled back successfully",
"details": {
"optimization_type": action_data.get('optimization_type'),
"rollback_type": "seo_optimization",
"original_state_restored": bool(original_seo_state)
}
}
except Exception as e:
return {
"success": False,
"error": f"Failed to rollback SEO optimization: {str(e)}"
}
async def _rollback_competitor_response(self, action_data: Dict[str, Any], system_state: Dict[str, Any]) -> Dict[str, Any]:
"""Rollback competitor response"""
try:
logger.info(f"Rolling back competitor response: {action_data.get('response_type', 'unknown')}")
return {
"success": True,
"message": "Competitor response rolled back successfully",
"details": {
"response_type": action_data.get('response_type'),
"rollback_type": "competitor_response",
"original_state_restored": True
}
}
except Exception as e:
return {
"success": False,
"error": f"Failed to rollback competitor response: {str(e)}"
}
async def _rollback_social_amplification(self, action_data: Dict[str, Any], system_state: Dict[str, Any]) -> Dict[str, Any]:
"""Rollback social amplification"""
try:
logger.info(f"Rolling back social amplification: {action_data.get('platform', 'unknown')}")
return {
"success": True,
"message": "Social amplification rolled back successfully",
"details": {
"platform": action_data.get('platform'),
"rollback_type": "social_amplification",
"original_state_restored": True
}
}
except Exception as e:
return {
"success": False,
"error": f"Failed to rollback social amplification: {str(e)}"
}
async def _rollback_generic(self, action_data: Dict[str, Any], system_state: Dict[str, Any]) -> Dict[str, Any]:
"""Generic rollback for unknown action types"""
try:
logger.info(f"Performing generic rollback for action: {action_data.get('action_type', 'unknown')}")
return {
"success": True,
"message": "Generic rollback completed",
"details": {
"action_type": action_data.get('action_type'),
"rollback_type": "generic",
"system_state_available": bool(system_state)
}
}
except Exception as e:
return {
"success": False,
"error": f"Failed to perform generic rollback: {str(e)}"
}
async def rollback_latest_actions(self, count: int = 1) -> List[Dict[str, Any]]:
"""Rollback the latest N actions"""
results = []
# Get latest checkpoints
latest_checkpoints = self.checkpoints[-count:] if self.checkpoints else []
for checkpoint in reversed(latest_checkpoints):
result = await self.rollback_to_checkpoint(checkpoint.checkpoint_id)
results.append(result)
return results
def get_checkpoints(self, limit: int = 50) -> List[Dict[str, Any]]:
"""Get recent checkpoints"""
checkpoints_data = []
for checkpoint in self.checkpoints[-limit:]:
checkpoints_data.append({
"checkpoint_id": checkpoint.checkpoint_id,
"action_id": checkpoint.action_id,
"action_type": checkpoint.action_type,
"agent_id": checkpoint.agent_id,
"created_at": checkpoint.created_at,
"system_state_keys": list(checkpoint.system_state.keys())
})
return checkpoints_data
def get_rollback_history(self, limit: int = 50) -> List[Dict[str, Any]]:
"""Get rollback history"""
return self.rollback_history[-limit:] if self.rollback_history else []
class UserApprovalSystem:
"""Manages user approval for high-risk actions"""
def __init__(self, user_id: str):
self.user_id = user_id
self.pending_approvals: Dict[str, Dict[str, Any]] = {}
self.approval_history: List[Dict[str, Any]] = []
logger.info(f"Initialized UserApprovalSystem for user: {user_id}")
async def request_approval(self, action_data: Dict[str, Any]) -> Dict[str, Any]:
"""Request user approval for an action"""
try:
approval_id = f"approval_{self.user_id}_{datetime.utcnow().strftime('%Y%m%d%H%M%S')}"
approval_request = {
"approval_id": approval_id,
"action_data": action_data,
"requested_at": datetime.utcnow().isoformat(),
"status": "pending",
"expires_at": (datetime.utcnow() + timedelta(hours=24)).isoformat()
}
self.pending_approvals[approval_id] = approval_request
logger.info(f"Created approval request for user {self.user_id}: {approval_id}")
return {
"success": True,
"approval_id": approval_id,
"status": "pending",
"message": "Approval request created successfully"
}
except Exception as e:
logger.error(f"Error creating approval request for user {self.user_id}: {e}")
return {
"success": False,
"error": str(e)
}
async def approve_action(self, approval_id: str, user_decision: str, user_comments: str = "") -> Dict[str, Any]:
"""Process user approval decision"""
try:
if approval_id not in self.pending_approvals:
return {
"success": False,
"error": "Approval request not found"
}
approval_request = self.pending_approvals[approval_id]
# Check if approval has expired
expires_at = datetime.fromisoformat(approval_request["expires_at"])
if datetime.utcnow() > expires_at:
del self.pending_approvals[approval_id]
return {
"success": False,
"error": "Approval request has expired"
}
# Process decision
approval_request["status"] = user_decision
approval_request["decision_at"] = datetime.utcnow().isoformat()
approval_request["user_comments"] = user_comments
# Record in history
self.approval_history.append(approval_request)
# Remove from pending
del self.pending_approvals[approval_id]
# Keep only recent history (last 100)
if len(self.approval_history) > 100:
self.approval_history = self.approval_history[-100:]
logger.info(f"Processed approval decision for user {self.user_id}: {approval_id} - {user_decision}")
return {
"success": True,
"approval_id": approval_id,
"status": user_decision,
"message": f"Action {user_decision} successfully"
}
except Exception as e:
logger.error(f"Error processing approval decision for user {self.user_id}: {e}")
return {
"success": False,
"error": str(e)
}
def get_pending_approvals(self) -> List[Dict[str, Any]]:
"""Get all pending approval requests"""
return list(self.pending_approvals.values())
def get_approval_history(self, limit: int = 50) -> List[Dict[str, Any]]:
"""Get recent approval history"""
return self.approval_history[-limit:] if self.approval_history else []
def get_approval_statistics(self) -> Dict[str, Any]:
"""Get approval statistics"""
if not self.approval_history:
return {
"total_approvals": 0,
"approved_count": 0,
"rejected_count": 0,
"approval_rate": 0.0,
"pending_count": len(self.pending_approvals)
}
total = len(self.approval_history)
approved = len([a for a in self.approval_history if a["status"] == "approved"])
rejected = len([a for a in self.approval_history if a["status"] == "rejected"])
return {
"total_approvals": total,
"approved_count": approved,
"rejected_count": rejected,
"approval_rate": approved / total if total > 0 else 0.0,
"pending_count": len(self.pending_approvals)
}
# Global safety framework instance
safety_framework_instances: Dict[str, Dict[str, Any]] = {}
def get_safety_framework(user_id: str) -> Dict[str, Any]:
"""Get or create safety framework components for a user"""
if user_id not in safety_framework_instances:
safety_framework_instances[user_id] = {
"constraint_manager": SafetyConstraintManager(user_id),
"rollback_manager": RollbackManager(user_id),
"approval_system": UserApprovalSystem(user_id)
}
return safety_framework_instances[user_id]
# Convenience functions
async def validate_agent_action(user_id: str, action_data: Dict[str, Any]) -> SafetyValidation:
"""Validate an agent action for a user"""
framework = get_safety_framework(user_id)
return await framework["constraint_manager"].validate_action(action_data)
async def create_action_checkpoint(user_id: str, action_data: Dict[str, Any], system_state: Dict[str, Any]) -> str:
"""Create a checkpoint for an action"""
framework = get_safety_framework(user_id)
return await framework["rollback_manager"].create_checkpoint(action_data, system_state)
async def rollback_to_checkpoint(user_id: str, checkpoint_id: str) -> Dict[str, Any]:
"""Rollback to a specific checkpoint"""
framework = get_safety_framework(user_id)
return await framework["rollback_manager"].rollback_to_checkpoint(checkpoint_id)
async def request_user_approval(user_id: str, action_data: Dict[str, Any]) -> Dict[str, Any]:
"""Request user approval for an action"""
framework = get_safety_framework(user_id)
return await framework["approval_system"].request_approval(action_data)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,223 @@
from __future__ import annotations
from typing import Any, Dict, List, Optional
AgentCatalogEntry = Dict[str, Any]
AGENT_TEAM_CATALOG: List[AgentCatalogEntry] = [
{
"agent_key": "strategy_orchestrator",
"agent_type": "StrategyOrchestrator",
"role": "Team Lead",
"responsibilities": [
"Coordinate all marketing agents and delegate work",
"Synthesize a unified daily strategy across channels",
"Prioritize actions based on impact and urgency",
"Maintain safety constraints and request approval when needed",
],
"tools": [
"market_signal_detector",
"google_trends_fetcher",
"agent_coordinator",
"performance_analyzer",
"strategy_synthesizer",
"task_delegator",
],
"defaults": {
"display_name_template": "{website_name} Marketing Team Lead",
"enabled": True,
"schedule": {"mode": "on_demand"},
"system_prompt_template": (
"You are the Marketing Strategy Orchestrator for {website_name}.\n\n"
"Mission: coordinate the AI marketing team to help {website_name} win in digital marketing.\n\n"
"Non-negotiables:\n"
"- Delegate tasks to specialists using the available team tools.\n"
"- Keep outputs practical for non-technical users.\n"
"- Maintain safety constraints and request approval for high-risk actions.\n\n"
"Context you may receive:\n"
"- website_url, brand_voice, target_audience, competitors, content pillars\n\n"
"Output style:\n"
"- Provide a concise plan with priorities, expected outcomes, and next steps."
),
"task_prompt_template": (
"Task: Create a unified marketing plan for today.\n"
"Use the provided context and delegate specialized work when needed.\n\n"
"Return JSON with:\n"
"{\n"
" \"summary\": string,\n"
" \"priorities\": [string],\n"
" \"delegations\": [{\"agent\": string, \"task\": string}],\n"
" \"next_actions\": [{\"title\": string, \"why\": string, \"expected_outcome\": string, \"risk_level\": \"low\"|\"medium\"|\"high\"}]\n"
"}\n"
),
},
},
{
"agent_key": "content_strategist",
"agent_type": "content_strategist",
"role": "Content Strategist",
"responsibilities": [
"Analyze content performance and engagement signals",
"Identify content gaps using semantic and sitemap analysis",
"Optimize content for clarity, SEO, and conversions",
"Track performance over time and recommend next actions",
],
"tools": [
"content_analyzer",
"semantic_gap_detector",
"content_optimizer",
"performance_tracker",
"sitemap_analyzer",
],
"defaults": {
"display_name_template": "{website_name} Content Strategist",
"enabled": True,
"schedule": {"mode": "weekly", "days": ["mon"], "time": "09:00"},
"system_prompt_template": (
"You are the Content Strategy Agent for {website_name}.\n\n"
"Mission: help {website_name} publish content that matches the brand voice and grows traffic.\n\n"
"Operating principles:\n"
"- Be specific, actionable, and non-technical.\n"
"- Prefer high-impact, low-effort recommendations first.\n"
"- Maintain brand consistency.\n\n"
"When you respond, include:\n"
"- What to do, why it matters, and what success looks like."
),
"task_prompt_template": (
"Task: Propose the next 5 content actions for {website_name}.\n"
"Inputs may include: website analysis, competitors, content pillars, recent results.\n\n"
"Return JSON with:\n"
"{\n"
" \"actions\": [{\"title\": string, \"why\": string, \"outline\": [string], \"cta\": string, \"risk_level\": \"low\"|\"medium\"|\"high\"}],\n"
" \"notes\": [string]\n"
"}\n"
),
},
},
{
"agent_key": "competitor_analyst",
"agent_type": "competitor_analyst",
"role": "Competitor Analyst",
"responsibilities": [
"Monitor competitor strategy and positioning using SIF",
"Assess threats and opportunities from competitor moves",
"Generate counter-strategy recommendations",
"Execute safe response actions (with approvals when needed)",
],
"tools": [
"competitor_monitor",
"threat_analyzer",
"response_generator",
"strategy_executor",
],
"defaults": {
"display_name_template": "{website_name} Competitor Analyst",
"enabled": True,
"schedule": {"mode": "weekly", "days": ["wed"], "time": "10:00"},
"system_prompt_template": (
"You are the Competitor Response Agent for {website_name}.\n\n"
"Mission: monitor competitor moves and translate them into clear actions for {website_name}.\n\n"
"Rules:\n"
"- Use semantic insights to avoid guesswork.\n"
"- Avoid panic. Prioritize only meaningful threats.\n"
"- Keep outputs concise and actionable."
),
"task_prompt_template": (
"Task: Summarize competitor moves and recommend responses.\n\n"
"Return JSON with:\n"
"{\n"
" \"threat_level\": \"low\"|\"medium\"|\"high\",\n"
" \"signals\": [string],\n"
" \"responses\": [{\"title\": string, \"why\": string, \"expected_outcome\": string, \"risk_level\": \"low\"|\"medium\"|\"high\"}]\n"
"}\n"
),
},
},
{
"agent_key": "seo_specialist",
"agent_type": "seo_specialist",
"role": "SEO Specialist",
"responsibilities": [
"Audit technical SEO and prioritize fixes by impact",
"Generate safe SEO fixes and improvements",
"Adjust keyword strategy based on data and trends",
"Validate changes against safety and quality constraints",
],
"tools": [
"seo_auditor",
"issue_prioritizer",
"auto_fix_executor",
"strategy_generator",
"query_seo_knowledge_base",
],
"defaults": {
"display_name_template": "{website_name} SEO Specialist",
"enabled": True,
"schedule": {"mode": "weekly", "days": ["fri"], "time": "11:00"},
"system_prompt_template": (
"You are the SEO Optimization Agent for {website_name}.\n\n"
"Mission: continuously improve technical SEO and on-page basics while preserving user experience.\n\n"
"Rules:\n"
"- Prioritize high-impact, low-risk fixes.\n"
"- Explain recommendations in simple language.\n"
"- If an action is risky, require approval."
),
"task_prompt_template": (
"Task: Produce a weekly SEO fix list for {website_name}.\n\n"
"Return JSON with:\n"
"{\n"
" \"fixes\": [{\"title\": string, \"why\": string, \"steps\": [string], \"risk_level\": \"low\"|\"medium\"|\"high\"}],\n"
" \"metrics_to_watch\": [string]\n"
"}\n"
),
},
},
{
"agent_key": "social_media_manager",
"agent_type": "social_media_manager",
"role": "Social Media Manager",
"responsibilities": [
"Monitor social trends and identify opportunities",
"Adapt content for platform-specific distribution",
"Optimize engagement signals (timing, hooks, hashtags)",
"Coordinate distribution safely (with approvals when needed)",
],
"tools": [
"social_monitor",
"content_adapter",
"engagement_optimizer",
"distribution_manager",
],
"defaults": {
"display_name_template": "{website_name} Social Media Manager",
"enabled": True,
"schedule": {"mode": "weekly", "days": ["tue"], "time": "09:30"},
"system_prompt_template": (
"You are the Social Media Manager for {website_name}.\n\n"
"Mission: help {website_name} distribute content effectively without spam.\n\n"
"Rules:\n"
"- Adapt to platform norms.\n"
"- Optimize for engagement ethically.\n"
"- Keep messages aligned with brand voice."
),
"task_prompt_template": (
"Task: Suggest a weekly distribution plan for {website_name}.\n\n"
"Return JSON with:\n"
"{\n"
" \"posts\": [{\"platform\": string, \"post\": string, \"best_time\": string, \"hashtags\": [string]}],\n"
" \"notes\": [string]\n"
"}\n"
),
},
},
]
def get_agent_catalog_entry(agent_key: str) -> Optional[AgentCatalogEntry]:
agent_key_value = (agent_key or "").strip()
for entry in AGENT_TEAM_CATALOG:
if entry.get("agent_key") == agent_key_value:
return entry
return None

View File

@@ -0,0 +1,165 @@
"""
Trend Surfer Agent
Agent for identifying and capitalizing on emerging market trends.
"""
import traceback
from typing import List, Dict, Any, Optional
from loguru import logger
from services.intelligence.agents.specialized_agents import SIFBaseAgent
from services.intelligence.agents.market_signal_detector import MarketSignalDetector, MarketSignal, UrgencyLevel, SignalType
from services.intelligence.txtai_service import TxtaiIntelligenceService
from services.research.trends.google_trends_service import GoogleTrendsService
class TrendSurferAgent(SIFBaseAgent):
"""
Agent for identifying and capitalizing on emerging market trends.
"Surfs" the trends detected by MarketSignalDetector to propose timely content.
"""
def __init__(self, intelligence_service: TxtaiIntelligenceService, user_id: str):
super().__init__(intelligence_service)
self.user_id = user_id
self.signal_detector = MarketSignalDetector(user_id)
self.trends_service = GoogleTrendsService()
async def surf_trends(self) -> List[Dict[str, Any]]:
"""
Identify high-potential trends and suggest content angles.
Integrates real-time Google Trends data with MarketSignalDetector signals.
"""
self._log_agent_operation("Surfing market trends")
try:
# 1. Get real-time trending searches from Google Trends
realtime_trends = await self.trends_service.get_trending_searches(user_id=self.user_id)
logger.info(f"[{self.__class__.__name__}] Found {len(realtime_trends)} real-time trends")
# 2. Detect internal market signals (competitors, SERP, etc.)
signals = await self.signal_detector.detect_market_signals()
# 3. Analyze real-time trends and convert to signals if actionable
trend_signals = await self._analyze_realtime_trends(realtime_trends)
signals.extend(trend_signals)
if not signals:
logger.info(f"[{self.__class__.__name__}] No active market signals found")
return []
# Filter for actionable trends (High/Critical urgency or High impact)
actionable_trends = [
s for s in signals
if s.urgency_level.value in ['high', 'critical'] or s.impact_score > 0.7
]
logger.info(f"[{self.__class__.__name__}] Found {len(actionable_trends)} actionable trends")
opportunities = []
for trend in actionable_trends:
opp = await self._analyze_opportunity(trend)
if opp:
opportunities.append(opp)
return opportunities
except Exception as e:
logger.error(f"[{self.__class__.__name__}] Trend surfing failed: {e}")
logger.error(f"[{self.__class__.__name__}] Full traceback: {traceback.format_exc()}")
return []
async def _analyze_realtime_trends(self, trends: List[str]) -> List[MarketSignal]:
"""
Analyze raw trend keywords and convert actionable ones to MarketSignals.
Uses pytrends (via GoogleTrendsService) to validate interest.
"""
signals = []
# Limit to top 5 for detailed analysis to avoid rate limits
top_trends = trends[:5]
for trend_kw in top_trends:
try:
# Get detailed data for the keyword
trend_data = await self.trends_service.analyze_trends(
keywords=[trend_kw],
timeframe="now 7-d", # Last 7 days to see immediate trajectory
geo="US" # Default to US for now, could be user-configured
)
# Check if rising
interest_over_time = trend_data.get("interest_over_time", [])
if not interest_over_time:
continue
# Simple logic: is the last point higher than the average?
values = [float(point.get(trend_kw, 0)) for point in interest_over_time if trend_kw in point]
if not values:
continue
avg_interest = sum(values) / len(values)
last_interest = values[-1]
# Calculate impact/urgency
impact_score = min(last_interest / 100.0, 1.0) # Normalized
urgency = UrgencyLevel.MEDIUM
if last_interest > 80:
urgency = UrgencyLevel.CRITICAL
elif last_interest > 50:
urgency = UrgencyLevel.HIGH
# Create Signal
signal = MarketSignal(
signal_id=f"trend_{trend_kw.replace(' ', '_')}_{int(values[-1])}",
signal_type=SignalType.SOCIAL_TREND, # Using SOCIAL_TREND as proxy for general search trend
source="google_trends",
description=f"Surging interest in '{trend_kw}'",
impact_score=impact_score,
urgency_level=urgency,
confidence_score=0.9,
related_topics=[t.get("topic_title", "") for t in trend_data.get("related_topics", {}).get("top", [])[:3]],
suggested_actions=["Create timely content", "Update social media"],
metadata=trend_data
)
signals.append(signal)
except Exception as e:
logger.warning(f"[{self.__class__.__name__}] Failed to analyze trend '{trend_kw}': {e}")
continue
return signals
async def _analyze_opportunity(self, trend: MarketSignal) -> Optional[Dict[str, Any]]:
"""
Analyze a specific trend signal to generate a content opportunity.
"""
try:
# Use semantic search to find if we already have content covering this
query = f"{trend.description} {' '.join(trend.related_topics)}"
existing_content = await self.intelligence.search(query, limit=3)
coverage_score = 0.0
if existing_content:
# If top result has high score, we might already cover it
coverage_score = existing_content[0].get('score', 0.0)
# If already well-covered, might skip or suggest update
if coverage_score > 0.8:
recommendation = "Update existing content"
else:
recommendation = "Create new content"
return {
"trend_id": trend.signal_id,
"topic": trend.description,
"source": trend.source,
"urgency": trend.urgency_level.value,
"impact_score": trend.impact_score,
"current_coverage": coverage_score,
"recommendation": recommendation,
"suggested_angle": f"Leverage {trend.source} trend on {trend.related_topics[0] if trend.related_topics else 'topic'}",
"detected_at": trend.detected_at
}
except Exception as e:
logger.warning(f"[{self.__class__.__name__}] Failed to analyze opportunity for signal {trend.signal_id}: {e}")
return None