Implement persona generation system with platform-specific adaptations
Co-authored-by: ajay.calsoft <ajay.calsoft@gmail.com>
This commit is contained in:
@@ -354,12 +354,29 @@ async def complete_onboarding():
|
||||
detail="Cannot complete onboarding. At least one AI provider API key must be configured."
|
||||
)
|
||||
|
||||
# Generate writing persona from onboarding data
|
||||
try:
|
||||
from services.persona_analysis_service import PersonaAnalysisService
|
||||
persona_service = PersonaAnalysisService()
|
||||
|
||||
# Use user_id = 1 for now (assuming single user system)
|
||||
user_id = 1
|
||||
persona_result = persona_service.generate_persona_from_onboarding(user_id)
|
||||
|
||||
if "error" not in persona_result:
|
||||
logger.info(f"✅ Writing persona generated during onboarding completion: {persona_result.get('persona_id')}")
|
||||
else:
|
||||
logger.warning(f"⚠️ Persona generation failed during onboarding: {persona_result['error']}")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Non-critical error generating persona during onboarding: {str(e)}")
|
||||
|
||||
progress.complete_onboarding()
|
||||
|
||||
return {
|
||||
"message": "Onboarding completed successfully",
|
||||
"completed_at": progress.completed_at,
|
||||
"completion_percentage": 100.0
|
||||
"completion_percentage": 100.0,
|
||||
"persona_generated": "error" not in persona_result if 'persona_result' in locals() else False
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -522,9 +539,11 @@ async def get_onboarding_summary():
|
||||
from services.database import get_db
|
||||
from services.website_analysis_service import WebsiteAnalysisService
|
||||
from services.research_preferences_service import ResearchPreferencesService
|
||||
from services.persona_analysis_service import PersonaAnalysisService
|
||||
|
||||
# Get current session (assuming session ID 1 for now)
|
||||
session_id = 1
|
||||
user_id = 1 # Assuming single user system for now
|
||||
|
||||
# Get API keys
|
||||
api_manager = get_api_key_manager()
|
||||
@@ -548,18 +567,37 @@ async def get_onboarding_summary():
|
||||
'brand_voice': research_preferences.get('writing_style', {}).get('complexity', 'Trustworthy and Expert')
|
||||
}
|
||||
|
||||
# Check persona generation readiness
|
||||
persona_service = PersonaAnalysisService()
|
||||
persona_readiness = None
|
||||
try:
|
||||
# Check if persona can be generated
|
||||
onboarding_data = persona_service._collect_onboarding_data(user_id)
|
||||
if onboarding_data:
|
||||
data_sufficiency = persona_service._calculate_data_sufficiency(onboarding_data)
|
||||
persona_readiness = {
|
||||
"ready": data_sufficiency >= 50.0,
|
||||
"data_sufficiency": data_sufficiency,
|
||||
"can_generate": website_analysis is not None
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not check persona readiness: {str(e)}")
|
||||
persona_readiness = {"ready": False, "error": str(e)}
|
||||
|
||||
return {
|
||||
"api_keys": api_keys,
|
||||
"website_url": website_analysis.get('website_url') if website_analysis else None,
|
||||
"style_analysis": website_analysis.get('style_analysis') if website_analysis else None,
|
||||
"research_preferences": research_preferences,
|
||||
"personalization_settings": personalization_settings,
|
||||
"persona_readiness": persona_readiness,
|
||||
"integrations": {}, # TODO: Implement integrations data
|
||||
"capabilities": {
|
||||
"ai_content": len(api_keys) > 0,
|
||||
"style_analysis": website_analysis is not None,
|
||||
"research_tools": research_preferences is not None,
|
||||
"personalization": personalization_settings is not None,
|
||||
"persona_generation": persona_readiness.get("ready", False) if persona_readiness else False,
|
||||
"integrations": False # TODO: Implement
|
||||
}
|
||||
}
|
||||
@@ -607,4 +645,43 @@ async def get_research_preferences_data():
|
||||
return preferences
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting research preferences data: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
# New persona-related endpoints
|
||||
|
||||
async def check_persona_generation_readiness(user_id: int = 1):
|
||||
"""Check if user has sufficient data for persona generation."""
|
||||
try:
|
||||
from api.persona import validate_persona_generation_readiness
|
||||
return await validate_persona_generation_readiness(user_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking persona readiness: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def generate_persona_preview(user_id: int = 1):
|
||||
"""Generate a preview of the writing persona without saving."""
|
||||
try:
|
||||
from api.persona import generate_persona_preview
|
||||
return await generate_persona_preview(user_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating persona preview: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def generate_writing_persona(user_id: int = 1):
|
||||
"""Generate and save a writing persona from onboarding data."""
|
||||
try:
|
||||
from api.persona import generate_persona, PersonaGenerationRequest
|
||||
request = PersonaGenerationRequest(force_regenerate=False)
|
||||
return await generate_persona(user_id, request)
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating writing persona: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def get_user_writing_personas(user_id: int = 1):
|
||||
"""Get all writing personas for the user."""
|
||||
try:
|
||||
from api.persona import get_user_personas
|
||||
return await get_user_personas(user_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user personas: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
385
backend/api/persona.py
Normal file
385
backend/api/persona.py
Normal file
@@ -0,0 +1,385 @@
|
||||
"""
|
||||
Persona API endpoints for ALwrity.
|
||||
Handles writing persona generation, management, and platform-specific adaptations.
|
||||
"""
|
||||
|
||||
from fastapi import HTTPException, Depends
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
from loguru import logger
|
||||
|
||||
from services.persona_analysis_service import PersonaAnalysisService
|
||||
from services.database import get_db
|
||||
|
||||
class PersonaGenerationRequest(BaseModel):
|
||||
"""Request model for persona generation."""
|
||||
onboarding_session_id: Optional[int] = Field(None, description="Specific onboarding session ID to use")
|
||||
force_regenerate: bool = Field(False, description="Force regeneration even if persona exists")
|
||||
|
||||
class PersonaResponse(BaseModel):
|
||||
"""Response model for persona data."""
|
||||
persona_id: int
|
||||
persona_name: str
|
||||
archetype: str
|
||||
core_belief: str
|
||||
confidence_score: float
|
||||
platforms: List[str]
|
||||
created_at: str
|
||||
|
||||
class PlatformPersonaResponse(BaseModel):
|
||||
"""Response model for platform-specific persona."""
|
||||
platform_type: str
|
||||
sentence_metrics: Dict[str, Any]
|
||||
lexical_features: Dict[str, Any]
|
||||
content_format_rules: Dict[str, Any]
|
||||
engagement_patterns: Dict[str, Any]
|
||||
platform_best_practices: Dict[str, Any]
|
||||
|
||||
class PersonaGenerationResponse(BaseModel):
|
||||
"""Response model for persona generation result."""
|
||||
success: bool
|
||||
persona_id: Optional[int] = None
|
||||
message: str
|
||||
confidence_score: Optional[float] = None
|
||||
data_sufficiency: Optional[float] = None
|
||||
platforms_generated: List[str] = []
|
||||
|
||||
# Dependency to get persona service
|
||||
def get_persona_service() -> PersonaAnalysisService:
|
||||
"""Get the persona analysis service instance."""
|
||||
return PersonaAnalysisService()
|
||||
|
||||
async def generate_persona(user_id: int, request: PersonaGenerationRequest):
|
||||
"""Generate a new writing persona from onboarding data."""
|
||||
try:
|
||||
logger.info(f"Generating persona for user {user_id}")
|
||||
|
||||
persona_service = get_persona_service()
|
||||
|
||||
# Check if persona already exists and force_regenerate is False
|
||||
if not request.force_regenerate:
|
||||
existing_personas = persona_service.get_user_personas(user_id)
|
||||
if existing_personas:
|
||||
return PersonaGenerationResponse(
|
||||
success=False,
|
||||
message="Persona already exists. Use force_regenerate=true to create a new one.",
|
||||
persona_id=existing_personas[0]["id"]
|
||||
)
|
||||
|
||||
# Generate new persona
|
||||
result = persona_service.generate_persona_from_onboarding(
|
||||
user_id=user_id,
|
||||
onboarding_session_id=request.onboarding_session_id
|
||||
)
|
||||
|
||||
if "error" in result:
|
||||
return PersonaGenerationResponse(
|
||||
success=False,
|
||||
message=result["error"]
|
||||
)
|
||||
|
||||
return PersonaGenerationResponse(
|
||||
success=True,
|
||||
persona_id=result["persona_id"],
|
||||
message="Persona generated successfully",
|
||||
confidence_score=result["analysis_metadata"]["confidence_score"],
|
||||
data_sufficiency=result["analysis_metadata"].get("data_sufficiency", 0.0),
|
||||
platforms_generated=list(result["platform_personas"].keys())
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating persona: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to generate persona: {str(e)}")
|
||||
|
||||
async def get_user_personas(user_id: int):
|
||||
"""Get all personas for a user."""
|
||||
try:
|
||||
persona_service = get_persona_service()
|
||||
personas = persona_service.get_user_personas(user_id)
|
||||
|
||||
return {
|
||||
"personas": personas,
|
||||
"total_count": len(personas)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user personas: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get personas: {str(e)}")
|
||||
|
||||
async def get_persona_details(user_id: int, persona_id: int):
|
||||
"""Get detailed information about a specific persona."""
|
||||
try:
|
||||
from services.database import get_db_session
|
||||
from models.persona_models import WritingPersona, PlatformPersona
|
||||
|
||||
session = get_db_session()
|
||||
|
||||
# Get persona
|
||||
persona = session.query(WritingPersona).filter(
|
||||
WritingPersona.id == persona_id,
|
||||
WritingPersona.user_id == user_id,
|
||||
WritingPersona.is_active == True
|
||||
).first()
|
||||
|
||||
if not persona:
|
||||
raise HTTPException(status_code=404, detail="Persona not found")
|
||||
|
||||
# Get platform adaptations
|
||||
platform_personas = session.query(PlatformPersona).filter(
|
||||
PlatformPersona.writing_persona_id == persona_id,
|
||||
PlatformPersona.is_active == True
|
||||
).all()
|
||||
|
||||
result = persona.to_dict()
|
||||
result["platform_adaptations"] = [pp.to_dict() for pp in platform_personas]
|
||||
|
||||
session.close()
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting persona details: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get persona details: {str(e)}")
|
||||
|
||||
async def get_platform_persona(user_id: int, platform: str):
|
||||
"""Get persona adaptation for a specific platform."""
|
||||
try:
|
||||
persona_service = get_persona_service()
|
||||
platform_persona = persona_service.get_persona_for_platform(user_id, platform)
|
||||
|
||||
if not platform_persona:
|
||||
raise HTTPException(status_code=404, detail=f"No persona found for platform {platform}")
|
||||
|
||||
return platform_persona
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting platform persona: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get platform persona: {str(e)}")
|
||||
|
||||
async def update_persona(user_id: int, persona_id: int, update_data: Dict[str, Any]):
|
||||
"""Update an existing persona."""
|
||||
try:
|
||||
from services.database import get_db_session
|
||||
from models.persona_models import WritingPersona
|
||||
|
||||
session = get_db_session()
|
||||
|
||||
persona = session.query(WritingPersona).filter(
|
||||
WritingPersona.id == persona_id,
|
||||
WritingPersona.user_id == user_id
|
||||
).first()
|
||||
|
||||
if not persona:
|
||||
raise HTTPException(status_code=404, detail="Persona not found")
|
||||
|
||||
# Update allowed fields
|
||||
updatable_fields = [
|
||||
'persona_name', 'archetype', 'core_belief', 'brand_voice_description',
|
||||
'linguistic_fingerprint', 'platform_adaptations'
|
||||
]
|
||||
|
||||
for field in updatable_fields:
|
||||
if field in update_data:
|
||||
setattr(persona, field, update_data[field])
|
||||
|
||||
persona.updated_at = datetime.utcnow()
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
return {
|
||||
"message": "Persona updated successfully",
|
||||
"persona_id": persona_id,
|
||||
"updated_at": persona.updated_at.isoformat()
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating persona: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to update persona: {str(e)}")
|
||||
|
||||
async def delete_persona(user_id: int, persona_id: int):
|
||||
"""Delete a persona (soft delete by setting is_active=False)."""
|
||||
try:
|
||||
from services.database import get_db_session
|
||||
from models.persona_models import WritingPersona, PlatformPersona
|
||||
|
||||
session = get_db_session()
|
||||
|
||||
persona = session.query(WritingPersona).filter(
|
||||
WritingPersona.id == persona_id,
|
||||
WritingPersona.user_id == user_id
|
||||
).first()
|
||||
|
||||
if not persona:
|
||||
raise HTTPException(status_code=404, detail="Persona not found")
|
||||
|
||||
# Soft delete persona and platform adaptations
|
||||
persona.is_active = False
|
||||
persona.updated_at = datetime.utcnow()
|
||||
|
||||
platform_personas = session.query(PlatformPersona).filter(
|
||||
PlatformPersona.writing_persona_id == persona_id
|
||||
).all()
|
||||
|
||||
for pp in platform_personas:
|
||||
pp.is_active = False
|
||||
pp.updated_at = datetime.utcnow()
|
||||
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
return {
|
||||
"message": "Persona deleted successfully",
|
||||
"persona_id": persona_id
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting persona: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to delete persona: {str(e)}")
|
||||
|
||||
async def validate_persona_generation_readiness(user_id: int):
|
||||
"""Check if user has sufficient onboarding data for persona generation."""
|
||||
try:
|
||||
persona_service = get_persona_service()
|
||||
|
||||
# Get onboarding data
|
||||
onboarding_data = persona_service._collect_onboarding_data(user_id)
|
||||
|
||||
if not onboarding_data:
|
||||
return {
|
||||
"ready": False,
|
||||
"message": "No onboarding data found. Please complete onboarding first.",
|
||||
"missing_steps": ["All onboarding steps"],
|
||||
"data_sufficiency": 0.0
|
||||
}
|
||||
|
||||
data_sufficiency = persona_service._calculate_data_sufficiency(onboarding_data)
|
||||
|
||||
missing_steps = []
|
||||
if not onboarding_data.get("website_analysis"):
|
||||
missing_steps.append("Website Analysis (Step 2)")
|
||||
if not onboarding_data.get("research_preferences"):
|
||||
missing_steps.append("Research Preferences (Step 3)")
|
||||
|
||||
ready = data_sufficiency >= 50.0 # Require at least 50% data sufficiency
|
||||
|
||||
return {
|
||||
"ready": ready,
|
||||
"message": "Ready for persona generation" if ready else "Insufficient data for reliable persona generation",
|
||||
"missing_steps": missing_steps,
|
||||
"data_sufficiency": data_sufficiency,
|
||||
"recommendations": [
|
||||
"Complete website analysis for better style detection",
|
||||
"Provide research preferences for content type optimization"
|
||||
] if not ready else []
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating persona generation readiness: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to validate readiness: {str(e)}")
|
||||
|
||||
async def generate_persona_preview(user_id: int):
|
||||
"""Generate a preview of what the persona would look like without saving."""
|
||||
try:
|
||||
persona_service = get_persona_service()
|
||||
|
||||
# Get onboarding data
|
||||
onboarding_data = persona_service._collect_onboarding_data(user_id)
|
||||
|
||||
if not onboarding_data:
|
||||
raise HTTPException(status_code=400, detail="No onboarding data available")
|
||||
|
||||
# Generate core persona (without saving)
|
||||
core_persona = persona_service._generate_core_persona(onboarding_data)
|
||||
|
||||
if "error" in core_persona:
|
||||
raise HTTPException(status_code=400, detail=core_persona["error"])
|
||||
|
||||
# Generate sample platform adaptation (just one for preview)
|
||||
sample_platform = "linkedin"
|
||||
platform_preview = persona_service._generate_single_platform_persona(
|
||||
core_persona, sample_platform, onboarding_data
|
||||
)
|
||||
|
||||
return {
|
||||
"preview": {
|
||||
"identity": core_persona.get("identity", {}),
|
||||
"linguistic_fingerprint": core_persona.get("linguistic_fingerprint", {}),
|
||||
"tonal_range": core_persona.get("tonal_range", {}),
|
||||
"sample_platform": {
|
||||
"platform": sample_platform,
|
||||
"adaptation": platform_preview
|
||||
}
|
||||
},
|
||||
"confidence_score": core_persona.get("confidence_score", 0.0),
|
||||
"data_sufficiency": persona_service._calculate_data_sufficiency(onboarding_data)
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating persona preview: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to generate preview: {str(e)}")
|
||||
|
||||
async def get_supported_platforms():
|
||||
"""Get list of supported platforms for persona generation."""
|
||||
return {
|
||||
"platforms": [
|
||||
{
|
||||
"id": "twitter",
|
||||
"name": "Twitter/X",
|
||||
"description": "Microblogging platform optimized for short, engaging content",
|
||||
"character_limit": 280,
|
||||
"optimal_length": "120-150 characters"
|
||||
},
|
||||
{
|
||||
"id": "linkedin",
|
||||
"name": "LinkedIn",
|
||||
"description": "Professional networking platform for thought leadership content",
|
||||
"character_limit": 3000,
|
||||
"optimal_length": "150-300 words"
|
||||
},
|
||||
{
|
||||
"id": "instagram",
|
||||
"name": "Instagram",
|
||||
"description": "Visual-first platform with engaging captions",
|
||||
"character_limit": 2200,
|
||||
"optimal_length": "125-150 words"
|
||||
},
|
||||
{
|
||||
"id": "facebook",
|
||||
"name": "Facebook",
|
||||
"description": "Social networking platform for community engagement",
|
||||
"character_limit": 63206,
|
||||
"optimal_length": "40-80 words"
|
||||
},
|
||||
{
|
||||
"id": "blog",
|
||||
"name": "Blog Posts",
|
||||
"description": "Long-form content optimized for SEO and engagement",
|
||||
"word_count": "800-2000 words",
|
||||
"seo_optimized": True
|
||||
},
|
||||
{
|
||||
"id": "medium",
|
||||
"name": "Medium",
|
||||
"description": "Publishing platform for storytelling and thought leadership",
|
||||
"word_count": "1000-3000 words",
|
||||
"storytelling_focus": True
|
||||
},
|
||||
{
|
||||
"id": "substack",
|
||||
"name": "Substack",
|
||||
"description": "Newsletter platform for building subscriber relationships",
|
||||
"format": "email newsletter",
|
||||
"subscription_focus": True
|
||||
}
|
||||
]
|
||||
}
|
||||
167
backend/api/persona_routes.py
Normal file
167
backend/api/persona_routes.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
FastAPI routes for persona management.
|
||||
Integrates persona generation and management into the main API.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from api.persona import (
|
||||
generate_persona,
|
||||
get_user_personas,
|
||||
get_persona_details,
|
||||
get_platform_persona,
|
||||
update_persona,
|
||||
delete_persona,
|
||||
validate_persona_generation_readiness,
|
||||
generate_persona_preview,
|
||||
get_supported_platforms,
|
||||
PersonaGenerationRequest
|
||||
)
|
||||
|
||||
from services.persona_replication_engine import PersonaReplicationEngine
|
||||
|
||||
# Create router
|
||||
router = APIRouter(prefix="/api/personas", tags=["personas"])
|
||||
|
||||
@router.post("/generate")
|
||||
async def generate_persona_endpoint(
|
||||
request: PersonaGenerationRequest,
|
||||
user_id: int = Query(1, description="User ID")
|
||||
):
|
||||
"""Generate a new writing persona from onboarding data."""
|
||||
return await generate_persona(user_id, request)
|
||||
|
||||
@router.get("/user/{user_id}")
|
||||
async def get_user_personas_endpoint(user_id: int):
|
||||
"""Get all personas for a user."""
|
||||
return await get_user_personas(user_id)
|
||||
|
||||
@router.get("/{persona_id}")
|
||||
async def get_persona_details_endpoint(
|
||||
persona_id: int,
|
||||
user_id: int = Query(..., description="User ID")
|
||||
):
|
||||
"""Get detailed information about a specific persona."""
|
||||
return await get_persona_details(user_id, persona_id)
|
||||
|
||||
@router.get("/platform/{platform}")
|
||||
async def get_platform_persona_endpoint(
|
||||
platform: str,
|
||||
user_id: int = Query(1, description="User ID")
|
||||
):
|
||||
"""Get persona adaptation for a specific platform."""
|
||||
return await get_platform_persona(user_id, platform)
|
||||
|
||||
@router.put("/{persona_id}")
|
||||
async def update_persona_endpoint(
|
||||
persona_id: int,
|
||||
update_data: Dict[str, Any],
|
||||
user_id: int = Query(..., description="User ID")
|
||||
):
|
||||
"""Update an existing persona."""
|
||||
return await update_persona(user_id, persona_id, update_data)
|
||||
|
||||
@router.delete("/{persona_id}")
|
||||
async def delete_persona_endpoint(
|
||||
persona_id: int,
|
||||
user_id: int = Query(..., description="User ID")
|
||||
):
|
||||
"""Delete a persona."""
|
||||
return await delete_persona(user_id, persona_id)
|
||||
|
||||
@router.get("/check/readiness")
|
||||
async def check_persona_readiness_endpoint(
|
||||
user_id: int = Query(1, description="User ID")
|
||||
):
|
||||
"""Check if user has sufficient data for persona generation."""
|
||||
return await validate_persona_generation_readiness(user_id)
|
||||
|
||||
@router.get("/preview/generate")
|
||||
async def generate_preview_endpoint(
|
||||
user_id: int = Query(1, description="User ID")
|
||||
):
|
||||
"""Generate a preview of the writing persona without saving."""
|
||||
return await generate_persona_preview(user_id)
|
||||
|
||||
@router.get("/platforms/supported")
|
||||
async def get_supported_platforms_endpoint():
|
||||
"""Get list of supported platforms for persona generation."""
|
||||
return await get_supported_platforms()
|
||||
|
||||
@router.post("/generate-content")
|
||||
async def generate_content_with_persona_endpoint(
|
||||
request: Dict[str, Any]
|
||||
):
|
||||
"""Generate content using persona replication engine."""
|
||||
try:
|
||||
user_id = request.get("user_id", 1)
|
||||
platform = request.get("platform")
|
||||
content_request = request.get("content_request")
|
||||
content_type = request.get("content_type", "post")
|
||||
|
||||
if not platform or not content_request:
|
||||
raise HTTPException(status_code=400, detail="Platform and content_request are required")
|
||||
|
||||
engine = PersonaReplicationEngine()
|
||||
result = engine.generate_content_with_persona(
|
||||
user_id=user_id,
|
||||
platform=platform,
|
||||
content_request=content_request,
|
||||
content_type=content_type
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Content generation failed: {str(e)}")
|
||||
|
||||
@router.get("/export/{platform}")
|
||||
async def export_persona_prompt_endpoint(
|
||||
platform: str,
|
||||
user_id: int = Query(1, description="User ID")
|
||||
):
|
||||
"""Export hardened persona prompt for external use."""
|
||||
try:
|
||||
engine = PersonaReplicationEngine()
|
||||
export_package = engine.export_persona_for_external_use(user_id, platform)
|
||||
|
||||
if "error" in export_package:
|
||||
raise HTTPException(status_code=400, detail=export_package["error"])
|
||||
|
||||
return export_package
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Export failed: {str(e)}")
|
||||
|
||||
@router.post("/validate-content")
|
||||
async def validate_content_endpoint(
|
||||
request: Dict[str, Any]
|
||||
):
|
||||
"""Validate content against persona constraints."""
|
||||
try:
|
||||
user_id = request.get("user_id", 1)
|
||||
platform = request.get("platform")
|
||||
content = request.get("content")
|
||||
|
||||
if not platform or not content:
|
||||
raise HTTPException(status_code=400, detail="Platform and content are required")
|
||||
|
||||
engine = PersonaReplicationEngine()
|
||||
persona_data = engine.persona_service.get_persona_for_platform(user_id, platform)
|
||||
|
||||
if not persona_data:
|
||||
raise HTTPException(status_code=404, detail="No persona found for platform")
|
||||
|
||||
validation_result = engine._validate_content_fidelity(content, persona_data, platform)
|
||||
|
||||
return {
|
||||
"validation_result": validation_result,
|
||||
"persona_id": persona_data["core_persona"]["id"],
|
||||
"platform": platform
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Validation failed: {str(e)}")
|
||||
@@ -374,6 +374,10 @@ app.include_router(content_planning_router)
|
||||
app.include_router(user_data_router)
|
||||
app.include_router(strategy_copilot_router)
|
||||
|
||||
# Include persona router
|
||||
from api.persona_routes import router as persona_router
|
||||
app.include_router(persona_router)
|
||||
|
||||
# SEO Dashboard endpoints
|
||||
@app.get("/api/seo-dashboard/data")
|
||||
async def seo_dashboard_data():
|
||||
|
||||
197
backend/deploy_persona_system.py
Normal file
197
backend/deploy_persona_system.py
Normal file
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Deployment script for the Persona System.
|
||||
Sets up database tables and validates the complete system.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the backend directory to the Python path
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from loguru import logger
|
||||
|
||||
def deploy_persona_system():
|
||||
"""Deploy the complete persona system."""
|
||||
|
||||
logger.info("🚀 Deploying Persona System")
|
||||
|
||||
try:
|
||||
# Step 1: Create database tables
|
||||
logger.info("📊 Step 1: Creating database tables...")
|
||||
from scripts.create_persona_tables import create_persona_tables
|
||||
create_persona_tables()
|
||||
logger.info("✅ Database tables created")
|
||||
|
||||
# Step 2: Validate Gemini integration
|
||||
logger.info("🤖 Step 2: Validating Gemini integration...")
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response
|
||||
|
||||
test_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {"type": "string"},
|
||||
"timestamp": {"type": "string"}
|
||||
},
|
||||
"required": ["status"]
|
||||
}
|
||||
|
||||
test_response = gemini_structured_json_response(
|
||||
prompt="Return status='ready' and current timestamp",
|
||||
schema=test_schema,
|
||||
temperature=0.1,
|
||||
max_tokens=1024
|
||||
)
|
||||
|
||||
if "error" in test_response:
|
||||
logger.warning(f"⚠️ Gemini test warning: {test_response['error']}")
|
||||
else:
|
||||
logger.info("✅ Gemini integration validated")
|
||||
|
||||
# Step 3: Test persona service
|
||||
logger.info("🧠 Step 3: Testing persona service...")
|
||||
from services.persona_analysis_service import PersonaAnalysisService
|
||||
persona_service = PersonaAnalysisService()
|
||||
logger.info("✅ Persona service initialized")
|
||||
|
||||
# Step 4: Test replication engine
|
||||
logger.info("⚙️ Step 4: Testing replication engine...")
|
||||
from services.persona_replication_engine import PersonaReplicationEngine
|
||||
replication_engine = PersonaReplicationEngine()
|
||||
logger.info("✅ Replication engine initialized")
|
||||
|
||||
# Step 5: Validate API endpoints
|
||||
logger.info("🌐 Step 5: Validating API endpoints...")
|
||||
from api.persona_routes import router
|
||||
logger.info(f"✅ Persona router configured with {len(router.routes)} routes")
|
||||
|
||||
logger.info("🎉 Persona System deployed successfully!")
|
||||
|
||||
# Print deployment summary
|
||||
print_deployment_summary()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Deployment failed: {str(e)}")
|
||||
return False
|
||||
|
||||
def print_deployment_summary():
|
||||
"""Print deployment summary and next steps."""
|
||||
|
||||
logger.info("📋 PERSONA SYSTEM DEPLOYMENT SUMMARY")
|
||||
logger.info("=" * 50)
|
||||
|
||||
logger.info("✅ Database Tables:")
|
||||
logger.info(" - writing_personas")
|
||||
logger.info(" - platform_personas")
|
||||
logger.info(" - persona_analysis_results")
|
||||
logger.info(" - persona_validation_results")
|
||||
|
||||
logger.info("✅ Services:")
|
||||
logger.info(" - PersonaAnalysisService")
|
||||
logger.info(" - PersonaReplicationEngine")
|
||||
|
||||
logger.info("✅ API Endpoints:")
|
||||
logger.info(" - POST /api/personas/generate")
|
||||
logger.info(" - GET /api/personas/user/{user_id}")
|
||||
logger.info(" - GET /api/personas/platform/{platform}")
|
||||
logger.info(" - GET /api/personas/export/{platform}")
|
||||
|
||||
logger.info("✅ Platform Support:")
|
||||
logger.info(" - Twitter/X, LinkedIn, Instagram, Facebook")
|
||||
logger.info(" - Blog, Medium, Substack")
|
||||
|
||||
logger.info("🔧 NEXT STEPS:")
|
||||
logger.info("1. Complete onboarding with website analysis (Step 2)")
|
||||
logger.info("2. Set research preferences (Step 3)")
|
||||
logger.info("3. Generate persona in Final Step (Step 6)")
|
||||
logger.info("4. Export hardened prompts for external AI systems")
|
||||
logger.info("5. Use persona for consistent content generation")
|
||||
|
||||
logger.info("=" * 50)
|
||||
|
||||
def validate_deployment():
|
||||
"""Validate that all components are working correctly."""
|
||||
|
||||
logger.info("🔍 Validating deployment...")
|
||||
|
||||
validation_results = {
|
||||
"database": False,
|
||||
"gemini": False,
|
||||
"persona_service": False,
|
||||
"replication_engine": False,
|
||||
"api_routes": False
|
||||
}
|
||||
|
||||
try:
|
||||
# Test database
|
||||
from services.database import get_db_session
|
||||
session = get_db_session()
|
||||
if session:
|
||||
session.close()
|
||||
validation_results["database"] = True
|
||||
logger.info("✅ Database connection validated")
|
||||
|
||||
# Test Gemini
|
||||
from services.llm_providers.gemini_provider import get_gemini_api_key
|
||||
api_key = get_gemini_api_key()
|
||||
if api_key and api_key != "your_gemini_api_key_here":
|
||||
validation_results["gemini"] = True
|
||||
logger.info("✅ Gemini API key configured")
|
||||
else:
|
||||
logger.warning("⚠️ Gemini API key not configured")
|
||||
|
||||
# Test services
|
||||
from services.persona_analysis_service import PersonaAnalysisService
|
||||
from services.persona_replication_engine import PersonaReplicationEngine
|
||||
|
||||
PersonaAnalysisService()
|
||||
PersonaReplicationEngine()
|
||||
validation_results["persona_service"] = True
|
||||
validation_results["replication_engine"] = True
|
||||
logger.info("✅ Services validated")
|
||||
|
||||
# Test API routes
|
||||
from api.persona_routes import router
|
||||
if len(router.routes) > 0:
|
||||
validation_results["api_routes"] = True
|
||||
logger.info("✅ API routes validated")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Validation error: {str(e)}")
|
||||
|
||||
# Summary
|
||||
passed = sum(validation_results.values())
|
||||
total = len(validation_results)
|
||||
|
||||
logger.info(f"📊 Validation Results: {passed}/{total} components validated")
|
||||
|
||||
if passed == total:
|
||||
logger.info("🎉 All components validated successfully!")
|
||||
return True
|
||||
else:
|
||||
logger.warning("⚠️ Some components failed validation")
|
||||
for component, status in validation_results.items():
|
||||
status_icon = "✅" if status else "❌"
|
||||
logger.info(f" {status_icon} {component}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Deploy system
|
||||
deployment_success = deploy_persona_system()
|
||||
|
||||
if deployment_success:
|
||||
# Validate deployment
|
||||
validation_success = validate_deployment()
|
||||
|
||||
if validation_success:
|
||||
logger.info("🎉 Persona System ready for production!")
|
||||
sys.exit(0)
|
||||
else:
|
||||
logger.error("❌ Deployment validation failed")
|
||||
sys.exit(1)
|
||||
else:
|
||||
logger.error("❌ Deployment failed")
|
||||
sys.exit(1)
|
||||
234
backend/models/persona_models.py
Normal file
234
backend/models/persona_models.py
Normal file
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
Writing Persona Database Models
|
||||
Defines database schema for storing writing personas based on onboarding data analysis.
|
||||
Each persona represents a platform-specific writing style derived from user's onboarding data.
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, Float, JSON, ForeignKey, Boolean
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
class WritingPersona(Base):
|
||||
"""Main writing persona model that stores the core persona profile."""
|
||||
|
||||
__tablename__ = "writing_personas"
|
||||
|
||||
# Primary fields
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, nullable=False)
|
||||
persona_name = Column(String(255), nullable=False) # e.g., "Professional LinkedIn Voice", "Casual Blog Writer"
|
||||
|
||||
# Core Identity
|
||||
archetype = Column(String(100), nullable=True) # e.g., "The Pragmatic Futurist", "The Thoughtful Educator"
|
||||
core_belief = Column(Text, nullable=True) # Central philosophy or belief system
|
||||
brand_voice_description = Column(Text, nullable=True) # Detailed brand voice description
|
||||
|
||||
# Linguistic Fingerprint - Quantitative Analysis
|
||||
linguistic_fingerprint = Column(JSON, nullable=True) # Complete linguistic analysis
|
||||
|
||||
# Platform-specific adaptations
|
||||
platform_adaptations = Column(JSON, nullable=True) # How persona adapts across platforms
|
||||
|
||||
# Source data tracking
|
||||
onboarding_session_id = Column(Integer, nullable=True) # Link to onboarding session
|
||||
source_website_analysis = Column(JSON, nullable=True) # Website analysis data used
|
||||
source_research_preferences = Column(JSON, nullable=True) # Research preferences used
|
||||
|
||||
# AI Analysis metadata
|
||||
ai_analysis_version = Column(String(50), nullable=True) # Version of AI analysis used
|
||||
confidence_score = Column(Float, nullable=True) # AI confidence in persona accuracy
|
||||
analysis_date = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# Metadata
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
# Relationships
|
||||
platform_personas = relationship("PlatformPersona", back_populates="writing_persona", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<WritingPersona(id={self.id}, name='{self.persona_name}', user_id={self.user_id})>"
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert model to dictionary."""
|
||||
return {
|
||||
'id': self.id,
|
||||
'user_id': self.user_id,
|
||||
'persona_name': self.persona_name,
|
||||
'archetype': self.archetype,
|
||||
'core_belief': self.core_belief,
|
||||
'brand_voice_description': self.brand_voice_description,
|
||||
'linguistic_fingerprint': self.linguistic_fingerprint,
|
||||
'platform_adaptations': self.platform_adaptations,
|
||||
'onboarding_session_id': self.onboarding_session_id,
|
||||
'source_website_analysis': self.source_website_analysis,
|
||||
'source_research_preferences': self.source_research_preferences,
|
||||
'ai_analysis_version': self.ai_analysis_version,
|
||||
'confidence_score': self.confidence_score,
|
||||
'analysis_date': self.analysis_date.isoformat() if self.analysis_date else None,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
||||
'is_active': self.is_active
|
||||
}
|
||||
|
||||
class PlatformPersona(Base):
|
||||
"""Platform-specific persona adaptations for different social media platforms and blogging."""
|
||||
|
||||
__tablename__ = "platform_personas"
|
||||
|
||||
# Primary fields
|
||||
id = Column(Integer, primary_key=True)
|
||||
writing_persona_id = Column(Integer, ForeignKey("writing_personas.id"), nullable=False)
|
||||
platform_type = Column(String(50), nullable=False) # twitter, linkedin, instagram, facebook, blog, medium, substack
|
||||
|
||||
# Platform-specific linguistic constraints
|
||||
sentence_metrics = Column(JSON, nullable=True) # Platform-optimized sentence structure
|
||||
lexical_features = Column(JSON, nullable=True) # Platform-specific vocabulary and phrases
|
||||
rhetorical_devices = Column(JSON, nullable=True) # Platform-appropriate rhetorical patterns
|
||||
tonal_range = Column(JSON, nullable=True) # Permitted tones for this platform
|
||||
stylistic_constraints = Column(JSON, nullable=True) # Platform formatting rules
|
||||
|
||||
# Platform-specific content guidelines
|
||||
content_format_rules = Column(JSON, nullable=True) # Character limits, hashtag usage, etc.
|
||||
engagement_patterns = Column(JSON, nullable=True) # How to engage on this platform
|
||||
posting_frequency = Column(JSON, nullable=True) # Optimal posting schedule
|
||||
content_types = Column(JSON, nullable=True) # Preferred content types for platform
|
||||
|
||||
# Performance optimization
|
||||
platform_best_practices = Column(JSON, nullable=True) # Platform-specific best practices
|
||||
algorithm_considerations = Column(JSON, nullable=True) # Platform algorithm optimization
|
||||
|
||||
# Metadata
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
# Relationships
|
||||
writing_persona = relationship("WritingPersona", back_populates="platform_personas")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<PlatformPersona(id={self.id}, platform='{self.platform_type}', persona_id={self.writing_persona_id})>"
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert model to dictionary."""
|
||||
return {
|
||||
'id': self.id,
|
||||
'writing_persona_id': self.writing_persona_id,
|
||||
'platform_type': self.platform_type,
|
||||
'sentence_metrics': self.sentence_metrics,
|
||||
'lexical_features': self.lexical_features,
|
||||
'rhetorical_devices': self.rhetorical_devices,
|
||||
'tonal_range': self.tonal_range,
|
||||
'stylistic_constraints': self.stylistic_constraints,
|
||||
'content_format_rules': self.content_format_rules,
|
||||
'engagement_patterns': self.engagement_patterns,
|
||||
'posting_frequency': self.posting_frequency,
|
||||
'content_types': self.content_types,
|
||||
'platform_best_practices': self.platform_best_practices,
|
||||
'algorithm_considerations': self.algorithm_considerations,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
||||
'is_active': self.is_active
|
||||
}
|
||||
|
||||
class PersonaAnalysisResult(Base):
|
||||
"""Stores AI analysis results used to generate personas."""
|
||||
|
||||
__tablename__ = "persona_analysis_results"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, nullable=False)
|
||||
writing_persona_id = Column(Integer, ForeignKey("writing_personas.id"), nullable=True)
|
||||
|
||||
# Analysis input data
|
||||
analysis_prompt = Column(Text, nullable=True) # The prompt used for analysis
|
||||
input_data = Column(JSON, nullable=True) # Raw input data from onboarding
|
||||
|
||||
# AI Analysis results
|
||||
linguistic_analysis = Column(JSON, nullable=True) # Detailed linguistic fingerprint analysis
|
||||
personality_analysis = Column(JSON, nullable=True) # Personality and archetype analysis
|
||||
platform_recommendations = Column(JSON, nullable=True) # Platform-specific recommendations
|
||||
style_guidelines = Column(JSON, nullable=True) # Generated style guidelines
|
||||
|
||||
# Quality metrics
|
||||
analysis_confidence = Column(Float, nullable=True) # AI confidence in analysis
|
||||
data_sufficiency_score = Column(Float, nullable=True) # How much data was available for analysis
|
||||
recommendation_quality = Column(Float, nullable=True) # Quality of generated recommendations
|
||||
|
||||
# AI service metadata
|
||||
ai_provider = Column(String(50), nullable=True) # gemini, openai, anthropic
|
||||
model_version = Column(String(100), nullable=True) # Specific model version used
|
||||
processing_time = Column(Float, nullable=True) # Processing time in seconds
|
||||
|
||||
# Metadata
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<PersonaAnalysisResult(id={self.id}, user_id={self.user_id}, provider='{self.ai_provider}')>"
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert model to dictionary."""
|
||||
return {
|
||||
'id': self.id,
|
||||
'user_id': self.user_id,
|
||||
'writing_persona_id': self.writing_persona_id,
|
||||
'analysis_prompt': self.analysis_prompt,
|
||||
'input_data': self.input_data,
|
||||
'linguistic_analysis': self.linguistic_analysis,
|
||||
'personality_analysis': self.personality_analysis,
|
||||
'platform_recommendations': self.platform_recommendations,
|
||||
'style_guidelines': self.style_guidelines,
|
||||
'analysis_confidence': self.analysis_confidence,
|
||||
'data_sufficiency_score': self.data_sufficiency_score,
|
||||
'recommendation_quality': self.recommendation_quality,
|
||||
'ai_provider': self.ai_provider,
|
||||
'model_version': self.model_version,
|
||||
'processing_time': self.processing_time,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
class PersonaValidationResult(Base):
|
||||
"""Stores validation results for generated personas."""
|
||||
|
||||
__tablename__ = "persona_validation_results"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
writing_persona_id = Column(Integer, ForeignKey("writing_personas.id"), nullable=False)
|
||||
platform_persona_id = Column(Integer, ForeignKey("platform_personas.id"), nullable=True)
|
||||
|
||||
# Validation metrics
|
||||
stylometric_accuracy = Column(Float, nullable=True) # How well persona matches original style
|
||||
consistency_score = Column(Float, nullable=True) # Consistency across generated content
|
||||
platform_compliance = Column(Float, nullable=True) # How well adapted to platform constraints
|
||||
|
||||
# Test results
|
||||
sample_outputs = Column(JSON, nullable=True) # Sample content generated with persona
|
||||
validation_feedback = Column(JSON, nullable=True) # User or automated feedback
|
||||
improvement_suggestions = Column(JSON, nullable=True) # Suggestions for persona refinement
|
||||
|
||||
# Metadata
|
||||
validation_date = Column(DateTime, default=datetime.utcnow)
|
||||
validator_type = Column(String(50), nullable=True) # automated, user, ai_review
|
||||
|
||||
def __repr__(self):
|
||||
return f"<PersonaValidationResult(id={self.id}, persona_id={self.writing_persona_id}, accuracy={self.stylometric_accuracy})>"
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert model to dictionary."""
|
||||
return {
|
||||
'id': self.id,
|
||||
'writing_persona_id': self.writing_persona_id,
|
||||
'platform_persona_id': self.platform_persona_id,
|
||||
'stylometric_accuracy': self.stylometric_accuracy,
|
||||
'consistency_score': self.consistency_score,
|
||||
'platform_compliance': self.platform_compliance,
|
||||
'sample_outputs': self.sample_outputs,
|
||||
'validation_feedback': self.validation_feedback,
|
||||
'improvement_suggestions': self.improvement_suggestions,
|
||||
'validation_date': self.validation_date.isoformat() if self.validation_date else None,
|
||||
'validator_type': self.validator_type
|
||||
}
|
||||
@@ -13,6 +13,7 @@ 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 models.persona_models import Base as PersonaBase
|
||||
from loguru import logger
|
||||
|
||||
def create_all_tables():
|
||||
@@ -30,6 +31,11 @@ def create_all_tables():
|
||||
MonitoringBase.metadata.create_all(bind=engine)
|
||||
logger.info("✅ Monitoring tables created!")
|
||||
|
||||
# Step 3: Create persona tables
|
||||
logger.info("Step 3: Creating persona tables...")
|
||||
PersonaBase.metadata.create_all(bind=engine)
|
||||
logger.info("✅ Persona tables created!")
|
||||
|
||||
logger.info("✅ All tables created successfully!")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
53
backend/scripts/create_persona_tables.py
Normal file
53
backend/scripts/create_persona_tables.py
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to create persona database tables.
|
||||
This script creates the new persona-related tables for storing writing personas.
|
||||
"""
|
||||
|
||||
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.persona_models import Base as PersonaBase
|
||||
from loguru import logger
|
||||
|
||||
def create_persona_tables():
|
||||
"""Create all persona-related tables"""
|
||||
try:
|
||||
logger.info("Creating persona database tables...")
|
||||
|
||||
# Create persona tables
|
||||
logger.info("Creating persona tables...")
|
||||
PersonaBase.metadata.create_all(bind=engine)
|
||||
logger.info("✅ Persona tables created!")
|
||||
|
||||
logger.info("✅ All persona tables created successfully!")
|
||||
|
||||
# Verify tables were created
|
||||
from sqlalchemy import inspect
|
||||
inspector = inspect(engine)
|
||||
tables = inspector.get_table_names()
|
||||
|
||||
persona_tables = [
|
||||
'writing_personas',
|
||||
'platform_personas',
|
||||
'persona_analysis_results',
|
||||
'persona_validation_results'
|
||||
]
|
||||
|
||||
created_tables = [table for table in persona_tables if table in tables]
|
||||
logger.info(f"✅ Verified tables created: {created_tables}")
|
||||
|
||||
if len(created_tables) != len(persona_tables):
|
||||
missing = [table for table in persona_tables if table not in created_tables]
|
||||
logger.warning(f"⚠️ Missing tables: {missing}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error creating persona tables: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_persona_tables()
|
||||
BIN
backend/services/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/services/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/services/__pycache__/api_key_manager.cpython-313.pyc
Normal file
BIN
backend/services/__pycache__/api_key_manager.cpython-313.pyc
Normal file
Binary file not shown.
@@ -17,6 +17,7 @@ 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
|
||||
from models.persona_models import Base as PersonaBase
|
||||
|
||||
# Database configuration
|
||||
DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///./alwrity.db')
|
||||
@@ -57,7 +58,8 @@ def init_database():
|
||||
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")
|
||||
PersonaBase.metadata.create_all(bind=engine)
|
||||
logger.info("Database initialized successfully with all models including personas")
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Error initializing database: {str(e)}")
|
||||
raise
|
||||
|
||||
668
backend/services/persona_analysis_service.py
Normal file
668
backend/services/persona_analysis_service.py
Normal file
@@ -0,0 +1,668 @@
|
||||
"""
|
||||
Persona Analysis Service
|
||||
Uses Gemini structured responses to analyze onboarding data and create writing personas.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from loguru import logger
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
from services.database import get_db_session
|
||||
from models.onboarding import OnboardingSession, WebsiteAnalysis, ResearchPreferences
|
||||
from models.persona_models import WritingPersona, PlatformPersona, PersonaAnalysisResult
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response
|
||||
|
||||
class PersonaAnalysisService:
|
||||
"""Service for analyzing onboarding data and generating writing personas using Gemini AI."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the persona analysis service."""
|
||||
logger.info("PersonaAnalysisService initialized")
|
||||
|
||||
def generate_persona_from_onboarding(self, user_id: int, onboarding_session_id: int = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate a comprehensive writing persona from user's onboarding data.
|
||||
|
||||
Args:
|
||||
user_id: User ID to generate persona for
|
||||
onboarding_session_id: Optional specific onboarding session ID
|
||||
|
||||
Returns:
|
||||
Generated persona data with platform adaptations
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Generating persona for user {user_id}")
|
||||
|
||||
# Get onboarding data
|
||||
onboarding_data = self._collect_onboarding_data(user_id, onboarding_session_id)
|
||||
|
||||
if not onboarding_data:
|
||||
logger.warning(f"No onboarding data found for user {user_id}")
|
||||
return {"error": "No onboarding data available for persona generation"}
|
||||
|
||||
# Generate core persona using Gemini
|
||||
core_persona = self._generate_core_persona(onboarding_data)
|
||||
|
||||
if "error" in core_persona:
|
||||
return core_persona
|
||||
|
||||
# Generate platform-specific adaptations
|
||||
platform_personas = self._generate_platform_adaptations(core_persona, onboarding_data)
|
||||
|
||||
# Save to database
|
||||
saved_persona = self._save_persona_to_db(user_id, core_persona, platform_personas, onboarding_data)
|
||||
|
||||
return {
|
||||
"persona_id": saved_persona.id,
|
||||
"core_persona": core_persona,
|
||||
"platform_personas": platform_personas,
|
||||
"analysis_metadata": {
|
||||
"confidence_score": core_persona.get("confidence_score", 0.0),
|
||||
"data_sufficiency": self._calculate_data_sufficiency(onboarding_data),
|
||||
"generated_at": datetime.utcnow().isoformat()
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating persona for user {user_id}: {str(e)}")
|
||||
return {"error": f"Failed to generate persona: {str(e)}"}
|
||||
|
||||
def _collect_onboarding_data(self, user_id: int, session_id: int = None) -> Optional[Dict[str, Any]]:
|
||||
"""Collect comprehensive onboarding data for persona analysis."""
|
||||
try:
|
||||
session = get_db_session()
|
||||
|
||||
# Find onboarding session
|
||||
if session_id:
|
||||
onboarding_session = session.query(OnboardingSession).filter(
|
||||
OnboardingSession.id == session_id,
|
||||
OnboardingSession.user_id == user_id
|
||||
).first()
|
||||
else:
|
||||
onboarding_session = session.query(OnboardingSession).filter(
|
||||
OnboardingSession.user_id == user_id
|
||||
).order_by(OnboardingSession.updated_at.desc()).first()
|
||||
|
||||
if not onboarding_session:
|
||||
return None
|
||||
|
||||
# Get website analysis
|
||||
website_analysis = session.query(WebsiteAnalysis).filter(
|
||||
WebsiteAnalysis.session_id == onboarding_session.id
|
||||
).first()
|
||||
|
||||
# Get research preferences
|
||||
research_prefs = session.query(ResearchPreferences).filter(
|
||||
ResearchPreferences.session_id == onboarding_session.id
|
||||
).first()
|
||||
|
||||
# Compile comprehensive data
|
||||
onboarding_data = {
|
||||
"session_info": {
|
||||
"session_id": onboarding_session.id,
|
||||
"current_step": onboarding_session.current_step,
|
||||
"progress": onboarding_session.progress,
|
||||
"started_at": onboarding_session.started_at.isoformat() if onboarding_session.started_at else None
|
||||
},
|
||||
"website_analysis": website_analysis.to_dict() if website_analysis else None,
|
||||
"research_preferences": research_prefs.to_dict() if research_prefs else None
|
||||
}
|
||||
|
||||
session.close()
|
||||
return onboarding_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error collecting onboarding data: {str(e)}")
|
||||
return None
|
||||
|
||||
def _generate_core_persona(self, onboarding_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Generate core writing persona using Gemini structured response."""
|
||||
|
||||
# Build analysis prompt
|
||||
prompt = self._build_persona_analysis_prompt(onboarding_data)
|
||||
|
||||
# Define schema for structured response
|
||||
persona_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identity": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"persona_name": {"type": "string"},
|
||||
"archetype": {"type": "string"},
|
||||
"core_belief": {"type": "string"},
|
||||
"brand_voice_description": {"type": "string"}
|
||||
},
|
||||
"required": ["persona_name", "archetype", "core_belief"]
|
||||
},
|
||||
"linguistic_fingerprint": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sentence_metrics": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"average_sentence_length_words": {"type": "number"},
|
||||
"preferred_sentence_type": {"type": "string"},
|
||||
"active_to_passive_ratio": {"type": "string"},
|
||||
"complexity_level": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"lexical_features": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"go_to_words": {"type": "array", "items": {"type": "string"}},
|
||||
"go_to_phrases": {"type": "array", "items": {"type": "string"}},
|
||||
"avoid_words": {"type": "array", "items": {"type": "string"}},
|
||||
"contractions": {"type": "string"},
|
||||
"filler_words": {"type": "string"},
|
||||
"vocabulary_level": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"rhetorical_devices": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"metaphors": {"type": "string"},
|
||||
"analogies": {"type": "string"},
|
||||
"rhetorical_questions": {"type": "string"},
|
||||
"storytelling_style": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tonal_range": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default_tone": {"type": "string"},
|
||||
"permissible_tones": {"type": "array", "items": {"type": "string"}},
|
||||
"forbidden_tones": {"type": "array", "items": {"type": "string"}},
|
||||
"emotional_range": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"stylistic_constraints": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"punctuation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ellipses": {"type": "string"},
|
||||
"em_dash": {"type": "string"},
|
||||
"exclamation_points": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"formatting": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"paragraphs": {"type": "string"},
|
||||
"lists": {"type": "string"},
|
||||
"markdown": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"confidence_score": {"type": "number"},
|
||||
"analysis_notes": {"type": "string"}
|
||||
},
|
||||
"required": ["identity", "linguistic_fingerprint", "tonal_range", "confidence_score"]
|
||||
}
|
||||
|
||||
try:
|
||||
# Generate structured response using Gemini
|
||||
response = gemini_structured_json_response(
|
||||
prompt=prompt,
|
||||
schema=persona_schema,
|
||||
temperature=0.2, # Low temperature for consistent analysis
|
||||
max_tokens=8192,
|
||||
system_prompt="You are an expert writing style analyst and persona developer. Analyze the provided data to create a precise, actionable writing persona."
|
||||
)
|
||||
|
||||
if "error" in response:
|
||||
logger.error(f"Gemini API error: {response['error']}")
|
||||
return {"error": f"AI analysis failed: {response['error']}"}
|
||||
|
||||
logger.info("✅ Core persona generated successfully")
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating core persona: {str(e)}")
|
||||
return {"error": f"Failed to generate core persona: {str(e)}"}
|
||||
|
||||
def _generate_platform_adaptations(self, core_persona: Dict[str, Any], onboarding_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Generate platform-specific persona adaptations."""
|
||||
|
||||
platforms = ["twitter", "linkedin", "instagram", "facebook", "blog", "medium", "substack"]
|
||||
platform_personas = {}
|
||||
|
||||
for platform in platforms:
|
||||
try:
|
||||
platform_persona = self._generate_single_platform_persona(core_persona, platform, onboarding_data)
|
||||
if "error" not in platform_persona:
|
||||
platform_personas[platform] = platform_persona
|
||||
else:
|
||||
logger.warning(f"Failed to generate {platform} persona: {platform_persona['error']}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating {platform} persona: {str(e)}")
|
||||
|
||||
return platform_personas
|
||||
|
||||
def _generate_single_platform_persona(self, core_persona: Dict[str, Any], platform: str, onboarding_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Generate persona adaptation for a specific platform."""
|
||||
|
||||
prompt = self._build_platform_adaptation_prompt(core_persona, platform, onboarding_data)
|
||||
|
||||
# Platform-specific schema
|
||||
platform_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"platform_type": {"type": "string"},
|
||||
"sentence_metrics": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"max_sentence_length": {"type": "number"},
|
||||
"optimal_sentence_length": {"type": "number"},
|
||||
"sentence_variety": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"lexical_adaptations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"platform_specific_words": {"type": "array", "items": {"type": "string"}},
|
||||
"hashtag_strategy": {"type": "string"},
|
||||
"emoji_usage": {"type": "string"},
|
||||
"mention_strategy": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"content_format_rules": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"character_limit": {"type": "number"},
|
||||
"paragraph_structure": {"type": "string"},
|
||||
"call_to_action_style": {"type": "string"},
|
||||
"link_placement": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"engagement_patterns": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"posting_frequency": {"type": "string"},
|
||||
"optimal_posting_times": {"type": "array", "items": {"type": "string"}},
|
||||
"engagement_tactics": {"type": "array", "items": {"type": "string"}},
|
||||
"community_interaction": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"platform_best_practices": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"required": ["platform_type", "sentence_metrics", "content_format_rules", "engagement_patterns"]
|
||||
}
|
||||
|
||||
try:
|
||||
response = gemini_structured_json_response(
|
||||
prompt=prompt,
|
||||
schema=platform_schema,
|
||||
temperature=0.2,
|
||||
max_tokens=4096,
|
||||
system_prompt=f"You are an expert in {platform} content strategy and platform-specific writing optimization."
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating {platform} persona: {str(e)}")
|
||||
return {"error": f"Failed to generate {platform} persona: {str(e)}"}
|
||||
|
||||
def _build_persona_analysis_prompt(self, onboarding_data: Dict[str, Any]) -> str:
|
||||
"""Build the main persona analysis prompt."""
|
||||
|
||||
website_analysis = onboarding_data.get("website_analysis", {})
|
||||
research_prefs = onboarding_data.get("research_preferences", {})
|
||||
|
||||
prompt = f"""
|
||||
PERSONA GENERATION TASK: Create a comprehensive writing persona based on user onboarding data.
|
||||
|
||||
ONBOARDING DATA ANALYSIS:
|
||||
|
||||
Website Analysis:
|
||||
- URL: {website_analysis.get('website_url', 'Not provided')}
|
||||
- Writing Style: {json.dumps(website_analysis.get('writing_style', {}), indent=2)}
|
||||
- Content Characteristics: {json.dumps(website_analysis.get('content_characteristics', {}), indent=2)}
|
||||
- Target Audience: {json.dumps(website_analysis.get('target_audience', {}), indent=2)}
|
||||
- Content Type: {json.dumps(website_analysis.get('content_type', {}), indent=2)}
|
||||
- Style Patterns: {json.dumps(website_analysis.get('style_patterns', {}), indent=2)}
|
||||
|
||||
Research Preferences:
|
||||
- Research Depth: {research_prefs.get('research_depth', 'Not set')}
|
||||
- Content Types: {research_prefs.get('content_types', [])}
|
||||
- Auto Research: {research_prefs.get('auto_research', False)}
|
||||
- Factual Content: {research_prefs.get('factual_content', False)}
|
||||
|
||||
PERSONA GENERATION REQUIREMENTS:
|
||||
|
||||
1. IDENTITY CREATION:
|
||||
- Create a memorable persona name that captures the essence of the writing style
|
||||
- Define a clear archetype (e.g., "The Pragmatic Futurist", "The Thoughtful Educator")
|
||||
- Articulate a core belief that drives the writing philosophy
|
||||
- Write a comprehensive brand voice description
|
||||
|
||||
2. LINGUISTIC FINGERPRINT (Quantitative Analysis):
|
||||
- Calculate average sentence length based on website analysis
|
||||
- Determine preferred sentence types (simple, compound, complex)
|
||||
- Analyze active vs passive voice ratio
|
||||
- Identify go-to words and phrases from the content analysis
|
||||
- List words and phrases to avoid
|
||||
- Determine contraction usage patterns
|
||||
- Assess vocabulary complexity level
|
||||
|
||||
3. RHETORICAL ANALYSIS:
|
||||
- Identify metaphor patterns and themes
|
||||
- Analyze analogy usage
|
||||
- Assess rhetorical question frequency and style
|
||||
- Determine storytelling approach
|
||||
|
||||
4. TONAL RANGE:
|
||||
- Define the default tone
|
||||
- List permissible tones for different contexts
|
||||
- Identify forbidden tones that don't match the brand
|
||||
- Describe emotional range and expression
|
||||
|
||||
5. STYLISTIC CONSTRAINTS:
|
||||
- Define punctuation preferences and rules
|
||||
- Set formatting guidelines
|
||||
- Establish paragraph structure preferences
|
||||
|
||||
ANALYSIS INSTRUCTIONS:
|
||||
- Base your analysis on the actual data provided from the website analysis
|
||||
- If data is limited, make reasonable inferences but note the confidence level
|
||||
- Ensure the persona is actionable and specific enough for AI content generation
|
||||
- Provide a confidence score (0-100) based on data availability and quality
|
||||
- Include analysis notes explaining your reasoning
|
||||
|
||||
Generate a comprehensive persona profile that can be used to replicate this writing style across different platforms.
|
||||
"""
|
||||
|
||||
return prompt
|
||||
|
||||
def _build_platform_adaptation_prompt(self, core_persona: Dict[str, Any], platform: str, onboarding_data: Dict[str, Any]) -> str:
|
||||
"""Build prompt for platform-specific persona adaptation."""
|
||||
|
||||
platform_constraints = self._get_platform_constraints(platform)
|
||||
|
||||
prompt = f"""
|
||||
PLATFORM ADAPTATION TASK: Adapt the core writing persona for {platform.upper()}.
|
||||
|
||||
CORE PERSONA:
|
||||
{json.dumps(core_persona, indent=2)}
|
||||
|
||||
PLATFORM: {platform.upper()}
|
||||
|
||||
PLATFORM CONSTRAINTS:
|
||||
{json.dumps(platform_constraints, indent=2)}
|
||||
|
||||
ADAPTATION REQUIREMENTS:
|
||||
|
||||
1. SENTENCE METRICS:
|
||||
- Adjust sentence length for platform optimal performance
|
||||
- Adapt sentence variety for platform engagement
|
||||
- Consider platform reading patterns
|
||||
|
||||
2. LEXICAL ADAPTATIONS:
|
||||
- Identify platform-specific vocabulary and slang
|
||||
- Define hashtag strategy (if applicable)
|
||||
- Set emoji usage guidelines
|
||||
- Establish mention and tagging strategy
|
||||
|
||||
3. CONTENT FORMAT RULES:
|
||||
- Respect character/word limits
|
||||
- Optimize paragraph structure for platform
|
||||
- Define call-to-action style
|
||||
- Set link placement strategy
|
||||
|
||||
4. ENGAGEMENT PATTERNS:
|
||||
- Determine optimal posting frequency
|
||||
- Identify best posting times for audience
|
||||
- Define engagement tactics
|
||||
- Set community interaction guidelines
|
||||
|
||||
5. PLATFORM BEST PRACTICES:
|
||||
- List platform-specific optimization techniques
|
||||
- Consider algorithm preferences
|
||||
- Include trending format adaptations
|
||||
|
||||
INSTRUCTIONS:
|
||||
- Maintain the core persona identity while optimizing for platform performance
|
||||
- Ensure all adaptations align with the original brand voice
|
||||
- Consider platform-specific audience behavior
|
||||
- Provide actionable, specific guidelines
|
||||
|
||||
Generate a platform-optimized persona adaptation that maintains brand consistency while maximizing platform performance.
|
||||
"""
|
||||
|
||||
return prompt
|
||||
|
||||
def _get_platform_constraints(self, platform: str) -> Dict[str, Any]:
|
||||
"""Get platform-specific constraints and best practices."""
|
||||
|
||||
constraints = {
|
||||
"twitter": {
|
||||
"character_limit": 280,
|
||||
"optimal_length": "120-150 characters",
|
||||
"hashtag_limit": 3,
|
||||
"image_support": True,
|
||||
"thread_support": True,
|
||||
"link_shortening": True
|
||||
},
|
||||
"linkedin": {
|
||||
"character_limit": 3000,
|
||||
"optimal_length": "150-300 words",
|
||||
"professional_tone": True,
|
||||
"hashtag_limit": 5,
|
||||
"rich_media": True,
|
||||
"long_form": True
|
||||
},
|
||||
"instagram": {
|
||||
"caption_limit": 2200,
|
||||
"optimal_length": "125-150 words",
|
||||
"hashtag_limit": 30,
|
||||
"visual_first": True,
|
||||
"story_support": True,
|
||||
"emoji_friendly": True
|
||||
},
|
||||
"facebook": {
|
||||
"character_limit": 63206,
|
||||
"optimal_length": "40-80 words",
|
||||
"algorithm_favors": "engagement",
|
||||
"link_preview": True,
|
||||
"event_support": True,
|
||||
"group_sharing": True
|
||||
},
|
||||
"blog": {
|
||||
"word_count": "800-2000 words",
|
||||
"seo_important": True,
|
||||
"header_structure": True,
|
||||
"internal_linking": True,
|
||||
"meta_descriptions": True,
|
||||
"readability_score": True
|
||||
},
|
||||
"medium": {
|
||||
"word_count": "1000-3000 words",
|
||||
"storytelling_focus": True,
|
||||
"subtitle_support": True,
|
||||
"publication_support": True,
|
||||
"clap_optimization": True,
|
||||
"follower_building": True
|
||||
},
|
||||
"substack": {
|
||||
"newsletter_format": True,
|
||||
"email_optimization": True,
|
||||
"subscription_focus": True,
|
||||
"long_form": True,
|
||||
"personal_connection": True,
|
||||
"monetization_support": True
|
||||
}
|
||||
}
|
||||
|
||||
return constraints.get(platform, {})
|
||||
|
||||
def _save_persona_to_db(self, user_id: int, core_persona: Dict[str, Any], platform_personas: Dict[str, Any], onboarding_data: Dict[str, Any]) -> WritingPersona:
|
||||
"""Save generated persona to database."""
|
||||
try:
|
||||
session = get_db_session()
|
||||
|
||||
# Create main persona record
|
||||
writing_persona = WritingPersona(
|
||||
user_id=user_id,
|
||||
persona_name=core_persona.get("identity", {}).get("persona_name", "Generated Persona"),
|
||||
archetype=core_persona.get("identity", {}).get("archetype"),
|
||||
core_belief=core_persona.get("identity", {}).get("core_belief"),
|
||||
brand_voice_description=core_persona.get("identity", {}).get("brand_voice_description"),
|
||||
linguistic_fingerprint=core_persona.get("linguistic_fingerprint", {}),
|
||||
platform_adaptations={"platforms": list(platform_personas.keys())},
|
||||
onboarding_session_id=onboarding_data.get("session_info", {}).get("session_id"),
|
||||
source_website_analysis=onboarding_data.get("website_analysis"),
|
||||
source_research_preferences=onboarding_data.get("research_preferences"),
|
||||
ai_analysis_version="gemini_v1.0",
|
||||
confidence_score=core_persona.get("confidence_score", 0.0)
|
||||
)
|
||||
|
||||
session.add(writing_persona)
|
||||
session.commit()
|
||||
session.refresh(writing_persona)
|
||||
|
||||
# Create platform-specific persona records
|
||||
for platform, platform_data in platform_personas.items():
|
||||
platform_persona = PlatformPersona(
|
||||
writing_persona_id=writing_persona.id,
|
||||
platform_type=platform,
|
||||
sentence_metrics=platform_data.get("sentence_metrics", {}),
|
||||
lexical_features=platform_data.get("lexical_adaptations", {}),
|
||||
rhetorical_devices=core_persona.get("linguistic_fingerprint", {}).get("rhetorical_devices", {}),
|
||||
tonal_range=core_persona.get("tonal_range", {}),
|
||||
stylistic_constraints=core_persona.get("stylistic_constraints", {}),
|
||||
content_format_rules=platform_data.get("content_format_rules", {}),
|
||||
engagement_patterns=platform_data.get("engagement_patterns", {}),
|
||||
platform_best_practices={"practices": platform_data.get("platform_best_practices", [])}
|
||||
)
|
||||
session.add(platform_persona)
|
||||
|
||||
# Save analysis result
|
||||
analysis_result = PersonaAnalysisResult(
|
||||
user_id=user_id,
|
||||
writing_persona_id=writing_persona.id,
|
||||
analysis_prompt=self._build_persona_analysis_prompt(onboarding_data)[:5000], # Truncate for storage
|
||||
input_data=onboarding_data,
|
||||
linguistic_analysis=core_persona.get("linguistic_fingerprint", {}),
|
||||
personality_analysis=core_persona.get("identity", {}),
|
||||
platform_recommendations=platform_personas,
|
||||
style_guidelines=core_persona.get("stylistic_constraints", {}),
|
||||
analysis_confidence=core_persona.get("confidence_score", 0.0),
|
||||
data_sufficiency_score=self._calculate_data_sufficiency(onboarding_data),
|
||||
ai_provider="gemini",
|
||||
model_version="gemini-2.5-flash"
|
||||
)
|
||||
session.add(analysis_result)
|
||||
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
logger.info(f"✅ Persona saved to database with ID: {writing_persona.id}")
|
||||
return writing_persona
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving persona to database: {str(e)}")
|
||||
if session:
|
||||
session.rollback()
|
||||
session.close()
|
||||
raise
|
||||
|
||||
def _calculate_data_sufficiency(self, onboarding_data: Dict[str, Any]) -> float:
|
||||
"""Calculate how sufficient the onboarding data is for persona generation."""
|
||||
score = 0.0
|
||||
|
||||
website_analysis = onboarding_data.get("website_analysis", {})
|
||||
research_prefs = onboarding_data.get("research_preferences", {})
|
||||
|
||||
# Website analysis components (70% of score)
|
||||
if website_analysis.get("writing_style"):
|
||||
score += 25
|
||||
if website_analysis.get("content_characteristics"):
|
||||
score += 20
|
||||
if website_analysis.get("target_audience"):
|
||||
score += 15
|
||||
if website_analysis.get("style_patterns"):
|
||||
score += 10
|
||||
|
||||
# Research preferences components (30% of score)
|
||||
if research_prefs.get("research_depth"):
|
||||
score += 10
|
||||
if research_prefs.get("content_types"):
|
||||
score += 10
|
||||
if research_prefs.get("writing_style"):
|
||||
score += 10
|
||||
|
||||
return min(score, 100.0)
|
||||
|
||||
def get_user_personas(self, user_id: int) -> List[Dict[str, Any]]:
|
||||
"""Get all personas for a user."""
|
||||
try:
|
||||
session = get_db_session()
|
||||
|
||||
personas = session.query(WritingPersona).filter(
|
||||
WritingPersona.user_id == user_id,
|
||||
WritingPersona.is_active == True
|
||||
).all()
|
||||
|
||||
result = []
|
||||
for persona in personas:
|
||||
persona_dict = persona.to_dict()
|
||||
|
||||
# Get platform personas
|
||||
platform_personas = session.query(PlatformPersona).filter(
|
||||
PlatformPersona.writing_persona_id == persona.id,
|
||||
PlatformPersona.is_active == True
|
||||
).all()
|
||||
|
||||
persona_dict["platforms"] = [pp.to_dict() for pp in platform_personas]
|
||||
result.append(persona_dict)
|
||||
|
||||
session.close()
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user personas: {str(e)}")
|
||||
return []
|
||||
|
||||
def get_persona_for_platform(self, user_id: int, platform: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get the best persona for a specific platform."""
|
||||
try:
|
||||
session = get_db_session()
|
||||
|
||||
# Get the most recent active persona
|
||||
persona = session.query(WritingPersona).filter(
|
||||
WritingPersona.user_id == user_id,
|
||||
WritingPersona.is_active == True
|
||||
).order_by(WritingPersona.created_at.desc()).first()
|
||||
|
||||
if not persona:
|
||||
return None
|
||||
|
||||
# Get platform-specific adaptation
|
||||
platform_persona = session.query(PlatformPersona).filter(
|
||||
PlatformPersona.writing_persona_id == persona.id,
|
||||
PlatformPersona.platform_type == platform,
|
||||
PlatformPersona.is_active == True
|
||||
).first()
|
||||
|
||||
result = {
|
||||
"core_persona": persona.to_dict(),
|
||||
"platform_adaptation": platform_persona.to_dict() if platform_persona else None
|
||||
}
|
||||
|
||||
session.close()
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting persona for platform {platform}: {str(e)}")
|
||||
return None
|
||||
506
backend/services/persona_replication_engine.py
Normal file
506
backend/services/persona_replication_engine.py
Normal file
@@ -0,0 +1,506 @@
|
||||
"""
|
||||
Persona Replication Engine
|
||||
Implements the hardened persona replication system for high-fidelity content generation.
|
||||
Based on quantitative analysis and structured constraints.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from loguru import logger
|
||||
import json
|
||||
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response
|
||||
from services.persona_analysis_service import PersonaAnalysisService
|
||||
|
||||
class PersonaReplicationEngine:
|
||||
"""
|
||||
High-fidelity persona replication engine that generates content
|
||||
indistinguishable from the original author's work.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the persona replication engine."""
|
||||
self.persona_service = PersonaAnalysisService()
|
||||
logger.info("PersonaReplicationEngine initialized")
|
||||
|
||||
def generate_content_with_persona(self,
|
||||
user_id: int,
|
||||
platform: str,
|
||||
content_request: str,
|
||||
content_type: str = "post") -> Dict[str, Any]:
|
||||
"""
|
||||
Generate content using the hardened persona replication system.
|
||||
|
||||
Args:
|
||||
user_id: User ID for persona lookup
|
||||
platform: Target platform (twitter, linkedin, blog, etc.)
|
||||
content_request: What content to generate
|
||||
content_type: Type of content (post, article, thread, etc.)
|
||||
|
||||
Returns:
|
||||
Generated content with persona fidelity metrics
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Generating {content_type} for {platform} using persona replication")
|
||||
|
||||
# Get platform-specific persona
|
||||
persona_data = self.persona_service.get_persona_for_platform(user_id, platform)
|
||||
|
||||
if not persona_data:
|
||||
return {"error": "No persona found for user and platform"}
|
||||
|
||||
# Build hardened system prompt
|
||||
system_prompt = self._build_hardened_system_prompt(persona_data, platform)
|
||||
|
||||
# Build content generation prompt
|
||||
content_prompt = self._build_content_prompt(content_request, content_type, platform, persona_data)
|
||||
|
||||
# Generate content with strict persona constraints
|
||||
content_result = self._generate_constrained_content(
|
||||
system_prompt, content_prompt, platform, persona_data
|
||||
)
|
||||
|
||||
if "error" in content_result:
|
||||
return content_result
|
||||
|
||||
# Validate content against persona
|
||||
validation_result = self._validate_content_fidelity(
|
||||
content_result["content"], persona_data, platform
|
||||
)
|
||||
|
||||
return {
|
||||
"content": content_result["content"],
|
||||
"persona_fidelity_score": validation_result["fidelity_score"],
|
||||
"platform_optimization_score": validation_result["platform_score"],
|
||||
"persona_compliance": validation_result["compliance_check"],
|
||||
"generation_metadata": {
|
||||
"persona_id": persona_data["core_persona"]["id"],
|
||||
"platform": platform,
|
||||
"content_type": content_type,
|
||||
"generated_at": content_result.get("generated_at"),
|
||||
"constraints_applied": validation_result["constraints_checked"]
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in persona replication engine: {str(e)}")
|
||||
return {"error": f"Content generation failed: {str(e)}"}
|
||||
|
||||
def _build_hardened_system_prompt(self, persona_data: Dict[str, Any], platform: str) -> str:
|
||||
"""Build the hardened system prompt for persona replication."""
|
||||
|
||||
core_persona = persona_data["core_persona"]
|
||||
platform_adaptation = persona_data.get("platform_adaptation", {})
|
||||
|
||||
# Extract key persona elements
|
||||
identity = core_persona.get("linguistic_fingerprint", {})
|
||||
sentence_metrics = identity.get("sentence_metrics", {})
|
||||
lexical_features = identity.get("lexical_features", {})
|
||||
rhetorical_devices = identity.get("rhetorical_devices", {})
|
||||
tonal_range = core_persona.get("tonal_range", {})
|
||||
|
||||
# Platform-specific constraints
|
||||
platform_constraints = platform_adaptation.get("content_format_rules", {})
|
||||
engagement_patterns = platform_adaptation.get("engagement_patterns", {})
|
||||
|
||||
system_prompt = f"""# COMMAND PROTOCOL: PERSONA REPLICATION ENGINE
|
||||
# MODEL: [GEMINI-2.5-FLASH]
|
||||
# PERSONA: [{core_persona.get('persona_name', 'Generated Persona')}]
|
||||
# PLATFORM: [{platform.upper()}]
|
||||
# MODE: STRICT MIMICRY
|
||||
|
||||
## PRIMARY DIRECTIVE:
|
||||
You are now {core_persona.get('persona_name', 'the generated persona')}. Your sole function is to generate {platform} content that is linguistically indistinguishable from the authentic writing of this persona. You must output content that passes stylometric analysis as their work.
|
||||
|
||||
## PERSONA PROFILE (IMMUTABLE):
|
||||
- **Identity:** {core_persona.get('archetype', 'Professional Writer')}. Core belief: {core_persona.get('core_belief', 'Quality content drives engagement')}.
|
||||
- **Tone:** {tonal_range.get('default_tone', 'professional')}. Permissible tones: {', '.join(tonal_range.get('permissible_tones', []))}.
|
||||
- **Style:** Average sentence length: {sentence_metrics.get('average_sentence_length_words', 15)} words. Preferred type: {sentence_metrics.get('preferred_sentence_type', 'simple_and_compound')}. Active voice ratio: {sentence_metrics.get('active_to_passive_ratio', '80:20')}.
|
||||
- **Lexical Command:**
|
||||
- USE: {', '.join(lexical_features.get('go_to_words', [])[:5])}
|
||||
- PHRASES: {', '.join(lexical_features.get('go_to_phrases', [])[:3])}
|
||||
- AVOID: {', '.join(lexical_features.get('avoid_words', [])[:5])}
|
||||
- **Rhetorical Style:** {rhetorical_devices.get('metaphors', 'minimal metaphors')}, {rhetorical_devices.get('rhetorical_questions', 'occasional questions')}.
|
||||
|
||||
## PLATFORM CONSTRAINTS ({platform.upper()}):
|
||||
- **Format:** {self._get_platform_format_rules(platform, platform_constraints)}
|
||||
- **Engagement:** {engagement_patterns.get('posting_frequency', 'regular posting')}
|
||||
- **Optimization:** {self._get_platform_optimization_rules(platform)}
|
||||
|
||||
## OPERATIONAL PARAMETERS:
|
||||
1. **Fidelity Check:** Before generating, simulate a stylometric analysis of your draft. Does it match the profile's sentence length, word choice, and rhetorical patterns? If not, revise.
|
||||
2. **Platform Compliance:** Ensure content meets {platform} best practices and constraints.
|
||||
3. **Error State:** If you cannot generate content that meets the Persona Profile standards, output only: "[PERSONA_VIOLATION: Cannot comply without breaking character parameters]".
|
||||
4. **Output Format:** Your output must be PURE CONTENT for {platform}. No introductory clauses. No markdown unless platform supports it.
|
||||
|
||||
## ACKNOWLEDGEMENT:
|
||||
You must silently acknowledge this protocol and begin all responses in character. No confirmation is necessary.
|
||||
|
||||
// END PROTOCOL"""
|
||||
|
||||
return system_prompt
|
||||
|
||||
def _build_content_prompt(self, content_request: str, content_type: str, platform: str, persona_data: Dict[str, Any]) -> str:
|
||||
"""Build the content generation prompt."""
|
||||
|
||||
platform_adaptation = persona_data.get("platform_adaptation", {})
|
||||
content_format_rules = platform_adaptation.get("content_format_rules", {})
|
||||
|
||||
prompt = f"""Generate a {content_type} for {platform} about: {content_request}
|
||||
|
||||
CONTENT REQUIREMENTS:
|
||||
- Platform: {platform}
|
||||
- Type: {content_type}
|
||||
- Topic: {content_request}
|
||||
|
||||
PLATFORM SPECIFICATIONS:
|
||||
- Character/Word Limit: {content_format_rules.get('character_limit', 'No limit')}
|
||||
- Optimal Length: {content_format_rules.get('optimal_length', 'Platform appropriate')}
|
||||
- Format Requirements: {content_format_rules.get('paragraph_structure', 'Standard')}
|
||||
|
||||
PERSONA COMPLIANCE:
|
||||
- Must match the established linguistic fingerprint
|
||||
- Must use the specified lexical features
|
||||
- Must maintain the defined tonal range
|
||||
- Must follow platform-specific adaptations
|
||||
|
||||
Generate content that is indistinguishable from the original author's work while optimized for {platform} performance."""
|
||||
|
||||
return prompt
|
||||
|
||||
def _generate_constrained_content(self, system_prompt: str, content_prompt: str, platform: str, persona_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Generate content with strict persona constraints."""
|
||||
|
||||
# Define content generation schema
|
||||
content_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"persona_compliance_check": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sentence_length_check": {"type": "boolean"},
|
||||
"lexical_compliance": {"type": "boolean"},
|
||||
"tonal_compliance": {"type": "boolean"},
|
||||
"platform_optimization": {"type": "boolean"}
|
||||
}
|
||||
},
|
||||
"platform_specific_elements": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hashtags": {"type": "array", "items": {"type": "string"}},
|
||||
"mentions": {"type": "array", "items": {"type": "string"}},
|
||||
"call_to_action": {"type": "string"},
|
||||
"engagement_hooks": {"type": "array", "items": {"type": "string"}}
|
||||
}
|
||||
},
|
||||
"confidence_score": {"type": "number"}
|
||||
},
|
||||
"required": ["content", "persona_compliance_check", "confidence_score"]
|
||||
}
|
||||
|
||||
try:
|
||||
response = gemini_structured_json_response(
|
||||
prompt=content_prompt,
|
||||
schema=content_schema,
|
||||
temperature=0.1, # Very low temperature for consistent persona replication
|
||||
max_tokens=4096,
|
||||
system_prompt=system_prompt
|
||||
)
|
||||
|
||||
if "error" in response:
|
||||
return {"error": f"Content generation failed: {response['error']}"}
|
||||
|
||||
response["generated_at"] = logger.info("Content generated with persona constraints")
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating constrained content: {str(e)}")
|
||||
return {"error": f"Content generation error: {str(e)}"}
|
||||
|
||||
def _validate_content_fidelity(self, content: str, persona_data: Dict[str, Any], platform: str) -> Dict[str, Any]:
|
||||
"""Validate generated content against persona constraints."""
|
||||
|
||||
try:
|
||||
# Basic validation metrics
|
||||
validation_result = {
|
||||
"fidelity_score": 0.0,
|
||||
"platform_score": 0.0,
|
||||
"compliance_check": {},
|
||||
"constraints_checked": []
|
||||
}
|
||||
|
||||
core_persona = persona_data["core_persona"]
|
||||
platform_adaptation = persona_data.get("platform_adaptation", {})
|
||||
|
||||
# Check sentence length compliance
|
||||
sentences = content.split('.')
|
||||
avg_length = sum(len(s.split()) for s in sentences if s.strip()) / max(len([s for s in sentences if s.strip()]), 1)
|
||||
|
||||
target_length = core_persona.get("linguistic_fingerprint", {}).get("sentence_metrics", {}).get("average_sentence_length_words", 15)
|
||||
length_compliance = abs(avg_length - target_length) <= 5 # Allow 5-word variance
|
||||
|
||||
validation_result["compliance_check"]["sentence_length"] = length_compliance
|
||||
validation_result["constraints_checked"].append("sentence_length")
|
||||
|
||||
# Check lexical compliance
|
||||
lexical_features = core_persona.get("linguistic_fingerprint", {}).get("lexical_features", {})
|
||||
go_to_words = lexical_features.get("go_to_words", [])
|
||||
avoid_words = lexical_features.get("avoid_words", [])
|
||||
|
||||
content_lower = content.lower()
|
||||
uses_go_to_words = any(word.lower() in content_lower for word in go_to_words[:3])
|
||||
avoids_bad_words = not any(word.lower() in content_lower for word in avoid_words)
|
||||
|
||||
lexical_compliance = uses_go_to_words and avoids_bad_words
|
||||
validation_result["compliance_check"]["lexical_features"] = lexical_compliance
|
||||
validation_result["constraints_checked"].append("lexical_features")
|
||||
|
||||
# Check platform constraints
|
||||
platform_constraints = platform_adaptation.get("content_format_rules", {})
|
||||
char_limit = platform_constraints.get("character_limit")
|
||||
|
||||
platform_compliance = True
|
||||
if char_limit and len(content) > char_limit:
|
||||
platform_compliance = False
|
||||
|
||||
validation_result["compliance_check"]["platform_constraints"] = platform_compliance
|
||||
validation_result["constraints_checked"].append("platform_constraints")
|
||||
|
||||
# Calculate overall scores
|
||||
compliance_checks = validation_result["compliance_check"]
|
||||
fidelity_score = sum(compliance_checks.values()) / len(compliance_checks) * 100
|
||||
platform_score = 100 if platform_compliance else 50 # Heavy penalty for platform violations
|
||||
|
||||
validation_result["fidelity_score"] = fidelity_score
|
||||
validation_result["platform_score"] = platform_score
|
||||
|
||||
logger.info(f"Content validation: Fidelity={fidelity_score}%, Platform={platform_score}%")
|
||||
return validation_result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating content fidelity: {str(e)}")
|
||||
return {
|
||||
"fidelity_score": 0.0,
|
||||
"platform_score": 0.0,
|
||||
"compliance_check": {"error": str(e)},
|
||||
"constraints_checked": []
|
||||
}
|
||||
|
||||
def _get_platform_format_rules(self, platform: str, constraints: Dict[str, Any]) -> str:
|
||||
"""Get formatted platform rules for system prompt."""
|
||||
|
||||
char_limit = constraints.get("character_limit", "No limit")
|
||||
optimal_length = constraints.get("optimal_length", "Platform appropriate")
|
||||
|
||||
return f"Character limit: {char_limit}, Optimal length: {optimal_length}"
|
||||
|
||||
def _get_platform_optimization_rules(self, platform: str) -> str:
|
||||
"""Get platform optimization rules."""
|
||||
|
||||
rules = {
|
||||
"twitter": "Use hashtags strategically (max 3), engage with questions, optimize for retweets",
|
||||
"linkedin": "Professional tone, thought leadership focus, encourage professional discussion",
|
||||
"instagram": "Visual-first approach, emoji usage, story-friendly format",
|
||||
"facebook": "Community engagement, shareable content, algorithm-friendly",
|
||||
"blog": "SEO-optimized, scannable format, internal linking",
|
||||
"medium": "Storytelling focus, publication-ready, clap optimization",
|
||||
"substack": "Newsletter format, subscriber value, email-friendly"
|
||||
}
|
||||
|
||||
return rules.get(platform, "Platform-appropriate optimization")
|
||||
|
||||
def create_hardened_persona_prompt(self, persona_data: Dict[str, Any], platform: str) -> str:
|
||||
"""
|
||||
Create the hardened persona prompt for direct use in AI interfaces.
|
||||
This is the fire-and-forget prompt that can be copied into any AI system.
|
||||
"""
|
||||
|
||||
core_persona = persona_data["core_persona"]
|
||||
platform_adaptation = persona_data.get("platform_adaptation", {})
|
||||
|
||||
# Extract quantitative data
|
||||
linguistic = core_persona.get("linguistic_fingerprint", {})
|
||||
sentence_metrics = linguistic.get("sentence_metrics", {})
|
||||
lexical_features = linguistic.get("lexical_features", {})
|
||||
rhetorical_devices = linguistic.get("rhetorical_devices", {})
|
||||
tonal_range = core_persona.get("tonal_range", {})
|
||||
|
||||
hardened_prompt = f"""# COMMAND PROTOCOL: PERSONA REPLICATION ENGINE
|
||||
# MODEL: [AI-MODEL]
|
||||
# PERSONA: [{core_persona.get('persona_name', 'Generated Persona')}]
|
||||
# PLATFORM: [{platform.upper()}]
|
||||
# MODE: STRICT MIMICRY
|
||||
|
||||
## PRIMARY DIRECTIVE:
|
||||
You are now {core_persona.get('persona_name', 'the persona')}. Your sole function is to generate {platform} content that is linguistically indistinguishable from the authentic writing of this persona. You must output content that passes stylometric analysis as their work.
|
||||
|
||||
## PERSONA PROFILE (IMMUTABLE):
|
||||
- **Identity:** {core_persona.get('archetype', 'Professional Writer')}. Core belief: {core_persona.get('core_belief', 'Quality content drives engagement')}.
|
||||
- **Tone:** {tonal_range.get('default_tone', 'professional')}. {f"Permissible: {', '.join(tonal_range.get('permissible_tones', []))}" if tonal_range.get('permissible_tones') else ''}. {f"Forbidden: {', '.join(tonal_range.get('forbidden_tones', []))}" if tonal_range.get('forbidden_tones') else ''}.
|
||||
- **Style:** Avg sentence: {sentence_metrics.get('average_sentence_length_words', 15)} words. Type: {sentence_metrics.get('preferred_sentence_type', 'simple_and_compound')}. Active voice: {sentence_metrics.get('active_to_passive_ratio', '80:20')}.
|
||||
- **Lexical Command:**
|
||||
- USE: {', '.join(lexical_features.get('go_to_words', [])[:5]) if lexical_features.get('go_to_words') else 'professional vocabulary'}
|
||||
- PHRASES: {', '.join(lexical_features.get('go_to_phrases', [])[:3]) if lexical_features.get('go_to_phrases') else 'natural transitions'}
|
||||
- AVOID: {', '.join(lexical_features.get('avoid_words', [])[:5]) if lexical_features.get('avoid_words') else 'corporate jargon'}
|
||||
- **Rhetorical Style:** {rhetorical_devices.get('metaphors', 'minimal metaphors')}, {rhetorical_devices.get('rhetorical_questions', 'occasional questions')}.
|
||||
|
||||
## PLATFORM CONSTRAINTS ({platform.upper()}):
|
||||
{self._format_platform_constraints(platform, platform_adaptation)}
|
||||
|
||||
## OPERATIONAL PARAMETERS:
|
||||
1. **Fidelity Check:** Before generating, verify your draft matches the profile's sentence length ({sentence_metrics.get('average_sentence_length_words', 15)} words avg), word choice, and rhetorical patterns. If not, revise.
|
||||
2. **Platform Compliance:** Ensure content meets {platform} format requirements and optimization rules.
|
||||
3. **Error State:** If you cannot generate content meeting Persona Profile standards, output: "[PERSONA_VIOLATION: Cannot comply without breaking character parameters]".
|
||||
4. **Output Format:** Generate PURE {platform.upper()} CONTENT. No introductory text. No explanations. Only the requested content.
|
||||
|
||||
## ACKNOWLEDGEMENT:
|
||||
You must silently acknowledge this protocol and begin all responses in character. No confirmation necessary.
|
||||
|
||||
// END PROTOCOL
|
||||
|
||||
---
|
||||
|
||||
## USAGE INSTRUCTIONS:
|
||||
1. Copy this entire prompt into your AI system's System Message/Instructions field
|
||||
2. Use normal user prompts to request content (e.g., "Write a post about AI trends")
|
||||
3. The AI will generate content that matches the persona's style exactly
|
||||
4. No additional prompting or style instructions needed
|
||||
|
||||
## QUALITY ASSURANCE:
|
||||
- Generated content should pass stylometric analysis as the original author
|
||||
- Sentence length should average {sentence_metrics.get('average_sentence_length_words', 15)} words
|
||||
- Must use specified vocabulary and avoid forbidden words
|
||||
- Must maintain {tonal_range.get('default_tone', 'professional')} tone throughout
|
||||
- Must comply with {platform} format and engagement requirements"""
|
||||
|
||||
return hardened_prompt
|
||||
|
||||
def _format_platform_constraints(self, platform: str, platform_adaptation: Dict[str, Any]) -> str:
|
||||
"""Format platform constraints for the hardened prompt."""
|
||||
|
||||
content_rules = platform_adaptation.get("content_format_rules", {})
|
||||
engagement = platform_adaptation.get("engagement_patterns", {})
|
||||
|
||||
constraints = []
|
||||
|
||||
if content_rules.get("character_limit"):
|
||||
constraints.append(f"Character limit: {content_rules['character_limit']}")
|
||||
|
||||
if content_rules.get("optimal_length"):
|
||||
constraints.append(f"Optimal length: {content_rules['optimal_length']}")
|
||||
|
||||
if engagement.get("posting_frequency"):
|
||||
constraints.append(f"Frequency: {engagement['posting_frequency']}")
|
||||
|
||||
if platform == "twitter":
|
||||
constraints.extend([
|
||||
"Max 3 hashtags",
|
||||
"Thread-friendly format",
|
||||
"Engagement-optimized"
|
||||
])
|
||||
elif platform == "linkedin":
|
||||
constraints.extend([
|
||||
"Professional networking focus",
|
||||
"Thought leadership tone",
|
||||
"Business value emphasis"
|
||||
])
|
||||
elif platform == "blog":
|
||||
constraints.extend([
|
||||
"SEO-optimized structure",
|
||||
"Scannable format",
|
||||
"Clear headings"
|
||||
])
|
||||
|
||||
return "- " + "\n- ".join(constraints) if constraints else "- Standard platform optimization"
|
||||
|
||||
def export_persona_for_external_use(self, user_id: int, platform: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Export a complete persona package for use in external AI systems.
|
||||
This creates a self-contained persona replication system.
|
||||
"""
|
||||
try:
|
||||
# Get persona data
|
||||
persona_data = self.persona_service.get_persona_for_platform(user_id, platform)
|
||||
|
||||
if not persona_data:
|
||||
return {"error": "No persona found"}
|
||||
|
||||
# Create hardened prompt
|
||||
hardened_prompt = self.create_hardened_persona_prompt(persona_data, platform)
|
||||
|
||||
# Create usage examples
|
||||
examples = self._generate_usage_examples(persona_data, platform)
|
||||
|
||||
# Create validation checklist
|
||||
validation_checklist = self._create_validation_checklist(persona_data, platform)
|
||||
|
||||
export_package = {
|
||||
"persona_metadata": {
|
||||
"persona_id": persona_data["core_persona"]["id"],
|
||||
"persona_name": persona_data["core_persona"]["persona_name"],
|
||||
"platform": platform,
|
||||
"generated_at": datetime.utcnow().isoformat(),
|
||||
"confidence_score": persona_data["core_persona"].get("confidence_score", 0.0)
|
||||
},
|
||||
"hardened_system_prompt": hardened_prompt,
|
||||
"usage_examples": examples,
|
||||
"validation_checklist": validation_checklist,
|
||||
"quick_reference": {
|
||||
"avg_sentence_length": persona_data["core_persona"].get("linguistic_fingerprint", {}).get("sentence_metrics", {}).get("average_sentence_length_words", 15),
|
||||
"go_to_words": persona_data["core_persona"].get("linguistic_fingerprint", {}).get("lexical_features", {}).get("go_to_words", [])[:5],
|
||||
"default_tone": persona_data["core_persona"].get("tonal_range", {}).get("default_tone", "professional"),
|
||||
"platform_limit": persona_data.get("platform_adaptation", {}).get("content_format_rules", {}).get("character_limit", "No limit")
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(f"✅ Persona export package created for {platform}")
|
||||
return export_package
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error exporting persona: {str(e)}")
|
||||
return {"error": f"Export failed: {str(e)}"}
|
||||
|
||||
def _generate_usage_examples(self, persona_data: Dict[str, Any], platform: str) -> List[Dict[str, Any]]:
|
||||
"""Generate usage examples for the exported persona."""
|
||||
|
||||
examples = [
|
||||
{
|
||||
"request": f"Write a {platform} post about AI trends",
|
||||
"expected_style": "Should match persona's sentence length and lexical features",
|
||||
"validation_points": [
|
||||
"Check average sentence length",
|
||||
"Verify use of go-to words",
|
||||
"Confirm tonal compliance",
|
||||
f"Ensure {platform} optimization"
|
||||
]
|
||||
},
|
||||
{
|
||||
"request": f"Create {platform} content about productivity tips",
|
||||
"expected_style": "Should maintain consistent voice and rhetorical patterns",
|
||||
"validation_points": [
|
||||
"Verify rhetorical device usage",
|
||||
"Check for forbidden words",
|
||||
"Confirm platform constraints",
|
||||
"Validate engagement elements"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
return examples
|
||||
|
||||
def _create_validation_checklist(self, persona_data: Dict[str, Any], platform: str) -> List[str]:
|
||||
"""Create a validation checklist for generated content."""
|
||||
|
||||
core_persona = persona_data["core_persona"]
|
||||
linguistic = core_persona.get("linguistic_fingerprint", {})
|
||||
|
||||
checklist = [
|
||||
f"✓ Average sentence length ~{linguistic.get('sentence_metrics', {}).get('average_sentence_length_words', 15)} words",
|
||||
f"✓ Uses go-to words: {', '.join(linguistic.get('lexical_features', {}).get('go_to_words', [])[:3])}",
|
||||
f"✓ Avoids forbidden words: {', '.join(linguistic.get('lexical_features', {}).get('avoid_words', [])[:3])}",
|
||||
f"✓ Maintains {core_persona.get('tonal_range', {}).get('default_tone', 'professional')} tone",
|
||||
f"✓ Follows {platform} format requirements",
|
||||
f"✓ Includes appropriate {platform} engagement elements"
|
||||
]
|
||||
|
||||
return checklist
|
||||
202
backend/test_persona_system.py
Normal file
202
backend/test_persona_system.py
Normal file
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for the persona generation system.
|
||||
Tests the complete flow from onboarding data to persona creation.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# Add the backend directory to the Python path
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from loguru import logger
|
||||
|
||||
def test_persona_system():
|
||||
"""Test the complete persona generation system."""
|
||||
|
||||
logger.info("🧪 Testing Persona Generation System")
|
||||
|
||||
try:
|
||||
# Test 1: Check database models
|
||||
logger.info("📊 Test 1: Checking database models...")
|
||||
from models.persona_models import WritingPersona, PlatformPersona, PersonaAnalysisResult
|
||||
logger.info("✅ Persona models imported successfully")
|
||||
|
||||
# Test 2: Check service initialization
|
||||
logger.info("🔧 Test 2: Testing service initialization...")
|
||||
from services.persona_analysis_service import PersonaAnalysisService
|
||||
persona_service = PersonaAnalysisService()
|
||||
logger.info("✅ PersonaAnalysisService initialized successfully")
|
||||
|
||||
# Test 3: Create sample onboarding data
|
||||
logger.info("📝 Test 3: Creating sample onboarding data...")
|
||||
sample_onboarding_data = create_sample_onboarding_data()
|
||||
logger.info("✅ Sample onboarding data created")
|
||||
|
||||
# Test 4: Test core persona generation
|
||||
logger.info("🤖 Test 4: Testing core persona generation...")
|
||||
core_persona = persona_service._generate_core_persona(sample_onboarding_data)
|
||||
|
||||
if "error" in core_persona:
|
||||
logger.error(f"❌ Core persona generation failed: {core_persona['error']}")
|
||||
return False
|
||||
else:
|
||||
logger.info("✅ Core persona generated successfully")
|
||||
logger.info(f" Persona Name: {core_persona.get('identity', {}).get('persona_name', 'N/A')}")
|
||||
logger.info(f" Archetype: {core_persona.get('identity', {}).get('archetype', 'N/A')}")
|
||||
logger.info(f" Confidence: {core_persona.get('confidence_score', 0)}%")
|
||||
|
||||
# Test 5: Test platform adaptations
|
||||
logger.info("📱 Test 5: Testing platform adaptations...")
|
||||
platforms = ["twitter", "linkedin", "blog"]
|
||||
|
||||
for platform in platforms:
|
||||
platform_persona = persona_service._generate_single_platform_persona(
|
||||
core_persona, platform, sample_onboarding_data
|
||||
)
|
||||
|
||||
if "error" in platform_persona:
|
||||
logger.warning(f"⚠️ {platform} persona generation failed: {platform_persona['error']}")
|
||||
else:
|
||||
logger.info(f"✅ {platform} persona generated successfully")
|
||||
|
||||
# Test 6: Test data sufficiency calculation
|
||||
logger.info("📊 Test 6: Testing data sufficiency calculation...")
|
||||
data_sufficiency = persona_service._calculate_data_sufficiency(sample_onboarding_data)
|
||||
logger.info(f"✅ Data sufficiency calculated: {data_sufficiency}%")
|
||||
|
||||
logger.info("🎉 All persona system tests completed successfully!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Persona system test failed: {str(e)}")
|
||||
return False
|
||||
|
||||
def create_sample_onboarding_data():
|
||||
"""Create realistic sample onboarding data for testing."""
|
||||
|
||||
return {
|
||||
"session_info": {
|
||||
"session_id": 1,
|
||||
"current_step": 6,
|
||||
"progress": 100.0,
|
||||
"started_at": datetime.utcnow().isoformat()
|
||||
},
|
||||
"website_analysis": {
|
||||
"id": 1,
|
||||
"website_url": "https://techstartup.example.com",
|
||||
"writing_style": {
|
||||
"tone": "professional",
|
||||
"voice": "authoritative",
|
||||
"complexity": "intermediate",
|
||||
"engagement_level": "high"
|
||||
},
|
||||
"content_characteristics": {
|
||||
"sentence_structure": "varied",
|
||||
"vocabulary": "technical",
|
||||
"paragraph_organization": "logical",
|
||||
"average_sentence_length": 15.2
|
||||
},
|
||||
"target_audience": {
|
||||
"demographics": ["startup founders", "tech professionals", "investors"],
|
||||
"expertise_level": "intermediate",
|
||||
"industry_focus": "technology"
|
||||
},
|
||||
"content_type": {
|
||||
"primary_type": "blog",
|
||||
"secondary_types": ["case_study", "tutorial"],
|
||||
"purpose": "educational"
|
||||
},
|
||||
"style_patterns": {
|
||||
"common_phrases": ["let's dive in", "the key insight", "bottom line"],
|
||||
"sentence_starters": ["Here's the thing:", "The reality is", "Consider this:"],
|
||||
"rhetorical_devices": ["metaphors", "data_points", "examples"]
|
||||
},
|
||||
"style_guidelines": {
|
||||
"tone_guidelines": "Maintain professional but approachable tone",
|
||||
"structure_guidelines": "Use clear headings and bullet points",
|
||||
"voice_guidelines": "Confident and knowledgeable without being condescending"
|
||||
},
|
||||
"status": "completed"
|
||||
},
|
||||
"research_preferences": {
|
||||
"id": 1,
|
||||
"research_depth": "Comprehensive",
|
||||
"content_types": ["blog", "case_study", "whitepaper"],
|
||||
"auto_research": True,
|
||||
"factual_content": True,
|
||||
"writing_style": {
|
||||
"tone": "professional",
|
||||
"voice": "authoritative",
|
||||
"complexity": "intermediate"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_gemini_structured_response():
|
||||
"""Test Gemini structured response functionality."""
|
||||
|
||||
logger.info("🔬 Testing Gemini Structured Response")
|
||||
|
||||
try:
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response
|
||||
|
||||
# Simple test schema
|
||||
test_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"test_field": {"type": "string"},
|
||||
"confidence": {"type": "number"}
|
||||
},
|
||||
"required": ["test_field", "confidence"]
|
||||
}
|
||||
|
||||
test_prompt = "Generate a test response with test_field='Hello World' and confidence=95.5"
|
||||
|
||||
response = gemini_structured_json_response(
|
||||
prompt=test_prompt,
|
||||
schema=test_schema,
|
||||
temperature=0.1,
|
||||
max_tokens=1024
|
||||
)
|
||||
|
||||
if "error" in response:
|
||||
logger.error(f"❌ Gemini test failed: {response['error']}")
|
||||
return False
|
||||
else:
|
||||
logger.info(f"✅ Gemini structured response test successful: {response}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Gemini test error: {str(e)}")
|
||||
return False
|
||||
|
||||
def run_comprehensive_test():
|
||||
"""Run comprehensive test of the persona system."""
|
||||
|
||||
logger.info("🚀 Starting Comprehensive Persona System Test")
|
||||
|
||||
# Test 1: Gemini functionality
|
||||
gemini_works = test_gemini_structured_response()
|
||||
|
||||
# Test 2: Persona system
|
||||
persona_works = test_persona_system()
|
||||
|
||||
# Summary
|
||||
logger.info("📋 Test Summary:")
|
||||
logger.info(f" Gemini Structured Response: {'✅ PASS' if gemini_works else '❌ FAIL'}")
|
||||
logger.info(f" Persona Generation System: {'✅ PASS' if persona_works else '❌ FAIL'}")
|
||||
|
||||
if gemini_works and persona_works:
|
||||
logger.info("🎉 All tests passed! Persona system is ready for production.")
|
||||
return True
|
||||
else:
|
||||
logger.error("❌ Some tests failed. Please check the logs and fix issues.")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = run_comprehensive_test()
|
||||
sys.exit(0 if success else 1)
|
||||
Reference in New Issue
Block a user