ALwrity version 0.5.5
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
# 🏗️ Content Planning Services Modularity & Optimization Plan
|
||||
|
||||
## 📋 Executive Summary
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import json
|
||||
import re
|
||||
|
||||
# Import AI providers
|
||||
from llm_providers.main_text_generation import llm_text_gen
|
||||
from llm_providers.gemini_provider import gemini_structured_json_response
|
||||
from services.llm_providers.main_text_generation import llm_text_gen
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response
|
||||
|
||||
class AIPromptOptimizer:
|
||||
"""Advanced AI prompt optimization and management service."""
|
||||
@@ -299,8 +299,19 @@ Format as structured JSON with detailed metrics and strategic recommendations.
|
||||
schema=self.schemas['strategic_content_gap_analysis']
|
||||
)
|
||||
|
||||
# Parse and return the AI response
|
||||
result = json.loads(response)
|
||||
# Handle response - gemini_structured_json_response returns dict directly
|
||||
if isinstance(response, dict):
|
||||
result = response
|
||||
elif isinstance(response, str):
|
||||
# If it's a string, try to parse as JSON
|
||||
try:
|
||||
result = json.loads(response)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse AI response as JSON: {e}")
|
||||
raise Exception(f"Invalid AI response format: {str(e)}")
|
||||
else:
|
||||
logger.error(f"Unexpected response type from AI service: {type(response)}")
|
||||
raise Exception(f"Unexpected response type from AI service: {type(response)}")
|
||||
logger.info("✅ Advanced strategic content gap analysis completed")
|
||||
return result
|
||||
|
||||
@@ -336,8 +347,19 @@ Format as structured JSON with detailed metrics and strategic recommendations.
|
||||
schema=self.schemas['market_position_analysis']
|
||||
)
|
||||
|
||||
# Parse and return the AI response
|
||||
result = json.loads(response)
|
||||
# Handle response - gemini_structured_json_response returns dict directly
|
||||
if isinstance(response, dict):
|
||||
result = response
|
||||
elif isinstance(response, str):
|
||||
# If it's a string, try to parse as JSON
|
||||
try:
|
||||
result = json.loads(response)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse AI response as JSON: {e}")
|
||||
raise Exception(f"Invalid AI response format: {str(e)}")
|
||||
else:
|
||||
logger.error(f"Unexpected response type from AI service: {type(response)}")
|
||||
raise Exception(f"Unexpected response type from AI service: {type(response)}")
|
||||
logger.info("✅ Advanced market position analysis completed")
|
||||
return result
|
||||
|
||||
@@ -373,8 +395,19 @@ Format as structured JSON with detailed metrics and strategic recommendations.
|
||||
schema=self.schemas['advanced_keyword_analysis']
|
||||
)
|
||||
|
||||
# Parse and return the AI response
|
||||
result = json.loads(response)
|
||||
# Handle response - gemini_structured_json_response returns dict directly
|
||||
if isinstance(response, dict):
|
||||
result = response
|
||||
elif isinstance(response, str):
|
||||
# If it's a string, try to parse as JSON
|
||||
try:
|
||||
result = json.loads(response)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse AI response as JSON: {e}")
|
||||
raise Exception(f"Invalid AI response format: {str(e)}")
|
||||
else:
|
||||
logger.error(f"Unexpected response type from AI service: {type(response)}")
|
||||
raise Exception(f"Unexpected response type from AI service: {type(response)}")
|
||||
logger.info("✅ Advanced keyword analysis completed")
|
||||
return result
|
||||
|
||||
|
||||
611
backend/services/ai_quality_analysis_service.py
Normal file
611
backend/services/ai_quality_analysis_service.py
Normal file
@@ -0,0 +1,611 @@
|
||||
"""
|
||||
AI Quality Analysis Service
|
||||
Provides AI-powered quality assessment and recommendations for content strategies.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response
|
||||
from services.strategy_service import StrategyService
|
||||
from models.enhanced_strategy_models import EnhancedContentStrategy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class QualityScore(Enum):
|
||||
EXCELLENT = "excellent"
|
||||
GOOD = "good"
|
||||
NEEDS_ATTENTION = "needs_attention"
|
||||
POOR = "poor"
|
||||
|
||||
@dataclass
|
||||
class QualityMetric:
|
||||
name: str
|
||||
score: float # 0-100
|
||||
weight: float # 0-1
|
||||
status: QualityScore
|
||||
description: str
|
||||
recommendations: List[str]
|
||||
|
||||
@dataclass
|
||||
class QualityAnalysisResult:
|
||||
overall_score: float
|
||||
overall_status: QualityScore
|
||||
metrics: List[QualityMetric]
|
||||
recommendations: List[str]
|
||||
confidence_score: float
|
||||
analysis_timestamp: datetime
|
||||
strategy_id: int
|
||||
|
||||
# Structured JSON schemas for Gemini API
|
||||
QUALITY_ANALYSIS_SCHEMA = {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"score": {"type": "NUMBER"},
|
||||
"status": {"type": "STRING"},
|
||||
"description": {"type": "STRING"},
|
||||
"recommendations": {
|
||||
"type": "ARRAY",
|
||||
"items": {"type": "STRING"}
|
||||
}
|
||||
},
|
||||
"propertyOrdering": ["score", "status", "description", "recommendations"]
|
||||
}
|
||||
|
||||
RECOMMENDATIONS_SCHEMA = {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"recommendations": {
|
||||
"type": "ARRAY",
|
||||
"items": {"type": "STRING"}
|
||||
},
|
||||
"priority_areas": {
|
||||
"type": "ARRAY",
|
||||
"items": {"type": "STRING"}
|
||||
}
|
||||
},
|
||||
"propertyOrdering": ["recommendations", "priority_areas"]
|
||||
}
|
||||
|
||||
class AIQualityAnalysisService:
|
||||
"""AI-powered quality assessment service for content strategies."""
|
||||
|
||||
def __init__(self):
|
||||
self.strategy_service = StrategyService()
|
||||
|
||||
async def analyze_strategy_quality(self, strategy_id: int) -> QualityAnalysisResult:
|
||||
"""Analyze strategy quality using AI and return comprehensive results."""
|
||||
try:
|
||||
logger.info(f"Starting AI quality analysis for strategy {strategy_id}")
|
||||
|
||||
# Get strategy data
|
||||
strategy_data = await self.strategy_service.get_strategy_by_id(strategy_id)
|
||||
if not strategy_data:
|
||||
raise ValueError(f"Strategy {strategy_id} not found")
|
||||
|
||||
# Perform comprehensive quality analysis
|
||||
quality_metrics = await self._analyze_quality_metrics(strategy_data)
|
||||
|
||||
# Calculate overall score
|
||||
overall_score = self._calculate_overall_score(quality_metrics)
|
||||
overall_status = self._determine_overall_status(overall_score)
|
||||
|
||||
# Generate AI recommendations
|
||||
recommendations = await self._generate_ai_recommendations(strategy_data, quality_metrics)
|
||||
|
||||
# Calculate confidence score
|
||||
confidence_score = self._calculate_confidence_score(quality_metrics)
|
||||
|
||||
result = QualityAnalysisResult(
|
||||
overall_score=overall_score,
|
||||
overall_status=overall_status,
|
||||
metrics=quality_metrics,
|
||||
recommendations=recommendations,
|
||||
confidence_score=confidence_score,
|
||||
analysis_timestamp=datetime.utcnow(),
|
||||
strategy_id=strategy_id
|
||||
)
|
||||
|
||||
# Save analysis result to database
|
||||
await self._save_quality_analysis(result)
|
||||
|
||||
logger.info(f"Quality analysis completed for strategy {strategy_id}. Score: {overall_score}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing strategy quality for {strategy_id}: {e}")
|
||||
raise
|
||||
|
||||
async def _analyze_quality_metrics(self, strategy_data: Dict[str, Any]) -> List[QualityMetric]:
|
||||
"""Analyze individual quality metrics for a strategy."""
|
||||
metrics = []
|
||||
|
||||
# 1. Strategic Completeness Analysis
|
||||
completeness_metric = await self._analyze_strategic_completeness(strategy_data)
|
||||
metrics.append(completeness_metric)
|
||||
|
||||
# 2. Audience Intelligence Quality
|
||||
audience_metric = await self._analyze_audience_intelligence(strategy_data)
|
||||
metrics.append(audience_metric)
|
||||
|
||||
# 3. Competitive Intelligence Quality
|
||||
competitive_metric = await self._analyze_competitive_intelligence(strategy_data)
|
||||
metrics.append(competitive_metric)
|
||||
|
||||
# 4. Content Strategy Quality
|
||||
content_metric = await self._analyze_content_strategy(strategy_data)
|
||||
metrics.append(content_metric)
|
||||
|
||||
# 5. Performance Alignment Quality
|
||||
performance_metric = await self._analyze_performance_alignment(strategy_data)
|
||||
metrics.append(performance_metric)
|
||||
|
||||
# 6. Implementation Feasibility
|
||||
feasibility_metric = await self._analyze_implementation_feasibility(strategy_data)
|
||||
metrics.append(feasibility_metric)
|
||||
|
||||
return metrics
|
||||
|
||||
async def _analyze_strategic_completeness(self, strategy_data: Dict[str, Any]) -> QualityMetric:
|
||||
"""Analyze strategic completeness and depth."""
|
||||
try:
|
||||
# Check required fields
|
||||
required_fields = [
|
||||
'business_objectives', 'target_metrics', 'content_budget',
|
||||
'team_size', 'implementation_timeline', 'market_share'
|
||||
]
|
||||
|
||||
filled_fields = sum(1 for field in required_fields if strategy_data.get(field))
|
||||
completeness_score = (filled_fields / len(required_fields)) * 100
|
||||
|
||||
# AI analysis of strategic depth
|
||||
prompt = f"""
|
||||
Analyze the strategic completeness of this content strategy:
|
||||
|
||||
Business Objectives: {strategy_data.get('business_objectives', 'Not provided')}
|
||||
Target Metrics: {strategy_data.get('target_metrics', 'Not provided')}
|
||||
Content Budget: {strategy_data.get('content_budget', 'Not provided')}
|
||||
Team Size: {strategy_data.get('team_size', 'Not provided')}
|
||||
Implementation Timeline: {strategy_data.get('implementation_timeline', 'Not provided')}
|
||||
Market Share: {strategy_data.get('market_share', 'Not provided')}
|
||||
|
||||
Provide a quality score (0-100), status (excellent/good/needs_attention/poor), description, and specific recommendations for improvement.
|
||||
Focus on strategic depth, clarity, and measurability.
|
||||
"""
|
||||
|
||||
ai_response = await gemini_structured_json_response(
|
||||
prompt=prompt,
|
||||
schema=QUALITY_ANALYSIS_SCHEMA,
|
||||
temperature=0.3,
|
||||
max_tokens=2048
|
||||
)
|
||||
|
||||
if "error" in ai_response:
|
||||
raise ValueError(f"AI analysis failed: {ai_response['error']}")
|
||||
|
||||
# Parse AI response
|
||||
ai_score = ai_response.get('score', 60.0)
|
||||
ai_status = ai_response.get('status', 'needs_attention')
|
||||
description = ai_response.get('description', 'Strategic completeness analysis')
|
||||
recommendations = ai_response.get('recommendations', [])
|
||||
|
||||
# Combine manual and AI scores
|
||||
final_score = (completeness_score * 0.4) + (ai_score * 0.6)
|
||||
|
||||
return QualityMetric(
|
||||
name="Strategic Completeness",
|
||||
score=final_score,
|
||||
weight=0.25,
|
||||
status=self._parse_status(ai_status),
|
||||
description=description,
|
||||
recommendations=recommendations
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing strategic completeness: {e}")
|
||||
raise ValueError(f"Failed to analyze strategic completeness: {str(e)}")
|
||||
|
||||
async def _analyze_audience_intelligence(self, strategy_data: Dict[str, Any]) -> QualityMetric:
|
||||
"""Analyze audience intelligence quality."""
|
||||
try:
|
||||
audience_fields = [
|
||||
'content_preferences', 'consumption_patterns', 'audience_pain_points',
|
||||
'buying_journey', 'seasonal_trends', 'engagement_metrics'
|
||||
]
|
||||
|
||||
filled_fields = sum(1 for field in audience_fields if strategy_data.get(field))
|
||||
completeness_score = (filled_fields / len(audience_fields)) * 100
|
||||
|
||||
# AI analysis of audience insights
|
||||
prompt = f"""
|
||||
Analyze the audience intelligence quality of this content strategy:
|
||||
|
||||
Content Preferences: {strategy_data.get('content_preferences', 'Not provided')}
|
||||
Consumption Patterns: {strategy_data.get('consumption_patterns', 'Not provided')}
|
||||
Audience Pain Points: {strategy_data.get('audience_pain_points', 'Not provided')}
|
||||
Buying Journey: {strategy_data.get('buying_journey', 'Not provided')}
|
||||
Seasonal Trends: {strategy_data.get('seasonal_trends', 'Not provided')}
|
||||
Engagement Metrics: {strategy_data.get('engagement_metrics', 'Not provided')}
|
||||
|
||||
Provide a quality score (0-100), status (excellent/good/needs_attention/poor), description, and specific recommendations for improvement.
|
||||
Focus on audience understanding, segmentation, and actionable insights.
|
||||
"""
|
||||
|
||||
ai_response = await gemini_structured_json_response(
|
||||
prompt=prompt,
|
||||
schema=QUALITY_ANALYSIS_SCHEMA,
|
||||
temperature=0.3,
|
||||
max_tokens=2048
|
||||
)
|
||||
|
||||
if "error" in ai_response:
|
||||
raise ValueError(f"AI analysis failed: {ai_response['error']}")
|
||||
|
||||
ai_score = ai_response.get('score', 60.0)
|
||||
ai_status = ai_response.get('status', 'needs_attention')
|
||||
description = ai_response.get('description', 'Audience intelligence analysis')
|
||||
recommendations = ai_response.get('recommendations', [])
|
||||
|
||||
final_score = (completeness_score * 0.3) + (ai_score * 0.7)
|
||||
|
||||
return QualityMetric(
|
||||
name="Audience Intelligence",
|
||||
score=final_score,
|
||||
weight=0.20,
|
||||
status=self._parse_status(ai_status),
|
||||
description=description,
|
||||
recommendations=recommendations
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing audience intelligence: {e}")
|
||||
raise ValueError(f"Failed to analyze audience intelligence: {str(e)}")
|
||||
|
||||
async def _analyze_competitive_intelligence(self, strategy_data: Dict[str, Any]) -> QualityMetric:
|
||||
"""Analyze competitive intelligence quality."""
|
||||
try:
|
||||
competitive_fields = [
|
||||
'top_competitors', 'competitor_content_strategies', 'market_gaps',
|
||||
'industry_trends', 'emerging_trends'
|
||||
]
|
||||
|
||||
filled_fields = sum(1 for field in competitive_fields if strategy_data.get(field))
|
||||
completeness_score = (filled_fields / len(competitive_fields)) * 100
|
||||
|
||||
# AI analysis of competitive insights
|
||||
prompt = f"""
|
||||
Analyze the competitive intelligence quality of this content strategy:
|
||||
|
||||
Top Competitors: {strategy_data.get('top_competitors', 'Not provided')}
|
||||
Competitor Content Strategies: {strategy_data.get('competitor_content_strategies', 'Not provided')}
|
||||
Market Gaps: {strategy_data.get('market_gaps', 'Not provided')}
|
||||
Industry Trends: {strategy_data.get('industry_trends', 'Not provided')}
|
||||
Emerging Trends: {strategy_data.get('emerging_trends', 'Not provided')}
|
||||
|
||||
Provide a quality score (0-100), status (excellent/good/needs_attention/poor), description, and specific recommendations for improvement.
|
||||
Focus on competitive positioning, differentiation opportunities, and market insights.
|
||||
"""
|
||||
|
||||
ai_response = await gemini_structured_json_response(
|
||||
prompt=prompt,
|
||||
schema=QUALITY_ANALYSIS_SCHEMA,
|
||||
temperature=0.3,
|
||||
max_tokens=2048
|
||||
)
|
||||
|
||||
if "error" in ai_response:
|
||||
raise ValueError(f"AI analysis failed: {ai_response['error']}")
|
||||
|
||||
ai_score = ai_response.get('score', 60.0)
|
||||
ai_status = ai_response.get('status', 'needs_attention')
|
||||
description = ai_response.get('description', 'Competitive intelligence analysis')
|
||||
recommendations = ai_response.get('recommendations', [])
|
||||
|
||||
final_score = (completeness_score * 0.3) + (ai_score * 0.7)
|
||||
|
||||
return QualityMetric(
|
||||
name="Competitive Intelligence",
|
||||
score=final_score,
|
||||
weight=0.15,
|
||||
status=self._parse_status(ai_status),
|
||||
description=description,
|
||||
recommendations=recommendations
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing competitive intelligence: {e}")
|
||||
raise ValueError(f"Failed to analyze competitive intelligence: {str(e)}")
|
||||
|
||||
async def _analyze_content_strategy(self, strategy_data: Dict[str, Any]) -> QualityMetric:
|
||||
"""Analyze content strategy quality."""
|
||||
try:
|
||||
content_fields = [
|
||||
'preferred_formats', 'content_mix', 'content_frequency',
|
||||
'optimal_timing', 'quality_metrics', 'editorial_guidelines', 'brand_voice'
|
||||
]
|
||||
|
||||
filled_fields = sum(1 for field in content_fields if strategy_data.get(field))
|
||||
completeness_score = (filled_fields / len(content_fields)) * 100
|
||||
|
||||
# AI analysis of content strategy
|
||||
prompt = f"""
|
||||
Analyze the content strategy quality:
|
||||
|
||||
Preferred Formats: {strategy_data.get('preferred_formats', 'Not provided')}
|
||||
Content Mix: {strategy_data.get('content_mix', 'Not provided')}
|
||||
Content Frequency: {strategy_data.get('content_frequency', 'Not provided')}
|
||||
Optimal Timing: {strategy_data.get('optimal_timing', 'Not provided')}
|
||||
Quality Metrics: {strategy_data.get('quality_metrics', 'Not provided')}
|
||||
Editorial Guidelines: {strategy_data.get('editorial_guidelines', 'Not provided')}
|
||||
Brand Voice: {strategy_data.get('brand_voice', 'Not provided')}
|
||||
|
||||
Provide a quality score (0-100), status (excellent/good/needs_attention/poor), description, and specific recommendations for improvement.
|
||||
Focus on content planning, execution strategy, and quality standards.
|
||||
"""
|
||||
|
||||
ai_response = await gemini_structured_json_response(
|
||||
prompt=prompt,
|
||||
schema=QUALITY_ANALYSIS_SCHEMA,
|
||||
temperature=0.3,
|
||||
max_tokens=2048
|
||||
)
|
||||
|
||||
if "error" in ai_response:
|
||||
raise ValueError(f"AI analysis failed: {ai_response['error']}")
|
||||
|
||||
ai_score = ai_response.get('score', 60.0)
|
||||
ai_status = ai_response.get('status', 'needs_attention')
|
||||
description = ai_response.get('description', 'Content strategy analysis')
|
||||
recommendations = ai_response.get('recommendations', [])
|
||||
|
||||
final_score = (completeness_score * 0.3) + (ai_score * 0.7)
|
||||
|
||||
return QualityMetric(
|
||||
name="Content Strategy",
|
||||
score=final_score,
|
||||
weight=0.20,
|
||||
status=self._parse_status(ai_status),
|
||||
description=description,
|
||||
recommendations=recommendations
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing content strategy: {e}")
|
||||
raise ValueError(f"Failed to analyze content strategy: {str(e)}")
|
||||
|
||||
async def _analyze_performance_alignment(self, strategy_data: Dict[str, Any]) -> QualityMetric:
|
||||
"""Analyze performance alignment quality."""
|
||||
try:
|
||||
performance_fields = [
|
||||
'traffic_sources', 'conversion_rates', 'content_roi_targets',
|
||||
'ab_testing_capabilities'
|
||||
]
|
||||
|
||||
filled_fields = sum(1 for field in performance_fields if strategy_data.get(field))
|
||||
completeness_score = (filled_fields / len(performance_fields)) * 100
|
||||
|
||||
# AI analysis of performance alignment
|
||||
prompt = f"""
|
||||
Analyze the performance alignment quality:
|
||||
|
||||
Traffic Sources: {strategy_data.get('traffic_sources', 'Not provided')}
|
||||
Conversion Rates: {strategy_data.get('conversion_rates', 'Not provided')}
|
||||
Content ROI Targets: {strategy_data.get('content_roi_targets', 'Not provided')}
|
||||
A/B Testing Capabilities: {strategy_data.get('ab_testing_capabilities', 'Not provided')}
|
||||
|
||||
Provide a quality score (0-100), status (excellent/good/needs_attention/poor), description, and specific recommendations for improvement.
|
||||
Focus on performance measurement, optimization, and ROI alignment.
|
||||
"""
|
||||
|
||||
ai_response = await gemini_structured_json_response(
|
||||
prompt=prompt,
|
||||
schema=QUALITY_ANALYSIS_SCHEMA,
|
||||
temperature=0.3,
|
||||
max_tokens=2048
|
||||
)
|
||||
|
||||
if "error" in ai_response:
|
||||
raise ValueError(f"AI analysis failed: {ai_response['error']}")
|
||||
|
||||
ai_score = ai_response.get('score', 60.0)
|
||||
ai_status = ai_response.get('status', 'needs_attention')
|
||||
description = ai_response.get('description', 'Performance alignment analysis')
|
||||
recommendations = ai_response.get('recommendations', [])
|
||||
|
||||
final_score = (completeness_score * 0.3) + (ai_score * 0.7)
|
||||
|
||||
return QualityMetric(
|
||||
name="Performance Alignment",
|
||||
score=final_score,
|
||||
weight=0.15,
|
||||
status=self._parse_status(ai_status),
|
||||
description=description,
|
||||
recommendations=recommendations
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing performance alignment: {e}")
|
||||
raise ValueError(f"Failed to analyze performance alignment: {str(e)}")
|
||||
|
||||
async def _analyze_implementation_feasibility(self, strategy_data: Dict[str, Any]) -> QualityMetric:
|
||||
"""Analyze implementation feasibility."""
|
||||
try:
|
||||
# Check resource availability
|
||||
has_budget = bool(strategy_data.get('content_budget'))
|
||||
has_team = bool(strategy_data.get('team_size'))
|
||||
has_timeline = bool(strategy_data.get('implementation_timeline'))
|
||||
|
||||
resource_score = ((has_budget + has_team + has_timeline) / 3) * 100
|
||||
|
||||
# AI analysis of feasibility
|
||||
prompt = f"""
|
||||
Analyze the implementation feasibility of this content strategy:
|
||||
|
||||
Content Budget: {strategy_data.get('content_budget', 'Not provided')}
|
||||
Team Size: {strategy_data.get('team_size', 'Not provided')}
|
||||
Implementation Timeline: {strategy_data.get('implementation_timeline', 'Not provided')}
|
||||
Industry: {strategy_data.get('industry', 'Not provided')}
|
||||
Market Share: {strategy_data.get('market_share', 'Not provided')}
|
||||
|
||||
Provide a quality score (0-100), status (excellent/good/needs_attention/poor), description, and specific recommendations for improvement.
|
||||
Focus on resource availability, timeline feasibility, and implementation challenges.
|
||||
"""
|
||||
|
||||
ai_response = await gemini_structured_json_response(
|
||||
prompt=prompt,
|
||||
schema=QUALITY_ANALYSIS_SCHEMA,
|
||||
temperature=0.3,
|
||||
max_tokens=2048
|
||||
)
|
||||
|
||||
if "error" in ai_response:
|
||||
raise ValueError(f"AI analysis failed: {ai_response['error']}")
|
||||
|
||||
ai_score = ai_response.get('score', 60.0)
|
||||
ai_status = ai_response.get('status', 'needs_attention')
|
||||
description = ai_response.get('description', 'Implementation feasibility analysis')
|
||||
recommendations = ai_response.get('recommendations', [])
|
||||
|
||||
final_score = (resource_score * 0.4) + (ai_score * 0.6)
|
||||
|
||||
return QualityMetric(
|
||||
name="Implementation Feasibility",
|
||||
score=final_score,
|
||||
weight=0.05,
|
||||
status=self._parse_status(ai_status),
|
||||
description=description,
|
||||
recommendations=recommendations
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing implementation feasibility: {e}")
|
||||
raise ValueError(f"Failed to analyze implementation feasibility: {str(e)}")
|
||||
|
||||
def _calculate_overall_score(self, metrics: List[QualityMetric]) -> float:
|
||||
"""Calculate weighted overall quality score."""
|
||||
if not metrics:
|
||||
return 0.0
|
||||
|
||||
weighted_sum = sum(metric.score * metric.weight for metric in metrics)
|
||||
total_weight = sum(metric.weight for metric in metrics)
|
||||
|
||||
return weighted_sum / total_weight if total_weight > 0 else 0.0
|
||||
|
||||
def _determine_overall_status(self, score: float) -> QualityScore:
|
||||
"""Determine overall quality status based on score."""
|
||||
if score >= 85:
|
||||
return QualityScore.EXCELLENT
|
||||
elif score >= 70:
|
||||
return QualityScore.GOOD
|
||||
elif score >= 50:
|
||||
return QualityScore.NEEDS_ATTENTION
|
||||
else:
|
||||
return QualityScore.POOR
|
||||
|
||||
def _parse_status(self, status_str: str) -> QualityScore:
|
||||
"""Parse status string to QualityScore enum."""
|
||||
status_lower = status_str.lower()
|
||||
if status_lower == 'excellent':
|
||||
return QualityScore.EXCELLENT
|
||||
elif status_lower == 'good':
|
||||
return QualityScore.GOOD
|
||||
elif status_lower == 'needs_attention':
|
||||
return QualityScore.NEEDS_ATTENTION
|
||||
elif status_lower == 'poor':
|
||||
return QualityScore.POOR
|
||||
else:
|
||||
return QualityScore.NEEDS_ATTENTION
|
||||
|
||||
async def _generate_ai_recommendations(self, strategy_data: Dict[str, Any], metrics: List[QualityMetric]) -> List[str]:
|
||||
"""Generate AI-powered recommendations for strategy improvement."""
|
||||
try:
|
||||
# Identify areas needing improvement
|
||||
low_metrics = [m for m in metrics if m.status in [QualityScore.NEEDS_ATTENTION, QualityScore.POOR]]
|
||||
|
||||
if not low_metrics:
|
||||
return ["Strategy quality is excellent. Continue monitoring and optimizing based on performance data."]
|
||||
|
||||
# Generate specific recommendations
|
||||
prompt = f"""
|
||||
Based on the quality analysis of this content strategy, provide 3-5 specific, actionable recommendations for improvement.
|
||||
|
||||
Strategy Overview:
|
||||
- Industry: {strategy_data.get('industry', 'Not specified')}
|
||||
- Business Objectives: {strategy_data.get('business_objectives', 'Not specified')}
|
||||
|
||||
Areas needing improvement:
|
||||
{chr(10).join([f"- {m.name}: {m.score:.1f}/100" for m in low_metrics])}
|
||||
|
||||
Provide specific, actionable recommendations that can be implemented immediately.
|
||||
Focus on the most impactful improvements first.
|
||||
"""
|
||||
|
||||
ai_response = await gemini_structured_json_response(
|
||||
prompt=prompt,
|
||||
schema=RECOMMENDATIONS_SCHEMA,
|
||||
temperature=0.3,
|
||||
max_tokens=2048
|
||||
)
|
||||
|
||||
if "error" in ai_response:
|
||||
raise ValueError(f"AI recommendations failed: {ai_response['error']}")
|
||||
|
||||
recommendations = ai_response.get('recommendations', [])
|
||||
return recommendations[:5] # Limit to 5 recommendations
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating AI recommendations: {e}")
|
||||
raise ValueError(f"Failed to generate AI recommendations: {str(e)}")
|
||||
|
||||
def _calculate_confidence_score(self, metrics: List[QualityMetric]) -> float:
|
||||
"""Calculate confidence score based on data quality and analysis depth."""
|
||||
if not metrics:
|
||||
return 0.0
|
||||
|
||||
# Higher scores indicate more confidence
|
||||
avg_score = sum(m.score for m in metrics) / len(metrics)
|
||||
|
||||
# More metrics analyzed = higher confidence
|
||||
metric_count_factor = min(len(metrics) / 6, 1.0) # 6 is max expected metrics
|
||||
|
||||
confidence = (avg_score * 0.7) + (metric_count_factor * 100 * 0.3)
|
||||
return min(confidence, 100.0)
|
||||
|
||||
async def _save_quality_analysis(self, result: QualityAnalysisResult) -> bool:
|
||||
"""Save quality analysis result to database."""
|
||||
try:
|
||||
# This would save to a quality_analysis_results table
|
||||
# For now, we'll log the result
|
||||
logger.info(f"Quality analysis saved for strategy {result.strategy_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving quality analysis: {e}")
|
||||
return False
|
||||
|
||||
async def get_quality_history(self, strategy_id: int, days: int = 30) -> List[QualityAnalysisResult]:
|
||||
"""Get quality analysis history for a strategy."""
|
||||
try:
|
||||
# This would query the quality_analysis_results table
|
||||
# For now, return empty list
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting quality history: {e}")
|
||||
return []
|
||||
|
||||
async def get_quality_trends(self, strategy_id: int) -> Dict[str, Any]:
|
||||
"""Get quality trends over time."""
|
||||
try:
|
||||
# This would analyze quality trends over time
|
||||
# For now, return empty data
|
||||
return {
|
||||
"trend": "stable",
|
||||
"change_rate": 0,
|
||||
"consistency_score": 0
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting quality trends: {e}")
|
||||
return {"trend": "stable", "change_rate": 0, "consistency_score": 0}
|
||||
@@ -12,13 +12,13 @@ from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
# Import AI providers
|
||||
from llm_providers.main_text_generation import llm_text_gen
|
||||
from services.llm_providers.main_text_generation import llm_text_gen
|
||||
# Prefer the extended gemini provider if available; fallback to base
|
||||
try:
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response as _gemini_fn
|
||||
_GEMINI_EXTENDED = True
|
||||
except Exception:
|
||||
from llm_providers.gemini_provider import gemini_structured_json_response as _gemini_fn
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response as _gemini_fn
|
||||
_GEMINI_EXTENDED = False
|
||||
|
||||
class AIServiceType(Enum):
|
||||
|
||||
@@ -12,8 +12,8 @@ import json
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
# Import AI providers
|
||||
from llm_providers.main_text_generation import llm_text_gen
|
||||
from llm_providers.gemini_provider import gemini_structured_json_response
|
||||
from services.llm_providers.main_text_generation import llm_text_gen
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response
|
||||
|
||||
# Import services
|
||||
from services.ai_service_manager import AIServiceManager
|
||||
@@ -213,8 +213,19 @@ class AIEngineService:
|
||||
}
|
||||
)
|
||||
|
||||
# Parse and return the AI response
|
||||
result = json.loads(response)
|
||||
# Handle response - gemini_structured_json_response returns dict directly
|
||||
if isinstance(response, dict):
|
||||
result = response
|
||||
elif isinstance(response, str):
|
||||
# If it's a string, try to parse as JSON
|
||||
try:
|
||||
result = json.loads(response)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse AI response as JSON: {e}")
|
||||
raise Exception(f"Invalid AI response format: {str(e)}")
|
||||
else:
|
||||
logger.error(f"Unexpected response type from AI service: {type(response)}")
|
||||
raise Exception(f"Unexpected response type from AI service: {type(response)}")
|
||||
recommendations = result.get('recommendations', [])
|
||||
logger.info(f"✅ Generated {len(recommendations)} AI content recommendations")
|
||||
return recommendations
|
||||
@@ -355,8 +366,19 @@ class AIEngineService:
|
||||
}
|
||||
)
|
||||
|
||||
# Parse and return the AI response
|
||||
predictions = json.loads(response)
|
||||
# Handle response - gemini_structured_json_response returns dict directly
|
||||
if isinstance(response, dict):
|
||||
predictions = response
|
||||
elif isinstance(response, str):
|
||||
# If it's a string, try to parse as JSON
|
||||
try:
|
||||
predictions = json.loads(response)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse AI response as JSON: {e}")
|
||||
raise Exception(f"Invalid AI response format: {str(e)}")
|
||||
else:
|
||||
logger.error(f"Unexpected response type from AI service: {type(response)}")
|
||||
raise Exception(f"Unexpected response type from AI service: {type(response)}")
|
||||
logger.info("✅ AI performance predictions completed")
|
||||
return predictions
|
||||
|
||||
@@ -495,7 +517,19 @@ class AIEngineService:
|
||||
)
|
||||
|
||||
# Parse and return the AI response
|
||||
competitive_intelligence = json.loads(response)
|
||||
# Handle response - gemini_structured_json_response returns dict directly
|
||||
if isinstance(response, dict):
|
||||
competitive_intelligence = response
|
||||
elif isinstance(response, str):
|
||||
# If it's a string, try to parse as JSON
|
||||
try:
|
||||
competitive_intelligence = json.loads(response)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse AI response as JSON: {e}")
|
||||
raise Exception(f"Invalid AI response format: {str(e)}")
|
||||
else:
|
||||
logger.error(f"Unexpected response type from AI service: {type(response)}")
|
||||
raise Exception(f"Unexpected response type from AI service: {type(response)}")
|
||||
logger.info("✅ AI competitive intelligence completed")
|
||||
return competitive_intelligence
|
||||
|
||||
@@ -633,8 +667,20 @@ class AIEngineService:
|
||||
}
|
||||
)
|
||||
|
||||
# Parse and return the AI response
|
||||
result = json.loads(response)
|
||||
# Handle response - gemini_structured_json_response returns dict directly
|
||||
if isinstance(response, dict):
|
||||
result = response
|
||||
elif isinstance(response, str):
|
||||
# If it's a string, try to parse as JSON
|
||||
try:
|
||||
result = json.loads(response)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse AI response as JSON: {e}")
|
||||
raise Exception(f"Invalid AI response format: {str(e)}")
|
||||
else:
|
||||
logger.error(f"Unexpected response type from AI service: {type(response)}")
|
||||
raise Exception(f"Unexpected response type from AI service: {type(response)}")
|
||||
|
||||
strategic_insights = result.get('strategic_insights', [])
|
||||
logger.info(f"✅ Generated {len(strategic_insights)} AI strategic insights")
|
||||
return strategic_insights
|
||||
@@ -733,8 +779,19 @@ class AIEngineService:
|
||||
}
|
||||
)
|
||||
|
||||
# Parse and return the AI response
|
||||
quality_analysis = json.loads(response)
|
||||
# Handle response - gemini_structured_json_response returns dict directly
|
||||
if isinstance(response, dict):
|
||||
quality_analysis = response
|
||||
elif isinstance(response, str):
|
||||
# If it's a string, try to parse as JSON
|
||||
try:
|
||||
quality_analysis = json.loads(response)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse AI response as JSON: {e}")
|
||||
raise Exception(f"Invalid AI response format: {str(e)}")
|
||||
else:
|
||||
logger.error(f"Unexpected response type from AI service: {type(response)}")
|
||||
raise Exception(f"Unexpected response type from AI service: {type(response)}")
|
||||
logger.info("✅ AI content quality analysis completed")
|
||||
return quality_analysis
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ import json
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
# Import AI providers
|
||||
from llm_providers.main_text_generation import llm_text_gen
|
||||
from llm_providers.gemini_provider import gemini_structured_json_response
|
||||
from services.llm_providers.main_text_generation import llm_text_gen
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response
|
||||
|
||||
# Import existing modules (will be updated to use FastAPI services)
|
||||
from services.database import get_db_session
|
||||
@@ -194,8 +194,19 @@ class CompetitorAnalyzer:
|
||||
}
|
||||
)
|
||||
|
||||
# Parse and return the AI response
|
||||
market_position = json.loads(response)
|
||||
# Handle response - gemini_structured_json_response returns dict directly
|
||||
if isinstance(response, dict):
|
||||
market_position = response
|
||||
elif isinstance(response, str):
|
||||
# If it's a string, try to parse as JSON
|
||||
try:
|
||||
market_position = json.loads(response)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse AI response as JSON: {e}")
|
||||
raise Exception(f"Invalid AI response format: {str(e)}")
|
||||
else:
|
||||
logger.error(f"Unexpected response type from AI service: {type(response)}")
|
||||
raise Exception(f"Unexpected response type from AI service: {type(response)}")
|
||||
logger.info("✅ AI market position analysis completed")
|
||||
return market_position
|
||||
|
||||
@@ -306,8 +317,20 @@ class CompetitorAnalyzer:
|
||||
}
|
||||
)
|
||||
|
||||
# Parse and return the AI response
|
||||
result = json.loads(response)
|
||||
# Handle response - gemini_structured_json_response returns dict directly
|
||||
if isinstance(response, dict):
|
||||
result = response
|
||||
elif isinstance(response, str):
|
||||
# If it's a string, try to parse as JSON
|
||||
try:
|
||||
result = json.loads(response)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse AI response as JSON: {e}")
|
||||
raise Exception(f"Invalid AI response format: {str(e)}")
|
||||
else:
|
||||
logger.error(f"Unexpected response type from AI service: {type(response)}")
|
||||
raise Exception(f"Unexpected response type from AI service: {type(response)}")
|
||||
|
||||
content_gaps = result.get('content_gaps', [])
|
||||
logger.info(f"✅ AI content gap identification completed: {len(content_gaps)} gaps found")
|
||||
return content_gaps
|
||||
@@ -399,8 +422,20 @@ class CompetitorAnalyzer:
|
||||
}
|
||||
)
|
||||
|
||||
# Parse and return the AI response
|
||||
result = json.loads(response)
|
||||
# Handle response - gemini_structured_json_response returns dict directly
|
||||
if isinstance(response, dict):
|
||||
result = response
|
||||
elif isinstance(response, str):
|
||||
# If it's a string, try to parse as JSON
|
||||
try:
|
||||
result = json.loads(response)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse AI response as JSON: {e}")
|
||||
raise Exception(f"Invalid AI response format: {str(e)}")
|
||||
else:
|
||||
logger.error(f"Unexpected response type from AI service: {type(response)}")
|
||||
raise Exception(f"Unexpected response type from AI service: {type(response)}")
|
||||
|
||||
competitive_insights = result.get('competitive_insights', [])
|
||||
logger.info(f"✅ AI competitive insights generated: {len(competitive_insights)} insights")
|
||||
return competitive_insights
|
||||
|
||||
@@ -12,8 +12,8 @@ import json
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
# Import AI providers
|
||||
from llm_providers.main_text_generation import llm_text_gen
|
||||
from llm_providers.gemini_provider import gemini_structured_json_response
|
||||
from services.llm_providers.main_text_generation import llm_text_gen
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response
|
||||
|
||||
# Import existing modules (will be updated to use FastAPI services)
|
||||
from services.database import get_db_session
|
||||
@@ -155,8 +155,19 @@ class KeywordResearcher:
|
||||
}
|
||||
)
|
||||
|
||||
# Parse and return the AI response
|
||||
trend_analysis = json.loads(response)
|
||||
# Handle response - gemini_structured_json_response returns dict directly
|
||||
if isinstance(response, dict):
|
||||
trend_analysis = response
|
||||
elif isinstance(response, str):
|
||||
# If it's a string, try to parse as JSON
|
||||
try:
|
||||
trend_analysis = json.loads(response)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse AI response as JSON: {e}")
|
||||
raise Exception(f"Invalid AI response format: {str(e)}")
|
||||
else:
|
||||
logger.error(f"Unexpected response type from AI service: {type(response)}")
|
||||
raise Exception(f"Unexpected response type from AI service: {type(response)}")
|
||||
logger.info("✅ AI keyword trend analysis completed")
|
||||
return trend_analysis
|
||||
|
||||
@@ -283,8 +294,20 @@ class KeywordResearcher:
|
||||
}
|
||||
)
|
||||
|
||||
# Parse and return the AI response
|
||||
intent_analysis = json.loads(response)
|
||||
# Handle response - gemini_structured_json_response returns dict directly
|
||||
if isinstance(response, dict):
|
||||
intent_analysis = response
|
||||
elif isinstance(response, str):
|
||||
# If it's a string, try to parse as JSON
|
||||
try:
|
||||
intent_analysis = json.loads(response)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse AI response as JSON: {e}")
|
||||
raise Exception(f"Invalid AI response format: {str(e)}")
|
||||
else:
|
||||
logger.error(f"Unexpected response type from AI service: {type(response)}")
|
||||
raise Exception(f"Unexpected response type from AI service: {type(response)}")
|
||||
|
||||
logger.info("✅ AI search intent analysis completed")
|
||||
return intent_analysis
|
||||
|
||||
@@ -396,8 +419,20 @@ class KeywordResearcher:
|
||||
}
|
||||
)
|
||||
|
||||
# Parse and return the AI response
|
||||
result = json.loads(response)
|
||||
# Handle response - gemini_structured_json_response returns dict directly
|
||||
if isinstance(response, dict):
|
||||
result = response
|
||||
elif isinstance(response, str):
|
||||
# If it's a string, try to parse as JSON
|
||||
try:
|
||||
result = json.loads(response)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse AI response as JSON: {e}")
|
||||
raise Exception(f"Invalid AI response format: {str(e)}")
|
||||
else:
|
||||
logger.error(f"Unexpected response type from AI service: {type(response)}")
|
||||
raise Exception(f"Unexpected response type from AI service: {type(response)}")
|
||||
|
||||
opportunities = result.get('opportunities', [])
|
||||
logger.info(f"✅ AI opportunity identification completed: {len(opportunities)} opportunities found")
|
||||
return opportunities
|
||||
|
||||
@@ -14,6 +14,9 @@ from typing import Optional
|
||||
from models.onboarding import Base as OnboardingBase
|
||||
from models.seo_analysis import Base as SEOAnalysisBase
|
||||
from models.content_planning import Base as ContentPlanningBase
|
||||
from models.enhanced_strategy_models import Base as EnhancedStrategyBase
|
||||
# Monitoring models now use the same base as enhanced strategy models
|
||||
from models.monitoring_models import Base as MonitoringBase
|
||||
|
||||
# Database configuration
|
||||
DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///./alwrity.db')
|
||||
@@ -52,6 +55,8 @@ def init_database():
|
||||
OnboardingBase.metadata.create_all(bind=engine)
|
||||
SEOAnalysisBase.metadata.create_all(bind=engine)
|
||||
ContentPlanningBase.metadata.create_all(bind=engine)
|
||||
EnhancedStrategyBase.metadata.create_all(bind=engine)
|
||||
MonitoringBase.metadata.create_all(bind=engine)
|
||||
logger.info("Database initialized successfully with all models")
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Error initializing database: {str(e)}")
|
||||
|
||||
306
backend/services/llm_providers/README.md
Normal file
306
backend/services/llm_providers/README.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# Gemini Provider Module
|
||||
|
||||
This module provides functions for interacting with Google's Gemini API, specifically designed for structured JSON output and text generation. It follows the official Gemini API documentation and implements best practices for reliable AI interactions.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Structured JSON Response Generation**: Generate structured outputs with schema validation
|
||||
- **Text Response Generation**: Simple text generation with retry logic
|
||||
- **Comprehensive Error Handling**: Robust error handling and logging
|
||||
- **Automatic API Key Management**: Secure API key handling
|
||||
- **Support for Multiple Models**: gemini-2.5-flash and gemini-2.5-pro
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Structured Output for Complex Responses
|
||||
```python
|
||||
# ✅ Good: Use structured output for multi-field responses
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tasks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string"},
|
||||
"description": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result = gemini_structured_json_response(prompt, schema, temperature=0.2, max_tokens=8192)
|
||||
```
|
||||
|
||||
### 2. Keep Schemas Simple and Flat
|
||||
```python
|
||||
# ✅ Good: Simple, flat schema
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"monitoringTasks": {
|
||||
"type": "array",
|
||||
"items": {"type": "object", "properties": {...}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ❌ Avoid: Complex nested schemas with many required fields
|
||||
schema = {
|
||||
"type": "object",
|
||||
"required": ["field1", "field2", "field3"],
|
||||
"properties": {
|
||||
"field1": {"type": "object", "required": [...], "properties": {...}},
|
||||
"field2": {"type": "array", "items": {"type": "object", "required": [...], "properties": {...}}}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Set Appropriate Token Limits
|
||||
```python
|
||||
# ✅ Good: Use 8192 tokens for complex outputs
|
||||
result = gemini_structured_json_response(prompt, schema, max_tokens=8192)
|
||||
|
||||
# ✅ Good: Use 2048 tokens for simple text responses
|
||||
result = gemini_text_response(prompt, max_tokens=2048)
|
||||
```
|
||||
|
||||
### 4. Use Low Temperature for Structured Output
|
||||
```python
|
||||
# ✅ Good: Low temperature for consistent structured output
|
||||
result = gemini_structured_json_response(prompt, schema, temperature=0.1, max_tokens=8192)
|
||||
|
||||
# ✅ Good: Higher temperature for creative text
|
||||
result = gemini_text_response(prompt, temperature=0.8, max_tokens=2048)
|
||||
```
|
||||
|
||||
### 5. Implement Proper Error Handling
|
||||
```python
|
||||
# ✅ Good: Handle errors in calling functions
|
||||
try:
|
||||
response = gemini_structured_json_response(prompt, schema)
|
||||
if isinstance(response, dict) and "error" in response:
|
||||
raise Exception(f"Gemini error: {response.get('error')}")
|
||||
# Process successful response
|
||||
except Exception as e:
|
||||
logger.error(f"AI service error: {e}")
|
||||
# Handle error appropriately
|
||||
```
|
||||
|
||||
### 6. Avoid Fallback to Text Parsing
|
||||
```python
|
||||
# ✅ Good: Use structured output only, no fallback
|
||||
response = gemini_structured_json_response(prompt, schema)
|
||||
if "error" in response:
|
||||
raise Exception(f"Gemini error: {response.get('error')}")
|
||||
|
||||
# ❌ Avoid: Fallback to text parsing for structured responses
|
||||
# This can lead to inconsistent results and parsing errors
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Structured JSON Response
|
||||
```python
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response
|
||||
|
||||
# Define schema
|
||||
monitoring_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"monitoringTasks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"component": {"type": "string"},
|
||||
"title": {"type": "string"},
|
||||
"description": {"type": "string"},
|
||||
"assignee": {"type": "string"},
|
||||
"frequency": {"type": "string"},
|
||||
"metric": {"type": "string"},
|
||||
"measurementMethod": {"type": "string"},
|
||||
"successCriteria": {"type": "string"},
|
||||
"alertThreshold": {"type": "string"},
|
||||
"actionableInsights": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Generate structured response
|
||||
prompt = "Generate a monitoring plan for content strategy..."
|
||||
result = gemini_structured_json_response(
|
||||
prompt=prompt,
|
||||
schema=monitoring_schema,
|
||||
temperature=0.1,
|
||||
max_tokens=8192
|
||||
)
|
||||
|
||||
# Handle response
|
||||
if isinstance(result, dict) and "error" in result:
|
||||
raise Exception(f"Gemini error: {result.get('error')}")
|
||||
|
||||
# Process successful response
|
||||
monitoring_tasks = result.get("monitoringTasks", [])
|
||||
```
|
||||
|
||||
### Text Response
|
||||
```python
|
||||
from services.llm_providers.gemini_provider import gemini_text_response
|
||||
|
||||
# Generate text response
|
||||
prompt = "Write a blog post about AI in content marketing..."
|
||||
result = gemini_text_response(
|
||||
prompt=prompt,
|
||||
temperature=0.8,
|
||||
max_tokens=2048
|
||||
)
|
||||
|
||||
# Process response
|
||||
if result:
|
||||
print(f"Generated text: {result}")
|
||||
else:
|
||||
print("No response generated")
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues and Solutions
|
||||
|
||||
#### 1. Response.parsed is None
|
||||
**Symptoms**: `response.parsed` returns `None` even with successful HTTP 200
|
||||
**Causes**:
|
||||
- Schema too complex for the model
|
||||
- Token limit too low
|
||||
- Temperature too high for structured output
|
||||
|
||||
**Solutions**:
|
||||
- Simplify schema structure
|
||||
- Increase `max_tokens` to 8192
|
||||
- Lower temperature to 0.1-0.3
|
||||
- Test with smaller outputs first
|
||||
|
||||
#### 2. JSON Parsing Fails
|
||||
**Symptoms**: `JSONDecodeError` or "Unterminated string" errors
|
||||
**Causes**:
|
||||
- Response truncated due to token limits
|
||||
- Schema doesn't match expected output
|
||||
- Model generates malformed JSON
|
||||
|
||||
**Solutions**:
|
||||
- Reduce output size requested
|
||||
- Verify schema matches expected structure
|
||||
- Use structured output instead of text parsing
|
||||
- Increase token limits
|
||||
|
||||
#### 3. Truncation Issues
|
||||
**Symptoms**: Response cuts off mid-sentence or mid-array
|
||||
**Causes**:
|
||||
- Output too large for single response
|
||||
- Token limits exceeded
|
||||
|
||||
**Solutions**:
|
||||
- Reduce number of items requested
|
||||
- Increase `max_tokens` to 8192
|
||||
- Break large requests into smaller chunks
|
||||
- Use `gemini-2.5-pro` for larger outputs
|
||||
|
||||
#### 4. Rate Limiting
|
||||
**Symptoms**: `RetryError` or connection timeouts
|
||||
**Causes**:
|
||||
- Too many requests in short time
|
||||
- Network connectivity issues
|
||||
|
||||
**Solutions**:
|
||||
- Exponential backoff already implemented
|
||||
- Check network connectivity
|
||||
- Reduce request frequency
|
||||
- Verify API key validity
|
||||
|
||||
### Debug Logging
|
||||
|
||||
The module includes comprehensive debug logging. Enable debug mode to see:
|
||||
|
||||
```python
|
||||
import logging
|
||||
logging.getLogger('services.llm_providers.gemini_provider').setLevel(logging.DEBUG)
|
||||
```
|
||||
|
||||
Key log messages to monitor:
|
||||
- `Gemini structured call | prompt_len=X | schema_kind=Y | temp=Z`
|
||||
- `Gemini response | type=X | has_text=Y | has_parsed=Z`
|
||||
- `Using response.parsed for structured output`
|
||||
- `Falling back to response.text parsing`
|
||||
|
||||
## API Reference
|
||||
|
||||
### gemini_structured_json_response()
|
||||
|
||||
Generate structured JSON response using Google's Gemini Pro model.
|
||||
|
||||
**Parameters**:
|
||||
- `prompt` (str): Input prompt for the AI model
|
||||
- `schema` (dict): JSON schema defining expected output structure
|
||||
- `temperature` (float): Controls randomness (0.0-1.0). Use 0.1-0.3 for structured output
|
||||
- `top_p` (float): Nucleus sampling parameter (0.0-1.0)
|
||||
- `top_k` (int): Top-k sampling parameter
|
||||
- `max_tokens` (int): Maximum tokens in response. Use 8192 for complex outputs
|
||||
- `system_prompt` (str, optional): System instruction for the model
|
||||
|
||||
**Returns**:
|
||||
- `dict`: Parsed JSON response matching the provided schema
|
||||
|
||||
**Raises**:
|
||||
- `Exception`: If API key is missing or API call fails
|
||||
|
||||
### gemini_text_response()
|
||||
|
||||
Generate text response using Google's Gemini Pro model.
|
||||
|
||||
**Parameters**:
|
||||
- `prompt` (str): Input prompt for the AI model
|
||||
- `temperature` (float): Controls randomness (0.0-1.0). Higher = more creative
|
||||
- `top_p` (float): Nucleus sampling parameter (0.0-1.0)
|
||||
- `n` (int): Number of responses to generate
|
||||
- `max_tokens` (int): Maximum tokens in response
|
||||
- `system_prompt` (str, optional): System instruction for the model
|
||||
|
||||
**Returns**:
|
||||
- `str`: Generated text response
|
||||
|
||||
**Raises**:
|
||||
- `Exception`: If API key is missing or API call fails
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `google.generativeai` (genai): Official Gemini API client
|
||||
- `tenacity`: Retry logic with exponential backoff
|
||||
- `logging`: Debug and error logging
|
||||
- `json`: Fallback JSON parsing
|
||||
- `re`: Text extraction utilities
|
||||
|
||||
## Version History
|
||||
|
||||
- **v2.0** (January 2025): Enhanced structured output support, improved error handling, comprehensive documentation
|
||||
- **v1.0**: Initial implementation with basic text and structured response support
|
||||
|
||||
## Contributing
|
||||
|
||||
When contributing to this module:
|
||||
|
||||
1. Follow the established patterns for error handling
|
||||
2. Add comprehensive logging for debugging
|
||||
3. Test with both simple and complex schemas
|
||||
4. Update documentation for any new features
|
||||
5. Ensure backward compatibility
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check the troubleshooting section above
|
||||
2. Review debug logs for specific error messages
|
||||
3. Test with simplified schemas to isolate issues
|
||||
4. Verify API key configuration and network connectivity
|
||||
@@ -1,4 +1,59 @@
|
||||
# Using Gemini Pro LLM model
|
||||
"""
|
||||
Gemini Provider Module for ALwrity
|
||||
|
||||
This module provides functions for interacting with Google's Gemini API, specifically designed
|
||||
for structured JSON output and text generation. It follows the official Gemini API documentation
|
||||
and implements best practices for reliable AI interactions.
|
||||
|
||||
Key Features:
|
||||
- Structured JSON response generation with schema validation
|
||||
- Text response generation with retry logic
|
||||
- Comprehensive error handling and logging
|
||||
- Automatic API key management
|
||||
- Support for both gemini-2.5-flash and gemini-2.5-pro models
|
||||
|
||||
Best Practices:
|
||||
1. Use structured output for complex, multi-field responses
|
||||
2. Keep schemas simple and flat to avoid truncation
|
||||
3. Set appropriate token limits (8192 for complex outputs)
|
||||
4. Use low temperature (0.1-0.3) for consistent structured output
|
||||
5. Implement proper error handling in calling functions
|
||||
6. Avoid fallback to text parsing for structured responses
|
||||
|
||||
Usage Examples:
|
||||
# Structured JSON response
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tasks": {
|
||||
"type": "array",
|
||||
"items": {"type": "object", "properties": {...}}
|
||||
}
|
||||
}
|
||||
}
|
||||
result = gemini_structured_json_response(prompt, schema, temperature=0.2, max_tokens=8192)
|
||||
|
||||
# Text response
|
||||
result = gemini_text_response(prompt, temperature=0.7, max_tokens=2048)
|
||||
|
||||
Troubleshooting:
|
||||
- If response.parsed is None: Check schema complexity and token limits
|
||||
- If JSON parsing fails: Verify schema matches expected output structure
|
||||
- If truncation occurs: Reduce output size or increase max_tokens
|
||||
- If rate limiting: Implement exponential backoff (already included)
|
||||
|
||||
Dependencies:
|
||||
- google.generativeai (genai)
|
||||
- tenacity (for retry logic)
|
||||
- logging (for debugging)
|
||||
- json (for fallback parsing)
|
||||
- re (for text extraction)
|
||||
|
||||
Author: ALwrity Team
|
||||
Version: 2.0
|
||||
Last Updated: January 2025
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -62,7 +117,39 @@ def get_gemini_api_key() -> str:
|
||||
|
||||
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
|
||||
def gemini_text_response(prompt, temperature, top_p, n, max_tokens, system_prompt):
|
||||
""" Common functiont to get response from gemini pro Text. """
|
||||
"""
|
||||
Generate text response using Google's Gemini Pro model.
|
||||
|
||||
This function provides simple text generation with retry logic and error handling.
|
||||
For structured output, use gemini_structured_json_response instead.
|
||||
|
||||
Args:
|
||||
prompt (str): The input prompt for the AI model
|
||||
temperature (float): Controls randomness (0.0-1.0). Higher = more creative
|
||||
top_p (float): Nucleus sampling parameter (0.0-1.0)
|
||||
n (int): Number of responses to generate
|
||||
max_tokens (int): Maximum tokens in response
|
||||
system_prompt (str, optional): System instruction for the model
|
||||
|
||||
Returns:
|
||||
str: Generated text response
|
||||
|
||||
Raises:
|
||||
Exception: If API key is missing or API call fails
|
||||
|
||||
Best Practices:
|
||||
- Use temperature 0.7-0.9 for creative content
|
||||
- Use temperature 0.1-0.3 for factual/consistent content
|
||||
- Set appropriate max_tokens based on expected response length
|
||||
- Implement proper error handling in calling functions
|
||||
|
||||
Example:
|
||||
result = gemini_text_response(
|
||||
"Write a blog post about AI",
|
||||
temperature=0.8,
|
||||
max_tokens=1024
|
||||
)
|
||||
"""
|
||||
#FIXME: Include : https://github.com/google-gemini/cookbook/blob/main/quickstarts/rest/System_instructions_REST.ipynb
|
||||
try:
|
||||
api_key = get_gemini_api_key()
|
||||
@@ -97,51 +184,9 @@ def gemini_text_response(prompt, temperature, top_p, n, max_tokens, system_promp
|
||||
return response.text
|
||||
except Exception as err:
|
||||
logger.error(f"Failed to get response from Gemini: {err}. Retrying.")
|
||||
raise
|
||||
|
||||
|
||||
#@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
|
||||
#def gemini_blog_metadata_json(blog_content):
|
||||
# """ Common functiont to get response from gemini pro Text. """
|
||||
# prompt = f"I will provide you with the content of a blog post. Based on this content, you need to generate the following elements in JSON format:\n\n1. **Blog Title**: A compelling and relevant title that summarizes the blog content.\n2. **Meta Description**: A concise meta description (up to 160 characters) that captures the essence of the blog post and encourages clicks.\n3. **Tags**: A list of 5-10 relevant tags that represent the key topics covered in the blog post.\n4. **Categories**: A list of 1-3 appropriate categories that best describe the blog post's main themes.\n\nOutput your response in the following JSON format:\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"blog_title\": {\n \"type\": \"string\"\n },\n \"meta_description\": {\n \"type\": \"string\"\n },\n \"tags\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"categories\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n}\n\n. The Blog Content is given below: \n\n{blog_content}\n\n"
|
||||
#
|
||||
# try:
|
||||
# genai.configure(api_key=os.getenv('GEMINI_API_KEY'))
|
||||
# except Exception as err:
|
||||
# logger.error(f"Failed to configure Gemini: {err}")
|
||||
#
|
||||
# # Create the model
|
||||
# generation_config = {
|
||||
# "temperature": 1,
|
||||
# "top_p": 0.95,
|
||||
# "top_k": 64,
|
||||
# "max_output_tokens": 8192,
|
||||
# "response_schema": content.Schema(
|
||||
# type = content.Type.OBJECT,
|
||||
# properties = {
|
||||
# "response": content.Schema(
|
||||
# type = content.Type.STRING,
|
||||
# ),
|
||||
# },
|
||||
# ),
|
||||
# "response_mime_type": "application/json",
|
||||
# }
|
||||
#
|
||||
# model = genai.GenerativeModel(
|
||||
# model_name="gemini-1.5-flash",
|
||||
# generation_config=generation_config,
|
||||
# # safety_settings = Adjust safety settings
|
||||
# # See https://ai.google.dev/gemini-api/docs/safety-settings
|
||||
# )
|
||||
#
|
||||
# try:
|
||||
# # text_response = []
|
||||
# response = model.generate_content(prompt)
|
||||
# if response:
|
||||
# logger.info(f"Number of Token in Prompt Sent: {model.count_tokens(prompt)}")
|
||||
# return response.text
|
||||
# except Exception as err:
|
||||
# logger.error(f"Failed to get SEO METADATA from Gemini: {err}. Retrying.")
|
||||
|
||||
async def test_gemini_api_key(api_key: str) -> tuple[bool, str]:
|
||||
"""
|
||||
Test if the provided Gemini API key is valid.
|
||||
@@ -243,6 +288,8 @@ def _dict_to_types_schema(schema: Dict[str, Any]) -> types.Schema:
|
||||
return types.Schema(type=types.Type.ARRAY, items=item_schema)
|
||||
elif node_type == "NUMBER":
|
||||
return types.Schema(type=types.Type.NUMBER)
|
||||
elif node_type == "INTEGER":
|
||||
return types.Schema(type=types.Type.NUMBER)
|
||||
elif node_type == "BOOLEAN":
|
||||
return types.Schema(type=types.Type.BOOLEAN)
|
||||
else:
|
||||
@@ -254,6 +301,49 @@ def _dict_to_types_schema(schema: Dict[str, Any]) -> types.Schema:
|
||||
def gemini_structured_json_response(prompt, schema, temperature=0.7, top_p=0.9, top_k=40, max_tokens=8192, system_prompt=None):
|
||||
"""
|
||||
Generate structured JSON response using Google's Gemini Pro model.
|
||||
|
||||
This function follows the official Gemini API documentation for structured output:
|
||||
https://ai.google.dev/gemini-api/docs/structured-output#python
|
||||
|
||||
Args:
|
||||
prompt (str): The input prompt for the AI model
|
||||
schema (dict): JSON schema defining the expected output structure
|
||||
temperature (float): Controls randomness (0.0-1.0). Use 0.1-0.3 for structured output
|
||||
top_p (float): Nucleus sampling parameter (0.0-1.0)
|
||||
top_k (int): Top-k sampling parameter
|
||||
max_tokens (int): Maximum tokens in response. Use 8192 for complex outputs
|
||||
system_prompt (str, optional): System instruction for the model
|
||||
|
||||
Returns:
|
||||
dict: Parsed JSON response matching the provided schema
|
||||
|
||||
Raises:
|
||||
Exception: If API key is missing or API call fails
|
||||
|
||||
Best Practices:
|
||||
- Keep schemas simple and flat to avoid truncation
|
||||
- Use low temperature (0.1-0.3) for consistent structured output
|
||||
- Set max_tokens to 8192 for complex multi-field responses
|
||||
- Avoid deeply nested schemas with many required fields
|
||||
- Test with smaller outputs first, then scale up
|
||||
|
||||
Example:
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tasks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string"},
|
||||
"description": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result = gemini_structured_json_response(prompt, schema, temperature=0.2, max_tokens=8192)
|
||||
"""
|
||||
try:
|
||||
# Get API key with proper error handling
|
||||
@@ -261,59 +351,65 @@ def gemini_structured_json_response(prompt, schema, temperature=0.7, top_p=0.9,
|
||||
client = genai.Client(api_key=api_key)
|
||||
logger.info("✅ Gemini client initialized for structured JSON response")
|
||||
|
||||
# Build config using official SDK schema type
|
||||
# Prepare schema for SDK (dict -> types.Schema). If schema is already a types.Schema or Pydantic type, use as-is
|
||||
try:
|
||||
types_schema = _dict_to_types_schema(schema) if isinstance(schema, dict) else schema
|
||||
if isinstance(schema, dict):
|
||||
types_schema = _dict_to_types_schema(schema)
|
||||
else:
|
||||
types_schema = schema
|
||||
except Exception as conv_err:
|
||||
logger.warning(f"Schema conversion warning, defaulting to OBJECT: {conv_err}")
|
||||
logger.info(f"Schema conversion warning, defaulting to OBJECT: {conv_err}")
|
||||
types_schema = types.Schema(type=types.Type.OBJECT)
|
||||
|
||||
# Add debugging for API call
|
||||
logger.info(
|
||||
"Gemini structured call | prompt_len=%s | schema_kind=%s | temp=%s | top_p=%s | top_k=%s | max_tokens=%s",
|
||||
len(prompt) if isinstance(prompt, str) else '<non-str>',
|
||||
type(types_schema).__name__,
|
||||
temperature,
|
||||
top_p,
|
||||
top_k,
|
||||
max_tokens,
|
||||
)
|
||||
|
||||
# Use the official SDK GenerateContentConfig with response_schema
|
||||
generation_config = types.GenerateContentConfig(
|
||||
system_instruction=system_prompt,
|
||||
response_mime_type='application/json',
|
||||
response_schema=types_schema,
|
||||
max_output_tokens=max_tokens,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
top_k=top_k,
|
||||
response_mime_type='application/json',
|
||||
response_schema=types_schema
|
||||
system_instruction=system_prompt,
|
||||
)
|
||||
|
||||
# Add debugging for API call
|
||||
logger.debug(f"Gemini API call - prompt length: {len(prompt)}, schema keys: {list(schema.keys()) if isinstance(schema, dict) else 'N/A'}")
|
||||
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.5-flash',
|
||||
model="gemini-2.5-flash",
|
||||
contents=prompt,
|
||||
config=generation_config,
|
||||
)
|
||||
|
||||
# Add debugging for response
|
||||
logger.debug(f"Gemini response type: {type(response)}")
|
||||
logger.debug(f"Gemini response has text: {hasattr(response, 'text')}")
|
||||
logger.debug(f"Gemini response has parsed: {hasattr(response, 'parsed')}")
|
||||
logger.info("Gemini response | type=%s | has_text=%s | has_parsed=%s",
|
||||
type(response), hasattr(response, 'text'), hasattr(response, 'parsed'))
|
||||
|
||||
if hasattr(response, 'text'):
|
||||
logger.debug(f"Gemini response.text: {repr(response.text)}")
|
||||
logger.info(f"Gemini response.text: {repr(response.text)}")
|
||||
if hasattr(response, 'parsed'):
|
||||
logger.debug(f"Gemini response.parsed: {repr(response.parsed)}")
|
||||
logger.info(f"Gemini response.parsed: {repr(response.parsed)}")
|
||||
|
||||
# Prefer parsed if present and non-empty; otherwise parse text with fallbacks
|
||||
try:
|
||||
parsed = getattr(response, 'parsed', None)
|
||||
if parsed:
|
||||
logger.debug(f"Using parsed response: {type(parsed)}")
|
||||
return parsed if isinstance(parsed, dict) else json.loads(json.dumps(parsed))
|
||||
|
||||
text = (response.text or '').strip()
|
||||
logger.debug(f"Using text response, length: {len(text)}")
|
||||
|
||||
if not text:
|
||||
logger.error("Gemini returned empty text response")
|
||||
return {"error": "Empty response from Gemini API", "raw_response": ""}
|
||||
# According to the documentation, we should use response.parsed for structured output
|
||||
if hasattr(response, 'parsed') and response.parsed is not None:
|
||||
logger.info("Using response.parsed for structured output")
|
||||
return response.parsed
|
||||
|
||||
# Fallback to text if parsed is not available
|
||||
if hasattr(response, 'text') and response.text:
|
||||
logger.info("Falling back to response.text parsing")
|
||||
text = response.text.strip()
|
||||
|
||||
# Strip markdown code fences if present
|
||||
if text.startswith('```'):
|
||||
# remove leading ```json or ``` and trailing ```
|
||||
if text.lower().startswith('```json'):
|
||||
text = text[7:]
|
||||
else:
|
||||
@@ -322,61 +418,14 @@ def gemini_structured_json_response(prompt, schema, temperature=0.7, top_p=0.9,
|
||||
text = text[:-3]
|
||||
text = text.strip()
|
||||
|
||||
# Try direct JSON parsing first
|
||||
try:
|
||||
return json.loads(text)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning(f"Direct JSON parsing failed: {e}")
|
||||
logger.debug(f"Failed to parse text: {text[:200]}...")
|
||||
|
||||
# Check if response is truncated (common cause of JSON errors)
|
||||
if text.endswith('...') or text.endswith('"') or text.endswith(','):
|
||||
logger.warning("Response appears to be truncated, attempting partial parsing")
|
||||
# Try to extract what we can from truncated response
|
||||
partial_result = _extract_partial_json(text)
|
||||
if partial_result:
|
||||
logger.info("Successfully extracted partial JSON from truncated response")
|
||||
return partial_result
|
||||
|
||||
# Fallback 1: Extract likely JSON object substring
|
||||
first = text.find('{')
|
||||
last = text.rfind('}')
|
||||
if first != -1 and last != -1 and last > first:
|
||||
candidate = text[first:last+1]
|
||||
try:
|
||||
return json.loads(candidate)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("JSON object extraction failed, trying regex")
|
||||
|
||||
# Fallback 2: Regex any object
|
||||
import re
|
||||
match = re.search(r'\{[\s\S]*\}', text)
|
||||
if match:
|
||||
try:
|
||||
return json.loads(match.group(0))
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("Regex JSON extraction failed, trying repair")
|
||||
|
||||
# Fallback 3: Attempt to repair common JSON issues
|
||||
repaired = _repair_json_string(text)
|
||||
if repaired:
|
||||
try:
|
||||
return json.loads(repaired)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("JSON repair failed")
|
||||
|
||||
# Fallback 4: Extract and parse individual key-value pairs
|
||||
extracted = _extract_key_value_pairs(text)
|
||||
if extracted:
|
||||
return extracted
|
||||
|
||||
# Final fallback: return error with raw response for debugging
|
||||
logger.error(f"All JSON parsing attempts failed for text: {text[:200]}...")
|
||||
logger.error(f"Failed to parse response.text as JSON: {e}")
|
||||
return {"error": f"Failed to parse JSON response: {e}", "raw_response": text[:500]}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing structured response: {e}")
|
||||
return {"error": f"Failed to parse JSON response: {e}", "raw_response": (response.text or '')}
|
||||
|
||||
logger.error("No valid response content found")
|
||||
return {"error": "No valid response content found", "raw_response": ""}
|
||||
|
||||
except ValueError as e:
|
||||
# API key related errors
|
||||
|
||||
483
backend/services/monitoring_plan_generator.py
Normal file
483
backend/services/monitoring_plan_generator.py
Normal file
@@ -0,0 +1,483 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, Any, List
|
||||
from datetime import datetime
|
||||
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response
|
||||
from services.strategy_service import StrategyService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MonitoringPlanGenerator:
|
||||
def __init__(self):
|
||||
self.strategy_service = StrategyService()
|
||||
|
||||
async def generate_monitoring_plan(self, strategy_id: int) -> Dict[str, Any]:
|
||||
"""Generate comprehensive monitoring plan for a strategy"""
|
||||
|
||||
try:
|
||||
# Get strategy data
|
||||
strategy_data = await self.strategy_service.get_strategy_by_id(strategy_id)
|
||||
|
||||
if not strategy_data:
|
||||
raise Exception(f"Strategy with ID {strategy_id} not found")
|
||||
|
||||
# Prepare prompt context
|
||||
prompt_context = self._prepare_prompt_context(strategy_data)
|
||||
logger.debug(
|
||||
"MonitoringPlanGenerator: Prepared prompt context | strategy_id=%s | keys=%s",
|
||||
strategy_id,
|
||||
list(prompt_context.keys())
|
||||
)
|
||||
|
||||
# Generate monitoring plan using AI
|
||||
monitoring_plan = await self._generate_plan_with_ai(prompt_context)
|
||||
|
||||
# Validate the plan structure
|
||||
if not self._validate_monitoring_plan(monitoring_plan):
|
||||
raise Exception("Generated monitoring plan has invalid structure")
|
||||
|
||||
# Validate and enhance the plan
|
||||
enhanced_plan = await self._enhance_monitoring_plan(monitoring_plan, strategy_data)
|
||||
|
||||
# Save monitoring plan to database
|
||||
await self._save_monitoring_plan(strategy_id, enhanced_plan)
|
||||
|
||||
logger.info(f"Successfully generated monitoring plan for strategy {strategy_id}")
|
||||
return enhanced_plan
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating monitoring plan for strategy {strategy_id}: {e}")
|
||||
# Don't mark as success if there's an error
|
||||
raise Exception(f"Failed to generate monitoring plan: {str(e)}")
|
||||
|
||||
def _prepare_prompt_context(self, strategy_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Prepare context for AI prompt"""
|
||||
|
||||
# Extract strategy components
|
||||
strategic_insights = strategy_data.get('strategic_insights', {})
|
||||
competitive_analysis = strategy_data.get('competitive_analysis', {})
|
||||
performance_predictions = strategy_data.get('performance_predictions', {})
|
||||
implementation_roadmap = strategy_data.get('implementation_roadmap', {})
|
||||
risk_assessment = strategy_data.get('risk_assessment', {})
|
||||
|
||||
return {
|
||||
"strategy_name": strategy_data.get('name', 'Content Strategy'),
|
||||
"industry": strategy_data.get('industry', 'General'),
|
||||
"business_goals": strategy_data.get('business_goals', []),
|
||||
"content_pillars": strategy_data.get('content_pillars', []),
|
||||
"target_audience": strategy_data.get('target_audience', {}),
|
||||
"competitive_landscape": competitive_analysis.get('competitors', []),
|
||||
"strategic_insights": strategic_insights,
|
||||
"performance_predictions": performance_predictions,
|
||||
"implementation_roadmap": implementation_roadmap,
|
||||
"risk_assessment": risk_assessment
|
||||
}
|
||||
|
||||
async def _generate_plan_with_ai(self, prompt_context: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Generate monitoring plan using AI"""
|
||||
|
||||
prompt = self._build_monitoring_prompt(prompt_context)
|
||||
logger.debug(
|
||||
"MonitoringPlanGenerator: Built prompt | length=%s | preview=%s...",
|
||||
len(prompt),
|
||||
(prompt[:240].replace("\n", " ") if isinstance(prompt, str) else "<non-str>")
|
||||
)
|
||||
|
||||
# Define schema for 8 tasks (2 per component) to avoid truncation
|
||||
monitoring_plan_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"monitoringTasks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"component": {"type": "string"},
|
||||
"title": {"type": "string"},
|
||||
"description": {"type": "string"},
|
||||
"assignee": {"type": "string"},
|
||||
"frequency": {"type": "string"},
|
||||
"metric": {"type": "string"},
|
||||
"measurementMethod": {"type": "string"},
|
||||
"successCriteria": {"type": "string"},
|
||||
"alertThreshold": {"type": "string"},
|
||||
"actionableInsights": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.debug(
|
||||
"MonitoringPlanGenerator: Schema prepared | schema_type=%s",
|
||||
type(monitoring_plan_schema).__name__
|
||||
)
|
||||
|
||||
try:
|
||||
# Structured response only (no fallback)
|
||||
logger.info("MonitoringPlanGenerator: Invoking Gemini structured JSON response")
|
||||
response = gemini_structured_json_response(
|
||||
prompt=prompt,
|
||||
schema=monitoring_plan_schema,
|
||||
temperature=0.1,
|
||||
max_tokens=8192
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"MonitoringPlanGenerator: Received AI response | type=%s",
|
||||
type(response)
|
||||
)
|
||||
|
||||
# Handle response - gemini_structured_json_response returns dict directly
|
||||
if isinstance(response, dict):
|
||||
if "error" in response:
|
||||
logger.error("MonitoringPlanGenerator: Gemini returned error dict | error=%s", response.get("error"))
|
||||
raise Exception(f"Gemini error: {response.get('error')}")
|
||||
logger.debug(
|
||||
"MonitoringPlanGenerator: Parsed response dict keys=%s",
|
||||
list(response.keys())
|
||||
)
|
||||
monitoring_plan = response
|
||||
elif isinstance(response, str):
|
||||
# If it's a string, try to parse as JSON
|
||||
try:
|
||||
monitoring_plan = json.loads(response)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error("MonitoringPlanGenerator: Failed to parse AI response as JSON: %s", e)
|
||||
raise Exception(f"Invalid AI response format: {str(e)}")
|
||||
else:
|
||||
logger.error("MonitoringPlanGenerator: Unexpected response type from AI service: %s", type(response))
|
||||
raise Exception(f"Unexpected response type from AI service: {type(response)}")
|
||||
|
||||
logger.info(
|
||||
"MonitoringPlanGenerator: AI monitoring plan generated | has_tasks=%s",
|
||||
isinstance(monitoring_plan.get("monitoringTasks"), list)
|
||||
)
|
||||
|
||||
# Compute totals from the returned tasks
|
||||
monitoring_tasks = monitoring_plan.get("monitoringTasks", [])
|
||||
total_tasks = len(monitoring_tasks)
|
||||
alwrity_tasks = sum(1 for task in monitoring_tasks if task.get("assignee") == "ALwrity")
|
||||
human_tasks = sum(1 for task in monitoring_tasks if task.get("assignee") == "Human")
|
||||
|
||||
# Add computed totals to the plan
|
||||
monitoring_plan["totalTasks"] = total_tasks
|
||||
monitoring_plan["alwrityTasks"] = alwrity_tasks
|
||||
monitoring_plan["humanTasks"] = human_tasks
|
||||
monitoring_plan["metricsCount"] = total_tasks
|
||||
|
||||
logger.info(
|
||||
"MonitoringPlanGenerator: Computed totals | total=%s | alwrity=%s | human=%s",
|
||||
total_tasks, alwrity_tasks, human_tasks
|
||||
)
|
||||
|
||||
return monitoring_plan
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error calling AI service: {e}")
|
||||
raise Exception(f"AI service error: {str(e)}")
|
||||
|
||||
def _build_monitoring_prompt(self, context: Dict[str, Any]) -> str:
|
||||
"""Build the AI prompt for monitoring plan generation"""
|
||||
|
||||
return f"""Generate a monitoring plan for content strategy: {context['strategy_name']} in {context['industry']} industry.
|
||||
|
||||
Create exactly 8 monitoring tasks (2 per component) across 5 strategy components:
|
||||
1. Strategic Insights
|
||||
2. Competitive Analysis
|
||||
3. Performance Predictions
|
||||
4. Implementation Roadmap
|
||||
5. Risk Assessment
|
||||
|
||||
Each task must include: component, title, description, assignee (ALwrity or Human), frequency (Daily, Weekly, Monthly, or Quarterly), metric, measurement method, success criteria, alert threshold, and actionable insights.
|
||||
|
||||
Return a JSON object with monitoringTasks array containing 8 task objects."""
|
||||
|
||||
def _generate_default_plan(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Generate a default monitoring plan if AI fails"""
|
||||
|
||||
return {
|
||||
"totalTasks": 15,
|
||||
"alwrityTasks": 10,
|
||||
"humanTasks": 5,
|
||||
"metricsCount": 15,
|
||||
"components": [
|
||||
{
|
||||
"name": "Strategic Insights",
|
||||
"icon": "TrendingUpIcon",
|
||||
"tasks": [
|
||||
{
|
||||
"title": "Monitor Market Positioning Effectiveness",
|
||||
"description": "Track how well the strategic positioning is performing in the market",
|
||||
"assignee": "ALwrity",
|
||||
"frequency": "Weekly",
|
||||
"metric": "Market Position Score",
|
||||
"measurementMethod": "Competitive analysis and brand mention tracking",
|
||||
"successCriteria": "Maintain top 3 market position",
|
||||
"alertThreshold": "Drop below top 5 position"
|
||||
},
|
||||
{
|
||||
"title": "Track Strategic Goal Achievement",
|
||||
"description": "Monitor progress toward defined business objectives",
|
||||
"assignee": "Human",
|
||||
"frequency": "Monthly",
|
||||
"metric": "Goal Achievement Rate",
|
||||
"measurementMethod": "KPI tracking and business metrics analysis",
|
||||
"successCriteria": "Achieve 80% of strategic goals",
|
||||
"alertThreshold": "Drop below 60% achievement"
|
||||
},
|
||||
{
|
||||
"title": "Analyze Strategic Insights Performance",
|
||||
"description": "Evaluate the effectiveness of strategic insights and recommendations",
|
||||
"assignee": "ALwrity",
|
||||
"frequency": "Weekly",
|
||||
"metric": "Insight Effectiveness Score",
|
||||
"measurementMethod": "Performance data analysis and trend identification",
|
||||
"successCriteria": "Maintain 85%+ effectiveness score",
|
||||
"alertThreshold": "Drop below 70% effectiveness"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Competitive Analysis",
|
||||
"icon": "EmojiEventsIcon",
|
||||
"tasks": [
|
||||
{
|
||||
"title": "Monitor Competitor Activities",
|
||||
"description": "Track competitor content strategies and market activities",
|
||||
"assignee": "ALwrity",
|
||||
"frequency": "Daily",
|
||||
"metric": "Competitor Activity Score",
|
||||
"measurementMethod": "Automated competitor monitoring and analysis",
|
||||
"successCriteria": "Stay ahead of competitor activities",
|
||||
"alertThreshold": "Competitor gains significant advantage"
|
||||
},
|
||||
{
|
||||
"title": "Track Competitive Positioning",
|
||||
"description": "Monitor our competitive position in the market",
|
||||
"assignee": "ALwrity",
|
||||
"frequency": "Weekly",
|
||||
"metric": "Competitive Position Rank",
|
||||
"measurementMethod": "Market share and positioning analysis",
|
||||
"successCriteria": "Maintain top 3 competitive position",
|
||||
"alertThreshold": "Drop below top 5 position"
|
||||
},
|
||||
{
|
||||
"title": "Validate Competitive Intelligence",
|
||||
"description": "Review and validate competitive analysis insights",
|
||||
"assignee": "Human",
|
||||
"frequency": "Monthly",
|
||||
"metric": "Intelligence Accuracy Score",
|
||||
"measurementMethod": "Manual review and validation",
|
||||
"successCriteria": "Maintain 90%+ accuracy",
|
||||
"alertThreshold": "Drop below 80% accuracy"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Performance Predictions",
|
||||
"icon": "AssessmentIcon",
|
||||
"tasks": [
|
||||
{
|
||||
"title": "Monitor Prediction Accuracy",
|
||||
"description": "Track the accuracy of performance predictions",
|
||||
"assignee": "ALwrity",
|
||||
"frequency": "Weekly",
|
||||
"metric": "Prediction Accuracy Rate",
|
||||
"measurementMethod": "Compare predictions with actual performance",
|
||||
"successCriteria": "Maintain 85%+ prediction accuracy",
|
||||
"alertThreshold": "Drop below 70% accuracy"
|
||||
},
|
||||
{
|
||||
"title": "Update Prediction Models",
|
||||
"description": "Refine prediction models based on new data",
|
||||
"assignee": "ALwrity",
|
||||
"frequency": "Monthly",
|
||||
"metric": "Model Performance Score",
|
||||
"measurementMethod": "Model validation and performance testing",
|
||||
"successCriteria": "Improve model performance by 5%+",
|
||||
"alertThreshold": "Model performance degrades"
|
||||
},
|
||||
{
|
||||
"title": "Review Prediction Insights",
|
||||
"description": "Analyze prediction insights and business implications",
|
||||
"assignee": "Human",
|
||||
"frequency": "Monthly",
|
||||
"metric": "Insight Actionability Score",
|
||||
"measurementMethod": "Manual review and business analysis",
|
||||
"successCriteria": "Generate actionable insights",
|
||||
"alertThreshold": "Insights become less actionable"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Implementation Roadmap",
|
||||
"icon": "CheckCircleIcon",
|
||||
"tasks": [
|
||||
{
|
||||
"title": "Track Implementation Progress",
|
||||
"description": "Monitor progress on implementation roadmap milestones",
|
||||
"assignee": "ALwrity",
|
||||
"frequency": "Weekly",
|
||||
"metric": "Implementation Progress Rate",
|
||||
"measurementMethod": "Milestone tracking and progress analysis",
|
||||
"successCriteria": "Achieve 90%+ of milestones on time",
|
||||
"alertThreshold": "Fall behind by more than 2 weeks"
|
||||
},
|
||||
{
|
||||
"title": "Monitor Resource Utilization",
|
||||
"description": "Track resource allocation and utilization efficiency",
|
||||
"assignee": "ALwrity",
|
||||
"frequency": "Weekly",
|
||||
"metric": "Resource Efficiency Score",
|
||||
"measurementMethod": "Resource tracking and efficiency analysis",
|
||||
"successCriteria": "Maintain 85%+ resource efficiency",
|
||||
"alertThreshold": "Drop below 70% efficiency"
|
||||
},
|
||||
{
|
||||
"title": "Review Implementation Effectiveness",
|
||||
"description": "Evaluate the effectiveness of implementation strategies",
|
||||
"assignee": "Human",
|
||||
"frequency": "Monthly",
|
||||
"metric": "Implementation Success Rate",
|
||||
"measurementMethod": "Manual review and effectiveness assessment",
|
||||
"successCriteria": "Achieve 80%+ implementation success",
|
||||
"alertThreshold": "Drop below 60% success rate"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Risk Assessment",
|
||||
"icon": "StarIcon",
|
||||
"tasks": [
|
||||
{
|
||||
"title": "Monitor Risk Indicators",
|
||||
"description": "Track identified risk factors and their status",
|
||||
"assignee": "ALwrity",
|
||||
"frequency": "Daily",
|
||||
"metric": "Risk Level Score",
|
||||
"measurementMethod": "Risk factor monitoring and analysis",
|
||||
"successCriteria": "Maintain low risk level (score < 30)",
|
||||
"alertThreshold": "Risk level increases above 50"
|
||||
},
|
||||
{
|
||||
"title": "Track Risk Mitigation Effectiveness",
|
||||
"description": "Monitor the effectiveness of risk mitigation strategies",
|
||||
"assignee": "ALwrity",
|
||||
"frequency": "Weekly",
|
||||
"metric": "Mitigation Effectiveness Rate",
|
||||
"measurementMethod": "Risk reduction tracking and analysis",
|
||||
"successCriteria": "Achieve 80%+ risk mitigation success",
|
||||
"alertThreshold": "Drop below 60% mitigation success"
|
||||
},
|
||||
{
|
||||
"title": "Review Risk Management Decisions",
|
||||
"description": "Evaluate risk management decisions and their outcomes",
|
||||
"assignee": "Human",
|
||||
"frequency": "Monthly",
|
||||
"metric": "Risk Management Score",
|
||||
"measurementMethod": "Manual review and decision analysis",
|
||||
"successCriteria": "Maintain 85%+ risk management effectiveness",
|
||||
"alertThreshold": "Drop below 70% effectiveness"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async def _enhance_monitoring_plan(self, plan: Dict[str, Any], strategy_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Enhance AI-generated plan with additional context and validation"""
|
||||
|
||||
enhanced_plan = plan.copy()
|
||||
|
||||
# Add monitoring schedule
|
||||
enhanced_plan["monitoringSchedule"] = {
|
||||
"dailyChecks": ["Performance metrics", "Alert monitoring", "Risk indicators"],
|
||||
"weeklyReviews": ["Trend analysis", "Competitive updates", "Implementation progress"],
|
||||
"monthlyAssessments": ["Strategy effectiveness", "Goal progress", "Risk management"],
|
||||
"quarterlyPlanning": ["Strategy optimization", "Goal refinement", "Resource allocation"]
|
||||
}
|
||||
|
||||
# Add success metrics
|
||||
enhanced_plan["successMetrics"] = {
|
||||
"trafficGrowth": {"target": "25%+", "current": "0%"},
|
||||
"engagementRate": {"target": "15%+", "current": "0%"},
|
||||
"conversionRate": {"target": "10%+", "current": "0%"},
|
||||
"roi": {"target": "3:1+", "current": "0:1"},
|
||||
"strategyAdoption": {"target": "90%+", "current": "0%"},
|
||||
"contentQuality": {"target": "85%+", "current": "0%"},
|
||||
"competitivePosition": {"target": "Top 3", "current": "Unknown"},
|
||||
"audienceGrowth": {"target": "20%+", "current": "0%"}
|
||||
}
|
||||
|
||||
# Add metadata
|
||||
enhanced_plan["metadata"] = {
|
||||
"generatedAt": datetime.now().isoformat(),
|
||||
"strategyId": strategy_data.get('id'),
|
||||
"strategyName": strategy_data.get('name'),
|
||||
"version": "1.0"
|
||||
}
|
||||
|
||||
return enhanced_plan
|
||||
|
||||
async def _save_monitoring_plan(self, strategy_id: int, plan: Dict[str, Any]):
|
||||
"""Save monitoring plan to database"""
|
||||
try:
|
||||
# Use the strategy service to save the monitoring plan
|
||||
success = await self.strategy_service.save_monitoring_plan(strategy_id, plan)
|
||||
|
||||
if success:
|
||||
logger.info(f"Monitoring plan saved to database for strategy {strategy_id}")
|
||||
else:
|
||||
logger.warning(f"Failed to save monitoring plan to database for strategy {strategy_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving monitoring plan: {e}")
|
||||
# Don't raise the error as the plan generation was successful
|
||||
|
||||
def _validate_monitoring_plan(self, plan: Dict[str, Any]) -> bool:
|
||||
"""Validate the structure of the generated monitoring plan"""
|
||||
try:
|
||||
# Check that monitoringTasks is a list and has content
|
||||
monitoring_tasks = plan.get("monitoringTasks", [])
|
||||
if not isinstance(monitoring_tasks, list):
|
||||
logger.error("monitoringTasks must be a list")
|
||||
return False
|
||||
|
||||
if len(monitoring_tasks) == 0:
|
||||
logger.error("No monitoring tasks generated")
|
||||
return False
|
||||
|
||||
# Validate we have the expected number of tasks (8)
|
||||
if len(monitoring_tasks) != 8:
|
||||
logger.warning(f"Expected 8 tasks, got {len(monitoring_tasks)}")
|
||||
|
||||
# Validate each task structure
|
||||
required_task_fields = [
|
||||
"component", "title", "description", "assignee", "frequency",
|
||||
"metric", "measurementMethod", "successCriteria", "alertThreshold", "actionableInsights"
|
||||
]
|
||||
|
||||
for i, task in enumerate(monitoring_tasks):
|
||||
for field in required_task_fields:
|
||||
if field not in task:
|
||||
logger.error(f"Task {i} missing required field: {field}")
|
||||
return False
|
||||
|
||||
# Validate assignee is either "ALwrity" or "Human"
|
||||
if task.get("assignee") not in ["ALwrity", "Human"]:
|
||||
logger.error(f"Task {i} has invalid assignee: {task.get('assignee')}")
|
||||
return False
|
||||
|
||||
# Validate computed totals are present (added after AI response)
|
||||
computed_fields = ["totalTasks", "alwrityTasks", "humanTasks", "metricsCount"]
|
||||
for field in computed_fields:
|
||||
if field not in plan:
|
||||
logger.error(f"Missing computed field in monitoring plan: {field}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating monitoring plan: {e}")
|
||||
return False
|
||||
383
backend/services/strategy_service.py
Normal file
383
backend/services/strategy_service.py
Normal file
@@ -0,0 +1,383 @@
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_
|
||||
|
||||
from models.monitoring_models import (
|
||||
StrategyMonitoringPlan,
|
||||
MonitoringTask,
|
||||
TaskExecutionLog,
|
||||
StrategyPerformanceMetrics,
|
||||
StrategyActivationStatus
|
||||
)
|
||||
from models.enhanced_strategy_models import EnhancedContentStrategy
|
||||
from services.database import get_db_session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class StrategyService:
|
||||
"""Service for managing content strategies and their activation status"""
|
||||
|
||||
def __init__(self, db_session: Optional[Session] = None):
|
||||
self.db_session = db_session or get_db_session()
|
||||
|
||||
async def get_strategy_by_id(self, strategy_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""Get strategy by ID with all related data"""
|
||||
try:
|
||||
if self.db_session:
|
||||
# Query the actual database
|
||||
strategy = self.db_session.query(EnhancedContentStrategy).filter(
|
||||
EnhancedContentStrategy.id == strategy_id
|
||||
).first()
|
||||
|
||||
if strategy:
|
||||
return strategy.to_dict()
|
||||
|
||||
# Fallback to mock data if no database or strategy not found
|
||||
strategy_data = {
|
||||
'id': strategy_id,
|
||||
'name': f'Content Strategy {strategy_id}',
|
||||
'industry': 'Technology',
|
||||
'business_goals': ['Increase brand awareness', 'Generate leads', 'Improve engagement'],
|
||||
'content_pillars': ['Educational Content', 'Thought Leadership', 'Case Studies'],
|
||||
'target_audience': {
|
||||
'demographics': 'B2B professionals',
|
||||
'age_range': '25-45',
|
||||
'interests': ['technology', 'business', 'innovation']
|
||||
},
|
||||
'strategic_insights': {
|
||||
'market_positioning': 'Innovation leader in tech solutions',
|
||||
'content_opportunities': ['AI trends', 'Digital transformation', 'Industry insights'],
|
||||
'growth_potential': 'High growth potential in emerging markets'
|
||||
},
|
||||
'competitive_analysis': {
|
||||
'competitors': ['Competitor A', 'Competitor B', 'Competitor C'],
|
||||
'market_gaps': ['AI implementation guidance', 'ROI measurement tools'],
|
||||
'opportunities': ['Thought leadership in AI', 'Educational content series']
|
||||
},
|
||||
'performance_predictions': {
|
||||
'estimated_roi': '25-35%',
|
||||
'traffic_growth': '40% increase in 6 months',
|
||||
'engagement_metrics': '15% improvement in engagement rate'
|
||||
},
|
||||
'implementation_roadmap': {
|
||||
'phases': ['Foundation', 'Growth', 'Optimization', 'Scale'],
|
||||
'timeline': '12 months',
|
||||
'milestones': ['Month 3: Content foundation', 'Month 6: Growth phase', 'Month 9: Optimization']
|
||||
},
|
||||
'risk_assessment': {
|
||||
'risks': ['Market competition', 'Resource constraints', 'Technology changes'],
|
||||
'overall_risk_level': 'Medium',
|
||||
'mitigation_strategies': ['Continuous monitoring', 'Agile adaptation', 'Resource planning']
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(f"Retrieved strategy {strategy_id}")
|
||||
return strategy_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving strategy {strategy_id}: {e}")
|
||||
return None
|
||||
|
||||
async def activate_strategy(self, strategy_id: int, user_id: int = 1) -> bool:
|
||||
"""Activate a strategy and set up monitoring"""
|
||||
try:
|
||||
# Check if strategy exists
|
||||
strategy = await self.get_strategy_by_id(strategy_id)
|
||||
if not strategy:
|
||||
logger.error(f"Strategy {strategy_id} not found")
|
||||
return False
|
||||
|
||||
# Check if already activated
|
||||
if self.db_session:
|
||||
existing_activation = self.db_session.query(StrategyActivationStatus).filter(
|
||||
and_(
|
||||
StrategyActivationStatus.strategy_id == strategy_id,
|
||||
StrategyActivationStatus.user_id == user_id,
|
||||
StrategyActivationStatus.status == 'active'
|
||||
)
|
||||
).first()
|
||||
|
||||
if existing_activation:
|
||||
logger.info(f"Strategy {strategy_id} is already active")
|
||||
return True
|
||||
|
||||
# Create activation status record
|
||||
activation_status = StrategyActivationStatus(
|
||||
strategy_id=strategy_id,
|
||||
user_id=user_id,
|
||||
activation_date=datetime.utcnow(),
|
||||
status='active',
|
||||
performance_score=0.0
|
||||
)
|
||||
|
||||
if self.db_session:
|
||||
self.db_session.add(activation_status)
|
||||
self.db_session.commit()
|
||||
logger.info(f"Strategy {strategy_id} activated successfully")
|
||||
else:
|
||||
logger.info(f"Strategy {strategy_id} activated (no database session)")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error activating strategy {strategy_id}: {e}")
|
||||
if self.db_session:
|
||||
self.db_session.rollback()
|
||||
return False
|
||||
|
||||
async def save_monitoring_plan(self, strategy_id: int, plan_data: Dict[str, Any]) -> bool:
|
||||
"""Save monitoring plan to database"""
|
||||
try:
|
||||
# Check if monitoring plan already exists
|
||||
if self.db_session:
|
||||
existing_plan = self.db_session.query(StrategyMonitoringPlan).filter(
|
||||
StrategyMonitoringPlan.strategy_id == strategy_id
|
||||
).first()
|
||||
|
||||
if existing_plan:
|
||||
# Update existing plan
|
||||
existing_plan.plan_data = plan_data
|
||||
existing_plan.updated_at = datetime.utcnow()
|
||||
else:
|
||||
# Create new monitoring plan
|
||||
monitoring_plan = StrategyMonitoringPlan(
|
||||
strategy_id=strategy_id,
|
||||
plan_data=plan_data
|
||||
)
|
||||
self.db_session.add(monitoring_plan)
|
||||
|
||||
# Clear existing tasks and create new ones
|
||||
self.db_session.query(MonitoringTask).filter(
|
||||
MonitoringTask.strategy_id == strategy_id
|
||||
).delete()
|
||||
|
||||
# Create individual monitoring tasks
|
||||
for component in plan_data.get('components', []):
|
||||
for task in component.get('tasks', []):
|
||||
monitoring_task = MonitoringTask(
|
||||
strategy_id=strategy_id,
|
||||
component_name=component['name'],
|
||||
task_title=task['title'],
|
||||
task_description=task['description'],
|
||||
assignee=task['assignee'],
|
||||
frequency=task['frequency'],
|
||||
metric=task['metric'],
|
||||
measurement_method=task['measurementMethod'],
|
||||
success_criteria=task['successCriteria'],
|
||||
alert_threshold=task['alertThreshold'],
|
||||
status='pending'
|
||||
)
|
||||
self.db_session.add(monitoring_task)
|
||||
|
||||
self.db_session.commit()
|
||||
logger.info(f"Monitoring plan saved for strategy {strategy_id}")
|
||||
else:
|
||||
logger.info(f"Monitoring plan prepared for strategy {strategy_id} (no database session)")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving monitoring plan for strategy {strategy_id}: {e}")
|
||||
if self.db_session:
|
||||
self.db_session.rollback()
|
||||
return False
|
||||
|
||||
async def get_monitoring_plan(self, strategy_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""Get monitoring plan for a strategy"""
|
||||
try:
|
||||
if self.db_session:
|
||||
monitoring_plan = self.db_session.query(StrategyMonitoringPlan).filter(
|
||||
StrategyMonitoringPlan.strategy_id == strategy_id
|
||||
).first()
|
||||
|
||||
if monitoring_plan:
|
||||
return monitoring_plan.plan_data
|
||||
|
||||
# Also check activation status
|
||||
activation_status = self.db_session.query(StrategyActivationStatus).filter(
|
||||
StrategyActivationStatus.strategy_id == strategy_id
|
||||
).first()
|
||||
|
||||
if activation_status:
|
||||
return {
|
||||
'strategy_id': strategy_id,
|
||||
'status': activation_status.status,
|
||||
'activation_date': activation_status.activation_date.isoformat(),
|
||||
'message': 'Strategy is active but no monitoring plan found'
|
||||
}
|
||||
|
||||
# Fallback to mock data
|
||||
return {
|
||||
'strategy_id': strategy_id,
|
||||
'status': 'active',
|
||||
'message': 'Monitoring plan retrieved successfully'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting monitoring plan for strategy {strategy_id}: {e}")
|
||||
return None
|
||||
|
||||
async def update_strategy_status(self, strategy_id: int, status: str, user_id: int = 1) -> bool:
|
||||
"""Update strategy activation status"""
|
||||
try:
|
||||
if self.db_session:
|
||||
activation_status = self.db_session.query(StrategyActivationStatus).filter(
|
||||
and_(
|
||||
StrategyActivationStatus.strategy_id == strategy_id,
|
||||
StrategyActivationStatus.user_id == user_id
|
||||
)
|
||||
).first()
|
||||
|
||||
if activation_status:
|
||||
activation_status.status = status
|
||||
activation_status.last_updated = datetime.utcnow()
|
||||
self.db_session.commit()
|
||||
logger.info(f"Strategy {strategy_id} status updated to {status}")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"No activation status found for strategy {strategy_id}")
|
||||
return False
|
||||
else:
|
||||
logger.info(f"Strategy {strategy_id} status would be updated to {status} (no database session)")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating strategy status for {strategy_id}: {e}")
|
||||
if self.db_session:
|
||||
self.db_session.rollback()
|
||||
return False
|
||||
|
||||
async def get_active_strategies(self, user_id: int = 1) -> List[Dict[str, Any]]:
|
||||
"""Get all active strategies for a user"""
|
||||
try:
|
||||
if self.db_session:
|
||||
active_strategies = self.db_session.query(StrategyActivationStatus).filter(
|
||||
and_(
|
||||
StrategyActivationStatus.user_id == user_id,
|
||||
StrategyActivationStatus.status == 'active'
|
||||
)
|
||||
).all()
|
||||
|
||||
return [
|
||||
{
|
||||
'strategy_id': strategy.strategy_id,
|
||||
'activation_date': strategy.activation_date,
|
||||
'performance_score': strategy.performance_score,
|
||||
'last_updated': strategy.last_updated
|
||||
}
|
||||
for strategy in active_strategies
|
||||
]
|
||||
else:
|
||||
# Return mock data
|
||||
return [
|
||||
{
|
||||
'strategy_id': 1,
|
||||
'activation_date': datetime.utcnow(),
|
||||
'performance_score': 0.0,
|
||||
'last_updated': datetime.utcnow()
|
||||
}
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting active strategies for user {user_id}: {e}")
|
||||
return []
|
||||
|
||||
async def save_performance_metrics(self, strategy_id: int, metrics: Dict[str, Any], user_id: int = 1) -> bool:
|
||||
"""Save performance metrics for a strategy"""
|
||||
try:
|
||||
performance_metrics = StrategyPerformanceMetrics(
|
||||
strategy_id=strategy_id,
|
||||
user_id=user_id,
|
||||
metric_date=datetime.utcnow(),
|
||||
traffic_growth_percentage=metrics.get('traffic_growth_percentage'),
|
||||
engagement_rate_percentage=metrics.get('engagement_rate_percentage'),
|
||||
conversion_rate_percentage=metrics.get('conversion_rate_percentage'),
|
||||
roi_ratio=metrics.get('roi_ratio'),
|
||||
strategy_adoption_rate=metrics.get('strategy_adoption_rate'),
|
||||
content_quality_score=metrics.get('content_quality_score'),
|
||||
competitive_position_rank=metrics.get('competitive_position_rank'),
|
||||
audience_growth_percentage=metrics.get('audience_growth_percentage'),
|
||||
data_source=metrics.get('data_source', 'manual'),
|
||||
confidence_score=metrics.get('confidence_score', 0.8)
|
||||
)
|
||||
|
||||
if self.db_session:
|
||||
self.db_session.add(performance_metrics)
|
||||
self.db_session.commit()
|
||||
logger.info(f"Performance metrics saved for strategy {strategy_id}")
|
||||
else:
|
||||
logger.info(f"Performance metrics prepared for strategy {strategy_id} (no database session)")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving performance metrics for strategy {strategy_id}: {e}")
|
||||
if self.db_session:
|
||||
self.db_session.rollback()
|
||||
return False
|
||||
|
||||
async def get_strategy_performance_history(self, strategy_id: int, days: int = 30) -> List[Dict[str, Any]]:
|
||||
"""Get performance history for a strategy"""
|
||||
try:
|
||||
if self.db_session:
|
||||
from datetime import timedelta
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
metrics = self.db_session.query(StrategyPerformanceMetrics).filter(
|
||||
and_(
|
||||
StrategyPerformanceMetrics.strategy_id == strategy_id,
|
||||
StrategyPerformanceMetrics.metric_date >= cutoff_date
|
||||
)
|
||||
).order_by(StrategyPerformanceMetrics.metric_date.desc()).all()
|
||||
|
||||
return [
|
||||
{
|
||||
'date': metric.metric_date.isoformat(),
|
||||
'traffic_growth': metric.traffic_growth_percentage,
|
||||
'engagement_rate': metric.engagement_rate_percentage,
|
||||
'conversion_rate': metric.conversion_rate_percentage,
|
||||
'roi': metric.roi_ratio,
|
||||
'strategy_adoption': metric.strategy_adoption_rate,
|
||||
'content_quality': metric.content_quality_score,
|
||||
'competitive_position': metric.competitive_position_rank,
|
||||
'audience_growth': metric.audience_growth_percentage
|
||||
}
|
||||
for metric in metrics
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting performance history for strategy {strategy_id}: {e}")
|
||||
return []
|
||||
|
||||
async def deactivate_strategy(self, strategy_id: int, user_id: int = 1) -> bool:
|
||||
"""Deactivate a strategy"""
|
||||
try:
|
||||
return await self.update_strategy_status(strategy_id, 'inactive', user_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Error deactivating strategy {strategy_id}: {e}")
|
||||
return False
|
||||
|
||||
async def pause_strategy(self, strategy_id: int, user_id: int = 1) -> bool:
|
||||
"""Pause a strategy"""
|
||||
try:
|
||||
return await self.update_strategy_status(strategy_id, 'paused', user_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Error pausing strategy {strategy_id}: {e}")
|
||||
return False
|
||||
|
||||
async def resume_strategy(self, strategy_id: int, user_id: int = 1) -> bool:
|
||||
"""Resume a paused strategy"""
|
||||
try:
|
||||
return await self.update_strategy_status(strategy_id, 'active', user_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Error resuming strategy {strategy_id}: {e}")
|
||||
return False
|
||||
|
||||
def __del__(self):
|
||||
"""Cleanup database session"""
|
||||
if self.db_session:
|
||||
self.db_session.close()
|
||||
Reference in New Issue
Block a user