Merge PR #226: Writing Persona System with platform-specific adaptations
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)}")
|
||||
@@ -386,6 +386,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)
|
||||
Binary file not shown.
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()
|
||||
Binary file not shown.
Binary file not shown.
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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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