Implement persona generation system with platform-specific adaptations
Co-authored-by: ajay.calsoft <ajay.calsoft@gmail.com>
This commit is contained in:
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
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user