ALwrity version 0.5.5
This commit is contained in:
@@ -1,280 +0,0 @@
|
||||
# Enhanced Strategy Service - Phase 1 Implementation Summary
|
||||
|
||||
## 🎯 **Phase 1 Complete: Foundation & Infrastructure**
|
||||
|
||||
**Implementation Period**: Weeks 1-2
|
||||
**Status**: ✅ **COMPLETED**
|
||||
**Date**: December 2024
|
||||
|
||||
---
|
||||
|
||||
## 📊 **Phase 1 Deliverables Achieved**
|
||||
|
||||
### ✅ **1.1 Database Schema Enhancement**
|
||||
|
||||
**Enhanced Database Schema with 30+ Strategic Input Fields**
|
||||
|
||||
- **EnhancedContentStrategy Model**: Complete with 30+ strategic input fields
|
||||
- Business Context (8 inputs): business_objectives, target_metrics, content_budget, team_size, implementation_timeline, market_share, competitive_position, performance_metrics
|
||||
- Audience Intelligence (6 inputs): content_preferences, consumption_patterns, audience_pain_points, buying_journey, seasonal_trends, engagement_metrics
|
||||
- Competitive Intelligence (5 inputs): top_competitors, competitor_content_strategies, market_gaps, industry_trends, emerging_trends
|
||||
- Content Strategy (7 inputs): preferred_formats, content_mix, content_frequency, optimal_timing, quality_metrics, editorial_guidelines, brand_voice
|
||||
- Performance & Analytics (4 inputs): traffic_sources, conversion_rates, content_roi_targets, ab_testing_capabilities
|
||||
|
||||
- **EnhancedAIAnalysisResult Model**: Stores comprehensive AI analysis results
|
||||
- 5 specialized analysis types: comprehensive_strategy, audience_intelligence, competitive_intelligence, performance_optimization, content_calendar_optimization
|
||||
- Enhanced data tracking with confidence scores and quality metrics
|
||||
- Performance monitoring and processing time tracking
|
||||
|
||||
- **OnboardingDataIntegration Model**: Tracks onboarding data integration
|
||||
- Auto-population field mapping
|
||||
- Data quality scoring
|
||||
- Confidence level calculation
|
||||
- Data freshness tracking
|
||||
|
||||
### ✅ **1.2 Enhanced Strategy Service Core**
|
||||
|
||||
**Complete EnhancedStrategyService Implementation**
|
||||
|
||||
- **Core Methods**:
|
||||
- `create_enhanced_strategy()`: Create strategies with 30+ inputs
|
||||
- `get_enhanced_strategies()`: Retrieve strategies with comprehensive data
|
||||
- `_enhance_strategy_with_onboarding_data()`: Auto-populate from onboarding
|
||||
- `_generate_comprehensive_ai_recommendations()`: Generate 5 types of recommendations
|
||||
|
||||
- **Data Integration Methods**:
|
||||
- `_extract_content_preferences_from_style()`: Intelligent content preference extraction
|
||||
- `_extract_brand_voice_from_guidelines()`: Brand voice analysis
|
||||
- `_extract_editorial_guidelines_from_style()`: Editorial guidelines generation
|
||||
- `_calculate_data_quality_scores()`: Data quality assessment
|
||||
- `_calculate_confidence_levels()`: Confidence level calculation
|
||||
|
||||
- **AI Analysis Methods**:
|
||||
- `_calculate_strategic_scores()`: Strategic performance scoring
|
||||
- `_extract_market_positioning()`: Market positioning analysis
|
||||
- `_extract_competitive_advantages()`: Competitive advantage identification
|
||||
- `_extract_strategic_risks()`: Risk assessment
|
||||
- `_extract_opportunity_analysis()`: Opportunity identification
|
||||
|
||||
### ✅ **1.3 AI Prompt Implementation**
|
||||
|
||||
**5 Specialized AI Prompts Implemented**
|
||||
|
||||
1. **Comprehensive Strategy Prompt**
|
||||
- Strategic positioning and market analysis
|
||||
- Content pillar recommendations
|
||||
- Audience targeting strategies
|
||||
- Competitive differentiation opportunities
|
||||
- Implementation roadmap and timeline
|
||||
- Success metrics and KPIs
|
||||
- Risk assessment and mitigation strategies
|
||||
|
||||
2. **Audience Intelligence Prompt**
|
||||
- Audience persona development
|
||||
- Content preference analysis
|
||||
- Consumption pattern optimization
|
||||
- Pain point addressing strategies
|
||||
- Buying journey optimization
|
||||
- Seasonal content opportunities
|
||||
- Engagement improvement tactics
|
||||
|
||||
3. **Competitive Intelligence Prompt**
|
||||
- Competitor content strategy analysis
|
||||
- Market gap identification
|
||||
- Competitive advantage opportunities
|
||||
- Industry trend analysis
|
||||
- Emerging trend identification
|
||||
- Differentiation strategies
|
||||
- Partnership opportunities
|
||||
|
||||
4. **Performance Optimization Prompt**
|
||||
- Traffic source optimization
|
||||
- Conversion rate improvement
|
||||
- Content ROI enhancement
|
||||
- A/B testing strategies
|
||||
- Performance monitoring setup
|
||||
- Analytics implementation
|
||||
- Continuous improvement processes
|
||||
|
||||
5. **Content Calendar Optimization Prompt**
|
||||
- Publishing schedule optimization
|
||||
- Content mix optimization
|
||||
- Seasonal strategy development
|
||||
- Engagement calendar creation
|
||||
- Content type distribution
|
||||
- Timing optimization
|
||||
- Workflow efficiency
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ **Database Service Implementation**
|
||||
|
||||
### ✅ **EnhancedStrategyDBService**
|
||||
|
||||
**Complete Database Operations**
|
||||
|
||||
- **CRUD Operations**:
|
||||
- `create_enhanced_strategy()`: Create new enhanced strategies
|
||||
- `get_enhanced_strategy()`: Retrieve individual strategies
|
||||
- `get_enhanced_strategies_by_user()`: Get all strategies for a user
|
||||
- `update_enhanced_strategy()`: Update strategy data
|
||||
- `delete_enhanced_strategy()`: Delete strategies
|
||||
|
||||
- **Analytics Operations**:
|
||||
- `get_enhanced_strategies_with_analytics()`: Comprehensive analytics
|
||||
- `get_latest_ai_analysis()`: Latest AI analysis results
|
||||
- `get_onboarding_integration()`: Onboarding data integration
|
||||
- `get_strategy_completion_stats()`: Completion statistics
|
||||
- `get_ai_analysis_history()`: AI analysis history
|
||||
|
||||
- **Advanced Operations**:
|
||||
- `search_enhanced_strategies()`: Strategy search functionality
|
||||
- `get_strategy_export_data()`: Comprehensive data export
|
||||
- `update_strategy_ai_analysis()`: AI analysis updates
|
||||
|
||||
---
|
||||
|
||||
## 🌐 **API Routes Implementation**
|
||||
|
||||
### ✅ **Enhanced Strategy API Routes**
|
||||
|
||||
**Complete REST API Endpoints**
|
||||
|
||||
- **Core Strategy Operations**:
|
||||
- `POST /enhanced-strategy/create`: Create enhanced strategy
|
||||
- `GET /enhanced-strategy/strategies`: Get strategies with filters
|
||||
- `GET /enhanced-strategy/strategies/{strategy_id}`: Get specific strategy
|
||||
- `PUT /enhanced-strategy/strategies/{strategy_id}`: Update strategy
|
||||
- `DELETE /enhanced-strategy/strategies/{strategy_id}`: Delete strategy
|
||||
|
||||
- **Analytics & AI Operations**:
|
||||
- `GET /enhanced-strategy/strategies/{strategy_id}/analytics`: Get comprehensive analytics
|
||||
- `GET /enhanced-strategy/strategies/{strategy_id}/ai-analysis`: Get AI analysis history
|
||||
- `POST /enhanced-strategy/strategies/{strategy_id}/regenerate-ai-analysis`: Regenerate AI analysis
|
||||
|
||||
- **Completion & Integration**:
|
||||
- `GET /enhanced-strategy/strategies/{strategy_id}/completion-stats`: Get completion statistics
|
||||
- `GET /enhanced-strategy/users/{user_id}/completion-stats`: Get user completion stats
|
||||
- `GET /enhanced-strategy/strategies/{strategy_id}/onboarding-integration`: Get onboarding integration
|
||||
|
||||
- **Search & Export**:
|
||||
- `GET /enhanced-strategy/strategies/search`: Search strategies
|
||||
- `GET /enhanced-strategy/strategies/{strategy_id}/export`: Export strategy data
|
||||
|
||||
---
|
||||
|
||||
## 🧪 **Testing & Validation**
|
||||
|
||||
### ✅ **Comprehensive Test Suite**
|
||||
|
||||
**All Phase 1 Tests Passing**
|
||||
|
||||
- **Model Tests**:
|
||||
- Enhanced strategy model creation with 30+ inputs
|
||||
- Completion percentage calculation (100% accuracy)
|
||||
- Enhanced strategy to_dict conversion
|
||||
- AI analysis result model validation
|
||||
- Onboarding integration model validation
|
||||
|
||||
- **Service Tests**:
|
||||
- Enhanced strategy service initialization (30 fields)
|
||||
- Specialized prompt creation for all 5 analysis types
|
||||
- Fallback recommendations for AI service failures
|
||||
- Data quality calculation accuracy
|
||||
- Confidence level calculation validation
|
||||
|
||||
- **AI Analysis Tests**:
|
||||
- Strategic scores calculation
|
||||
- Market positioning extraction
|
||||
- Competitive advantages extraction
|
||||
- Strategic risks extraction
|
||||
- Opportunity analysis extraction
|
||||
|
||||
---
|
||||
|
||||
## 📈 **Key Features Implemented**
|
||||
|
||||
### ✅ **Intelligent Auto-Population**
|
||||
|
||||
- **Onboarding Data Integration**: Automatically populates strategy fields from existing onboarding data
|
||||
- **Data Source Transparency**: Tracks which data sources were used for auto-population
|
||||
- **Confidence Scoring**: Calculates confidence levels for auto-populated data
|
||||
- **User Override Capability**: Allows users to modify auto-populated values
|
||||
|
||||
### ✅ **Comprehensive AI Recommendations**
|
||||
|
||||
- **5 Specialized Analysis Types**: Each with targeted prompts and recommendations
|
||||
- **Fallback Mechanisms**: Robust error handling when AI services fail
|
||||
- **Performance Monitoring**: Tracks processing time and service status
|
||||
- **Quality Scoring**: Measures recommendation quality and confidence
|
||||
|
||||
### ✅ **Strategic Input Management**
|
||||
|
||||
- **30+ Strategic Inputs**: Comprehensive coverage of content strategy requirements
|
||||
- **Progressive Disclosure**: Organized into logical categories for better UX
|
||||
- **Completion Tracking**: Real-time completion percentage calculation
|
||||
- **Data Validation**: Comprehensive validation for all input fields
|
||||
|
||||
---
|
||||
|
||||
## 🚀 **Performance Metrics**
|
||||
|
||||
### ✅ **Phase 1 Success Metrics**
|
||||
|
||||
- **Input Completeness**: 100% completion rate achieved in testing
|
||||
- **AI Accuracy**: Fallback mechanisms ensure 100% availability
|
||||
- **Performance**: <2 second response time for all operations
|
||||
- **User Experience**: Progressive disclosure reduces complexity
|
||||
|
||||
### ✅ **Technical Achievements**
|
||||
|
||||
- **Database Schema**: Enhanced with 30+ strategic input fields
|
||||
- **Service Architecture**: Modular, scalable, and maintainable
|
||||
- **API Design**: RESTful endpoints with comprehensive functionality
|
||||
- **Error Handling**: Robust error handling and fallback mechanisms
|
||||
|
||||
---
|
||||
|
||||
## 🎯 **Next Steps: Phase 2**
|
||||
|
||||
**Phase 2 Focus: User Experience & Frontend Integration**
|
||||
|
||||
1. **Enhanced Input System**
|
||||
- Progressive input disclosure
|
||||
- Comprehensive tooltip system
|
||||
- Smart defaults and auto-population
|
||||
- Input validation and guidance
|
||||
|
||||
2. **Frontend Component Development**
|
||||
- Strategy dashboard components
|
||||
- Data visualization components
|
||||
- Interactive components
|
||||
- Progress tracking system
|
||||
|
||||
3. **Data Mapping & Integration**
|
||||
- API response structure optimization
|
||||
- Frontend-backend data mapping
|
||||
- State management implementation
|
||||
- Real-time data synchronization
|
||||
|
||||
---
|
||||
|
||||
## ✅ **Phase 1 Conclusion**
|
||||
|
||||
**Phase 1 has been successfully completed with all deliverables achieved:**
|
||||
|
||||
- ✅ Enhanced database schema with 30+ input fields
|
||||
- ✅ Enhanced Strategy Service core implementation
|
||||
- ✅ 5 specialized AI prompt implementations
|
||||
- ✅ Onboarding data integration
|
||||
- ✅ Comprehensive AI recommendations
|
||||
- ✅ Complete API routes and database services
|
||||
- ✅ Comprehensive test suite with 100% pass rate
|
||||
|
||||
**The enhanced strategy service now provides a solid foundation for the subsequent content calendar phase and delivers significant value through improved personalization, comprehensiveness, and intelligent data integration.**
|
||||
|
||||
---
|
||||
|
||||
**Implementation Team**: AI Assistant
|
||||
**Review Date**: December 2024
|
||||
**Status**: ✅ **PHASE 1 COMPLETE**
|
||||
@@ -17,6 +17,12 @@ from .enhanced_strategy_routes import router as enhanced_strategy_router
|
||||
# Import content strategy routes
|
||||
from .content_strategy.routes import router as content_strategy_router
|
||||
|
||||
# Import monitoring routes
|
||||
from ..monitoring_routes import router as monitoring_router
|
||||
|
||||
# Import quality analysis routes
|
||||
from ..quality_analysis_routes import router as quality_analysis_router
|
||||
|
||||
# Create main router
|
||||
router = APIRouter(prefix="/api/content-planning", tags=["content-planning"])
|
||||
|
||||
@@ -34,6 +40,12 @@ router.include_router(enhanced_strategy_router, prefix="/enhanced-strategies")
|
||||
# Include content strategy routes
|
||||
router.include_router(content_strategy_router)
|
||||
|
||||
# Include monitoring routes
|
||||
router.include_router(monitoring_router)
|
||||
|
||||
# Include quality analysis routes
|
||||
router.include_router(quality_analysis_router)
|
||||
|
||||
# Add health check endpoint
|
||||
@router.get("/health")
|
||||
async def content_planning_health_check():
|
||||
|
||||
@@ -94,7 +94,7 @@ async def check_ai_services_health():
|
||||
|
||||
# Test Gemini provider
|
||||
try:
|
||||
from llm_providers.gemini_provider import get_gemini_api_key
|
||||
from services.llm_providers.gemini_provider import get_gemini_api_key
|
||||
api_key = get_gemini_api_key()
|
||||
if api_key:
|
||||
health_status["services"]["gemini_provider"] = True
|
||||
|
||||
695
backend/api/content_planning/monitoring_routes.py
Normal file
695
backend/api/content_planning/monitoring_routes.py
Normal file
@@ -0,0 +1,695 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query
|
||||
from typing import Dict, Any
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, desc
|
||||
import json
|
||||
|
||||
from services.monitoring_plan_generator import MonitoringPlanGenerator
|
||||
from services.strategy_service import StrategyService
|
||||
from services.database import get_db
|
||||
from models.monitoring_models import (
|
||||
StrategyMonitoringPlan, MonitoringTask, TaskExecutionLog,
|
||||
StrategyPerformanceMetrics, StrategyActivationStatus
|
||||
)
|
||||
from models.enhanced_strategy_models import EnhancedContentStrategy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/strategy", tags=["strategy-monitoring"])
|
||||
|
||||
@router.post("/{strategy_id}/generate-monitoring-plan")
|
||||
async def generate_monitoring_plan(strategy_id: int):
|
||||
"""Generate monitoring plan for a strategy"""
|
||||
try:
|
||||
generator = MonitoringPlanGenerator()
|
||||
plan = await generator.generate_monitoring_plan(strategy_id)
|
||||
|
||||
logger.info(f"Successfully generated monitoring plan for strategy {strategy_id}")
|
||||
return {
|
||||
"success": True,
|
||||
"data": plan,
|
||||
"message": "Monitoring plan generated successfully"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating monitoring plan for strategy {strategy_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to generate monitoring plan: {str(e)}"
|
||||
)
|
||||
|
||||
@router.post("/{strategy_id}/activate-with-monitoring")
|
||||
async def activate_strategy_with_monitoring(
|
||||
strategy_id: int,
|
||||
monitoring_plan: Dict[str, Any]
|
||||
):
|
||||
"""Activate strategy with monitoring plan"""
|
||||
try:
|
||||
strategy_service = StrategyService()
|
||||
|
||||
# Activate strategy
|
||||
activation_success = await strategy_service.activate_strategy(strategy_id)
|
||||
if not activation_success:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Failed to activate strategy {strategy_id}"
|
||||
)
|
||||
|
||||
# Save monitoring plan
|
||||
plan_success = await strategy_service.save_monitoring_plan(strategy_id, monitoring_plan)
|
||||
if not plan_success:
|
||||
logger.warning(f"Failed to save monitoring plan for strategy {strategy_id}")
|
||||
|
||||
logger.info(f"Successfully activated strategy {strategy_id} with monitoring")
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Strategy activated with monitoring successfully",
|
||||
"strategy_id": strategy_id
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error activating strategy {strategy_id} with monitoring: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to activate strategy with monitoring: {str(e)}"
|
||||
)
|
||||
|
||||
@router.get("/{strategy_id}/monitoring-plan")
|
||||
async def get_monitoring_plan(strategy_id: int):
|
||||
"""Get monitoring plan for a strategy"""
|
||||
try:
|
||||
strategy_service = StrategyService()
|
||||
monitoring_plan = await strategy_service.get_monitoring_plan(strategy_id)
|
||||
|
||||
if monitoring_plan:
|
||||
return {
|
||||
"success": True,
|
||||
"data": monitoring_plan
|
||||
}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Monitoring plan not found for strategy {strategy_id}"
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting monitoring plan for strategy {strategy_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to get monitoring plan: {str(e)}"
|
||||
)
|
||||
|
||||
@router.get("/{strategy_id}/performance-history")
|
||||
async def get_strategy_performance_history(strategy_id: int, days: int = 30):
|
||||
"""Get performance history for a strategy"""
|
||||
try:
|
||||
strategy_service = StrategyService()
|
||||
performance_history = await strategy_service.get_strategy_performance_history(strategy_id, days)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"strategy_id": strategy_id,
|
||||
"performance_history": performance_history,
|
||||
"days": days
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting performance history for strategy {strategy_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to get performance history: {str(e)}"
|
||||
)
|
||||
|
||||
@router.post("/{strategy_id}/deactivate")
|
||||
async def deactivate_strategy(strategy_id: int, user_id: int = 1):
|
||||
"""Deactivate a strategy"""
|
||||
try:
|
||||
strategy_service = StrategyService()
|
||||
success = await strategy_service.deactivate_strategy(strategy_id, user_id)
|
||||
|
||||
if success:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Strategy {strategy_id} deactivated successfully"
|
||||
}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Failed to deactivate strategy {strategy_id}"
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error deactivating strategy {strategy_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to deactivate strategy: {str(e)}"
|
||||
)
|
||||
|
||||
@router.post("/{strategy_id}/pause")
|
||||
async def pause_strategy(strategy_id: int, user_id: int = 1):
|
||||
"""Pause a strategy"""
|
||||
try:
|
||||
strategy_service = StrategyService()
|
||||
success = await strategy_service.pause_strategy(strategy_id, user_id)
|
||||
|
||||
if success:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Strategy {strategy_id} paused successfully"
|
||||
}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Failed to pause strategy {strategy_id}"
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error pausing strategy {strategy_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to pause strategy: {str(e)}"
|
||||
)
|
||||
|
||||
@router.post("/{strategy_id}/resume")
|
||||
async def resume_strategy(strategy_id: int, user_id: int = 1):
|
||||
"""Resume a paused strategy"""
|
||||
try:
|
||||
strategy_service = StrategyService()
|
||||
success = await strategy_service.resume_strategy(strategy_id, user_id)
|
||||
|
||||
if success:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Strategy {strategy_id} resumed successfully"
|
||||
}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Failed to resume strategy {strategy_id}"
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error resuming strategy {strategy_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to resume strategy: {str(e)}"
|
||||
)
|
||||
|
||||
@router.get("/{strategy_id}/performance-metrics")
|
||||
async def get_performance_metrics(
|
||||
strategy_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get performance metrics for a strategy
|
||||
"""
|
||||
try:
|
||||
# For now, return mock data - in real implementation, this would query the database
|
||||
mock_metrics = {
|
||||
"traffic_growth_percentage": 15.7,
|
||||
"engagement_rate_percentage": 8.3,
|
||||
"conversion_rate_percentage": 2.1,
|
||||
"roi_ratio": 3.2,
|
||||
"strategy_adoption_rate": 85,
|
||||
"content_quality_score": 92,
|
||||
"competitive_position_rank": 3,
|
||||
"audience_growth_percentage": 12.5,
|
||||
"confidence_score": 88,
|
||||
"last_updated": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": mock_metrics,
|
||||
"message": "Performance metrics retrieved successfully"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting performance metrics: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/{strategy_id}/trend-data")
|
||||
async def get_trend_data(
|
||||
strategy_id: int,
|
||||
time_range: str = Query("30d", description="Time range: 7d, 30d, 90d, 1y"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get trend data for a strategy over time
|
||||
"""
|
||||
try:
|
||||
# Mock trend data - in real implementation, this would query the database
|
||||
mock_trend_data = [
|
||||
{"date": "2024-01-01", "traffic_growth": 5.2, "engagement_rate": 6.1, "conversion_rate": 1.8, "content_quality_score": 85, "strategy_adoption_rate": 70},
|
||||
{"date": "2024-01-08", "traffic_growth": 7.8, "engagement_rate": 7.2, "conversion_rate": 2.0, "content_quality_score": 87, "strategy_adoption_rate": 75},
|
||||
{"date": "2024-01-15", "traffic_growth": 9.1, "engagement_rate": 7.8, "conversion_rate": 2.1, "content_quality_score": 89, "strategy_adoption_rate": 78},
|
||||
{"date": "2024-01-22", "traffic_growth": 11.3, "engagement_rate": 8.1, "conversion_rate": 2.0, "content_quality_score": 90, "strategy_adoption_rate": 82},
|
||||
{"date": "2024-01-29", "traffic_growth": 12.7, "engagement_rate": 8.3, "conversion_rate": 2.1, "content_quality_score": 91, "strategy_adoption_rate": 85},
|
||||
{"date": "2024-02-05", "traffic_growth": 14.2, "engagement_rate": 8.5, "conversion_rate": 2.2, "content_quality_score": 92, "strategy_adoption_rate": 87},
|
||||
{"date": "2024-02-12", "traffic_growth": 15.7, "engagement_rate": 8.3, "conversion_rate": 2.1, "content_quality_score": 92, "strategy_adoption_rate": 85}
|
||||
]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": mock_trend_data,
|
||||
"message": "Trend data retrieved successfully"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting trend data: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/{strategy_id}/test-transparency")
|
||||
async def test_transparency_endpoint(
|
||||
strategy_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Simple test endpoint to check if transparency data endpoint works
|
||||
"""
|
||||
try:
|
||||
# Check if strategy exists
|
||||
strategy = db.query(EnhancedContentStrategy).filter(
|
||||
EnhancedContentStrategy.id == strategy_id
|
||||
).first()
|
||||
|
||||
if not strategy:
|
||||
return {
|
||||
"success": False,
|
||||
"data": None,
|
||||
"message": f"Strategy with ID {strategy_id} not found"
|
||||
}
|
||||
|
||||
# Get monitoring plan
|
||||
monitoring_plan = db.query(StrategyMonitoringPlan).filter(
|
||||
StrategyMonitoringPlan.strategy_id == strategy_id
|
||||
).first()
|
||||
|
||||
# Get monitoring tasks count
|
||||
tasks_count = db.query(MonitoringTask).filter(
|
||||
MonitoringTask.strategy_id == strategy_id
|
||||
).count()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"strategy_id": strategy_id,
|
||||
"strategy_name": strategy.strategy_name if hasattr(strategy, 'strategy_name') else "Unknown",
|
||||
"monitoring_plan_exists": monitoring_plan is not None,
|
||||
"tasks_count": tasks_count
|
||||
},
|
||||
"message": "Test endpoint working"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in test endpoint: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"data": None,
|
||||
"message": f"Error: {str(e)}"
|
||||
}
|
||||
|
||||
@router.get("/{strategy_id}/monitoring-tasks")
|
||||
async def get_monitoring_tasks(
|
||||
strategy_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get all monitoring tasks for a strategy with their execution status
|
||||
"""
|
||||
try:
|
||||
# Check if strategy exists
|
||||
strategy = db.query(EnhancedContentStrategy).filter(
|
||||
EnhancedContentStrategy.id == strategy_id
|
||||
).first()
|
||||
|
||||
if not strategy:
|
||||
raise HTTPException(status_code=404, detail="Strategy not found")
|
||||
|
||||
# Get monitoring tasks with execution logs
|
||||
tasks = db.query(MonitoringTask).filter(
|
||||
MonitoringTask.strategy_id == strategy_id
|
||||
).all()
|
||||
|
||||
tasks_data = []
|
||||
for task in tasks:
|
||||
# Get latest execution log
|
||||
latest_log = db.query(TaskExecutionLog).filter(
|
||||
TaskExecutionLog.task_id == task.id
|
||||
).order_by(desc(TaskExecutionLog.execution_date)).first()
|
||||
|
||||
task_data = {
|
||||
"id": task.id,
|
||||
"title": task.task_title,
|
||||
"description": task.task_description,
|
||||
"assignee": task.assignee,
|
||||
"frequency": task.frequency,
|
||||
"metric": task.metric,
|
||||
"measurementMethod": task.measurement_method,
|
||||
"successCriteria": task.success_criteria,
|
||||
"alertThreshold": task.alert_threshold,
|
||||
"actionableInsights": getattr(task, 'actionable_insights', None),
|
||||
"status": "active", # This would be determined by task execution status
|
||||
"lastExecuted": latest_log.execution_date.isoformat() if latest_log else None,
|
||||
"executionCount": db.query(TaskExecutionLog).filter(
|
||||
TaskExecutionLog.task_id == task.id
|
||||
).count()
|
||||
}
|
||||
tasks_data.append(task_data)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": tasks_data,
|
||||
"message": "Monitoring tasks retrieved successfully"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving monitoring tasks: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@router.get("/{strategy_id}/data-freshness")
|
||||
async def get_data_freshness(
|
||||
strategy_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get data freshness information for all metrics
|
||||
"""
|
||||
try:
|
||||
# Check if strategy exists
|
||||
strategy = db.query(EnhancedContentStrategy).filter(
|
||||
EnhancedContentStrategy.id == strategy_id
|
||||
).first()
|
||||
|
||||
if not strategy:
|
||||
raise HTTPException(status_code=404, detail="Strategy not found")
|
||||
|
||||
# Get latest task execution logs
|
||||
latest_logs = db.query(TaskExecutionLog).join(MonitoringTask).filter(
|
||||
MonitoringTask.strategy_id == strategy_id
|
||||
).order_by(desc(TaskExecutionLog.execution_date)).limit(10).all()
|
||||
|
||||
# Get performance metrics
|
||||
performance_metrics = db.query(StrategyPerformanceMetrics).filter(
|
||||
StrategyPerformanceMetrics.strategy_id == strategy_id
|
||||
).order_by(desc(StrategyPerformanceMetrics.created_at)).first()
|
||||
|
||||
freshness_data = {
|
||||
"lastUpdated": latest_logs[0].execution_date.isoformat() if latest_logs else datetime.now().isoformat(),
|
||||
"updateFrequency": "Every 4 hours",
|
||||
"dataSource": "Multiple Analytics APIs + AI Analysis",
|
||||
"confidence": 90,
|
||||
"metrics": [
|
||||
{
|
||||
"name": "Traffic Growth",
|
||||
"lastUpdated": latest_logs[0].execution_date.isoformat() if latest_logs else datetime.now().isoformat(),
|
||||
"updateFrequency": "Every 4 hours",
|
||||
"dataSource": "Google Analytics + AI Analysis",
|
||||
"confidence": 92
|
||||
},
|
||||
{
|
||||
"name": "Engagement Rate",
|
||||
"lastUpdated": latest_logs[0].execution_date.isoformat() if latest_logs else datetime.now().isoformat(),
|
||||
"updateFrequency": "Every 2 hours",
|
||||
"dataSource": "Social Media Analytics + Website Analytics",
|
||||
"confidence": 88
|
||||
},
|
||||
{
|
||||
"name": "Conversion Rate",
|
||||
"lastUpdated": latest_logs[0].execution_date.isoformat() if latest_logs else datetime.now().isoformat(),
|
||||
"updateFrequency": "Every 6 hours",
|
||||
"dataSource": "Google Analytics + CRM Data",
|
||||
"confidence": 85
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": freshness_data,
|
||||
"message": "Data freshness information retrieved successfully"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving data freshness: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@router.get("/{strategy_id}/transparency-data")
|
||||
async def get_transparency_data(
|
||||
strategy_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get comprehensive transparency data for a strategy including:
|
||||
- Data freshness information
|
||||
- Measurement methodology
|
||||
- AI monitoring tasks
|
||||
- Strategy mapping
|
||||
- AI insights
|
||||
"""
|
||||
try:
|
||||
# Check if strategy exists
|
||||
strategy = db.query(EnhancedContentStrategy).filter(
|
||||
EnhancedContentStrategy.id == strategy_id
|
||||
).first()
|
||||
|
||||
if not strategy:
|
||||
return {
|
||||
"success": False,
|
||||
"data": None,
|
||||
"message": f"Strategy with ID {strategy_id} not found"
|
||||
}
|
||||
|
||||
# Get monitoring plan and tasks
|
||||
monitoring_plan = db.query(StrategyMonitoringPlan).filter(
|
||||
StrategyMonitoringPlan.strategy_id == strategy_id
|
||||
).first()
|
||||
|
||||
if not monitoring_plan:
|
||||
return {
|
||||
"success": False,
|
||||
"data": None,
|
||||
"message": "No monitoring plan found for this strategy"
|
||||
}
|
||||
|
||||
# Get all monitoring tasks
|
||||
monitoring_tasks = db.query(MonitoringTask).filter(
|
||||
MonitoringTask.strategy_id == strategy_id
|
||||
).all()
|
||||
|
||||
# Get task execution logs for data freshness
|
||||
task_logs = db.query(TaskExecutionLog).join(MonitoringTask).filter(
|
||||
MonitoringTask.strategy_id == strategy_id
|
||||
).order_by(desc(TaskExecutionLog.execution_date)).all()
|
||||
|
||||
# Get performance metrics for current values
|
||||
performance_metrics = db.query(StrategyPerformanceMetrics).filter(
|
||||
StrategyPerformanceMetrics.strategy_id == strategy_id
|
||||
).order_by(desc(StrategyPerformanceMetrics.created_at)).first()
|
||||
|
||||
# Build transparency data
|
||||
transparency_data = []
|
||||
|
||||
# Traffic Growth Metric
|
||||
traffic_growth_data = {
|
||||
"metricName": "Traffic Growth",
|
||||
"currentValue": 15.7, # This would come from actual analytics
|
||||
"unit": "%",
|
||||
"dataFreshness": {
|
||||
"lastUpdated": task_logs[0].execution_date.isoformat() if task_logs else datetime.now().isoformat(),
|
||||
"updateFrequency": "Every 4 hours",
|
||||
"dataSource": "Google Analytics + AI Analysis",
|
||||
"confidence": 92
|
||||
},
|
||||
"measurementMethodology": {
|
||||
"description": "Organic traffic growth compared to previous period",
|
||||
"calculationMethod": "Percentage change in organic sessions over 30-day rolling period, weighted by content performance and user engagement",
|
||||
"dataPoints": ["Organic Sessions", "Page Views", "Bounce Rate", "Time on Site", "Content Performance"],
|
||||
"validationProcess": "Cross-validated with Google Search Console data and AI-powered content performance analysis"
|
||||
},
|
||||
"monitoringTasks": [],
|
||||
"strategyMapping": {
|
||||
"relatedComponents": ["Strategic Insights", "Content Strategy", "Audience Analysis"],
|
||||
"impactAreas": ["Brand Awareness", "Lead Generation", "Market Reach"],
|
||||
"dependencies": ["SEO Optimization", "Content Quality", "User Experience"]
|
||||
},
|
||||
"aiInsights": {
|
||||
"trendAnalysis": "Traffic growth shows strong upward trend with 15.7% increase. Top-performing content categories are educational blog posts and case studies.",
|
||||
"recommendations": [
|
||||
"Increase content production in educational blog category by 25%",
|
||||
"Optimize case study content for better search visibility",
|
||||
"Implement A/B testing for content headlines",
|
||||
"Focus on long-form content (2000+ words) which shows 40% higher engagement"
|
||||
],
|
||||
"riskFactors": ["Seasonal traffic fluctuations", "Competitor content strategy changes", "Algorithm updates"],
|
||||
"opportunities": ["Video content expansion", "Guest posting opportunities", "Social media amplification"]
|
||||
}
|
||||
}
|
||||
|
||||
# Add real monitoring tasks - map based on task content and purpose
|
||||
for task in monitoring_tasks:
|
||||
task_title_lower = task.task_title.lower()
|
||||
task_description_lower = task.task_description.lower()
|
||||
|
||||
# Traffic Growth related tasks
|
||||
if any(keyword in task_title_lower or keyword in task_description_lower
|
||||
for keyword in ['traffic', 'organic', 'goal', 'strategic', 'performance', 'prediction']):
|
||||
task_data = {
|
||||
"title": task.task_title,
|
||||
"description": task.task_description,
|
||||
"assignee": task.assignee,
|
||||
"frequency": task.frequency,
|
||||
"metric": task.metric,
|
||||
"measurementMethod": task.measurement_method,
|
||||
"successCriteria": task.success_criteria,
|
||||
"alertThreshold": task.alert_threshold,
|
||||
"actionableInsights": getattr(task, 'actionable_insights', None),
|
||||
"status": "active",
|
||||
"lastExecuted": task_logs[0].execution_date.isoformat() if task_logs else None
|
||||
}
|
||||
traffic_growth_data["monitoringTasks"].append(task_data)
|
||||
|
||||
transparency_data.append(traffic_growth_data)
|
||||
|
||||
# Engagement Rate Metric
|
||||
engagement_data = {
|
||||
"metricName": "Engagement Rate",
|
||||
"currentValue": 8.3,
|
||||
"unit": "%",
|
||||
"dataFreshness": {
|
||||
"lastUpdated": task_logs[0].execution_date.isoformat() if task_logs else datetime.now().isoformat(),
|
||||
"updateFrequency": "Every 2 hours",
|
||||
"dataSource": "Social Media Analytics + Website Analytics",
|
||||
"confidence": 88
|
||||
},
|
||||
"measurementMethodology": {
|
||||
"description": "Average engagement rate across all content and social media",
|
||||
"calculationMethod": "Weighted average of likes, shares, comments, and time spent across all platforms",
|
||||
"dataPoints": ["Social Media Engagement", "Website Comments", "Time on Page", "Social Shares", "Email Engagement"],
|
||||
"validationProcess": "Cross-platform validation using multiple analytics tools and AI sentiment analysis"
|
||||
},
|
||||
"monitoringTasks": [],
|
||||
"strategyMapping": {
|
||||
"relatedComponents": ["Audience Analysis", "Content Strategy", "Social Media Strategy"],
|
||||
"impactAreas": ["Brand Engagement", "Community Building", "Customer Loyalty"],
|
||||
"dependencies": ["Content Quality", "Social Media Presence", "Community Management"]
|
||||
},
|
||||
"aiInsights": {
|
||||
"trendAnalysis": "Engagement rate is stable at 8.3% with peak engagement during lunch hours and early evenings.",
|
||||
"recommendations": [
|
||||
"Increase video content production by 50%",
|
||||
"Optimize posting times for peak engagement hours",
|
||||
"Implement interactive content elements",
|
||||
"Focus on community-building content"
|
||||
],
|
||||
"riskFactors": ["Platform algorithm changes", "Content fatigue", "Competition for attention"],
|
||||
"opportunities": ["Live streaming opportunities", "User-generated content campaigns", "Influencer collaborations"]
|
||||
}
|
||||
}
|
||||
|
||||
# Add engagement-related tasks
|
||||
for task in monitoring_tasks:
|
||||
task_title_lower = task.task_title.lower()
|
||||
task_description_lower = task.task_description.lower()
|
||||
|
||||
if any(keyword in task_title_lower or keyword in task_description_lower
|
||||
for keyword in ['engagement', 'social', 'community', 'audience', 'insight', 'competitive']):
|
||||
task_data = {
|
||||
"title": task.task_title,
|
||||
"description": task.task_description,
|
||||
"assignee": task.assignee,
|
||||
"frequency": task.frequency,
|
||||
"metric": task.metric,
|
||||
"measurementMethod": task.measurement_method,
|
||||
"successCriteria": task.success_criteria,
|
||||
"alertThreshold": task.alert_threshold,
|
||||
"actionableInsights": getattr(task, 'actionable_insights', None),
|
||||
"status": "active",
|
||||
"lastExecuted": task_logs[0].execution_date.isoformat() if task_logs else None
|
||||
}
|
||||
engagement_data["monitoringTasks"].append(task_data)
|
||||
|
||||
transparency_data.append(engagement_data)
|
||||
|
||||
# Conversion Rate Metric
|
||||
conversion_data = {
|
||||
"metricName": "Conversion Rate",
|
||||
"currentValue": 2.1,
|
||||
"unit": "%",
|
||||
"dataFreshness": {
|
||||
"lastUpdated": task_logs[0].execution_date.isoformat() if task_logs else datetime.now().isoformat(),
|
||||
"updateFrequency": "Every 6 hours",
|
||||
"dataSource": "Google Analytics + CRM Data",
|
||||
"confidence": 85
|
||||
},
|
||||
"measurementMethodology": {
|
||||
"description": "Content-driven conversion rate across all touchpoints",
|
||||
"calculationMethod": "Conversions divided by total visitors, weighted by content attribution and customer journey analysis",
|
||||
"dataPoints": ["Website Conversions", "Email Signups", "Lead Form Submissions", "Content Downloads", "Sales Attribution"],
|
||||
"validationProcess": "CRM integration validation and conversion funnel analysis"
|
||||
},
|
||||
"monitoringTasks": [],
|
||||
"strategyMapping": {
|
||||
"relatedComponents": ["Performance Predictions", "Implementation Roadmap", "Risk Assessment"],
|
||||
"impactAreas": ["Revenue Generation", "Lead Quality", "Customer Acquisition"],
|
||||
"dependencies": ["Content Quality", "User Experience", "Lead Nurturing"]
|
||||
},
|
||||
"aiInsights": {
|
||||
"trendAnalysis": "Conversion rate is improving steadily with 2.1% current rate. Top-converting content includes case studies and product demos.",
|
||||
"recommendations": [
|
||||
"Increase case study and demo content production",
|
||||
"Optimize mobile user experience further",
|
||||
"Implement personalized content recommendations",
|
||||
"A/B test call-to-action buttons and forms"
|
||||
],
|
||||
"riskFactors": ["Market competition", "Economic factors", "Technology changes"],
|
||||
"opportunities": ["Personalization opportunities", "Automation implementation", "Cross-selling strategies"]
|
||||
}
|
||||
}
|
||||
|
||||
# Add conversion-related tasks
|
||||
for task in monitoring_tasks:
|
||||
task_title_lower = task.task_title.lower()
|
||||
task_description_lower = task.task_description.lower()
|
||||
|
||||
if any(keyword in task_title_lower or keyword in task_description_lower
|
||||
for keyword in ['conversion', 'funnel', 'implementation', 'resource', 'risk', 'mitigation']):
|
||||
task_data = {
|
||||
"title": task.task_title,
|
||||
"description": task.task_description,
|
||||
"assignee": task.assignee,
|
||||
"frequency": task.frequency,
|
||||
"metric": task.metric,
|
||||
"measurementMethod": task.measurement_method,
|
||||
"successCriteria": task.success_criteria,
|
||||
"alertThreshold": task.alert_threshold,
|
||||
"actionableInsights": getattr(task, 'actionable_insights', None),
|
||||
"status": "active",
|
||||
"lastExecuted": task_logs[0].execution_date.isoformat() if task_logs else None
|
||||
}
|
||||
conversion_data["monitoringTasks"].append(task_data)
|
||||
|
||||
transparency_data.append(conversion_data)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": transparency_data,
|
||||
"message": "Transparency data retrieved successfully"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving transparency data: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"data": None,
|
||||
"message": f"Error: {str(e)}"
|
||||
}
|
||||
458
backend/api/content_planning/quality_analysis_routes.py
Normal file
458
backend/api/content_planning/quality_analysis_routes.py
Normal file
@@ -0,0 +1,458 @@
|
||||
"""
|
||||
Quality Analysis API Routes
|
||||
Provides endpoints for AI-powered quality assessment and recommendations.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query
|
||||
from typing import Dict, Any, List
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from services.ai_quality_analysis_service import AIQualityAnalysisService, QualityAnalysisResult
|
||||
from services.database import get_db
|
||||
from models.enhanced_strategy_models import EnhancedContentStrategy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/quality-analysis", tags=["quality-analysis"])
|
||||
|
||||
@router.post("/{strategy_id}/analyze")
|
||||
async def analyze_strategy_quality(
|
||||
strategy_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Analyze strategy quality using AI and return comprehensive results."""
|
||||
try:
|
||||
# Check if strategy exists
|
||||
strategy = db.query(EnhancedContentStrategy).filter(
|
||||
EnhancedContentStrategy.id == strategy_id
|
||||
).first()
|
||||
|
||||
if not strategy:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Strategy with ID {strategy_id} not found"
|
||||
)
|
||||
|
||||
# Initialize quality analysis service
|
||||
quality_service = AIQualityAnalysisService()
|
||||
|
||||
# Perform quality analysis
|
||||
analysis_result = await quality_service.analyze_strategy_quality(strategy_id)
|
||||
|
||||
# Convert result to dictionary for API response
|
||||
result_dict = {
|
||||
"strategy_id": analysis_result.strategy_id,
|
||||
"overall_score": analysis_result.overall_score,
|
||||
"overall_status": analysis_result.overall_status.value,
|
||||
"confidence_score": analysis_result.confidence_score,
|
||||
"analysis_timestamp": analysis_result.analysis_timestamp.isoformat(),
|
||||
"metrics": [
|
||||
{
|
||||
"name": metric.name,
|
||||
"score": metric.score,
|
||||
"weight": metric.weight,
|
||||
"status": metric.status.value,
|
||||
"description": metric.description,
|
||||
"recommendations": metric.recommendations
|
||||
}
|
||||
for metric in analysis_result.metrics
|
||||
],
|
||||
"recommendations": analysis_result.recommendations
|
||||
}
|
||||
|
||||
logger.info(f"Quality analysis completed for strategy {strategy_id}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": result_dict,
|
||||
"message": "Quality analysis completed successfully"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing strategy quality for {strategy_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to analyze strategy quality: {str(e)}"
|
||||
)
|
||||
|
||||
@router.get("/{strategy_id}/metrics")
|
||||
async def get_quality_metrics(
|
||||
strategy_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get quality metrics for a strategy."""
|
||||
try:
|
||||
# Check if strategy exists
|
||||
strategy = db.query(EnhancedContentStrategy).filter(
|
||||
EnhancedContentStrategy.id == strategy_id
|
||||
).first()
|
||||
|
||||
if not strategy:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Strategy with ID {strategy_id} not found"
|
||||
)
|
||||
|
||||
# Initialize quality analysis service
|
||||
quality_service = AIQualityAnalysisService()
|
||||
|
||||
# Perform quick quality analysis (cached if available)
|
||||
analysis_result = await quality_service.analyze_strategy_quality(strategy_id)
|
||||
|
||||
# Return metrics in a simplified format
|
||||
metrics_data = [
|
||||
{
|
||||
"name": metric.name,
|
||||
"score": metric.score,
|
||||
"status": metric.status.value,
|
||||
"description": metric.description
|
||||
}
|
||||
for metric in analysis_result.metrics
|
||||
]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"strategy_id": strategy_id,
|
||||
"overall_score": analysis_result.overall_score,
|
||||
"overall_status": analysis_result.overall_status.value,
|
||||
"metrics": metrics_data,
|
||||
"last_updated": analysis_result.analysis_timestamp.isoformat()
|
||||
},
|
||||
"message": "Quality metrics retrieved successfully"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting quality metrics for {strategy_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to get quality metrics: {str(e)}"
|
||||
)
|
||||
|
||||
@router.get("/{strategy_id}/recommendations")
|
||||
async def get_quality_recommendations(
|
||||
strategy_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get AI-powered quality improvement recommendations."""
|
||||
try:
|
||||
# Check if strategy exists
|
||||
strategy = db.query(EnhancedContentStrategy).filter(
|
||||
EnhancedContentStrategy.id == strategy_id
|
||||
).first()
|
||||
|
||||
if not strategy:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Strategy with ID {strategy_id} not found"
|
||||
)
|
||||
|
||||
# Initialize quality analysis service
|
||||
quality_service = AIQualityAnalysisService()
|
||||
|
||||
# Perform quality analysis to get recommendations
|
||||
analysis_result = await quality_service.analyze_strategy_quality(strategy_id)
|
||||
|
||||
# Get recommendations by category
|
||||
recommendations_by_category = {}
|
||||
for metric in analysis_result.metrics:
|
||||
if metric.recommendations:
|
||||
recommendations_by_category[metric.name] = metric.recommendations
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"strategy_id": strategy_id,
|
||||
"overall_recommendations": analysis_result.recommendations,
|
||||
"recommendations_by_category": recommendations_by_category,
|
||||
"priority_areas": [
|
||||
metric.name for metric in analysis_result.metrics
|
||||
if metric.status.value in ["needs_attention", "poor"]
|
||||
],
|
||||
"last_updated": analysis_result.analysis_timestamp.isoformat()
|
||||
},
|
||||
"message": "Quality recommendations retrieved successfully"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting quality recommendations for {strategy_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to get quality recommendations: {str(e)}"
|
||||
)
|
||||
|
||||
@router.get("/{strategy_id}/history")
|
||||
async def get_quality_history(
|
||||
strategy_id: int,
|
||||
days: int = Query(30, description="Number of days to look back"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get quality analysis history for a strategy."""
|
||||
try:
|
||||
# Check if strategy exists
|
||||
strategy = db.query(EnhancedContentStrategy).filter(
|
||||
EnhancedContentStrategy.id == strategy_id
|
||||
).first()
|
||||
|
||||
if not strategy:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Strategy with ID {strategy_id} not found"
|
||||
)
|
||||
|
||||
# Initialize quality analysis service
|
||||
quality_service = AIQualityAnalysisService()
|
||||
|
||||
# Get quality history
|
||||
history = await quality_service.get_quality_history(strategy_id, days)
|
||||
|
||||
# Convert history to API format
|
||||
history_data = [
|
||||
{
|
||||
"timestamp": result.analysis_timestamp.isoformat(),
|
||||
"overall_score": result.overall_score,
|
||||
"overall_status": result.overall_status.value,
|
||||
"confidence_score": result.confidence_score
|
||||
}
|
||||
for result in history
|
||||
]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"strategy_id": strategy_id,
|
||||
"history": history_data,
|
||||
"days": days,
|
||||
"total_analyses": len(history_data)
|
||||
},
|
||||
"message": "Quality history retrieved successfully"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting quality history for {strategy_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to get quality history: {str(e)}"
|
||||
)
|
||||
|
||||
@router.get("/{strategy_id}/trends")
|
||||
async def get_quality_trends(
|
||||
strategy_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get quality trends and patterns for a strategy."""
|
||||
try:
|
||||
# Check if strategy exists
|
||||
strategy = db.query(EnhancedContentStrategy).filter(
|
||||
EnhancedContentStrategy.id == strategy_id
|
||||
).first()
|
||||
|
||||
if not strategy:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Strategy with ID {strategy_id} not found"
|
||||
)
|
||||
|
||||
# Initialize quality analysis service
|
||||
quality_service = AIQualityAnalysisService()
|
||||
|
||||
# Get quality trends
|
||||
trends = await quality_service.get_quality_trends(strategy_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"strategy_id": strategy_id,
|
||||
"trends": trends,
|
||||
"last_updated": datetime.utcnow().isoformat()
|
||||
},
|
||||
"message": "Quality trends retrieved successfully"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting quality trends for {strategy_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to get quality trends: {str(e)}"
|
||||
)
|
||||
|
||||
@router.post("/{strategy_id}/quick-assessment")
|
||||
async def quick_quality_assessment(
|
||||
strategy_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Perform a quick quality assessment without full AI analysis."""
|
||||
try:
|
||||
# Check if strategy exists
|
||||
strategy = db.query(EnhancedContentStrategy).filter(
|
||||
EnhancedContentStrategy.id == strategy_id
|
||||
).first()
|
||||
|
||||
if not strategy:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Strategy with ID {strategy_id} not found"
|
||||
)
|
||||
|
||||
# Perform quick assessment based on data completeness
|
||||
completeness_score = self._calculate_completeness_score(strategy)
|
||||
|
||||
# Determine status based on score
|
||||
if completeness_score >= 80:
|
||||
status = "excellent"
|
||||
elif completeness_score >= 65:
|
||||
status = "good"
|
||||
elif completeness_score >= 45:
|
||||
status = "needs_attention"
|
||||
else:
|
||||
status = "poor"
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"strategy_id": strategy_id,
|
||||
"completeness_score": completeness_score,
|
||||
"status": status,
|
||||
"assessment_type": "quick",
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"message": "Quick assessment completed based on data completeness"
|
||||
},
|
||||
"message": "Quick quality assessment completed"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error performing quick assessment for {strategy_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to perform quick assessment: {str(e)}"
|
||||
)
|
||||
|
||||
def _calculate_completeness_score(self, strategy: EnhancedContentStrategy) -> float:
|
||||
"""Calculate completeness score based on filled fields."""
|
||||
try:
|
||||
# Define required fields for each category
|
||||
required_fields = {
|
||||
"business_context": [
|
||||
'business_objectives', 'target_metrics', 'content_budget',
|
||||
'team_size', 'implementation_timeline', 'market_share'
|
||||
],
|
||||
"audience_intelligence": [
|
||||
'content_preferences', 'consumption_patterns', 'audience_pain_points',
|
||||
'buying_journey', 'seasonal_trends', 'engagement_metrics'
|
||||
],
|
||||
"competitive_intelligence": [
|
||||
'top_competitors', 'competitor_content_strategies', 'market_gaps',
|
||||
'industry_trends', 'emerging_trends'
|
||||
],
|
||||
"content_strategy": [
|
||||
'preferred_formats', 'content_mix', 'content_frequency',
|
||||
'optimal_timing', 'quality_metrics', 'editorial_guidelines', 'brand_voice'
|
||||
],
|
||||
"performance_analytics": [
|
||||
'traffic_sources', 'conversion_rates', 'content_roi_targets',
|
||||
'ab_testing_capabilities'
|
||||
]
|
||||
}
|
||||
|
||||
total_fields = 0
|
||||
filled_fields = 0
|
||||
|
||||
for category, fields in required_fields.items():
|
||||
total_fields += len(fields)
|
||||
for field in fields:
|
||||
if hasattr(strategy, field) and getattr(strategy, field) is not None:
|
||||
filled_fields += 1
|
||||
|
||||
if total_fields == 0:
|
||||
return 0.0
|
||||
|
||||
return (filled_fields / total_fields) * 100
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error calculating completeness score: {e}")
|
||||
return 0.0
|
||||
|
||||
@router.get("/{strategy_id}/dashboard")
|
||||
async def get_quality_dashboard(
|
||||
strategy_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get comprehensive quality dashboard data."""
|
||||
try:
|
||||
# Check if strategy exists
|
||||
strategy = db.query(EnhancedContentStrategy).filter(
|
||||
EnhancedContentStrategy.id == strategy_id
|
||||
).first()
|
||||
|
||||
if not strategy:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Strategy with ID {strategy_id} not found"
|
||||
)
|
||||
|
||||
# Initialize quality analysis service
|
||||
quality_service = AIQualityAnalysisService()
|
||||
|
||||
# Get comprehensive analysis
|
||||
analysis_result = await quality_service.analyze_strategy_quality(strategy_id)
|
||||
|
||||
# Get trends
|
||||
trends = await quality_service.get_quality_trends(strategy_id)
|
||||
|
||||
# Prepare dashboard data
|
||||
dashboard_data = {
|
||||
"strategy_id": strategy_id,
|
||||
"overall_score": analysis_result.overall_score,
|
||||
"overall_status": analysis_result.overall_status.value,
|
||||
"confidence_score": analysis_result.confidence_score,
|
||||
"metrics": [
|
||||
{
|
||||
"name": metric.name,
|
||||
"score": metric.score,
|
||||
"status": metric.status.value,
|
||||
"description": metric.description,
|
||||
"recommendations": metric.recommendations
|
||||
}
|
||||
for metric in analysis_result.metrics
|
||||
],
|
||||
"recommendations": analysis_result.recommendations,
|
||||
"trends": trends,
|
||||
"priority_areas": [
|
||||
metric.name for metric in analysis_result.metrics
|
||||
if metric.status.value in ["needs_attention", "poor"]
|
||||
],
|
||||
"strengths": [
|
||||
metric.name for metric in analysis_result.metrics
|
||||
if metric.status.value == "excellent"
|
||||
],
|
||||
"last_updated": analysis_result.analysis_timestamp.isoformat()
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": dashboard_data,
|
||||
"message": "Quality dashboard data retrieved successfully"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting quality dashboard for {strategy_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to get quality dashboard: {str(e)}"
|
||||
)
|
||||
@@ -1,22 +0,0 @@
|
||||
"""LLM Providers Service for ALwrity Backend.
|
||||
|
||||
This service handles all LLM (Language Model) provider integrations,
|
||||
migrated from the legacy lib/gpt_providers functionality.
|
||||
"""
|
||||
|
||||
from .main_text_generation import llm_text_gen
|
||||
from .openai_provider import openai_chatgpt, test_openai_api_key
|
||||
from .gemini_provider import gemini_text_response, gemini_structured_json_response, test_gemini_api_key
|
||||
from .anthropic_provider import anthropic_text_response
|
||||
from .deepseek_provider import deepseek_text_response
|
||||
|
||||
__all__ = [
|
||||
"llm_text_gen",
|
||||
"openai_chatgpt",
|
||||
"test_openai_api_key",
|
||||
"gemini_text_response",
|
||||
"gemini_structured_json_response",
|
||||
"test_gemini_api_key",
|
||||
"anthropic_text_response",
|
||||
"deepseek_text_response"
|
||||
]
|
||||
@@ -1,109 +0,0 @@
|
||||
"""Anthropic Provider Service for ALwrity Backend.
|
||||
|
||||
This service handles Anthropic Claude API integrations,
|
||||
migrated from the legacy lib/gpt_providers/text_generation/anthropic_text_gen.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import anthropic
|
||||
from typing import Tuple
|
||||
from loguru import logger
|
||||
from tenacity import (
|
||||
retry,
|
||||
stop_after_attempt,
|
||||
wait_random_exponential,
|
||||
)
|
||||
|
||||
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
|
||||
def anthropic_text_response(prompt: str, model: str = "claude-3-5-sonnet-20241022",
|
||||
temperature: float = 0.7, max_tokens: int = 4000,
|
||||
system_prompt: str = None) -> str:
|
||||
"""
|
||||
Generate text using Anthropic's Claude model.
|
||||
|
||||
Args:
|
||||
prompt (str): The input text to generate completion for.
|
||||
model (str, optional): Model to be used for the completion. Defaults to "claude-3-5-sonnet-20241022".
|
||||
temperature (float, optional): Controls randomness. Lower values make responses more deterministic. Defaults to 0.7.
|
||||
max_tokens (int, optional): Maximum number of tokens to generate. Defaults to 4000.
|
||||
system_prompt (str, optional): System prompt for the conversation. Defaults to None.
|
||||
|
||||
Returns:
|
||||
str: The generated text completion.
|
||||
|
||||
Raises:
|
||||
SystemExit: If an API error, connection error, or rate limit error occurs.
|
||||
"""
|
||||
# Wait for 5 seconds to comply with rate limits
|
||||
for _ in range(5):
|
||||
time.sleep(1)
|
||||
|
||||
try:
|
||||
# Get API key from environment
|
||||
api_key = os.getenv('ANTHROPIC_API_KEY')
|
||||
if not api_key:
|
||||
raise ValueError("Anthropic API key not found in environment variables")
|
||||
|
||||
client = anthropic.Anthropic(api_key=api_key)
|
||||
|
||||
# Prepare messages
|
||||
messages = []
|
||||
if system_prompt:
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
response = client.messages.create(
|
||||
model=model,
|
||||
messages=messages,
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature
|
||||
)
|
||||
|
||||
logger.info(f"[anthropic_text_response] Generated response with {len(response.content[0].text)} characters")
|
||||
return response.content[0].text
|
||||
|
||||
except anthropic.AuthenticationError as e:
|
||||
logger.error(f"Anthropic Authentication Error: {e}")
|
||||
raise SystemExit from e
|
||||
except anthropic.RateLimitError as e:
|
||||
logger.error(f"Anthropic Rate Limit Error: {e}")
|
||||
raise SystemExit from e
|
||||
except anthropic.APIConnectionError as e:
|
||||
logger.error(f"Anthropic API Connection Error: {e}")
|
||||
raise SystemExit from e
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in Anthropic API call: {e}")
|
||||
raise SystemExit from e
|
||||
|
||||
async def test_anthropic_api_key(api_key: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
Test if the provided Anthropic API key is valid.
|
||||
|
||||
Args:
|
||||
api_key (str): The Anthropic API key to test
|
||||
|
||||
Returns:
|
||||
tuple[bool, str]: A tuple containing (is_valid, message)
|
||||
"""
|
||||
try:
|
||||
# Create Anthropic client with the provided key
|
||||
client = anthropic.Anthropic(api_key=api_key)
|
||||
|
||||
# Try to generate a simple response as a test
|
||||
response = client.messages.create(
|
||||
model="claude-3-5-sonnet-20241022",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
max_tokens=10,
|
||||
temperature=0.1
|
||||
)
|
||||
|
||||
# If we get here, the key is valid
|
||||
return True, "Anthropic API key is valid"
|
||||
|
||||
except anthropic.AuthenticationError:
|
||||
return False, "Invalid Anthropic API key"
|
||||
except anthropic.RateLimitError:
|
||||
return False, "Rate limit exceeded. Please try again later."
|
||||
except Exception as e:
|
||||
return False, f"Error testing Anthropic API key: {str(e)}"
|
||||
@@ -1,135 +0,0 @@
|
||||
"""DeepSeek Provider Service for ALwrity Backend.
|
||||
|
||||
This service handles DeepSeek API integrations,
|
||||
migrated from the legacy lib/gpt_providers/text_generation/deepseek_text_gen.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import requests
|
||||
from typing import Tuple
|
||||
from loguru import logger
|
||||
from tenacity import (
|
||||
retry,
|
||||
stop_after_attempt,
|
||||
wait_random_exponential,
|
||||
)
|
||||
|
||||
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
|
||||
def deepseek_text_response(prompt: str, model: str = "deepseek-chat",
|
||||
temperature: float = 0.7, max_tokens: int = 4000,
|
||||
system_prompt: str = None) -> str:
|
||||
"""
|
||||
Generate text using DeepSeek's API.
|
||||
|
||||
Args:
|
||||
prompt (str): The input text to generate completion for.
|
||||
model (str, optional): Model to be used for the completion. Defaults to "deepseek-chat".
|
||||
temperature (float, optional): Controls randomness. Lower values make responses more deterministic. Defaults to 0.7.
|
||||
max_tokens (int, optional): Maximum number of tokens to generate. Defaults to 4000.
|
||||
system_prompt (str, optional): System prompt for the conversation. Defaults to None.
|
||||
|
||||
Returns:
|
||||
str: The generated text completion.
|
||||
|
||||
Raises:
|
||||
SystemExit: If an API error, connection error, or rate limit error occurs.
|
||||
"""
|
||||
# Wait for 5 seconds to comply with rate limits
|
||||
for _ in range(5):
|
||||
time.sleep(1)
|
||||
|
||||
try:
|
||||
# Get API key from environment
|
||||
api_key = os.getenv('DEEPSEEK_API_KEY')
|
||||
if not api_key:
|
||||
raise ValueError("DeepSeek API key not found in environment variables")
|
||||
|
||||
# Prepare messages
|
||||
messages = []
|
||||
if system_prompt:
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
# Make API request
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
data = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
"stream": False
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
"https://api.deepseek.com/v1/chat/completions",
|
||||
headers=headers,
|
||||
json=data,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
content = result["choices"][0]["message"]["content"]
|
||||
logger.info(f"[deepseek_text_response] Generated response with {len(content)} characters")
|
||||
return content
|
||||
else:
|
||||
error_msg = f"DeepSeek API Error: {response.status_code} - {response.text}"
|
||||
logger.error(error_msg)
|
||||
raise SystemExit(error_msg)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"DeepSeek API Connection Error: {e}")
|
||||
raise SystemExit from e
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in DeepSeek API call: {e}")
|
||||
raise SystemExit from e
|
||||
|
||||
async def test_deepseek_api_key(api_key: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
Test if the provided DeepSeek API key is valid.
|
||||
|
||||
Args:
|
||||
api_key (str): The DeepSeek API key to test
|
||||
|
||||
Returns:
|
||||
tuple[bool, str]: A tuple containing (is_valid, message)
|
||||
"""
|
||||
try:
|
||||
# Make a simple API test request
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
data = {
|
||||
"model": "deepseek-chat",
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"max_tokens": 10,
|
||||
"temperature": 0.1
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
"https://api.deepseek.com/v1/chat/completions",
|
||||
headers=headers,
|
||||
json=data,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return True, "DeepSeek API key is valid"
|
||||
elif response.status_code == 401:
|
||||
return False, "Invalid DeepSeek API key"
|
||||
elif response.status_code == 429:
|
||||
return False, "Rate limit exceeded. Please try again later."
|
||||
else:
|
||||
return False, f"Error testing DeepSeek API key: {response.status_code} - {response.text}"
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
return False, f"Connection error testing DeepSeek API key: {str(e)}"
|
||||
except Exception as e:
|
||||
return False, f"Error testing DeepSeek API key: {str(e)}"
|
||||
@@ -1,339 +0,0 @@
|
||||
# Using Gemini Pro LLM model
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
import time
|
||||
import google.genai as genai
|
||||
from google.genai import types
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(Path('../../../.env'))
|
||||
from loguru import logger
|
||||
logger.remove()
|
||||
logger.add(sys.stdout,
|
||||
colorize=True,
|
||||
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}"
|
||||
)
|
||||
from tenacity import (
|
||||
retry,
|
||||
stop_after_attempt,
|
||||
wait_random_exponential,
|
||||
)
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
|
||||
# Configure standard logging
|
||||
import logging
|
||||
logging.basicConfig(level=logging.INFO, format='[%(asctime)s-%(levelname)s-%(module)s-%(lineno)d]- %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_gemini_api_key():
|
||||
"""Get Gemini API key from API key manager or environment."""
|
||||
try:
|
||||
# Try to get from API key manager first
|
||||
from services.api_key_manager import get_api_key_manager
|
||||
api_key_manager = get_api_key_manager()
|
||||
api_key = api_key_manager.get_api_key("gemini")
|
||||
if api_key:
|
||||
return api_key
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get API key from manager: {e}")
|
||||
|
||||
# Fallback to environment variable
|
||||
api_key = os.getenv('GEMINI_API_KEY')
|
||||
if not api_key:
|
||||
raise ValueError("Gemini API key not found in environment variables or API key manager")
|
||||
|
||||
return api_key
|
||||
|
||||
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
|
||||
def gemini_text_response(prompt, temperature=0.7, top_p=0.9, n=40, max_tokens=2048, system_prompt=None):
|
||||
"""Get response from Gemini Pro Text using official SDK pattern."""
|
||||
try:
|
||||
# Get API key
|
||||
api_key = get_gemini_api_key()
|
||||
|
||||
logger.info(f"Temp: {temperature}, MaxTokens: {max_tokens}, TopP: {top_p}, N: {n}")
|
||||
|
||||
# Create the client with API key (official SDK pattern)
|
||||
client = genai.Client(api_key=api_key)
|
||||
|
||||
# Prepare content with system instruction if provided
|
||||
if system_prompt:
|
||||
# Use system instruction in generation config (official SDK pattern)
|
||||
generation_config = types.GenerateContentConfig(
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
top_k=n,
|
||||
max_output_tokens=max_tokens,
|
||||
system_instruction=system_prompt
|
||||
)
|
||||
response = client.models.generate_content(
|
||||
model="gemini-2.0-flash-001", # Using the recommended model from docs
|
||||
contents=prompt,
|
||||
config=generation_config
|
||||
)
|
||||
else:
|
||||
# Standard generation without system instruction (official SDK pattern)
|
||||
generation_config = types.GenerateContentConfig(
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
top_k=n,
|
||||
max_output_tokens=max_tokens,
|
||||
)
|
||||
response = client.models.generate_content(
|
||||
model="gemini-2.0-flash-001", # Using the recommended model from docs
|
||||
contents=prompt,
|
||||
config=generation_config
|
||||
)
|
||||
|
||||
logger.info(f"[gemini_text_response] Generated response with {len(response.text)} characters")
|
||||
return response.text
|
||||
|
||||
except Exception as err:
|
||||
logger.error(f"Failed to get response from Gemini: {err}. Retrying.")
|
||||
raise
|
||||
|
||||
def _clean_schema_for_gemini(schema):
|
||||
"""Clean schema to remove unsupported properties for Gemini API."""
|
||||
if isinstance(schema, dict):
|
||||
# Remove unsupported properties
|
||||
unsupported_props = ['additionalProperties', 'pattern', 'format', 'minLength', 'maxLength']
|
||||
cleaned = {}
|
||||
|
||||
for key, value in schema.items():
|
||||
if key not in unsupported_props:
|
||||
if isinstance(value, dict):
|
||||
cleaned_value = _clean_schema_for_gemini(value)
|
||||
# Skip empty objects or objects with empty properties
|
||||
if key == "properties" and not cleaned_value:
|
||||
continue
|
||||
if key == "properties" and isinstance(cleaned_value, dict):
|
||||
# Remove any properties that have empty object definitions
|
||||
non_empty_props = {}
|
||||
for prop_key, prop_value in cleaned_value.items():
|
||||
if isinstance(prop_value, dict):
|
||||
if prop_value.get("type") == "object":
|
||||
# If it's an object type, ensure it has properties or change to string
|
||||
if not prop_value.get("properties"):
|
||||
non_empty_props[prop_key] = {"type": "string"}
|
||||
else:
|
||||
non_empty_props[prop_key] = prop_value
|
||||
else:
|
||||
non_empty_props[prop_key] = prop_value
|
||||
else:
|
||||
non_empty_props[prop_key] = prop_value
|
||||
cleaned[key] = non_empty_props
|
||||
else:
|
||||
cleaned[key] = cleaned_value
|
||||
elif isinstance(value, list):
|
||||
cleaned[key] = [_clean_schema_for_gemini(item) if isinstance(item, dict) else item for item in value]
|
||||
else:
|
||||
cleaned[key] = value
|
||||
|
||||
return cleaned
|
||||
elif isinstance(schema, list):
|
||||
return [_clean_schema_for_gemini(item) if isinstance(item, dict) else item for item in schema]
|
||||
else:
|
||||
return schema
|
||||
|
||||
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
|
||||
def gemini_structured_json_response(prompt: str, schema: Dict[str, Any], model_name: str = "gemini-2.0-flash-001") -> str:
|
||||
"""
|
||||
Generate structured JSON response using Gemini API according to official SDK
|
||||
"""
|
||||
try:
|
||||
api_key = get_gemini_api_key()
|
||||
if not api_key:
|
||||
logger.error("Gemini API key not found")
|
||||
return json.dumps({"error": "API key not found"})
|
||||
|
||||
# Clean and validate schema
|
||||
cleaned_schema = _clean_schema_for_gemini(schema)
|
||||
validated_schema = _validate_and_fix_schema(cleaned_schema)
|
||||
|
||||
logger.info(f"🤖 Making Gemini API call to {model_name}")
|
||||
logger.info(f"📝 Prompt: {prompt[:200]}...")
|
||||
logger.info(f"🔧 Schema: {json.dumps(validated_schema, indent=2)}")
|
||||
|
||||
# Create the client with API key (official SDK pattern)
|
||||
client = genai.Client(api_key=api_key)
|
||||
|
||||
generation_config = types.GenerateContentConfig(
|
||||
temperature=0.7,
|
||||
top_p=0.8,
|
||||
top_k=40,
|
||||
max_output_tokens=8192,
|
||||
)
|
||||
|
||||
# Create the prompt with schema
|
||||
full_prompt = f"""
|
||||
{prompt}
|
||||
|
||||
Please respond with a valid JSON object that matches this schema:
|
||||
|
||||
{json.dumps(validated_schema, indent=2)}
|
||||
|
||||
Ensure the response is valid JSON and matches the schema exactly.
|
||||
"""
|
||||
|
||||
logger.info(f"🚀 Sending request to Gemini API...")
|
||||
start_time = time.time()
|
||||
|
||||
# Generate content using official SDK pattern
|
||||
response = client.models.generate_content(
|
||||
model=model_name,
|
||||
contents=full_prompt,
|
||||
config=generation_config
|
||||
)
|
||||
|
||||
end_time = time.time()
|
||||
logger.info(f"⏱️ Gemini API response received in {end_time - start_time:.2f} seconds")
|
||||
logger.info(f"📄 Raw response: {response.text[:500]}...")
|
||||
|
||||
# Try to parse the response as JSON
|
||||
try:
|
||||
# First, try to extract JSON from the response
|
||||
json_text = response.text.strip()
|
||||
|
||||
# Remove markdown code blocks if present
|
||||
if json_text.startswith("```json"):
|
||||
json_text = json_text[7:]
|
||||
if json_text.endswith("```"):
|
||||
json_text = json_text[:-3]
|
||||
|
||||
json_text = json_text.strip()
|
||||
|
||||
# Try to parse as JSON
|
||||
parsed = json.loads(json_text)
|
||||
logger.info(f"✅ Successfully parsed JSON response: {json.dumps(parsed, indent=2)}")
|
||||
return json.dumps(parsed)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning(f"❌ JSON parsing failed: {e}")
|
||||
logger.warning(f"📄 Attempted to parse: {json_text}")
|
||||
|
||||
# Try to find JSON-like content in the response
|
||||
import re
|
||||
json_match = re.search(r'\{.*\}', response.text, re.DOTALL)
|
||||
if json_match:
|
||||
try:
|
||||
parsed = json.loads(json_match.group())
|
||||
logger.info(f"✅ Found and parsed JSON in response: {json.dumps(parsed, indent=2)}")
|
||||
return json.dumps(parsed)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("❌ Failed to parse extracted JSON")
|
||||
|
||||
logger.warning("❌ No valid JSON found in response, returning full text")
|
||||
return json.dumps({"error": "Invalid JSON response", "raw_text": response.text})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Gemini API error: {str(e)}")
|
||||
return json.dumps({"error": f"Gemini API error: {str(e)}"})
|
||||
|
||||
def _validate_and_fix_schema(schema):
|
||||
"""Validate and fix schema to ensure it's compatible with Gemini API."""
|
||||
if isinstance(schema, dict):
|
||||
# Check for empty object properties
|
||||
if "properties" in schema and isinstance(schema["properties"], dict):
|
||||
fixed_properties = {}
|
||||
for key, value in schema["properties"].items():
|
||||
if isinstance(value, dict):
|
||||
if value.get("type") == "object":
|
||||
# If object has no properties or empty properties, change to string
|
||||
if not value.get("properties") or not value["properties"]:
|
||||
fixed_properties[key] = {"type": "string"}
|
||||
else:
|
||||
# Recursively fix nested objects
|
||||
fixed_properties[key] = _validate_and_fix_schema(value)
|
||||
else:
|
||||
fixed_properties[key] = value
|
||||
else:
|
||||
fixed_properties[key] = value
|
||||
|
||||
schema["properties"] = fixed_properties
|
||||
|
||||
# Recursively fix nested objects
|
||||
for key, value in schema.items():
|
||||
if isinstance(value, dict):
|
||||
schema[key] = _validate_and_fix_schema(value)
|
||||
|
||||
return schema
|
||||
|
||||
async def test_gemini_api_key(api_key: str) -> tuple[bool, str]:
|
||||
"""
|
||||
Test if the provided Gemini API key is valid using official SDK pattern.
|
||||
|
||||
Args:
|
||||
api_key (str): The Gemini API key to test
|
||||
|
||||
Returns:
|
||||
tuple[bool, str]: A tuple containing (is_valid, message)
|
||||
"""
|
||||
try:
|
||||
# Try to generate a simple response as a test using official SDK pattern
|
||||
test_prompt = "Hello"
|
||||
client = genai.Client(api_key=api_key)
|
||||
response = client.models.generate_content(
|
||||
model="gemini-2.0-flash-001", # Using the recommended model from docs
|
||||
contents=test_prompt,
|
||||
config=types.GenerateContentConfig(
|
||||
temperature=0.1,
|
||||
max_output_tokens=50
|
||||
)
|
||||
)
|
||||
|
||||
# If we get here, the key is valid
|
||||
return True, "Gemini API key is valid"
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
if "API_KEY_INVALID" in error_msg or "authentication" in error_msg.lower():
|
||||
return False, "Invalid Gemini API key"
|
||||
elif "quota" in error_msg.lower() or "rate" in error_msg.lower():
|
||||
return False, "Rate limit exceeded. Please try again later."
|
||||
else:
|
||||
return False, f"Error testing Gemini API key: {error_msg}"
|
||||
|
||||
def gemini_pro_text_gen(prompt, temperature=0.7, top_p=0.9, top_k=40, max_tokens=2048):
|
||||
"""
|
||||
Generate text using Google's Gemini Pro model according to official SDK.
|
||||
|
||||
Args:
|
||||
prompt (str): The input text to generate completion for
|
||||
temperature (float, optional): Controls randomness. Defaults to 0.7
|
||||
top_p (float, optional): Controls diversity. Defaults to 0.9
|
||||
top_k (int, optional): Controls vocabulary size. Defaults to 40
|
||||
max_tokens (int, optional): Maximum number of tokens to generate. Defaults to 2048
|
||||
|
||||
Returns:
|
||||
str: The generated text completion
|
||||
"""
|
||||
try:
|
||||
# Get API key
|
||||
api_key = get_gemini_api_key()
|
||||
|
||||
# Create the client with API key (official SDK pattern)
|
||||
client = genai.Client(api_key=api_key)
|
||||
|
||||
# Generate content using the official SDK pattern
|
||||
response = client.models.generate_content(
|
||||
model='gemini-2.0-flash-001', # Using the recommended model from docs
|
||||
contents=prompt,
|
||||
config=types.GenerateContentConfig(
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
top_k=top_k,
|
||||
max_output_tokens=max_tokens,
|
||||
)
|
||||
)
|
||||
|
||||
# Return the generated text
|
||||
return response.text
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in Gemini Pro text generation: {e}")
|
||||
return str(e)
|
||||
@@ -1,234 +0,0 @@
|
||||
"""Main Text Generation Service for ALwrity Backend.
|
||||
|
||||
This service provides the main LLM text generation functionality,
|
||||
migrated from the legacy lib/gpt_providers/text_generation/main_text_generation.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
from typing import Optional, Dict, Any
|
||||
from loguru import logger
|
||||
from services.api_key_manager import APIKeyManager
|
||||
|
||||
from .openai_provider import openai_chatgpt
|
||||
from .gemini_provider import gemini_text_response, gemini_structured_json_response
|
||||
from .anthropic_provider import anthropic_text_response
|
||||
from .deepseek_provider import deepseek_text_response
|
||||
|
||||
def llm_text_gen(prompt: str, system_prompt: Optional[str] = None, json_struct: Optional[Dict[str, Any]] = None) -> str:
|
||||
"""
|
||||
Generate text using Language Model (LLM) based on the provided prompt.
|
||||
|
||||
Args:
|
||||
prompt (str): The prompt to generate text from.
|
||||
system_prompt (str, optional): Custom system prompt to use instead of the default one.
|
||||
json_struct (dict, optional): JSON schema structure for structured responses.
|
||||
|
||||
Returns:
|
||||
str: Generated text based on the prompt.
|
||||
"""
|
||||
try:
|
||||
logger.info("[llm_text_gen] Starting text generation")
|
||||
logger.debug(f"[llm_text_gen] Prompt length: {len(prompt)} characters")
|
||||
|
||||
# Initialize API key manager
|
||||
api_key_manager = APIKeyManager()
|
||||
|
||||
# Set default values for LLM parameters
|
||||
gpt_provider = "google" # Default to Google Gemini
|
||||
model = "models/gemini-2.0-flash"
|
||||
temperature = 0.7
|
||||
max_tokens = 4000
|
||||
top_p = 0.9
|
||||
n = 1
|
||||
fp = 16
|
||||
frequency_penalty = 0.0
|
||||
presence_penalty = 0.0
|
||||
|
||||
# Default blog characteristics
|
||||
blog_tone = "Professional"
|
||||
blog_demographic = "Professional"
|
||||
blog_type = "Informational"
|
||||
blog_language = "English"
|
||||
blog_output_format = "markdown"
|
||||
blog_length = 2000
|
||||
|
||||
# Try to get provider from environment or config
|
||||
try:
|
||||
# Check which providers have API keys available
|
||||
available_providers = []
|
||||
if api_key_manager.get_api_key("openai"):
|
||||
available_providers.append("openai")
|
||||
if api_key_manager.get_api_key("gemini"):
|
||||
available_providers.append("google")
|
||||
if api_key_manager.get_api_key("anthropic"):
|
||||
available_providers.append("anthropic")
|
||||
if api_key_manager.get_api_key("deepseek"):
|
||||
available_providers.append("deepseek")
|
||||
|
||||
# Prefer Google Gemini if available, otherwise use first available
|
||||
if "google" in available_providers:
|
||||
gpt_provider = "google"
|
||||
model = "models/gemini-2.0-flash"
|
||||
elif available_providers:
|
||||
gpt_provider = available_providers[0]
|
||||
if gpt_provider == "openai":
|
||||
model = "gpt-4o"
|
||||
elif gpt_provider == "anthropic":
|
||||
model = "claude-3-5-sonnet-20241022"
|
||||
elif gpt_provider == "deepseek":
|
||||
model = "deepseek-chat"
|
||||
else:
|
||||
logger.warning("[llm_text_gen] No API keys found, using mock response")
|
||||
return _get_mock_response(prompt)
|
||||
|
||||
logger.debug(f"[llm_text_gen] Using provider: {gpt_provider}, model: {model}")
|
||||
|
||||
except Exception as err:
|
||||
logger.warning(f"[llm_text_gen] Error determining provider, using defaults: {err}")
|
||||
gpt_provider = "google"
|
||||
model = "models/gemini-2.0-flash"
|
||||
|
||||
# Construct the system prompt if not provided
|
||||
if system_prompt is None:
|
||||
system_instructions = f"""You are a highly skilled content writer with a knack for creating engaging and informative content.
|
||||
Your expertise spans various writing styles and formats.
|
||||
|
||||
Writing Style Guidelines:
|
||||
- Tone: {blog_tone}
|
||||
- Target Audience: {blog_demographic}
|
||||
- Content Type: {blog_type}
|
||||
- Language: {blog_language}
|
||||
- Output Format: {blog_output_format}
|
||||
- Target Length: {blog_length} words
|
||||
|
||||
Please provide responses that are:
|
||||
- Well-structured and easy to read
|
||||
- Engaging and informative
|
||||
- Tailored to the specified tone and audience
|
||||
- Professional yet accessible
|
||||
- Optimized for the target content type
|
||||
"""
|
||||
else:
|
||||
system_instructions = system_prompt
|
||||
|
||||
# Generate response based on provider
|
||||
if gpt_provider == "openai":
|
||||
return openai_chatgpt(
|
||||
prompt=prompt,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
top_p=top_p,
|
||||
n=n,
|
||||
fp=fp,
|
||||
system_prompt=system_instructions
|
||||
)
|
||||
elif gpt_provider == "google":
|
||||
if json_struct:
|
||||
return gemini_structured_json_response(
|
||||
prompt=prompt,
|
||||
schema=json_struct,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
top_k=n,
|
||||
max_tokens=max_tokens,
|
||||
system_prompt=system_instructions
|
||||
)
|
||||
else:
|
||||
return gemini_text_response(
|
||||
prompt=prompt,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
n=n,
|
||||
max_tokens=max_tokens,
|
||||
system_prompt=system_instructions
|
||||
)
|
||||
elif gpt_provider == "anthropic":
|
||||
return anthropic_text_response(
|
||||
prompt=prompt,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
system_prompt=system_instructions
|
||||
)
|
||||
elif gpt_provider == "deepseek":
|
||||
return deepseek_text_response(
|
||||
prompt=prompt,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
system_prompt=system_instructions
|
||||
)
|
||||
else:
|
||||
logger.error(f"[llm_text_gen] Unknown provider: {gpt_provider}")
|
||||
return _get_mock_response(prompt)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[llm_text_gen] Error during text generation: {str(e)}")
|
||||
return _get_mock_response(prompt)
|
||||
|
||||
def _get_mock_response(prompt: str) -> str:
|
||||
"""Get a mock response when no API keys are available."""
|
||||
logger.warning("[llm_text_gen] Using mock response - no API keys configured")
|
||||
|
||||
# Return a structured mock response for style detection
|
||||
if "style analysis" in prompt.lower() or "writing style" in prompt.lower():
|
||||
return json.dumps({
|
||||
"writing_style": {
|
||||
"tone": "professional",
|
||||
"voice": "active",
|
||||
"complexity": "moderate",
|
||||
"engagement_level": "high"
|
||||
},
|
||||
"content_characteristics": {
|
||||
"sentence_structure": "well-structured",
|
||||
"vocabulary_level": "intermediate",
|
||||
"paragraph_organization": "logical flow",
|
||||
"content_flow": "smooth transitions"
|
||||
},
|
||||
"target_audience": {
|
||||
"demographics": ["professionals", "business users"],
|
||||
"expertise_level": "intermediate",
|
||||
"industry_focus": "technology",
|
||||
"geographic_focus": "global"
|
||||
},
|
||||
"content_type": {
|
||||
"primary_type": "blog",
|
||||
"secondary_types": ["article", "guide"],
|
||||
"purpose": "inform",
|
||||
"call_to_action": "moderate"
|
||||
},
|
||||
"recommended_settings": {
|
||||
"writing_tone": "professional",
|
||||
"target_audience": "business professionals",
|
||||
"content_type": "blog",
|
||||
"creativity_level": "medium",
|
||||
"geographic_location": "global"
|
||||
}
|
||||
})
|
||||
|
||||
# Generic mock response
|
||||
return "This is a mock response. Please configure API keys for real content generation."
|
||||
|
||||
def check_gpt_provider(gpt_provider: str) -> bool:
|
||||
"""Check if the specified GPT provider is supported."""
|
||||
supported_providers = ["openai", "google", "anthropic", "deepseek"]
|
||||
return gpt_provider in supported_providers
|
||||
|
||||
def get_api_key(gpt_provider: str) -> Optional[str]:
|
||||
"""Get API key for the specified provider."""
|
||||
try:
|
||||
api_key_manager = APIKeyManager()
|
||||
provider_mapping = {
|
||||
"openai": "openai",
|
||||
"google": "gemini",
|
||||
"anthropic": "anthropic",
|
||||
"deepseek": "deepseek"
|
||||
}
|
||||
|
||||
mapped_provider = provider_mapping.get(gpt_provider, gpt_provider)
|
||||
return api_key_manager.get_api_key(mapped_provider)
|
||||
except Exception as e:
|
||||
logger.error(f"[get_api_key] Error getting API key for {gpt_provider}: {str(e)}")
|
||||
return None
|
||||
@@ -1,128 +0,0 @@
|
||||
"""OpenAI Provider Service for ALwrity Backend.
|
||||
|
||||
This service handles OpenAI API integrations,
|
||||
migrated from the legacy lib/gpt_providers/text_generation/openai_text_gen.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import openai
|
||||
import asyncio
|
||||
from typing import Tuple
|
||||
from loguru import logger
|
||||
from tenacity import (
|
||||
retry,
|
||||
stop_after_attempt,
|
||||
wait_random_exponential,
|
||||
)
|
||||
|
||||
async def test_openai_api_key(api_key: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
Test if the provided OpenAI API key is valid.
|
||||
|
||||
Args:
|
||||
api_key (str): The OpenAI API key to test
|
||||
|
||||
Returns:
|
||||
tuple[bool, str]: A tuple containing (is_valid, message)
|
||||
"""
|
||||
try:
|
||||
# Create OpenAI client with the provided key
|
||||
client = openai.OpenAI(api_key=api_key)
|
||||
|
||||
# Try to list models as a simple API test
|
||||
models = client.models.list()
|
||||
|
||||
# If we get here, the key is valid
|
||||
return True, "OpenAI API key is valid"
|
||||
|
||||
except openai.AuthenticationError:
|
||||
return False, "Invalid OpenAI API key"
|
||||
except openai.RateLimitError:
|
||||
return False, "Rate limit exceeded. Please try again later."
|
||||
except Exception as e:
|
||||
return False, f"Error testing OpenAI API key: {str(e)}"
|
||||
|
||||
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
|
||||
def openai_chatgpt(prompt: str, model: str = "gpt-4o", temperature: float = 0.7,
|
||||
max_tokens: int = 4000, top_p: float = 0.9, n: int = 1,
|
||||
fp: int = 16, system_prompt: str = None) -> str:
|
||||
"""
|
||||
Wrapper function for OpenAI's ChatGPT completion.
|
||||
|
||||
Args:
|
||||
prompt (str): The input text to generate completion for.
|
||||
model (str, optional): Model to be used for the completion. Defaults to "gpt-4o".
|
||||
temperature (float, optional): Controls randomness. Lower values make responses more deterministic. Defaults to 0.7.
|
||||
max_tokens (int, optional): Maximum number of tokens to generate. Defaults to 4000.
|
||||
top_p (float, optional): Controls diversity. Defaults to 0.9.
|
||||
n (int, optional): Number of completions to generate. Defaults to 1.
|
||||
fp (int, optional): Frequency penalty. Defaults to 16.
|
||||
system_prompt (str, optional): System prompt for the conversation. Defaults to None.
|
||||
|
||||
Returns:
|
||||
str: The generated text completion.
|
||||
|
||||
Raises:
|
||||
SystemExit: If an API error, connection error, or rate limit error occurs.
|
||||
"""
|
||||
# Wait for 5 seconds to comply with rate limits
|
||||
for _ in range(5):
|
||||
time.sleep(1)
|
||||
|
||||
try:
|
||||
# Create variables to collect the stream of chunks
|
||||
collected_chunks = []
|
||||
collected_messages = []
|
||||
full_reply_content = None
|
||||
|
||||
# Get API key from environment
|
||||
api_key = os.getenv('OPENAI_API_KEY')
|
||||
if not api_key:
|
||||
raise ValueError("OpenAI API key not found in environment variables")
|
||||
|
||||
client = openai.OpenAI(api_key=api_key)
|
||||
|
||||
# Prepare messages
|
||||
messages = []
|
||||
if system_prompt:
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=messages,
|
||||
max_tokens=max_tokens,
|
||||
n=n,
|
||||
top_p=top_p,
|
||||
stream=True,
|
||||
frequency_penalty=fp,
|
||||
temperature=temperature
|
||||
)
|
||||
|
||||
# Iterate through the stream of events
|
||||
for chunk in response:
|
||||
collected_chunks.append(chunk) # save the event response
|
||||
chunk_message = chunk.choices[0].delta.content # extract the message
|
||||
collected_messages.append(chunk_message) # save the message
|
||||
print(chunk.choices[0].delta.content, end="", flush=True)
|
||||
|
||||
# Clean None in collected_messages
|
||||
collected_messages = [m for m in collected_messages if m is not None]
|
||||
full_reply_content = ''.join([m for m in collected_messages])
|
||||
|
||||
logger.info(f"[openai_chatgpt] Generated response with {len(full_reply_content)} characters")
|
||||
return full_reply_content
|
||||
|
||||
except openai.APIError as e:
|
||||
logger.error(f"OpenAI API Error: {e}")
|
||||
raise SystemExit from e
|
||||
except openai.RateLimitError as e:
|
||||
logger.error(f"OpenAI Rate Limit Error: {e}")
|
||||
raise SystemExit from e
|
||||
except openai.APIConnectionError as e:
|
||||
logger.error(f"OpenAI API Connection Error: {e}")
|
||||
raise SystemExit from e
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in OpenAI API call: {e}")
|
||||
raise SystemExit from e
|
||||
@@ -84,6 +84,12 @@ class EnhancedContentStrategy(Base):
|
||||
# Relationships
|
||||
autofill_insights = relationship("ContentStrategyAutofillInsights", back_populates="strategy", cascade="all, delete-orphan")
|
||||
|
||||
# Monitoring relationships
|
||||
monitoring_plans = relationship("StrategyMonitoringPlan", back_populates="strategy", cascade="all, delete-orphan")
|
||||
monitoring_tasks = relationship("MonitoringTask", back_populates="strategy", cascade="all, delete-orphan")
|
||||
performance_metrics = relationship("StrategyPerformanceMetrics", back_populates="strategy", cascade="all, delete-orphan")
|
||||
activation_status = relationship("StrategyActivationStatus", back_populates="strategy", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<EnhancedContentStrategy(id={self.id}, name='{self.name}', industry='{self.industry}')>"
|
||||
|
||||
|
||||
98
backend/models/monitoring_models.py
Normal file
98
backend/models/monitoring_models.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, JSON, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
|
||||
# Import the same Base from enhanced_strategy_models
|
||||
from models.enhanced_strategy_models import Base
|
||||
|
||||
class StrategyMonitoringPlan(Base):
|
||||
"""Model for storing strategy monitoring plans"""
|
||||
__tablename__ = "strategy_monitoring_plans"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
strategy_id = Column(Integer, ForeignKey("enhanced_content_strategies.id"), nullable=False)
|
||||
plan_data = Column(JSON, nullable=False) # Store the complete monitoring plan
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationship to strategy
|
||||
strategy = relationship("EnhancedContentStrategy", back_populates="monitoring_plans")
|
||||
|
||||
class MonitoringTask(Base):
|
||||
"""Model for storing individual monitoring tasks"""
|
||||
__tablename__ = "monitoring_tasks"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
strategy_id = Column(Integer, ForeignKey("enhanced_content_strategies.id"), nullable=False)
|
||||
component_name = Column(String(100), nullable=False)
|
||||
task_title = Column(String(200), nullable=False)
|
||||
task_description = Column(Text, nullable=False)
|
||||
assignee = Column(String(50), nullable=False) # 'ALwrity' or 'Human'
|
||||
frequency = Column(String(50), nullable=False) # 'Daily', 'Weekly', 'Monthly', 'Quarterly'
|
||||
metric = Column(String(100), nullable=False)
|
||||
measurement_method = Column(Text, nullable=False)
|
||||
success_criteria = Column(Text, nullable=False)
|
||||
alert_threshold = Column(Text, nullable=False)
|
||||
status = Column(String(50), default='pending') # 'pending', 'active', 'completed', 'failed'
|
||||
last_executed = Column(DateTime, nullable=True)
|
||||
next_execution = Column(DateTime, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
strategy = relationship("EnhancedContentStrategy", back_populates="monitoring_tasks")
|
||||
execution_logs = relationship("TaskExecutionLog", back_populates="task", cascade="all, delete-orphan")
|
||||
|
||||
class TaskExecutionLog(Base):
|
||||
"""Model for storing task execution logs"""
|
||||
__tablename__ = "task_execution_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
task_id = Column(Integer, ForeignKey("monitoring_tasks.id"), nullable=False)
|
||||
execution_date = Column(DateTime, default=datetime.utcnow)
|
||||
status = Column(String(50), nullable=False) # 'success', 'failed', 'skipped'
|
||||
result_data = Column(JSON, nullable=True)
|
||||
error_message = Column(Text, nullable=True)
|
||||
execution_time_ms = Column(Integer, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# Relationship to monitoring task
|
||||
task = relationship("MonitoringTask", back_populates="execution_logs")
|
||||
|
||||
class StrategyPerformanceMetrics(Base):
|
||||
"""Model for storing strategy performance metrics"""
|
||||
__tablename__ = "strategy_performance_metrics"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
strategy_id = Column(Integer, ForeignKey("enhanced_content_strategies.id"), nullable=False)
|
||||
user_id = Column(Integer, nullable=False)
|
||||
metric_date = Column(DateTime, default=datetime.utcnow)
|
||||
traffic_growth_percentage = Column(Integer, nullable=True)
|
||||
engagement_rate_percentage = Column(Integer, nullable=True)
|
||||
conversion_rate_percentage = Column(Integer, nullable=True)
|
||||
roi_ratio = Column(Integer, nullable=True)
|
||||
strategy_adoption_rate = Column(Integer, nullable=True)
|
||||
content_quality_score = Column(Integer, nullable=True)
|
||||
competitive_position_rank = Column(Integer, nullable=True)
|
||||
audience_growth_percentage = Column(Integer, nullable=True)
|
||||
data_source = Column(String(100), nullable=True)
|
||||
confidence_score = Column(Integer, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationship to strategy
|
||||
strategy = relationship("EnhancedContentStrategy", back_populates="performance_metrics")
|
||||
|
||||
class StrategyActivationStatus(Base):
|
||||
"""Model for storing strategy activation status"""
|
||||
__tablename__ = "strategy_activation_status"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
strategy_id = Column(Integer, ForeignKey("enhanced_content_strategies.id"), nullable=False)
|
||||
user_id = Column(Integer, nullable=False)
|
||||
activation_date = Column(DateTime, default=datetime.utcnow)
|
||||
status = Column(String(50), default='active') # 'active', 'inactive', 'paused'
|
||||
performance_score = Column(Integer, nullable=True)
|
||||
last_updated = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationship to strategy
|
||||
strategy = relationship("EnhancedContentStrategy", back_populates="activation_status")
|
||||
48
backend/scripts/check_database_tables.py
Normal file
48
backend/scripts/check_database_tables.py
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to check database tables and debug foreign key issues.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the backend directory to the Python path
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from services.database import engine
|
||||
from sqlalchemy import inspect
|
||||
from loguru import logger
|
||||
|
||||
def check_database_tables():
|
||||
"""Check what tables exist in the database"""
|
||||
try:
|
||||
logger.info("Checking database tables...")
|
||||
|
||||
# Get inspector
|
||||
inspector = inspect(engine)
|
||||
|
||||
# Get all table names
|
||||
table_names = inspector.get_table_names()
|
||||
|
||||
logger.info(f"Found {len(table_names)} tables:")
|
||||
for table_name in sorted(table_names):
|
||||
logger.info(f" - {table_name}")
|
||||
|
||||
# Check if enhanced_content_strategies exists
|
||||
if 'enhanced_content_strategies' in table_names:
|
||||
logger.info("✅ enhanced_content_strategies table exists!")
|
||||
|
||||
# Get columns for this table
|
||||
columns = inspector.get_columns('enhanced_content_strategies')
|
||||
logger.info(f"Columns in enhanced_content_strategies:")
|
||||
for column in columns:
|
||||
logger.info(f" - {column['name']}: {column['type']}")
|
||||
else:
|
||||
logger.error("❌ enhanced_content_strategies table does not exist!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error checking database tables: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_database_tables()
|
||||
40
backend/scripts/create_all_tables.py
Normal file
40
backend/scripts/create_all_tables.py
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to create all database tables in the correct order.
|
||||
This ensures foreign key dependencies are satisfied.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the backend directory to the Python path
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from services.database import engine
|
||||
from models.enhanced_strategy_models import Base as EnhancedStrategyBase
|
||||
from models.monitoring_models import Base as MonitoringBase
|
||||
from loguru import logger
|
||||
|
||||
def create_all_tables():
|
||||
"""Create all tables in the correct order"""
|
||||
try:
|
||||
logger.info("Creating all database tables...")
|
||||
|
||||
# Step 1: Create enhanced strategy tables first
|
||||
logger.info("Step 1: Creating enhanced strategy tables...")
|
||||
EnhancedStrategyBase.metadata.create_all(bind=engine)
|
||||
logger.info("✅ Enhanced strategy tables created!")
|
||||
|
||||
# Step 2: Create monitoring tables
|
||||
logger.info("Step 2: Creating monitoring tables...")
|
||||
MonitoringBase.metadata.create_all(bind=engine)
|
||||
logger.info("✅ Monitoring tables created!")
|
||||
|
||||
logger.info("✅ All tables created successfully!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error creating tables: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_all_tables()
|
||||
32
backend/scripts/create_enhanced_strategy_tables.py
Normal file
32
backend/scripts/create_enhanced_strategy_tables.py
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to create enhanced strategy tables in the database.
|
||||
Run this script to ensure all enhanced strategy tables are created before monitoring tables.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the backend directory to the Python path
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from services.database import engine
|
||||
from models.enhanced_strategy_models import Base as EnhancedStrategyBase
|
||||
from loguru import logger
|
||||
|
||||
def create_enhanced_strategy_tables():
|
||||
"""Create all enhanced strategy tables"""
|
||||
try:
|
||||
logger.info("Creating enhanced strategy tables...")
|
||||
|
||||
# Create enhanced strategy tables first
|
||||
EnhancedStrategyBase.metadata.create_all(bind=engine)
|
||||
|
||||
logger.info("✅ Enhanced strategy tables created successfully!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error creating enhanced strategy tables: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_enhanced_strategy_tables()
|
||||
47
backend/scripts/create_monitoring_tables.py
Normal file
47
backend/scripts/create_monitoring_tables.py
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to create monitoring tables in the database.
|
||||
Run this script to ensure all monitoring-related tables are created.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the backend directory to the Python path
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from services.database import init_database, get_db_session
|
||||
from models.monitoring_models import (
|
||||
StrategyMonitoringPlan,
|
||||
MonitoringTask,
|
||||
TaskExecutionLog,
|
||||
StrategyPerformanceMetrics,
|
||||
StrategyActivationStatus
|
||||
)
|
||||
from models.enhanced_strategy_models import EnhancedContentStrategy
|
||||
from loguru import logger
|
||||
|
||||
def create_monitoring_tables():
|
||||
"""Create all monitoring-related tables"""
|
||||
try:
|
||||
logger.info("Creating monitoring tables...")
|
||||
|
||||
# Initialize database with all models
|
||||
init_database()
|
||||
|
||||
logger.info("✅ Monitoring tables created successfully!")
|
||||
|
||||
# Test database connection
|
||||
db_session = get_db_session()
|
||||
if db_session:
|
||||
logger.info("✅ Database connection test successful!")
|
||||
db_session.close()
|
||||
else:
|
||||
logger.warning("⚠️ Database connection test failed!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error creating monitoring tables: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_monitoring_tables()
|
||||
41
backend/scripts/create_monitoring_tables_direct.py
Normal file
41
backend/scripts/create_monitoring_tables_direct.py
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to create monitoring tables directly.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the backend directory to the Python path
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from services.database import engine
|
||||
from models.monitoring_models import (
|
||||
StrategyMonitoringPlan,
|
||||
MonitoringTask,
|
||||
TaskExecutionLog,
|
||||
StrategyPerformanceMetrics,
|
||||
StrategyActivationStatus
|
||||
)
|
||||
from loguru import logger
|
||||
|
||||
def create_monitoring_tables_direct():
|
||||
"""Create monitoring tables directly"""
|
||||
try:
|
||||
logger.info("Creating monitoring tables directly...")
|
||||
|
||||
# Create tables directly
|
||||
StrategyMonitoringPlan.__table__.create(engine, checkfirst=True)
|
||||
MonitoringTask.__table__.create(engine, checkfirst=True)
|
||||
TaskExecutionLog.__table__.create(engine, checkfirst=True)
|
||||
StrategyPerformanceMetrics.__table__.create(engine, checkfirst=True)
|
||||
StrategyActivationStatus.__table__.create(engine, checkfirst=True)
|
||||
|
||||
logger.info("✅ Monitoring tables created successfully!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error creating monitoring tables: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_monitoring_tables_direct()
|
||||
@@ -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()
|
||||
@@ -1,589 +0,0 @@
|
||||
"""
|
||||
Test Enhanced Strategy Service - Phase 1 Implementation
|
||||
Validates the enhanced strategy service with 30+ strategic inputs and AI recommendations.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any
|
||||
|
||||
# Import models
|
||||
from models.enhanced_strategy_models import EnhancedContentStrategy, EnhancedAIAnalysisResult, OnboardingDataIntegration
|
||||
|
||||
# Import services
|
||||
from api.content_planning.services.enhanced_strategy_service import EnhancedStrategyService
|
||||
from services.enhanced_strategy_db_service import EnhancedStrategyDBService
|
||||
|
||||
class TestEnhancedStrategyPhase1:
|
||||
"""Test class for Enhanced Strategy Service Phase 1 implementation."""
|
||||
|
||||
def get_sample_strategy_data(self) -> Dict[str, Any]:
|
||||
"""Sample strategy data for testing."""
|
||||
return {
|
||||
'user_id': 1,
|
||||
'name': 'Test Enhanced Strategy',
|
||||
'industry': 'technology',
|
||||
|
||||
# Business Context (8 inputs)
|
||||
'business_objectives': {
|
||||
'primary': 'Increase brand awareness',
|
||||
'secondary': ['Lead generation', 'Customer engagement']
|
||||
},
|
||||
'target_metrics': {
|
||||
'traffic': '50% increase',
|
||||
'engagement': '25% improvement',
|
||||
'conversions': '15% growth'
|
||||
},
|
||||
'content_budget': 5000.0,
|
||||
'team_size': 3,
|
||||
'implementation_timeline': '6 months',
|
||||
'market_share': '2.5%',
|
||||
'competitive_position': 'challenger',
|
||||
'performance_metrics': {
|
||||
'current_traffic': 10000,
|
||||
'current_engagement': 3.2,
|
||||
'current_conversions': 2.1
|
||||
},
|
||||
|
||||
# Audience Intelligence (6 inputs)
|
||||
'content_preferences': {
|
||||
'formats': ['blog_posts', 'videos', 'infographics'],
|
||||
'topics': ['technology', 'business', 'innovation'],
|
||||
'tone': 'professional'
|
||||
},
|
||||
'consumption_patterns': {
|
||||
'peak_times': ['9-11 AM', '2-4 PM'],
|
||||
'devices': ['desktop', 'mobile'],
|
||||
'channels': ['website', 'social_media']
|
||||
},
|
||||
'audience_pain_points': [
|
||||
'Complex technology solutions',
|
||||
'Limited time for research',
|
||||
'Need for practical implementation'
|
||||
],
|
||||
'buying_journey': {
|
||||
'awareness': 'Social media, SEO',
|
||||
'consideration': 'Case studies, demos',
|
||||
'decision': 'Free trials, consultations'
|
||||
},
|
||||
'seasonal_trends': {
|
||||
'Q1': 'New year planning content',
|
||||
'Q2': 'Spring technology updates',
|
||||
'Q3': 'Summer optimization',
|
||||
'Q4': 'Year-end reviews'
|
||||
},
|
||||
'engagement_metrics': {
|
||||
'avg_time_on_page': 2.5,
|
||||
'bounce_rate': 45.2,
|
||||
'social_shares': 150
|
||||
},
|
||||
|
||||
# Competitive Intelligence (5 inputs)
|
||||
'top_competitors': [
|
||||
'Competitor A',
|
||||
'Competitor B',
|
||||
'Competitor C'
|
||||
],
|
||||
'competitor_content_strategies': {
|
||||
'Competitor A': 'High-frequency blog posts',
|
||||
'Competitor B': 'Video-focused content',
|
||||
'Competitor C': 'Whitepaper strategy'
|
||||
},
|
||||
'market_gaps': [
|
||||
'Interactive content experiences',
|
||||
'AI-powered personalization',
|
||||
'Industry-specific solutions'
|
||||
],
|
||||
'industry_trends': [
|
||||
'AI integration',
|
||||
'Remote work solutions',
|
||||
'Sustainability focus'
|
||||
],
|
||||
'emerging_trends': [
|
||||
'Voice search optimization',
|
||||
'Video-first content',
|
||||
'Personalization at scale'
|
||||
],
|
||||
|
||||
# Content Strategy (7 inputs)
|
||||
'preferred_formats': ['blog_posts', 'videos', 'webinars'],
|
||||
'content_mix': {
|
||||
'blog_posts': 40,
|
||||
'videos': 30,
|
||||
'webinars': 20,
|
||||
'infographics': 10
|
||||
},
|
||||
'content_frequency': 'weekly',
|
||||
'optimal_timing': {
|
||||
'blog_posts': 'Tuesday 9 AM',
|
||||
'videos': 'Thursday 2 PM',
|
||||
'social_posts': 'Daily 10 AM'
|
||||
},
|
||||
'quality_metrics': {
|
||||
'readability_score': 8.5,
|
||||
'engagement_threshold': 3.0,
|
||||
'conversion_target': 2.5
|
||||
},
|
||||
'editorial_guidelines': {
|
||||
'tone': 'professional',
|
||||
'style': 'clear and concise',
|
||||
'formatting': 'scannable'
|
||||
},
|
||||
'brand_voice': {
|
||||
'personality': 'innovative',
|
||||
'tone': 'authoritative',
|
||||
'style': 'informative'
|
||||
},
|
||||
|
||||
# Performance & Analytics (4 inputs)
|
||||
'traffic_sources': {
|
||||
'organic': 45,
|
||||
'social': 25,
|
||||
'direct': 20,
|
||||
'referral': 10
|
||||
},
|
||||
'conversion_rates': {
|
||||
'overall': 2.1,
|
||||
'blog_posts': 1.8,
|
||||
'videos': 3.2,
|
||||
'webinars': 5.5
|
||||
},
|
||||
'content_roi_targets': {
|
||||
'target_roi': 300,
|
||||
'cost_per_lead': 50,
|
||||
'lifetime_value': 500
|
||||
},
|
||||
'ab_testing_capabilities': True
|
||||
}
|
||||
|
||||
def test_enhanced_strategy_model_creation(self):
|
||||
"""Test creating enhanced strategy model with 30+ inputs."""
|
||||
sample_strategy_data = self.get_sample_strategy_data()
|
||||
strategy = EnhancedContentStrategy(**sample_strategy_data)
|
||||
|
||||
# Verify all fields are set
|
||||
assert strategy.user_id == 1
|
||||
assert strategy.name == 'Test Enhanced Strategy'
|
||||
assert strategy.industry == 'technology'
|
||||
|
||||
# Verify business context fields
|
||||
assert strategy.business_objectives is not None
|
||||
assert strategy.target_metrics is not None
|
||||
assert strategy.content_budget == 5000.0
|
||||
assert strategy.team_size == 3
|
||||
|
||||
# Verify audience intelligence fields
|
||||
assert strategy.content_preferences is not None
|
||||
assert strategy.consumption_patterns is not None
|
||||
assert strategy.audience_pain_points is not None
|
||||
|
||||
# Verify competitive intelligence fields
|
||||
assert strategy.top_competitors is not None
|
||||
assert strategy.market_gaps is not None
|
||||
assert strategy.industry_trends is not None
|
||||
|
||||
# Verify content strategy fields
|
||||
assert strategy.preferred_formats is not None
|
||||
assert strategy.content_mix is not None
|
||||
assert strategy.content_frequency == 'weekly'
|
||||
|
||||
# Verify performance analytics fields
|
||||
assert strategy.traffic_sources is not None
|
||||
assert strategy.conversion_rates is not None
|
||||
assert strategy.ab_testing_capabilities is True
|
||||
|
||||
print("✅ Enhanced strategy model creation test passed")
|
||||
|
||||
def test_completion_percentage_calculation(self):
|
||||
"""Test completion percentage calculation for 30+ inputs."""
|
||||
sample_strategy_data = self.get_sample_strategy_data()
|
||||
strategy = EnhancedContentStrategy(**sample_strategy_data)
|
||||
|
||||
# Calculate completion percentage
|
||||
completion = strategy.calculate_completion_percentage()
|
||||
|
||||
# Should be high since we provided most fields
|
||||
assert completion > 80
|
||||
assert strategy.completion_percentage > 80
|
||||
|
||||
print(f"✅ Completion percentage calculation test passed: {completion}%")
|
||||
|
||||
def test_enhanced_strategy_to_dict(self):
|
||||
"""Test enhanced strategy to_dict method."""
|
||||
sample_strategy_data = self.get_sample_strategy_data()
|
||||
strategy = EnhancedContentStrategy(**sample_strategy_data)
|
||||
strategy_dict = strategy.to_dict()
|
||||
|
||||
# Verify all categories are present
|
||||
assert 'business_objectives' in strategy_dict
|
||||
assert 'content_preferences' in strategy_dict
|
||||
assert 'top_competitors' in strategy_dict
|
||||
assert 'preferred_formats' in strategy_dict
|
||||
assert 'traffic_sources' in strategy_dict
|
||||
|
||||
# Verify metadata fields
|
||||
assert 'completion_percentage' in strategy_dict
|
||||
assert 'created_at' in strategy_dict
|
||||
assert 'updated_at' in strategy_dict
|
||||
|
||||
print("✅ Enhanced strategy to_dict test passed")
|
||||
|
||||
def test_ai_analysis_result_model(self):
|
||||
"""Test AI analysis result model creation."""
|
||||
analysis_data = {
|
||||
'user_id': 1,
|
||||
'strategy_id': 1,
|
||||
'analysis_type': 'comprehensive_strategy',
|
||||
'comprehensive_insights': {
|
||||
'strategic_positioning': 'Strong market position',
|
||||
'content_pillars': ['Educational', 'Thought Leadership', 'Case Studies']
|
||||
},
|
||||
'audience_intelligence': {
|
||||
'persona_insights': 'Tech-savvy professionals',
|
||||
'engagement_patterns': 'Peak engagement on Tuesdays'
|
||||
},
|
||||
'competitive_intelligence': {
|
||||
'competitor_analysis': 'Identified 3 key competitors',
|
||||
'differentiation_opportunities': ['AI integration', 'Personalization']
|
||||
},
|
||||
'performance_optimization': {
|
||||
'traffic_optimization': 'Focus on organic search',
|
||||
'conversion_improvement': 'A/B test landing pages'
|
||||
},
|
||||
'content_calendar_optimization': {
|
||||
'publishing_schedule': 'Tuesday/Thursday posts',
|
||||
'content_mix': '40% blog, 30% video, 30% other'
|
||||
},
|
||||
'processing_time': 2.5,
|
||||
'ai_service_status': 'operational'
|
||||
}
|
||||
|
||||
analysis_result = EnhancedAIAnalysisResult(**analysis_data)
|
||||
|
||||
assert analysis_result.user_id == 1
|
||||
assert analysis_result.strategy_id == 1
|
||||
assert analysis_result.analysis_type == 'comprehensive_strategy'
|
||||
assert analysis_result.processing_time == 2.5
|
||||
assert analysis_result.ai_service_status == 'operational'
|
||||
|
||||
print("✅ AI analysis result model test passed")
|
||||
|
||||
def test_onboarding_integration_model(self):
|
||||
"""Test onboarding data integration model creation."""
|
||||
integration_data = {
|
||||
'user_id': 1,
|
||||
'strategy_id': 1,
|
||||
'website_analysis_data': {
|
||||
'writing_style': {'tone': 'professional'},
|
||||
'target_audience': {'demographics': 'professionals'}
|
||||
},
|
||||
'research_preferences_data': {
|
||||
'content_types': ['blog_posts', 'videos'],
|
||||
'research_depth': 'comprehensive'
|
||||
},
|
||||
'auto_populated_fields': {
|
||||
'content_preferences': 'website_analysis',
|
||||
'target_audience': 'website_analysis',
|
||||
'preferred_formats': 'research_preferences'
|
||||
},
|
||||
'field_mappings': {
|
||||
'writing_style.tone': 'brand_voice.personality',
|
||||
'content_types': 'preferred_formats'
|
||||
},
|
||||
'data_quality_scores': {
|
||||
'website_analysis': 85.0,
|
||||
'research_preferences': 90.0
|
||||
},
|
||||
'confidence_levels': {
|
||||
'content_preferences': 0.8,
|
||||
'target_audience': 0.8,
|
||||
'preferred_formats': 0.7
|
||||
}
|
||||
}
|
||||
|
||||
integration = OnboardingDataIntegration(**integration_data)
|
||||
|
||||
assert integration.user_id == 1
|
||||
assert integration.strategy_id == 1
|
||||
assert integration.website_analysis_data is not None
|
||||
assert integration.research_preferences_data is not None
|
||||
assert integration.auto_populated_fields is not None
|
||||
|
||||
print("✅ Onboarding integration model test passed")
|
||||
|
||||
def test_enhanced_strategy_service_initialization(self):
|
||||
"""Test enhanced strategy service initialization."""
|
||||
service = EnhancedStrategyService()
|
||||
|
||||
# Verify strategic input fields are defined
|
||||
assert 'business_context' in service.strategic_input_fields
|
||||
assert 'audience_intelligence' in service.strategic_input_fields
|
||||
assert 'competitive_intelligence' in service.strategic_input_fields
|
||||
assert 'content_strategy' in service.strategic_input_fields
|
||||
assert 'performance_analytics' in service.strategic_input_fields
|
||||
|
||||
# Verify field counts
|
||||
total_fields = sum(len(fields) for fields in service.strategic_input_fields.values())
|
||||
assert total_fields >= 30 # 30+ strategic inputs
|
||||
|
||||
print(f"✅ Enhanced strategy service initialization test passed: {total_fields} fields")
|
||||
|
||||
def test_specialized_prompt_creation(self):
|
||||
"""Test specialized AI prompt creation."""
|
||||
service = EnhancedStrategyService()
|
||||
|
||||
strategy_data = {
|
||||
'name': 'Test Strategy',
|
||||
'industry': 'technology',
|
||||
'business_objectives': 'Increase brand awareness',
|
||||
'target_metrics': '50% traffic growth',
|
||||
'content_budget': 5000,
|
||||
'team_size': 3
|
||||
}
|
||||
|
||||
# Test each analysis type
|
||||
analysis_types = [
|
||||
'comprehensive_strategy',
|
||||
'audience_intelligence',
|
||||
'competitive_intelligence',
|
||||
'performance_optimization',
|
||||
'content_calendar_optimization'
|
||||
]
|
||||
|
||||
for analysis_type in analysis_types:
|
||||
prompt = service._create_specialized_prompt(analysis_type, strategy_data, None)
|
||||
|
||||
assert prompt is not None
|
||||
assert len(prompt) > 0
|
||||
assert 'Test Strategy' in prompt
|
||||
|
||||
# Check for either analysis type or relevant keywords
|
||||
if analysis_type == 'performance_optimization':
|
||||
assert 'optimization' in prompt.lower()
|
||||
elif analysis_type == 'content_calendar_optimization':
|
||||
assert 'optimization' in prompt.lower()
|
||||
else:
|
||||
assert analysis_type in prompt or 'analysis' in prompt.lower()
|
||||
|
||||
print("✅ Specialized prompt creation test passed")
|
||||
|
||||
def test_fallback_recommendations(self):
|
||||
"""Test fallback recommendations when AI service fails."""
|
||||
service = EnhancedStrategyService()
|
||||
|
||||
analysis_types = [
|
||||
'comprehensive_strategy',
|
||||
'audience_intelligence',
|
||||
'competitive_intelligence',
|
||||
'performance_optimization',
|
||||
'content_calendar_optimization'
|
||||
]
|
||||
|
||||
for analysis_type in analysis_types:
|
||||
fallback = service._get_fallback_recommendations(analysis_type)
|
||||
|
||||
assert fallback is not None
|
||||
assert 'recommendations' in fallback
|
||||
assert 'insights' in fallback
|
||||
assert 'metrics' in fallback
|
||||
assert 'score' in fallback['metrics']
|
||||
assert 'confidence' in fallback['metrics']
|
||||
|
||||
print("✅ Fallback recommendations test passed")
|
||||
|
||||
def test_data_quality_calculation(self):
|
||||
"""Test data quality score calculation."""
|
||||
service = EnhancedStrategyService()
|
||||
|
||||
data_sources = {
|
||||
'website_analysis': {
|
||||
'writing_style': {'tone': 'professional'},
|
||||
'target_audience': {'demographics': 'professionals'},
|
||||
'content_type': {'primary': 'blog_posts'}
|
||||
},
|
||||
'research_preferences': {
|
||||
'content_types': ['blog_posts', 'videos'],
|
||||
'research_depth': 'comprehensive'
|
||||
}
|
||||
}
|
||||
|
||||
quality_scores = service._calculate_data_quality_scores(data_sources)
|
||||
|
||||
assert 'website_analysis' in quality_scores
|
||||
assert 'research_preferences' in quality_scores
|
||||
assert quality_scores['website_analysis'] > 0
|
||||
assert quality_scores['research_preferences'] > 0
|
||||
|
||||
print("✅ Data quality calculation test passed")
|
||||
|
||||
def test_confidence_level_calculation(self):
|
||||
"""Test confidence level calculation for auto-populated fields."""
|
||||
service = EnhancedStrategyService()
|
||||
|
||||
auto_populated_fields = {
|
||||
'content_preferences': 'website_analysis',
|
||||
'target_audience': 'website_analysis',
|
||||
'preferred_formats': 'research_preferences'
|
||||
}
|
||||
|
||||
confidence_levels = service._calculate_confidence_levels(auto_populated_fields)
|
||||
|
||||
assert 'content_preferences' in confidence_levels
|
||||
assert 'target_audience' in confidence_levels
|
||||
assert 'preferred_formats' in confidence_levels
|
||||
|
||||
# Verify confidence levels are between 0 and 1
|
||||
for field, confidence in confidence_levels.items():
|
||||
assert 0 <= confidence <= 1
|
||||
|
||||
print("✅ Confidence level calculation test passed")
|
||||
|
||||
def test_strategic_scores_calculation(self):
|
||||
"""Test strategic scores calculation from AI recommendations."""
|
||||
service = EnhancedStrategyService()
|
||||
|
||||
ai_recommendations = {
|
||||
'comprehensive_strategy': {
|
||||
'metrics': {'score': 85, 'confidence': 0.9}
|
||||
},
|
||||
'audience_intelligence': {
|
||||
'metrics': {'score': 80, 'confidence': 0.8}
|
||||
},
|
||||
'competitive_intelligence': {
|
||||
'metrics': {'score': 75, 'confidence': 0.7}
|
||||
}
|
||||
}
|
||||
|
||||
scores = service._calculate_strategic_scores(ai_recommendations)
|
||||
|
||||
assert 'overall_score' in scores
|
||||
assert 'content_quality_score' in scores
|
||||
assert 'engagement_score' in scores
|
||||
assert 'conversion_score' in scores
|
||||
assert 'innovation_score' in scores
|
||||
|
||||
# Verify scores are calculated
|
||||
assert scores['overall_score'] > 0
|
||||
|
||||
print("✅ Strategic scores calculation test passed")
|
||||
|
||||
def test_market_positioning_extraction(self):
|
||||
"""Test market positioning extraction from AI recommendations."""
|
||||
service = EnhancedStrategyService()
|
||||
|
||||
ai_recommendations = {
|
||||
'comprehensive_strategy': {
|
||||
'metrics': {'score': 85, 'confidence': 0.9}
|
||||
}
|
||||
}
|
||||
|
||||
positioning = service._extract_market_positioning(ai_recommendations)
|
||||
|
||||
assert 'industry_position' in positioning
|
||||
assert 'competitive_advantage' in positioning
|
||||
assert 'market_share' in positioning
|
||||
assert 'positioning_score' in positioning
|
||||
|
||||
print("✅ Market positioning extraction test passed")
|
||||
|
||||
def test_competitive_advantages_extraction(self):
|
||||
"""Test competitive advantages extraction from AI recommendations."""
|
||||
service = EnhancedStrategyService()
|
||||
|
||||
ai_recommendations = {
|
||||
'competitive_intelligence': {
|
||||
'metrics': {'score': 80, 'confidence': 0.8}
|
||||
}
|
||||
}
|
||||
|
||||
advantages = service._extract_competitive_advantages(ai_recommendations)
|
||||
|
||||
assert isinstance(advantages, list)
|
||||
assert len(advantages) > 0
|
||||
|
||||
for advantage in advantages:
|
||||
assert 'advantage' in advantage
|
||||
assert 'impact' in advantage
|
||||
assert 'implementation' in advantage
|
||||
|
||||
print("✅ Competitive advantages extraction test passed")
|
||||
|
||||
def test_strategic_risks_extraction(self):
|
||||
"""Test strategic risks extraction from AI recommendations."""
|
||||
service = EnhancedStrategyService()
|
||||
|
||||
ai_recommendations = {
|
||||
'comprehensive_strategy': {
|
||||
'metrics': {'score': 85, 'confidence': 0.9}
|
||||
}
|
||||
}
|
||||
|
||||
risks = service._extract_strategic_risks(ai_recommendations)
|
||||
|
||||
assert isinstance(risks, list)
|
||||
assert len(risks) > 0
|
||||
|
||||
for risk in risks:
|
||||
assert 'risk' in risk
|
||||
assert 'probability' in risk
|
||||
assert 'impact' in risk
|
||||
|
||||
print("✅ Strategic risks extraction test passed")
|
||||
|
||||
def test_opportunity_analysis_extraction(self):
|
||||
"""Test opportunity analysis extraction from AI recommendations."""
|
||||
service = EnhancedStrategyService()
|
||||
|
||||
ai_recommendations = {
|
||||
'comprehensive_strategy': {
|
||||
'metrics': {'score': 85, 'confidence': 0.9}
|
||||
}
|
||||
}
|
||||
|
||||
opportunities = service._extract_opportunity_analysis(ai_recommendations)
|
||||
|
||||
assert isinstance(opportunities, list)
|
||||
assert len(opportunities) > 0
|
||||
|
||||
for opportunity in opportunities:
|
||||
assert 'opportunity' in opportunity
|
||||
assert 'potential_impact' in opportunity
|
||||
assert 'implementation_ease' in opportunity
|
||||
|
||||
print("✅ Opportunity analysis extraction test passed")
|
||||
|
||||
def run_enhanced_strategy_phase1_tests():
|
||||
"""Run all Phase 1 tests for enhanced strategy service."""
|
||||
print("🚀 Starting Enhanced Strategy Phase 1 Tests")
|
||||
print("=" * 50)
|
||||
|
||||
test_instance = TestEnhancedStrategyPhase1()
|
||||
|
||||
# Run all tests
|
||||
test_instance.test_enhanced_strategy_model_creation()
|
||||
test_instance.test_completion_percentage_calculation()
|
||||
test_instance.test_enhanced_strategy_to_dict()
|
||||
test_instance.test_ai_analysis_result_model()
|
||||
test_instance.test_onboarding_integration_model()
|
||||
test_instance.test_enhanced_strategy_service_initialization()
|
||||
test_instance.test_specialized_prompt_creation()
|
||||
test_instance.test_fallback_recommendations()
|
||||
test_instance.test_data_quality_calculation()
|
||||
test_instance.test_confidence_level_calculation()
|
||||
test_instance.test_strategic_scores_calculation()
|
||||
test_instance.test_market_positioning_extraction()
|
||||
test_instance.test_competitive_advantages_extraction()
|
||||
test_instance.test_strategic_risks_extraction()
|
||||
test_instance.test_opportunity_analysis_extraction()
|
||||
|
||||
print("=" * 50)
|
||||
print("✅ All Enhanced Strategy Phase 1 Tests Passed!")
|
||||
print("🎯 Phase 1 Implementation Complete:")
|
||||
print(" - Enhanced database schema with 30+ input fields ✓")
|
||||
print(" - Enhanced Strategy Service core implementation ✓")
|
||||
print(" - 5 specialized AI prompt implementations ✓")
|
||||
print(" - Onboarding data integration ✓")
|
||||
print(" - Comprehensive AI recommendations ✓")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_enhanced_strategy_phase1_tests()
|
||||
@@ -1,142 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to check environment variables and API key loading.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add the backend directory to the Python path
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
def test_environment_loading():
|
||||
"""Test environment variable loading."""
|
||||
print("🔍 Testing environment variable loading...")
|
||||
|
||||
# Check current working directory
|
||||
print(f"Current working directory: {os.getcwd()}")
|
||||
|
||||
# Check if .env file exists in various locations
|
||||
possible_env_paths = [
|
||||
Path('.env'), # Current directory
|
||||
Path('../.env'), # Parent directory
|
||||
Path('../../.env'), # Grandparent directory
|
||||
Path('../../../.env'), # Great-grandparent directory
|
||||
Path('backend/.env'), # Backend directory
|
||||
]
|
||||
|
||||
print("\n📁 Checking for .env files:")
|
||||
for env_path in possible_env_paths:
|
||||
if env_path.exists():
|
||||
print(f"✅ Found .env file: {env_path.absolute()}")
|
||||
else:
|
||||
print(f"❌ No .env file: {env_path.absolute()}")
|
||||
|
||||
# Try to load .env from different locations
|
||||
print("\n🔄 Attempting to load .env files:")
|
||||
for env_path in possible_env_paths:
|
||||
if env_path.exists():
|
||||
print(f"Loading .env from: {env_path.absolute()}")
|
||||
load_dotenv(env_path)
|
||||
break
|
||||
else:
|
||||
print("⚠️ No .env file found, trying to load from current directory")
|
||||
load_dotenv()
|
||||
|
||||
# Check environment variables
|
||||
print("\n🔑 Checking environment variables:")
|
||||
env_vars_to_check = [
|
||||
'GEMINI_API_KEY',
|
||||
'GOOGLE_API_KEY',
|
||||
'OPENAI_API_KEY',
|
||||
'DATABASE_URL',
|
||||
'SECRET_KEY'
|
||||
]
|
||||
|
||||
for var in env_vars_to_check:
|
||||
value = os.getenv(var)
|
||||
if value:
|
||||
# Show first few characters for security
|
||||
masked_value = value[:8] + "..." if len(value) > 8 else "***"
|
||||
print(f"✅ {var}: {masked_value}")
|
||||
else:
|
||||
print(f"❌ {var}: Not set")
|
||||
|
||||
# Test specific Gemini API key loading
|
||||
print("\n🤖 Testing Gemini API key loading:")
|
||||
gemini_key = os.getenv('GEMINI_API_KEY')
|
||||
if gemini_key:
|
||||
print(f"✅ GEMINI_API_KEY found: {gemini_key[:8]}...")
|
||||
|
||||
# Test if the key looks valid
|
||||
if len(gemini_key) > 20:
|
||||
print("✅ API key length looks valid")
|
||||
else:
|
||||
print("⚠️ API key seems too short")
|
||||
else:
|
||||
print("❌ GEMINI_API_KEY not found")
|
||||
|
||||
# Check alternative names
|
||||
alternative_keys = ['GOOGLE_API_KEY', 'GEMINI_KEY', 'GOOGLE_AI_API_KEY']
|
||||
for alt_key in alternative_keys:
|
||||
alt_value = os.getenv(alt_key)
|
||||
if alt_value:
|
||||
print(f"⚠️ Found alternative key {alt_key}: {alt_value[:8]}...")
|
||||
|
||||
return gemini_key is not None
|
||||
|
||||
def test_gemini_provider_import():
|
||||
"""Test importing the Gemini provider."""
|
||||
print("\n🧪 Testing Gemini provider import...")
|
||||
|
||||
try:
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response
|
||||
print("✅ Successfully imported gemini_structured_json_response")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to import Gemini provider: {e}")
|
||||
return False
|
||||
|
||||
def test_ai_service_manager_import():
|
||||
"""Test importing the AI service manager."""
|
||||
print("\n🧪 Testing AI service manager import...")
|
||||
|
||||
try:
|
||||
from services.ai_service_manager import AIServiceManager
|
||||
print("✅ Successfully imported AIServiceManager")
|
||||
|
||||
# Try to create an instance
|
||||
ai_manager = AIServiceManager()
|
||||
print("✅ Successfully created AIServiceManager instance")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to import/create AI service manager: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🚀 Starting environment and API key validation tests")
|
||||
print("=" * 60)
|
||||
|
||||
# Test environment loading
|
||||
env_ok = test_environment_loading()
|
||||
|
||||
# Test imports
|
||||
gemini_import_ok = test_gemini_provider_import()
|
||||
ai_manager_ok = test_ai_service_manager_import()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("📊 Test Results Summary:")
|
||||
print(f"Environment loading: {'✅ PASS' if env_ok else '❌ FAIL'}")
|
||||
print(f"Gemini provider import: {'✅ PASS' if gemini_import_ok else '❌ FAIL'}")
|
||||
print(f"AI service manager: {'✅ PASS' if ai_manager_ok else '❌ FAIL'}")
|
||||
|
||||
if not env_ok:
|
||||
print("\n💡 To fix environment issues:")
|
||||
print("1. Create a .env file in the backend directory")
|
||||
print("2. Add your GEMINI_API_KEY to the .env file")
|
||||
print("3. Example: GEMINI_API_KEY=your_actual_api_key_here")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
@@ -1,104 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Debug script to test Gemini API and identify the empty response issue.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
# Add current directory to path
|
||||
sys.path.append('.')
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def test_gemini_api():
|
||||
"""Test Gemini API to identify the issue."""
|
||||
|
||||
# Check if API key is set
|
||||
api_key = os.getenv('GEMINI_API_KEY')
|
||||
if not api_key:
|
||||
logger.error("❌ GEMINI_API_KEY environment variable not set")
|
||||
return False
|
||||
|
||||
logger.info(f"🔑 Found Gemini API key: {api_key[:10]}...")
|
||||
|
||||
try:
|
||||
# Test basic API connectivity
|
||||
from services.llm_providers.gemini_provider import test_gemini_api_key
|
||||
is_valid, message = await test_gemini_api_key(api_key)
|
||||
|
||||
if is_valid:
|
||||
logger.info(f"✅ {message}")
|
||||
else:
|
||||
logger.error(f"❌ {message}")
|
||||
return False
|
||||
|
||||
# Test simple text generation
|
||||
from services.llm_providers.gemini_provider import gemini_pro_text_gen
|
||||
simple_response = gemini_pro_text_gen("Hello, this is a test. Please respond with 'Test successful'.")
|
||||
logger.info(f"📝 Simple text response: {simple_response}")
|
||||
|
||||
# Test structured JSON generation with a simple schema
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response
|
||||
|
||||
simple_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {"type": "string"},
|
||||
"status": {"type": "string"}
|
||||
}
|
||||
}
|
||||
|
||||
simple_prompt = "Generate a simple JSON response with a message and status."
|
||||
|
||||
logger.info("🧪 Testing structured JSON generation...")
|
||||
structured_response = gemini_structured_json_response(simple_prompt, simple_schema)
|
||||
logger.info(f"📋 Structured response: {structured_response}")
|
||||
|
||||
# Test with the actual autofill schema
|
||||
from api.content_planning.services.content_strategy.autofill.ai_structured_autofill import AIStructuredAutofillService
|
||||
|
||||
autofill_service = AIStructuredAutofillService()
|
||||
schema = autofill_service._build_schema()
|
||||
|
||||
logger.info(f"🔧 Autofill schema has {len(schema.get('properties', {}))} properties")
|
||||
|
||||
# Test with a minimal context
|
||||
test_context = {
|
||||
'user_id': 1,
|
||||
'website_analysis': {
|
||||
'url': 'https://test.com',
|
||||
'industry': 'Technology'
|
||||
}
|
||||
}
|
||||
|
||||
context_summary = autofill_service._build_context_summary(test_context)
|
||||
prompt = autofill_service._build_prompt(context_summary)
|
||||
|
||||
logger.info(f"📝 Autofill prompt length: {len(prompt)}")
|
||||
logger.info(f"📝 Autofill prompt preview: {prompt[:200]}...")
|
||||
|
||||
# Test the actual autofill call
|
||||
logger.info("🧪 Testing actual autofill generation...")
|
||||
autofill_result = await autofill_service.generate_autofill_fields(1, test_context)
|
||||
logger.info(f"📋 Autofill result: {autofill_result}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error testing Gemini API: {e}")
|
||||
import traceback
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = asyncio.run(test_gemini_api())
|
||||
if success:
|
||||
logger.info("✅ Gemini API test completed successfully")
|
||||
else:
|
||||
logger.error("❌ Gemini API test failed")
|
||||
sys.exit(1)
|
||||
@@ -1,463 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to validate onboarding data existence in the database.
|
||||
This script checks if onboarding data exists for test users and validates the data flow.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
# Add the backend directory to the Python path
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from services.database import get_db_session
|
||||
from models.onboarding import OnboardingSession, WebsiteAnalysis, ResearchPreferences, APIKey
|
||||
from models.enhanced_strategy_models import OnboardingDataIntegration
|
||||
from api.content_planning.services.content_strategy.onboarding.data_integration import OnboardingDataIntegrationService
|
||||
from api.content_planning.services.content_strategy.autofill.ai_structured_autofill import AIStructuredAutofillService
|
||||
from services.ai_service_manager import AIServiceManager
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout),
|
||||
logging.FileHandler('onboarding_test.log')
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class OnboardingDataValidator:
|
||||
"""Validator for onboarding data existence and quality."""
|
||||
|
||||
def __init__(self):
|
||||
self.db_session = get_db_session()
|
||||
self.data_integration_service = OnboardingDataIntegrationService()
|
||||
self.ai_service = AIStructuredAutofillService()
|
||||
self.ai_manager = AIServiceManager()
|
||||
|
||||
def test_database_connection(self) -> bool:
|
||||
"""Test database connection."""
|
||||
try:
|
||||
# Simple query to test connection
|
||||
from sqlalchemy import text
|
||||
result = self.db_session.execute(text("SELECT 1"))
|
||||
logger.info("✅ Database connection successful")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Database connection failed: {e}")
|
||||
return False
|
||||
|
||||
def check_onboarding_sessions(self, user_ids: list = None) -> Dict[int, Dict[str, Any]]:
|
||||
"""Check onboarding sessions for given user IDs."""
|
||||
if user_ids is None:
|
||||
user_ids = [1, 2, 3] # Default test user IDs
|
||||
|
||||
results = {}
|
||||
|
||||
for user_id in user_ids:
|
||||
logger.info(f"🔍 Checking onboarding session for user {user_id}")
|
||||
|
||||
try:
|
||||
session = self.db_session.query(OnboardingSession).filter(
|
||||
OnboardingSession.user_id == user_id
|
||||
).order_by(OnboardingSession.updated_at.desc()).first()
|
||||
|
||||
if session:
|
||||
results[user_id] = {
|
||||
'session_exists': True,
|
||||
'session_id': session.id,
|
||||
'status': session.status,
|
||||
'progress': session.progress,
|
||||
'created_at': session.created_at.isoformat(),
|
||||
'updated_at': session.updated_at.isoformat(),
|
||||
'data': session.to_dict() if hasattr(session, 'to_dict') else str(session)
|
||||
}
|
||||
logger.info(f"✅ Onboarding session found for user {user_id}: {session.status}")
|
||||
else:
|
||||
results[user_id] = {
|
||||
'session_exists': False,
|
||||
'error': 'No onboarding session found'
|
||||
}
|
||||
logger.warning(f"❌ No onboarding session found for user {user_id}")
|
||||
|
||||
except Exception as e:
|
||||
results[user_id] = {
|
||||
'session_exists': False,
|
||||
'error': str(e)
|
||||
}
|
||||
logger.error(f"❌ Error checking onboarding session for user {user_id}: {e}")
|
||||
|
||||
return results
|
||||
|
||||
def check_website_analysis(self, user_ids: list = None) -> Dict[int, Dict[str, Any]]:
|
||||
"""Check website analysis data for given user IDs."""
|
||||
if user_ids is None:
|
||||
user_ids = [1, 2, 3]
|
||||
|
||||
results = {}
|
||||
|
||||
for user_id in user_ids:
|
||||
logger.info(f"🔍 Checking website analysis for user {user_id}")
|
||||
|
||||
try:
|
||||
# Get onboarding session first
|
||||
session = self.db_session.query(OnboardingSession).filter(
|
||||
OnboardingSession.user_id == user_id
|
||||
).order_by(OnboardingSession.updated_at.desc()).first()
|
||||
|
||||
if not session:
|
||||
results[user_id] = {
|
||||
'website_analysis_exists': False,
|
||||
'error': 'No onboarding session found'
|
||||
}
|
||||
continue
|
||||
|
||||
# Get website analysis
|
||||
website_analysis = self.db_session.query(WebsiteAnalysis).filter(
|
||||
WebsiteAnalysis.session_id == session.id
|
||||
).order_by(WebsiteAnalysis.updated_at.desc()).first()
|
||||
|
||||
if website_analysis:
|
||||
results[user_id] = {
|
||||
'website_analysis_exists': True,
|
||||
'analysis_id': website_analysis.id,
|
||||
'website_url': website_analysis.website_url,
|
||||
'status': website_analysis.status,
|
||||
'created_at': website_analysis.created_at.isoformat(),
|
||||
'updated_at': website_analysis.updated_at.isoformat(),
|
||||
'data_keys': list(website_analysis.to_dict().keys()) if hasattr(website_analysis, 'to_dict') else []
|
||||
}
|
||||
logger.info(f"✅ Website analysis found for user {user_id}: {website_analysis.website_url}")
|
||||
else:
|
||||
results[user_id] = {
|
||||
'website_analysis_exists': False,
|
||||
'error': 'No website analysis found'
|
||||
}
|
||||
logger.warning(f"❌ No website analysis found for user {user_id}")
|
||||
|
||||
except Exception as e:
|
||||
results[user_id] = {
|
||||
'website_analysis_exists': False,
|
||||
'error': str(e)
|
||||
}
|
||||
logger.error(f"❌ Error checking website analysis for user {user_id}: {e}")
|
||||
|
||||
return results
|
||||
|
||||
def check_research_preferences(self, user_ids: list = None) -> Dict[int, Dict[str, Any]]:
|
||||
"""Check research preferences data for given user IDs."""
|
||||
if user_ids is None:
|
||||
user_ids = [1, 2, 3]
|
||||
|
||||
results = {}
|
||||
|
||||
for user_id in user_ids:
|
||||
logger.info(f"🔍 Checking research preferences for user {user_id}")
|
||||
|
||||
try:
|
||||
# Get onboarding session first
|
||||
session = self.db_session.query(OnboardingSession).filter(
|
||||
OnboardingSession.user_id == user_id
|
||||
).order_by(OnboardingSession.updated_at.desc()).first()
|
||||
|
||||
if not session:
|
||||
results[user_id] = {
|
||||
'research_preferences_exists': False,
|
||||
'error': 'No onboarding session found'
|
||||
}
|
||||
continue
|
||||
|
||||
# Get research preferences
|
||||
research_prefs = self.db_session.query(ResearchPreferences).filter(
|
||||
ResearchPreferences.session_id == session.id
|
||||
).first()
|
||||
|
||||
if research_prefs:
|
||||
results[user_id] = {
|
||||
'research_preferences_exists': True,
|
||||
'prefs_id': research_prefs.id,
|
||||
'research_depth': research_prefs.research_depth,
|
||||
'content_types': research_prefs.content_types,
|
||||
'created_at': research_prefs.created_at.isoformat(),
|
||||
'updated_at': research_prefs.updated_at.isoformat(),
|
||||
'data_keys': list(research_prefs.to_dict().keys()) if hasattr(research_prefs, 'to_dict') else []
|
||||
}
|
||||
logger.info(f"✅ Research preferences found for user {user_id}: {research_prefs.research_depth}")
|
||||
else:
|
||||
results[user_id] = {
|
||||
'research_preferences_exists': False,
|
||||
'error': 'No research preferences found'
|
||||
}
|
||||
logger.warning(f"❌ No research preferences found for user {user_id}")
|
||||
|
||||
except Exception as e:
|
||||
results[user_id] = {
|
||||
'research_preferences_exists': False,
|
||||
'error': str(e)
|
||||
}
|
||||
logger.error(f"❌ Error checking research preferences for user {user_id}: {e}")
|
||||
|
||||
return results
|
||||
|
||||
def check_api_keys(self, user_ids: list = None) -> Dict[int, Dict[str, Any]]:
|
||||
"""Check API keys data for given user IDs."""
|
||||
if user_ids is None:
|
||||
user_ids = [1, 2, 3]
|
||||
|
||||
results = {}
|
||||
|
||||
for user_id in user_ids:
|
||||
logger.info(f"🔍 Checking API keys for user {user_id}")
|
||||
|
||||
try:
|
||||
# Get onboarding session first
|
||||
session = self.db_session.query(OnboardingSession).filter(
|
||||
OnboardingSession.user_id == user_id
|
||||
).order_by(OnboardingSession.updated_at.desc()).first()
|
||||
|
||||
if not session:
|
||||
results[user_id] = {
|
||||
'api_keys_exist': False,
|
||||
'error': 'No onboarding session found'
|
||||
}
|
||||
continue
|
||||
|
||||
# Get API keys
|
||||
api_keys = self.db_session.query(APIKey).filter(
|
||||
APIKey.session_id == session.id
|
||||
).all()
|
||||
|
||||
if api_keys:
|
||||
results[user_id] = {
|
||||
'api_keys_exist': True,
|
||||
'count': len(api_keys),
|
||||
'providers': [key.provider for key in api_keys],
|
||||
'created_at': api_keys[0].created_at.isoformat() if api_keys else None,
|
||||
'updated_at': api_keys[0].updated_at.isoformat() if api_keys else None
|
||||
}
|
||||
logger.info(f"✅ API keys found for user {user_id}: {len(api_keys)} keys")
|
||||
else:
|
||||
results[user_id] = {
|
||||
'api_keys_exist': False,
|
||||
'error': 'No API keys found'
|
||||
}
|
||||
logger.warning(f"❌ No API keys found for user {user_id}")
|
||||
|
||||
except Exception as e:
|
||||
results[user_id] = {
|
||||
'api_keys_exist': False,
|
||||
'error': str(e)
|
||||
}
|
||||
logger.error(f"❌ Error checking API keys for user {user_id}: {e}")
|
||||
|
||||
return results
|
||||
|
||||
async def test_data_integration_service(self, user_id: int = 1) -> Dict[str, Any]:
|
||||
"""Test the data integration service."""
|
||||
logger.info(f"🔍 Testing data integration service for user {user_id}")
|
||||
|
||||
try:
|
||||
# Test the process_onboarding_data method
|
||||
integrated_data = await self.data_integration_service.process_onboarding_data(user_id, self.db_session)
|
||||
|
||||
if integrated_data:
|
||||
result = {
|
||||
'success': True,
|
||||
'has_website_analysis': bool(integrated_data.get('website_analysis')),
|
||||
'has_research_preferences': bool(integrated_data.get('research_preferences')),
|
||||
'has_api_keys_data': bool(integrated_data.get('api_keys_data')),
|
||||
'has_onboarding_session': bool(integrated_data.get('onboarding_session')),
|
||||
'data_quality': integrated_data.get('data_quality', {}),
|
||||
'processing_timestamp': integrated_data.get('processing_timestamp'),
|
||||
'context_keys': list(integrated_data.keys())
|
||||
}
|
||||
|
||||
logger.info(f"✅ Data integration successful for user {user_id}")
|
||||
logger.info(f" Website analysis: {result['has_website_analysis']}")
|
||||
logger.info(f" Research preferences: {result['has_research_preferences']}")
|
||||
logger.info(f" API keys: {result['has_api_keys_data']}")
|
||||
logger.info(f" Onboarding session: {result['has_onboarding_session']}")
|
||||
|
||||
return result
|
||||
else:
|
||||
logger.error(f"❌ Data integration returned None for user {user_id}")
|
||||
return {'success': False, 'error': 'No data returned'}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Data integration failed for user {user_id}: {e}")
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
async def test_ai_service_configuration(self) -> Dict[str, Any]:
|
||||
"""Test AI service configuration."""
|
||||
logger.info("🔍 Testing AI service configuration")
|
||||
|
||||
try:
|
||||
# Test basic AI service functionality
|
||||
test_prompt = "Generate a simple test response"
|
||||
test_schema = {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"test_field": {"type": "STRING", "description": "A test field"}
|
||||
},
|
||||
"required": ["test_field"]
|
||||
}
|
||||
|
||||
# Test the AI service manager
|
||||
result = await self.ai_manager.execute_structured_json_call(
|
||||
service_type="STRATEGIC_INTELLIGENCE",
|
||||
prompt=test_prompt,
|
||||
schema=test_schema
|
||||
)
|
||||
|
||||
if result and not result.get('error'):
|
||||
logger.info("✅ AI service configuration successful")
|
||||
return {
|
||||
'success': True,
|
||||
'ai_service_working': True,
|
||||
'test_response': result
|
||||
}
|
||||
else:
|
||||
logger.error(f"❌ AI service test failed: {result.get('error', 'Unknown error')}")
|
||||
return {
|
||||
'success': False,
|
||||
'ai_service_working': False,
|
||||
'error': result.get('error', 'Unknown error')
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ AI service configuration test failed: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'ai_service_working': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
async def test_ai_structured_autofill(self, user_id: int = 1) -> Dict[str, Any]:
|
||||
"""Test the AI structured autofill service."""
|
||||
logger.info(f"🔍 Testing AI structured autofill for user {user_id}")
|
||||
|
||||
try:
|
||||
# First get the context
|
||||
integrated_data = await self.data_integration_service.process_onboarding_data(user_id, self.db_session)
|
||||
|
||||
if not integrated_data:
|
||||
logger.error(f"❌ No integrated data available for user {user_id}")
|
||||
return {'success': False, 'error': 'No integrated data available'}
|
||||
|
||||
# Test the AI structured autofill
|
||||
result = await self.ai_service.generate_autofill_fields(user_id, integrated_data)
|
||||
|
||||
if result:
|
||||
meta = result.get('meta', {})
|
||||
fields = result.get('fields', {})
|
||||
|
||||
test_result = {
|
||||
'success': True,
|
||||
'ai_used': meta.get('ai_used', False),
|
||||
'ai_overrides_count': meta.get('ai_overrides_count', 0),
|
||||
'success_rate': meta.get('success_rate', 0),
|
||||
'attempts': meta.get('attempts', 0),
|
||||
'missing_fields': meta.get('missing_fields', []),
|
||||
'fields_generated': len(fields),
|
||||
'sample_fields': list(fields.keys())[:5] if fields else []
|
||||
}
|
||||
|
||||
logger.info(f"✅ AI structured autofill test completed for user {user_id}")
|
||||
logger.info(f" AI used: {test_result['ai_used']}")
|
||||
logger.info(f" Fields generated: {test_result['fields_generated']}")
|
||||
logger.info(f" Success rate: {test_result['success_rate']:.1f}%")
|
||||
logger.info(f" Attempts: {test_result['attempts']}")
|
||||
|
||||
return test_result
|
||||
else:
|
||||
logger.error(f"❌ AI structured autofill returned None for user {user_id}")
|
||||
return {'success': False, 'error': 'No result returned'}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ AI structured autofill test failed for user {user_id}: {e}")
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
def print_summary(self, results: Dict[str, Any]):
|
||||
"""Print a summary of all test results."""
|
||||
logger.info("\n" + "="*80)
|
||||
logger.info("📊 ONBOARDING DATA VALIDATION SUMMARY")
|
||||
logger.info("="*80)
|
||||
|
||||
for test_name, result in results.items():
|
||||
logger.info(f"\n🔍 {test_name.upper()}:")
|
||||
if isinstance(result, dict):
|
||||
for key, value in result.items():
|
||||
if isinstance(value, dict):
|
||||
logger.info(f" {key}:")
|
||||
for sub_key, sub_value in value.items():
|
||||
logger.info(f" {sub_key}: {sub_value}")
|
||||
else:
|
||||
logger.info(f" {key}: {value}")
|
||||
else:
|
||||
logger.info(f" {result}")
|
||||
|
||||
logger.info("\n" + "="*80)
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up database session."""
|
||||
if self.db_session:
|
||||
self.db_session.close()
|
||||
|
||||
async def main():
|
||||
"""Main test function."""
|
||||
logger.info("🚀 Starting onboarding data validation tests")
|
||||
|
||||
validator = OnboardingDataValidator()
|
||||
|
||||
try:
|
||||
# Test database connection
|
||||
db_connected = validator.test_database_connection()
|
||||
if not db_connected:
|
||||
logger.error("❌ Cannot proceed without database connection")
|
||||
return
|
||||
|
||||
# Test user IDs to check
|
||||
test_user_ids = [1, 2, 3]
|
||||
|
||||
# Run all tests
|
||||
results = {
|
||||
'database_connection': db_connected,
|
||||
'onboarding_sessions': validator.check_onboarding_sessions(test_user_ids),
|
||||
'website_analysis': validator.check_website_analysis(test_user_ids),
|
||||
'research_preferences': validator.check_research_preferences(test_user_ids),
|
||||
'api_keys': validator.check_api_keys(test_user_ids),
|
||||
'data_integration': await validator.test_data_integration_service(1),
|
||||
'ai_service_config': await validator.test_ai_service_configuration(),
|
||||
'ai_structured_autofill': await validator.test_ai_structured_autofill(1)
|
||||
}
|
||||
|
||||
# Print summary
|
||||
validator.print_summary(results)
|
||||
|
||||
# Determine overall status
|
||||
overall_success = all([
|
||||
results['database_connection'],
|
||||
any(session.get('session_exists', False) for session in results['onboarding_sessions'].values()),
|
||||
results['data_integration']['success'],
|
||||
results['ai_service_config']['success']
|
||||
])
|
||||
|
||||
if overall_success:
|
||||
logger.info("✅ All critical tests passed!")
|
||||
else:
|
||||
logger.error("❌ Some critical tests failed!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Test execution failed: {e}")
|
||||
finally:
|
||||
validator.cleanup()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
40
backend/test_simple_schema.py
Normal file
40
backend/test_simple_schema.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import asyncio
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response
|
||||
|
||||
async def test_simple_schema():
|
||||
"""Test with a very simple schema to see if structured output works at all"""
|
||||
|
||||
# Very simple schema
|
||||
simple_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"age": {"type": "integer"}
|
||||
}
|
||||
}
|
||||
|
||||
simple_prompt = "Generate a person with a name and age."
|
||||
|
||||
print("Testing simple schema...")
|
||||
print(f"Schema: {simple_schema}")
|
||||
print(f"Prompt: {simple_prompt}")
|
||||
print("\n" + "="*50 + "\n")
|
||||
|
||||
try:
|
||||
result = gemini_structured_json_response(
|
||||
prompt=simple_prompt,
|
||||
schema=simple_schema,
|
||||
temperature=0.3,
|
||||
max_tokens=100
|
||||
)
|
||||
|
||||
print("Result:")
|
||||
print(result)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_simple_schema())
|
||||
Reference in New Issue
Block a user