Added onboarding progress tracking & landing page

This commit is contained in:
ajaysi
2025-10-02 13:20:15 +05:30
parent e57d2577f8
commit 510b79bbf8
135 changed files with 25917 additions and 5768 deletions

View File

@@ -30,6 +30,9 @@ from services.component_logic.web_crawler_logic import WebCrawlerLogic
from services.research_preferences_service import ResearchPreferencesService
from services.database import get_db
# Import authentication for user isolation
from middleware.auth_middleware import get_current_user
# Import the website analysis service
from services.website_analysis_service import WebsiteAnalysisService
from services.database import get_db_session
@@ -70,10 +73,15 @@ async def validate_user_info(request: UserInfoRequest):
raise HTTPException(status_code=500, detail=str(e))
@router.post("/ai-research/configure-preferences", response_model=ResearchPreferencesResponse)
async def configure_research_preferences(request: ResearchPreferencesRequest, db: Session = Depends(get_db)):
"""Configure research preferences for AI research and save to database."""
async def configure_research_preferences(
request: ResearchPreferencesRequest,
db: Session = Depends(get_db),
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""Configure research preferences for AI research and save to database with user isolation."""
try:
logger.info("Configuring research preferences via API")
user_id = str(current_user.get('id'))
logger.info(f"Configuring research preferences for user: {user_id}")
# Validate preferences using business logic
preferences = {
@@ -90,11 +98,15 @@ async def configure_research_preferences(request: ResearchPreferencesRequest, db
# Save to database
preferences_service = ResearchPreferencesService(db)
# Use a default session ID for now (you might need to implement session management)
session_id = 1 # TODO: Get actual session ID from request context
# Use authenticated Clerk user ID for proper user isolation
# Convert user_id to int if service expects it, or update service to accept string
try:
user_id_int = int(user_id.replace('user_', '').replace('-', '')[:8], 16) % 2147483647
except:
user_id_int = hash(user_id) % 2147483647
# Save preferences with style data from step 2
preferences_id = preferences_service.save_preferences_with_style_data(session_id, preferences)
# Save preferences with user ID (not session_id)
preferences_id = preferences_service.save_preferences_with_style_data(user_id_int, preferences)
if preferences_id:
logger.info(f"Research preferences saved to database with ID: {preferences_id}")
@@ -468,10 +480,14 @@ async def crawl_website_content(request: WebCrawlRequest):
)
@router.post("/style-detection/complete", response_model=StyleDetectionResponse)
async def complete_style_detection(request: StyleDetectionRequest):
"""Complete style detection workflow (crawl + analyze + guidelines) with database storage."""
async def complete_style_detection(
request: StyleDetectionRequest,
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""Complete style detection workflow (crawl + analyze + guidelines) with database storage and user isolation."""
try:
logger.info("[complete_style_detection] Starting complete style detection")
user_id = str(current_user.get('id'))
logger.info(f"[complete_style_detection] Starting complete style detection for user: {user_id}")
# Get database session
db_session = get_db_session()
@@ -487,13 +503,16 @@ async def complete_style_detection(request: StyleDetectionRequest):
style_logic = StyleDetectionLogic()
analysis_service = WebsiteAnalysisService(db_session)
# Get session ID (for now using a default, in production this would come from user session)
session_id = 1 # TODO: Get from user session
# Use authenticated Clerk user ID for proper user isolation
try:
user_id_int = int(user_id.replace('user_', '').replace('-', '')[:8], 16) % 2147483647
except:
user_id_int = hash(user_id) % 2147483647
# Check for existing analysis if URL is provided
existing_analysis = None
if request.url:
existing_analysis = analysis_service.check_existing_analysis(session_id, request.url)
existing_analysis = analysis_service.check_existing_analysis(user_id_int, request.url)
# Step 1: Crawl content
if request.url:
@@ -509,7 +528,7 @@ async def complete_style_detection(request: StyleDetectionRequest):
if not crawl_result['success']:
# Save error analysis
analysis_service.save_error_analysis(session_id, request.url or "text_sample",
analysis_service.save_error_analysis(user_id_int, request.url or "text_sample",
crawl_result.get('error', 'Crawling failed'))
return StyleDetectionResponse(
success=False,
@@ -531,7 +550,7 @@ async def complete_style_detection(request: StyleDetectionRequest):
)
else:
# Save error analysis
analysis_service.save_error_analysis(session_id, request.url or "text_sample", error_msg)
analysis_service.save_error_analysis(user_id_int, request.url or "text_sample", error_msg)
return StyleDetectionResponse(
success=False,
error=f"Style analysis failed: {error_msg}",
@@ -568,7 +587,7 @@ async def complete_style_detection(request: StyleDetectionRequest):
# Save analysis to database
if request.url: # Only save for URL-based analysis
analysis_id = analysis_service.save_analysis(session_id, request.url, response_data)
analysis_id = analysis_service.save_analysis(user_id_int, request.url, response_data)
if analysis_id:
response_data['analysis_id'] = analysis_id
@@ -591,10 +610,14 @@ async def complete_style_detection(request: StyleDetectionRequest):
)
@router.get("/style-detection/check-existing/{website_url:path}")
async def check_existing_analysis(website_url: str):
"""Check if analysis exists for a website URL."""
async def check_existing_analysis(
website_url: str,
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""Check if analysis exists for a website URL with user isolation."""
try:
logger.info(f"[check_existing_analysis] Checking for URL: {website_url}")
user_id = str(current_user.get('id'))
logger.info(f"[check_existing_analysis] Checking for URL: {website_url} (user: {user_id})")
# Get database session
db_session = get_db_session()
@@ -604,11 +627,14 @@ async def check_existing_analysis(website_url: str):
# Initialize service
analysis_service = WebsiteAnalysisService(db_session)
# Get session ID (for now using a default, in production this would come from user session)
session_id = 1 # TODO: Get from user session
# Use authenticated Clerk user ID for proper user isolation
try:
user_id_int = int(user_id.replace('user_', '').replace('-', '')[:8], 16) % 2147483647
except:
user_id_int = hash(user_id) % 2147483647
# Check for existing analysis
existing_analysis = analysis_service.check_existing_analysis(session_id, website_url)
# Check for existing analysis for THIS USER ONLY
existing_analysis = analysis_service.check_existing_analysis(user_id_int, website_url)
return existing_analysis
@@ -643,10 +669,11 @@ async def get_analysis_by_id(analysis_id: int):
return {"error": f"Error retrieving analysis: {str(e)}"}
@router.get("/style-detection/session-analyses")
async def get_session_analyses():
"""Get all analyses for the current session."""
async def get_session_analyses(current_user: Dict[str, Any] = Depends(get_current_user)):
"""Get all analyses for the current user with proper user isolation."""
try:
logger.info("[get_session_analyses] Getting session analyses")
user_id = str(current_user.get('id'))
logger.info(f"[get_session_analyses] Getting analyses for user: {user_id}")
# Get database session
db_session = get_db_session()
@@ -656,12 +683,16 @@ async def get_session_analyses():
# Initialize service
analysis_service = WebsiteAnalysisService(db_session)
# Get session ID (for now using a default, in production this would come from user session)
session_id = 1 # TODO: Get from user session
# Use authenticated Clerk user ID for proper user isolation
try:
user_id_int = int(user_id.replace('user_', '').replace('-', '')[:8], 16) % 2147483647
except:
user_id_int = hash(user_id) % 2147483647
# Get analyses
analyses = analysis_service.get_session_analyses(session_id)
# Get analyses for THIS USER ONLY (not all users!)
analyses = analysis_service.get_session_analyses(user_id_int)
logger.info(f"[get_session_analyses] Found {len(analyses) if analyses else 0} analyses for user {user_id}")
return {"success": True, "analyses": analyses}
except Exception as e:

View File

@@ -12,6 +12,9 @@ import time
import asyncio
import random
# Import authentication
from middleware.auth_middleware import get_current_user
# Import database service
from services.database import get_db_session, get_db
from services.content_planning_db import ContentPlanningDBService
@@ -40,21 +43,43 @@ from ...services.calendar_generation_service import CalendarGenerationService
# Create router
router = APIRouter(prefix="/calendar-generation", tags=["calendar-generation"])
@router.post("/generate-calendar", response_model=CalendarGenerationResponse)
async def generate_comprehensive_calendar(request: CalendarGenerationRequest, db: Session = Depends(get_db)):
# Helper function to convert Clerk user ID to integer
def get_user_id_int(clerk_user_id: str) -> int:
"""
Generate a comprehensive AI-powered content calendar using database insights.
Convert Clerk user ID string to integer for database compatibility.
Uses consistent hashing to ensure same user always gets same ID.
"""
try:
# Try to extract numeric portion from Clerk ID format (user_XXXX)
numeric_part = clerk_user_id.replace('user_', '').replace('-', '')[:8]
return int(numeric_part, 16) % 2147483647
except:
# Fallback to hash if extraction fails
return hash(clerk_user_id) % 2147483647
@router.post("/generate-calendar", response_model=CalendarGenerationResponse)
async def generate_comprehensive_calendar(
request: CalendarGenerationRequest,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""
Generate a comprehensive AI-powered content calendar using database insights with user isolation.
This endpoint uses advanced AI analysis and comprehensive user data.
Now ensures Phase 1 and Phase 2 use the ACTIVE strategy with 3-tier caching.
"""
try:
logger.info(f"🎯 Generating comprehensive calendar for user {request.user_id}")
# Use authenticated user ID instead of request user ID for security
clerk_user_id = str(current_user.get('id'))
user_id_int = get_user_id_int(clerk_user_id)
logger.info(f"🎯 Generating comprehensive calendar for authenticated user {clerk_user_id} (int: {user_id_int})")
# Initialize service with database session for active strategy access
calendar_service = CalendarGenerationService(db)
calendar_data = await calendar_service.generate_comprehensive_calendar(
user_id=request.user_id,
user_id=user_id_int, # Use authenticated user ID
strategy_id=request.strategy_id,
calendar_type=request.calendar_type,
industry=request.industry,
@@ -180,13 +205,13 @@ async def repurpose_content_across_platforms(request: ContentRepurposingRequest,
@router.get("/trending-topics", response_model=TrendingTopicsResponse)
async def get_trending_topics(
user_id: int = Query(..., description="User ID"),
industry: str = Query(..., description="Industry for trending topics"),
limit: int = Query(10, description="Number of trending topics to return"),
db: Session = Depends(get_db)
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""
Get trending topics relevant to the user's industry and content gaps.
Get trending topics relevant to the user's industry and content gaps with user isolation.
This endpoint provides trending topics based on:
- Industry-specific trends
@@ -195,7 +220,11 @@ async def get_trending_topics(
- Competitor analysis insights
"""
try:
logger.info(f"📈 Getting trending topics for user {user_id} in {industry}")
# Use authenticated user ID instead of query parameter for security
clerk_user_id = str(current_user.get('id'))
user_id = get_user_id_int(clerk_user_id)
logger.info(f"📈 Getting trending topics for authenticated user {clerk_user_id} (int: {user_id}) in {industry}")
# Initialize service with database session for active strategy access
calendar_service = CalendarGenerationService(db)
@@ -217,16 +246,20 @@ async def get_trending_topics(
@router.get("/comprehensive-user-data")
async def get_comprehensive_user_data(
user_id: int = Query(..., description="User ID"),
force_refresh: bool = Query(False, description="Force refresh cache"),
db: Session = Depends(get_db)
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
) -> Dict[str, Any]:
"""
Get comprehensive user data for calendar generation with intelligent caching.
Get comprehensive user data for calendar generation with intelligent caching and user isolation.
This endpoint aggregates all data points needed for the calendar wizard.
"""
try:
logger.info(f"Getting comprehensive user data for user_id: {user_id} (force_refresh={force_refresh})")
# Use authenticated user ID instead of query parameter for security
clerk_user_id = str(current_user.get('id'))
user_id = get_user_id_int(clerk_user_id)
logger.info(f"Getting comprehensive user data for authenticated user {clerk_user_id} (int: {user_id}, force_refresh={force_refresh})")
# Initialize cache service
from services.comprehensive_user_data_cache_service import ComprehensiveUserDataCacheService
@@ -328,21 +361,30 @@ async def get_calendar_generation_progress(session_id: str, db: Session = Depend
raise HTTPException(status_code=500, detail="Failed to get progress")
@router.post("/start")
async def start_calendar_generation(request: CalendarGenerationRequest, db: Session = Depends(get_db)):
async def start_calendar_generation(
request: CalendarGenerationRequest,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""
Start calendar generation and return a session ID for progress tracking.
Start calendar generation and return a session ID for progress tracking with user isolation.
Prevents duplicate sessions for the same user.
"""
try:
# Use authenticated user ID instead of request user ID for security
clerk_user_id = str(current_user.get('id'))
user_id_int = get_user_id_int(clerk_user_id)
logger.info(f"🎯 Starting calendar generation for authenticated user {clerk_user_id} (int: {user_id_int})")
# Initialize service with database session for active strategy access
calendar_service = CalendarGenerationService(db)
# Check if user already has an active session
user_id = request.user_id
existing_session = calendar_service._get_active_session_for_user(user_id)
existing_session = calendar_service._get_active_session_for_user(user_id_int)
if existing_session:
logger.info(f"🔄 User {user_id} already has active session: {existing_session}")
logger.info(f"🔄 User {user_id_int} already has active session: {existing_session}")
return {
"session_id": existing_session,
"status": "existing",
@@ -353,15 +395,19 @@ async def start_calendar_generation(request: CalendarGenerationRequest, db: Sess
# Generate a unique session ID
session_id = f"calendar-session-{int(time.time())}-{random.randint(1000, 9999)}"
# Update request data with authenticated user ID
request_dict = request.dict()
request_dict['user_id'] = user_id_int # Override with authenticated user ID
# Initialize orchestrator session
success = calendar_service.initialize_orchestrator_session(session_id, request.dict())
success = calendar_service.initialize_orchestrator_session(session_id, request_dict)
if not success:
raise HTTPException(status_code=500, detail="Failed to initialize orchestrator session")
# Start the generation process asynchronously using orchestrator
# This will run in the background while the frontend polls for progress
asyncio.create_task(calendar_service.start_orchestrator_generation(session_id, request.dict()))
asyncio.create_task(calendar_service.start_orchestrator_generation(session_id, request_dict))
return {
"session_id": session_id,

View File

@@ -317,10 +317,15 @@ class CalendarGenerationService:
# Check database connectivity
db_status = "healthy"
try:
# Test database connection using direct database service
from services.content_planning_db import ContentPlanningDBService
db_service = ContentPlanningDBService(self.db_session)
await db_service.get_user_content_gap_analyses(1)
# Test database connection - just check if db_session is available
if self.db_session:
# Simple connectivity test without hardcoded user_id
from services.content_planning_db import ContentPlanningDBService
db_service = ContentPlanningDBService(self.db_session)
# Don't test with a specific user_id - just verify service initializes
db_status = "healthy"
else:
db_status = "no session"
except Exception as e:
db_status = f"error: {str(e)}"
@@ -358,7 +363,10 @@ class CalendarGenerationService:
return False
# Clean up old sessions for the same user
user_id = request_data.get("user_id", 1)
user_id = request_data.get("user_id")
if not user_id:
logger.error("❌ user_id is required in request_data")
return False
self._cleanup_old_sessions(user_id)
# Check for existing active sessions for this user
@@ -446,8 +454,12 @@ class CalendarGenerationService:
session["status"] = "running"
# Start the 12-step process
user_id = request_data.get("user_id")
if not user_id:
raise ValueError("user_id is required in request_data")
result = await self.orchestrator.generate_calendar(
user_id=request_data.get("user_id", 1),
user_id=user_id,
strategy_id=request_data.get("strategy_id"),
calendar_type=request_data.get("calendar_type", "monthly"),
industry=request_data.get("industry"),

View File

@@ -14,10 +14,12 @@ import time
from services.api_key_manager import (
OnboardingProgress,
get_onboarding_progress,
get_onboarding_progress_for_user,
StepStatus,
StepData,
APIKeyManager
)
from middleware.auth_middleware import get_current_user
from services.validation import check_all_api_keys
# Pydantic models for API requests/responses
@@ -76,220 +78,172 @@ def health_check():
"""Health check endpoint."""
return {"status": "healthy", "timestamp": datetime.now().isoformat()}
# Onboarding status endpoints
async def get_onboarding_status():
"""Get the current onboarding status."""
# Batch initialization endpoint - combines multiple calls into one
async def initialize_onboarding(current_user: Dict[str, Any] = Depends(get_current_user)):
"""
Single endpoint for onboarding initialization - reduces round trips.
Combines:
- User information
- Onboarding status
- Progress details
- Step data
This eliminates 3-4 separate API calls on initial load.
"""
try:
progress = get_onboarding_progress()
user_id = str(current_user.get('id'))
progress = get_onboarding_progress_for_user(user_id)
# Safety check: if all steps are completed, ensure is_completed is True
all_steps_completed = all(s.status in [StepStatus.COMPLETED, StepStatus.SKIPPED] for s in progress.steps)
if all_steps_completed and not progress.is_completed:
logger.info(f"[get_onboarding_status] All steps completed but is_completed was False, fixing...")
progress.is_completed = True
progress.completed_at = datetime.now().isoformat()
progress.current_step = len(progress.steps) # Ensure current_step is valid
progress.save_progress()
# Build comprehensive step data
steps_data = []
for step in progress.steps:
steps_data.append({
"step_number": step.step_number,
"title": step.title,
"description": step.description,
"status": step.status.value,
"completed_at": step.completed_at,
"has_data": step.data is not None and len(step.data) > 0 if step.data else False
})
logger.info(f"[get_onboarding_status] Current step: {progress.current_step}")
logger.info(f"[get_onboarding_status] Is completed: {progress.is_completed}")
logger.info(f"[get_onboarding_status] Steps status: {[f'{s.step_number}:{s.status.value}' for s in progress.steps]}")
# Get next incomplete step
next_step = progress.get_next_incomplete_step()
return OnboardingStatusResponse(
is_completed=progress.is_completed,
current_step=progress.current_step,
completion_percentage=progress.get_completion_percentage(),
next_step=progress.get_next_incomplete_step(),
started_at=progress.started_at,
completed_at=progress.completed_at,
can_proceed_to_final=progress.can_complete_onboarding()
response_data = {
"user": {
"id": user_id,
"email": current_user.get('email'),
"first_name": current_user.get('first_name'),
"last_name": current_user.get('last_name'),
"clerk_user_id": user_id # Clerk user ID is the session
},
"onboarding": {
"is_completed": progress.is_completed,
"current_step": progress.current_step,
"completion_percentage": progress.get_completion_percentage(),
"next_step": next_step,
"started_at": progress.started_at,
"last_updated": progress.last_updated,
"completed_at": progress.completed_at,
"can_proceed_to_final": progress.can_complete_onboarding(),
"steps": steps_data
},
"session": {
"session_id": user_id, # Clerk user ID is the session identifier
"initialized_at": datetime.now().isoformat()
}
}
logger.info(f"Batch init successful for user {user_id}: step {progress.current_step}/{len(progress.steps)}")
return response_data
except Exception as e:
logger.error(f"Error in initialize_onboarding: {str(e)}", exc_info=True)
raise HTTPException(
status_code=500,
detail=f"Failed to initialize onboarding: {str(e)}"
)
# Onboarding status endpoints
async def get_onboarding_status(current_user: Dict[str, Any]):
"""Get the current onboarding status (per user)."""
try:
from api.onboarding_utils.step_management_service import StepManagementService
step_service = StepManagementService()
return await step_service.get_onboarding_status(current_user)
except Exception as e:
logger.error(f"Error getting onboarding status: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def get_onboarding_progress_full():
async def get_onboarding_progress_full(current_user: Dict[str, Any]):
"""Get the full onboarding progress data."""
try:
progress = get_onboarding_progress()
# Convert StepData objects to Pydantic models
step_models = []
for step in progress.steps:
step_models.append(StepDataModel(
step_number=step.step_number,
title=step.title,
description=step.description,
status=step.status.value,
completed_at=step.completed_at,
data=step.data,
validation_errors=step.validation_errors or []
))
from api.onboarding_utils.step_management_service import StepManagementService
return OnboardingProgressModel(
steps=step_models,
current_step=progress.current_step,
started_at=progress.started_at,
last_updated=progress.last_updated,
is_completed=progress.is_completed,
completed_at=progress.completed_at
)
step_service = StepManagementService()
return await step_service.get_onboarding_progress_full(current_user)
except Exception as e:
logger.error(f"Error getting onboarding progress: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def get_step_data(step_number: int):
async def get_step_data(step_number: int, current_user: Dict[str, Any]):
"""Get data for a specific step."""
try:
progress = get_onboarding_progress()
step = progress.get_step_data(step_number)
from api.onboarding_utils.step_management_service import StepManagementService
if not step:
raise HTTPException(status_code=404, detail=f"Step {step_number} not found")
return StepDataModel(
step_number=step.step_number,
title=step.title,
description=step.description,
status=step.status.value,
completed_at=step.completed_at,
data=step.data,
validation_errors=step.validation_errors or []
)
except HTTPException:
raise
step_service = StepManagementService()
return await step_service.get_step_data(step_number, current_user)
except Exception as e:
logger.error(f"Error getting step data: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def complete_step(step_number: int, request: StepCompletionRequest):
async def complete_step(step_number: int, request: StepCompletionRequest, current_user: Dict[str, Any]):
"""Mark a step as completed."""
try:
logger.info(f"[complete_step] Completing step {step_number}")
progress = get_onboarding_progress()
step = progress.get_step_data(step_number)
from api.onboarding_utils.step_management_service import StepManagementService
if not step:
logger.error(f"[complete_step] Step {step_number} not found")
raise HTTPException(status_code=404, detail=f"Step {step_number} not found")
# Mark step as completed
progress.mark_step_completed(step_number, request.data)
logger.info(f"[complete_step] Step {step_number} completed successfully")
return {
"message": f"Step {step_number} completed successfully",
"step_number": step_number,
"data": request.data
}
step_service = StepManagementService()
return await step_service.complete_step(step_number, request.data, current_user)
except HTTPException:
# Propagate known HTTP errors (e.g., 400 validation failures) without converting to 500
raise
except Exception as e:
logger.error(f"Error completing step: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def skip_step(step_number: int):
async def skip_step(step_number: int, current_user: Dict[str, Any]):
"""Skip a step (for optional steps)."""
try:
progress = get_onboarding_progress()
step = progress.get_step_data(step_number)
from api.onboarding_utils.step_management_service import StepManagementService
if not step:
raise HTTPException(status_code=404, detail=f"Step {step_number} not found")
# Mark step as skipped
progress.mark_step_skipped(step_number)
return {
"message": f"Step {step_number} skipped successfully",
"step_number": step_number
}
except HTTPException:
raise
step_service = StepManagementService()
return await step_service.skip_step(step_number, current_user)
except Exception as e:
logger.error(f"Error skipping step: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def validate_step_access(step_number: int):
async def validate_step_access(step_number: int, current_user: Dict[str, Any]):
"""Validate if user can access a specific step."""
try:
progress = get_onboarding_progress()
from api.onboarding_utils.step_management_service import StepManagementService
if not progress.can_proceed_to_step(step_number):
return StepValidationResponse(
can_proceed=False,
validation_errors=[f"Cannot proceed to step {step_number}. Complete previous steps first."],
step_status="locked"
)
return StepValidationResponse(
can_proceed=True,
validation_errors=[],
step_status="available"
)
step_service = StepManagementService()
return await step_service.validate_step_access(step_number, current_user)
except Exception as e:
logger.error(f"Error validating step access: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
# Simple cache for API keys
_api_keys_cache = None
_cache_timestamp = 0
CACHE_DURATION = 30 # Cache for 30 seconds
async def get_api_keys():
"""Get all configured API keys (masked)."""
global _api_keys_cache, _cache_timestamp
current_time = time.time()
# Return cached result if still valid
if _api_keys_cache and (current_time - _cache_timestamp) < CACHE_DURATION:
logger.debug("Returning cached API keys")
return _api_keys_cache
try:
api_manager = APIKeyManager()
api_manager.load_api_keys() # Load keys from environment
api_keys = api_manager.api_keys # Get the loaded keys
from api.onboarding_utils.api_key_management_service import APIKeyManagementService
# Mask the API keys for security
masked_keys = {}
for provider, key in api_keys.items():
if key:
masked_keys[provider] = "*" * (len(key) - 4) + key[-4:] if len(key) > 4 else "*" * len(key)
else:
masked_keys[provider] = None
result = {
"api_keys": masked_keys,
"total_providers": len(api_keys),
"configured_providers": [k for k, v in api_keys.items() if v]
}
# Cache the result
_api_keys_cache = result
_cache_timestamp = current_time
return result
api_service = APIKeyManagementService()
return await api_service.get_api_keys()
except Exception as e:
logger.error(f"Error getting API keys: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def get_api_keys_for_onboarding():
"""Get all configured API keys for onboarding (unmasked)."""
try:
from api.onboarding_utils.api_key_management_service import APIKeyManagementService
api_service = APIKeyManagementService()
return await api_service.get_api_keys_for_onboarding()
except Exception as e:
logger.error(f"Error getting API keys for onboarding: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def save_api_key(request: APIKeyRequest):
"""Save an API key for a provider."""
try:
api_manager = APIKeyManager()
success = api_manager.save_api_key(request.provider, request.api_key)
from api.onboarding_utils.api_key_management_service import APIKeyManagementService
if success:
return {
"message": f"API key for {request.provider} saved successfully",
"provider": request.provider,
"status": "saved"
}
else:
raise HTTPException(status_code=400, detail=f"Failed to save API key for {request.provider}")
except HTTPException:
raise
api_service = APIKeyManagementService()
return await api_service.save_api_key(request.provider, request.api_key, request.description)
except Exception as e:
logger.error(f"Error saving API key: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
@@ -297,87 +251,32 @@ async def save_api_key(request: APIKeyRequest):
async def validate_api_keys():
"""Validate all configured API keys."""
try:
api_manager = APIKeyManager()
validation_results = check_all_api_keys(api_manager)
from api.onboarding_utils.api_key_management_service import APIKeyManagementService
return {
"validation_results": validation_results.get('results', {}),
"all_valid": validation_results.get('all_valid', False),
"total_providers": len(validation_results.get('results', {}))
}
api_service = APIKeyManagementService()
return await api_service.validate_api_keys()
except Exception as e:
logger.error(f"Error validating API keys: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def start_onboarding():
async def start_onboarding(current_user: Dict[str, Any]):
"""Start a new onboarding session."""
try:
progress = get_onboarding_progress()
progress.reset_progress()
from api.onboarding_utils.onboarding_control_service import OnboardingControlService
return {
"message": "Onboarding started successfully",
"current_step": progress.current_step,
"started_at": progress.started_at
}
control_service = OnboardingControlService()
return await control_service.start_onboarding(current_user)
except Exception as e:
logger.error(f"Error starting onboarding: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def complete_onboarding():
async def complete_onboarding(current_user: Dict[str, Any]):
"""Complete the onboarding process."""
try:
progress = get_onboarding_progress()
from api.onboarding_utils.onboarding_completion_service import OnboardingCompletionService
# Check which required steps are missing
required_steps = [1, 2, 3, 6] # Steps 1, 2, 3, and 6 are required
missing_steps = []
for step_num in required_steps:
step = progress.get_step_data(step_num)
if step and step.status not in [StepStatus.COMPLETED, StepStatus.SKIPPED]:
missing_steps.append(step.title)
if missing_steps:
missing_steps_str = ", ".join(missing_steps)
raise HTTPException(
status_code=400,
detail=f"Cannot complete onboarding. The following steps must be completed first: {missing_steps_str}"
)
# Additional validation: Check if API keys are configured
api_manager = get_api_key_manager()
api_keys = api_manager.get_all_keys()
if not api_keys:
raise HTTPException(
status_code=400,
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,
"persona_generated": "error" not in persona_result if 'persona_result' in locals() else False
}
completion_service = OnboardingCompletionService()
return await completion_service.complete_onboarding(current_user)
except HTTPException:
raise
except Exception as e:
@@ -387,14 +286,10 @@ async def complete_onboarding():
async def reset_onboarding():
"""Reset the onboarding progress."""
try:
progress = get_onboarding_progress()
progress.reset_progress()
from api.onboarding_utils.onboarding_control_service import OnboardingControlService
return {
"message": "Onboarding progress reset successfully",
"current_step": progress.current_step,
"started_at": progress.started_at
}
control_service = OnboardingControlService()
return await control_service.reset_onboarding()
except Exception as e:
logger.error(f"Error resetting onboarding: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
@@ -402,124 +297,56 @@ async def reset_onboarding():
async def get_resume_info():
"""Get information for resuming onboarding."""
try:
progress = get_onboarding_progress()
from api.onboarding_utils.onboarding_control_service import OnboardingControlService
if progress.is_completed:
return {
"can_resume": False,
"message": "Onboarding is already completed",
"completion_percentage": 100.0
}
resume_step = progress.get_resume_step()
return {
"can_resume": True,
"resume_step": resume_step,
"current_step": progress.current_step,
"completion_percentage": progress.get_completion_percentage(),
"started_at": progress.started_at,
"last_updated": progress.last_updated
}
control_service = OnboardingControlService()
return await control_service.get_resume_info()
except Exception as e:
logger.error(f"Error getting resume info: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
def get_onboarding_config():
"""Get onboarding configuration and requirements."""
return {
"total_steps": 6,
"steps": [
{
"number": 1,
"title": "AI LLM Providers",
"description": "Configure AI language model providers",
"required": True,
"providers": ["openai", "gemini", "anthropic"]
},
{
"number": 2,
"title": "Website Analysis",
"description": "Set up website analysis and crawling",
"required": True
},
{
"number": 3,
"title": "AI Research",
"description": "Configure AI research capabilities",
"required": True
},
{
"number": 4,
"title": "Personalization",
"description": "Set up personalization features",
"required": False
},
{
"number": 5,
"title": "Integrations",
"description": "Configure ALwrity integrations",
"required": False
},
{
"number": 6,
"title": "Complete Setup",
"description": "Finalize and complete onboarding",
"required": True
}
],
"requirements": {
"min_api_keys": 1,
"required_providers": ["openai"],
"optional_providers": ["gemini", "anthropic"]
}
}
try:
from api.onboarding_utils.onboarding_config_service import OnboardingConfigService
config_service = OnboardingConfigService()
return config_service.get_onboarding_config()
except Exception as e:
logger.error(f"Error getting onboarding config: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
# Add new endpoints for enhanced functionality
async def get_provider_setup_info(provider: str):
"""Get setup information for a specific provider."""
try:
providers_info = get_all_providers_info()
if provider in providers_info:
return providers_info[provider]
else:
raise HTTPException(status_code=404, detail=f"Provider {provider} not found")
from api.onboarding_utils.onboarding_config_service import OnboardingConfigService
config_service = OnboardingConfigService()
return await config_service.get_provider_setup_info(provider)
except Exception as e:
logger.error(f"Error getting provider setup info: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def get_all_providers_info():
"""Get setup information for all providers."""
return {
"openai": {
"name": "OpenAI",
"description": "GPT-4 and GPT-3.5 models for content generation",
"setup_url": "https://platform.openai.com/api-keys",
"required_fields": ["api_key"],
"optional_fields": ["organization_id"]
},
"gemini": {
"name": "Google Gemini",
"description": "Google's advanced AI models for content creation",
"setup_url": "https://makersuite.google.com/app/apikey",
"required_fields": ["api_key"],
"optional_fields": []
},
"anthropic": {
"name": "Anthropic",
"description": "Claude models for sophisticated content generation",
"setup_url": "https://console.anthropic.com/",
"required_fields": ["api_key"],
"optional_fields": []
}
}
try:
from api.onboarding_utils.onboarding_config_service import OnboardingConfigService
config_service = OnboardingConfigService()
return config_service.get_all_providers_info()
except Exception as e:
logger.error(f"Error getting all providers info: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def validate_provider_key(provider: str, request: APIKeyRequest):
"""Validate a specific provider's API key."""
try:
result = await validate_api_key(provider, request.api_key)
return result
from api.onboarding_utils.onboarding_config_service import OnboardingConfigService
config_service = OnboardingConfigService()
return await config_service.validate_provider_key(provider, request.api_key)
except Exception as e:
logger.error(f"Error validating provider key: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
@@ -527,122 +354,50 @@ async def validate_provider_key(provider: str, request: APIKeyRequest):
async def get_enhanced_validation_status():
"""Get enhanced validation status for all configured services."""
try:
return await check_all_api_keys(get_api_key_manager())
from api.onboarding_utils.onboarding_config_service import OnboardingConfigService
config_service = OnboardingConfigService()
return await config_service.get_enhanced_validation_status()
except Exception as e:
logger.error(f"Error getting enhanced validation status: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
# New endpoints for FinalStep data loading
async def get_onboarding_summary():
"""Get comprehensive onboarding summary for FinalStep."""
async def get_onboarding_summary(current_user: Dict[str, Any]):
"""Get comprehensive onboarding summary for FinalStep with user isolation."""
try:
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
from api.onboarding_utils.onboarding_summary_service import OnboardingSummaryService
# 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()
api_keys = api_manager.get_all_keys()
# Get website analysis data
db = next(get_db())
website_service = WebsiteAnalysisService(db)
website_analysis = website_service.get_analysis_by_session(session_id)
# Get research preferences
research_service = ResearchPreferencesService(db)
research_preferences = research_service.get_research_preferences(session_id)
# Get personalization settings (from research preferences)
personalization_settings = None
if research_preferences:
personalization_settings = {
'writing_style': research_preferences.get('writing_style', {}).get('tone', 'Professional'),
'tone': research_preferences.get('writing_style', {}).get('voice', 'Formal'),
'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
}
}
user_id = str(current_user.get('id'))
summary_service = OnboardingSummaryService(user_id)
logger.info(f"Getting onboarding summary for user {user_id}")
return await summary_service.get_onboarding_summary()
except Exception as e:
logger.error(f"Error getting onboarding summary: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def get_website_analysis_data():
"""Get website analysis data for FinalStep."""
async def get_website_analysis_data(current_user: Dict[str, Any]):
"""Get website analysis data for FinalStep with user isolation."""
try:
from services.database import get_db
from services.website_analysis_service import WebsiteAnalysisService
from api.onboarding_utils.onboarding_summary_service import OnboardingSummaryService
session_id = 1
db = next(get_db())
website_service = WebsiteAnalysisService(db)
analysis = website_service.get_analysis_by_session(session_id)
if analysis:
return {
"website_url": analysis.get('website_url'),
"style_analysis": analysis.get('style_analysis'),
"style_patterns": analysis.get('style_patterns'),
"style_guidelines": analysis.get('style_guidelines'),
"status": analysis.get('status'),
"completed_at": analysis.get('created_at')
}
else:
return None
user_id = str(current_user.get('id'))
summary_service = OnboardingSummaryService(user_id)
logger.info(f"Getting website analysis data for user {user_id}")
return await summary_service.get_website_analysis_data()
except Exception as e:
logger.error(f"Error getting website analysis data: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def get_research_preferences_data():
"""Get research preferences data for FinalStep."""
async def get_research_preferences_data(current_user: Dict[str, Any]):
"""Get research preferences data for FinalStep with user isolation."""
try:
from services.database import get_db
from services.research_preferences_service import ResearchPreferencesService
from api.onboarding_utils.onboarding_summary_service import OnboardingSummaryService
session_id = 1
db = next(get_db())
research_service = ResearchPreferencesService(db)
preferences = research_service.get_research_preferences(session_id)
return preferences
user_id = str(current_user.get('id'))
summary_service = OnboardingSummaryService(user_id)
logger.info(f"Getting research preferences data for user {user_id}")
return await summary_service.get_research_preferences_data()
except Exception as e:
logger.error(f"Error getting research preferences data: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
@@ -652,8 +407,10 @@ async def get_research_preferences_data():
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)
from api.onboarding_utils.persona_management_service import PersonaManagementService
persona_service = PersonaManagementService()
return await persona_service.check_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")
@@ -661,8 +418,10 @@ async def check_persona_generation_readiness(user_id: int = 1):
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)
from api.onboarding_utils.persona_management_service import PersonaManagementService
persona_service = PersonaManagementService()
return await persona_service.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")
@@ -670,9 +429,10 @@ async def generate_persona_preview(user_id: int = 1):
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)
from api.onboarding_utils.persona_management_service import PersonaManagementService
persona_service = PersonaManagementService()
return await persona_service.generate_writing_persona(user_id)
except Exception as e:
logger.error(f"Error generating writing persona: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
@@ -680,8 +440,10 @@ async def generate_writing_persona(user_id: int = 1):
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)
from api.onboarding_utils.persona_management_service import PersonaManagementService
persona_service = PersonaManagementService()
return await persona_service.get_user_writing_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")
@@ -690,13 +452,10 @@ async def get_user_writing_personas(user_id: int = 1):
async def save_business_info(business_info: 'BusinessInfoRequest'):
"""Save business information for users without websites."""
try:
from models.business_info_request import BusinessInfoRequest
from services.business_info_service import business_info_service
from api.onboarding_utils.business_info_service import BusinessInfoService
logger.info(f"🔄 Saving business info for user_id: {business_info.user_id}")
result = business_info_service.save_business_info(business_info)
logger.success(f"✅ Business info saved successfully for user_id: {business_info.user_id}")
return result
business_service = BusinessInfoService()
return await business_service.save_business_info(business_info)
except Exception as e:
logger.error(f"❌ Error saving business info: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to save business info: {str(e)}")
@@ -704,18 +463,10 @@ async def save_business_info(business_info: 'BusinessInfoRequest'):
async def get_business_info(business_info_id: int):
"""Get business information by ID."""
try:
from services.business_info_service import business_info_service
from api.onboarding_utils.business_info_service import BusinessInfoService
logger.info(f"🔄 Getting business info for ID: {business_info_id}")
result = business_info_service.get_business_info(business_info_id)
if result:
logger.success(f"✅ Business info retrieved for ID: {business_info_id}")
return result
else:
logger.warning(f"⚠️ No business info found for ID: {business_info_id}")
raise HTTPException(status_code=404, detail="Business info not found")
except HTTPException:
raise
business_service = BusinessInfoService()
return await business_service.get_business_info(business_info_id)
except Exception as e:
logger.error(f"❌ Error getting business info: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to get business info: {str(e)}")
@@ -723,18 +474,10 @@ async def get_business_info(business_info_id: int):
async def get_business_info_by_user(user_id: int):
"""Get business information by user ID."""
try:
from services.business_info_service import business_info_service
from api.onboarding_utils.business_info_service import BusinessInfoService
logger.info(f"🔄 Getting business info for user ID: {user_id}")
result = business_info_service.get_business_info_by_user(user_id)
if result:
logger.success(f"✅ Business info retrieved for user ID: {user_id}")
return result
else:
logger.warning(f"⚠️ No business info found for user ID: {user_id}")
raise HTTPException(status_code=404, detail="Business info not found")
except HTTPException:
raise
business_service = BusinessInfoService()
return await business_service.get_business_info_by_user(user_id)
except Exception as e:
logger.error(f"❌ Error getting business info: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to get business info: {str(e)}")
@@ -742,19 +485,10 @@ async def get_business_info_by_user(user_id: int):
async def update_business_info(business_info_id: int, business_info: 'BusinessInfoRequest'):
"""Update business information."""
try:
from models.business_info_request import BusinessInfoRequest
from services.business_info_service import business_info_service
from api.onboarding_utils.business_info_service import BusinessInfoService
logger.info(f"🔄 Updating business info for ID: {business_info_id}")
result = business_info_service.update_business_info(business_info_id, business_info)
if result:
logger.success(f"✅ Business info updated for ID: {business_info_id}")
return result
else:
logger.warning(f"⚠️ No business info found to update for ID: {business_info_id}")
raise HTTPException(status_code=404, detail="Business info not found")
except HTTPException:
raise
business_service = BusinessInfoService()
return await business_service.update_business_info(business_info_id, business_info)
except Exception as e:
logger.error(f"❌ Error updating business info: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to update business info: {str(e)}")

View File

@@ -0,0 +1,706 @@
# ALwrity Onboarding System - API Reference
## Overview
This document provides a comprehensive API reference for the ALwrity Onboarding System. All endpoints require authentication and return JSON responses.
## 🔐 Authentication
All endpoints require a valid Clerk JWT token in the Authorization header:
```
Authorization: Bearer <clerk_jwt_token>
```
## 📋 Core Endpoints
### Onboarding Status
#### GET `/api/onboarding/status`
Get the current onboarding status for the authenticated user.
**Response:**
```json
{
"is_completed": false,
"current_step": 2,
"completion_percentage": 33.33,
"next_step": 3,
"started_at": "2024-01-15T10:30:00Z",
"completed_at": null,
"can_proceed_to_final": false
}
```
#### GET `/api/onboarding/progress`
Get the full onboarding progress data.
**Response:**
```json
{
"steps": [
{
"step_number": 1,
"title": "AI LLM Providers Setup",
"description": "Configure your AI services",
"status": "completed",
"completed_at": "2024-01-15T10:35:00Z",
"data": {...},
"validation_errors": []
}
],
"current_step": 2,
"started_at": "2024-01-15T10:30:00Z",
"last_updated": "2024-01-15T10:35:00Z",
"is_completed": false,
"completed_at": null
}
```
### Step Management
#### GET `/api/onboarding/step/{step_number}`
Get data for a specific step.
**Parameters:**
- `step_number` (int): The step number (1-6)
**Response:**
```json
{
"step_number": 1,
"title": "AI LLM Providers Setup",
"description": "Configure your AI services",
"status": "in_progress",
"completed_at": null,
"data": {...},
"validation_errors": []
}
```
#### POST `/api/onboarding/step/{step_number}/complete`
Mark a step as completed.
**Parameters:**
- `step_number` (int): The step number (1-6)
**Request Body:**
```json
{
"data": {
"api_keys": {
"gemini": "your_gemini_key",
"exa": "your_exa_key",
"copilotkit": "your_copilotkit_key"
}
},
"validation_errors": []
}
```
**Response:**
```json
{
"message": "Step 1 completed successfully",
"step_number": 1,
"data": {...}
}
```
#### POST `/api/onboarding/step/{step_number}/skip`
Skip a step (for optional steps).
**Parameters:**
- `step_number` (int): The step number (1-6)
**Response:**
```json
{
"message": "Step 2 skipped successfully",
"step_number": 2
}
```
#### GET `/api/onboarding/step/{step_number}/validate`
Validate if user can access a specific step.
**Parameters:**
- `step_number` (int): The step number (1-6)
**Response:**
```json
{
"can_proceed": true,
"validation_errors": [],
"step_status": "available"
}
```
### Onboarding Control
#### POST `/api/onboarding/start`
Start a new onboarding session.
**Response:**
```json
{
"message": "Onboarding started successfully",
"current_step": 1,
"started_at": "2024-01-15T10:30:00Z"
}
```
#### POST `/api/onboarding/reset`
Reset the onboarding progress.
**Response:**
```json
{
"message": "Onboarding progress reset successfully",
"current_step": 1,
"started_at": "2024-01-15T10:30:00Z"
}
```
#### GET `/api/onboarding/resume`
Get information for resuming onboarding.
**Response:**
```json
{
"can_resume": true,
"resume_step": 2,
"current_step": 2,
"completion_percentage": 33.33,
"started_at": "2024-01-15T10:30:00Z",
"last_updated": "2024-01-15T10:35:00Z"
}
```
#### POST `/api/onboarding/complete`
Complete the onboarding process.
**Response:**
```json
{
"message": "Onboarding completed successfully",
"completion_data": {...},
"persona_generated": true,
"environment_setup": true
}
```
## 🔑 API Key Management
### GET `/api/onboarding/api-keys`
Get all configured API keys (masked for security).
**Response:**
```json
{
"api_keys": {
"gemini": "********************abcd",
"exa": "********************efgh",
"copilotkit": "********************ijkl"
},
"total_providers": 3,
"configured_providers": ["gemini", "exa", "copilotkit"]
}
```
### POST `/api/onboarding/api-keys`
Save an API key for a provider.
**Request Body:**
```json
{
"provider": "gemini",
"api_key": "your_api_key_here",
"description": "Gemini API key for content generation"
}
```
**Response:**
```json
{
"message": "API key for gemini saved successfully",
"provider": "gemini",
"status": "saved"
}
```
### GET `/api/onboarding/api-keys/validate`
Validate all configured API keys.
**Response:**
```json
{
"validation_results": {
"gemini": {
"valid": true,
"status": "active",
"quota_remaining": 1000
},
"exa": {
"valid": true,
"status": "active",
"quota_remaining": 500
}
},
"all_valid": true,
"total_providers": 2
}
```
## ⚙️ Configuration
### GET `/api/onboarding/config`
Get onboarding configuration and requirements.
**Response:**
```json
{
"total_steps": 6,
"required_steps": [1, 2, 3, 4, 6],
"optional_steps": [5],
"step_requirements": {
"1": ["gemini", "exa", "copilotkit"],
"2": ["website_url"],
"3": ["research_preferences"],
"4": ["personalization_settings"],
"5": ["integrations"],
"6": ["persona_generation"]
}
}
```
### GET `/api/onboarding/providers`
Get setup information for all providers.
**Response:**
```json
{
"providers": {
"gemini": {
"name": "Gemini AI",
"description": "Advanced content generation",
"setup_url": "https://ai.google.dev/",
"required": true,
"validation_endpoint": "https://generativelanguage.googleapis.com/v1beta/models"
},
"exa": {
"name": "Exa AI",
"description": "Intelligent web research",
"setup_url": "https://exa.ai/",
"required": true,
"validation_endpoint": "https://api.exa.ai/v1/search"
}
}
}
```
### GET `/api/onboarding/providers/{provider}`
Get setup information for a specific provider.
**Parameters:**
- `provider` (string): Provider name (gemini, exa, copilotkit)
**Response:**
```json
{
"name": "Gemini AI",
"description": "Advanced content generation",
"setup_url": "https://ai.google.dev/",
"required": true,
"validation_endpoint": "https://generativelanguage.googleapis.com/v1beta/models",
"setup_instructions": [
"Visit Google AI Studio",
"Create a new API key",
"Copy the API key",
"Paste it in the form above"
]
}
```
### POST `/api/onboarding/providers/{provider}/validate`
Validate a specific provider's API key.
**Parameters:**
- `provider` (string): Provider name (gemini, exa, copilotkit)
**Request Body:**
```json
{
"api_key": "your_api_key_here"
}
```
**Response:**
```json
{
"valid": true,
"status": "active",
"quota_remaining": 1000,
"provider": "gemini"
}
```
## 📊 Summary & Analytics
### GET `/api/onboarding/summary`
Get comprehensive onboarding summary for the final step.
**Response:**
```json
{
"user_info": {
"user_id": "user_123",
"onboarding_started": "2024-01-15T10:30:00Z",
"current_step": 6
},
"api_keys": {
"gemini": "configured",
"exa": "configured",
"copilotkit": "configured"
},
"website_analysis": {
"url": "https://example.com",
"status": "completed",
"style_analysis": "professional",
"content_count": 25
},
"research_preferences": {
"depth": "comprehensive",
"auto_research": true,
"fact_checking": true
},
"personalization": {
"brand_voice": "professional",
"target_audience": "B2B professionals",
"content_types": ["blog_posts", "social_media"]
}
}
```
### GET `/api/onboarding/website-analysis`
Get website analysis data.
**Response:**
```json
{
"url": "https://example.com",
"analysis_status": "completed",
"content_analyzed": 25,
"style_characteristics": {
"tone": "professional",
"voice": "authoritative",
"complexity": "intermediate"
},
"target_audience": "B2B professionals",
"content_themes": ["technology", "business", "innovation"]
}
```
### GET `/api/onboarding/research-preferences`
Get research preferences data.
**Response:**
```json
{
"research_depth": "comprehensive",
"auto_research_enabled": true,
"fact_checking_enabled": true,
"content_types": ["blog_posts", "articles", "social_media"],
"research_sources": ["web", "academic", "news"]
}
```
## 👤 Business Information
### POST `/api/onboarding/business-info`
Save business information for users without websites.
**Request Body:**
```json
{
"business_name": "Acme Corp",
"industry": "Technology",
"description": "AI-powered solutions",
"target_audience": "B2B professionals",
"brand_voice": "professional",
"content_goals": ["lead_generation", "brand_awareness"]
}
```
**Response:**
```json
{
"id": 1,
"business_name": "Acme Corp",
"industry": "Technology",
"description": "AI-powered solutions",
"target_audience": "B2B professionals",
"brand_voice": "professional",
"content_goals": ["lead_generation", "brand_awareness"],
"created_at": "2024-01-15T10:30:00Z"
}
```
### GET `/api/onboarding/business-info/{id}`
Get business information by ID.
**Parameters:**
- `id` (int): Business information ID
**Response:**
```json
{
"id": 1,
"business_name": "Acme Corp",
"industry": "Technology",
"description": "AI-powered solutions",
"target_audience": "B2B professionals",
"brand_voice": "professional",
"content_goals": ["lead_generation", "brand_awareness"],
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
```
### GET `/api/onboarding/business-info/user/{user_id}`
Get business information by user ID.
**Parameters:**
- `user_id` (int): User ID
**Response:**
```json
{
"id": 1,
"business_name": "Acme Corp",
"industry": "Technology",
"description": "AI-powered solutions",
"target_audience": "B2B professionals",
"brand_voice": "professional",
"content_goals": ["lead_generation", "brand_awareness"],
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
```
### PUT `/api/onboarding/business-info/{id}`
Update business information.
**Parameters:**
- `id` (int): Business information ID
**Request Body:**
```json
{
"business_name": "Acme Corp Updated",
"industry": "Technology",
"description": "Updated AI-powered solutions",
"target_audience": "B2B professionals",
"brand_voice": "professional",
"content_goals": ["lead_generation", "brand_awareness", "thought_leadership"]
}
```
**Response:**
```json
{
"id": 1,
"business_name": "Acme Corp Updated",
"industry": "Technology",
"description": "Updated AI-powered solutions",
"target_audience": "B2B professionals",
"brand_voice": "professional",
"content_goals": ["lead_generation", "brand_awareness", "thought_leadership"],
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T11:00:00Z"
}
```
## 🎭 Persona Management
### GET `/api/onboarding/persona/readiness/{user_id}`
Check if user has sufficient data for persona generation.
**Parameters:**
- `user_id` (int): User ID
**Response:**
```json
{
"ready": true,
"missing_data": [],
"completion_percentage": 100,
"recommendations": []
}
```
### GET `/api/onboarding/persona/preview/{user_id}`
Generate a preview of the writing persona without saving.
**Parameters:**
- `user_id` (int): User ID
**Response:**
```json
{
"persona_preview": {
"name": "Professional Content Creator",
"voice": "authoritative",
"tone": "professional",
"style_characteristics": {
"formality": "high",
"complexity": "intermediate",
"engagement": "informative"
},
"content_preferences": {
"length": "medium",
"format": "structured",
"research_depth": "comprehensive"
}
},
"generation_time": "2.5s",
"confidence_score": 0.95
}
```
### POST `/api/onboarding/persona/generate/{user_id}`
Generate and save a writing persona from onboarding data.
**Parameters:**
- `user_id` (int): User ID
**Response:**
```json
{
"persona_id": 1,
"name": "Professional Content Creator",
"voice": "authoritative",
"tone": "professional",
"style_characteristics": {...},
"content_preferences": {...},
"created_at": "2024-01-15T10:30:00Z",
"status": "active"
}
```
### GET `/api/onboarding/persona/user/{user_id}`
Get all writing personas for the user.
**Parameters:**
- `user_id` (int): User ID
**Response:**
```json
{
"personas": [
{
"id": 1,
"name": "Professional Content Creator",
"voice": "authoritative",
"tone": "professional",
"status": "active",
"created_at": "2024-01-15T10:30:00Z"
}
],
"total_count": 1,
"active_persona": 1
}
```
## 🚨 Error Responses
### 400 Bad Request
```json
{
"detail": "Invalid request data",
"error_code": "INVALID_REQUEST",
"validation_errors": [
"Field 'api_key' is required",
"Field 'provider' must be one of: gemini, exa, copilotkit"
]
}
```
### 401 Unauthorized
```json
{
"detail": "Authentication required",
"error_code": "UNAUTHORIZED"
}
```
### 404 Not Found
```json
{
"detail": "Step 7 not found",
"error_code": "STEP_NOT_FOUND"
}
```
### 500 Internal Server Error
```json
{
"detail": "Internal server error",
"error_code": "INTERNAL_ERROR"
}
```
## 📝 Request/Response Models
### StepCompletionRequest
```json
{
"data": {
"api_keys": {
"gemini": "string",
"exa": "string",
"copilotkit": "string"
}
},
"validation_errors": ["string"]
}
```
### APIKeyRequest
```json
{
"provider": "string",
"api_key": "string",
"description": "string"
}
```
### BusinessInfoRequest
```json
{
"business_name": "string",
"industry": "string",
"description": "string",
"target_audience": "string",
"brand_voice": "string",
"content_goals": ["string"]
}
```
## 🔄 Rate Limiting
- **Standard endpoints**: 100 requests per minute
- **API key validation**: 10 requests per minute
- **Persona generation**: 5 requests per minute
## 📊 Response Times
- **Status checks**: < 100ms
- **Step completion**: < 500ms
- **API key validation**: < 2s
- **Persona generation**: < 10s
- **Website analysis**: < 30s
---
*This API reference provides comprehensive documentation for all onboarding endpoints. For additional support, please refer to the main project documentation or contact the development team.*

View File

@@ -0,0 +1,330 @@
# ALwrity Onboarding System - Developer Guide
## Architecture Overview
The ALwrity Onboarding System is built with a modular, service-based architecture that separates concerns and promotes maintainability. The system is designed to handle user isolation, progressive setup, and comprehensive onboarding workflows.
## 🏗️ System Architecture
### Core Components
```
backend/api/onboarding_utils/
├── __init__.py # Package initialization
├── onboarding_completion_service.py # Final onboarding completion logic
├── onboarding_summary_service.py # Comprehensive summary generation
├── onboarding_config_service.py # Configuration and provider management
├── business_info_service.py # Business information CRUD operations
├── api_key_management_service.py # API key operations and validation
├── step_management_service.py # Step progression and validation
├── onboarding_control_service.py # Onboarding session management
├── persona_management_service.py # Persona generation and management
├── README.md # End-user documentation
└── DEVELOPER_GUIDE.md # This file
```
### Service Responsibilities
#### 1. OnboardingCompletionService
**Purpose**: Handles the complex logic for completing the onboarding process
**Key Methods**:
- `complete_onboarding()` - Main completion logic with validation
- `_validate_required_steps()` - Ensures all required steps are completed
- `_validate_api_keys()` - Validates API key configuration
- `_generate_persona_from_onboarding()` - Generates writing persona
#### 2. OnboardingSummaryService
**Purpose**: Generates comprehensive onboarding summaries for the final step
**Key Methods**:
- `get_onboarding_summary()` - Main summary generation
- `_get_api_keys()` - Retrieves configured API keys
- `_get_website_analysis()` - Gets website analysis data
- `_get_research_preferences()` - Retrieves research preferences
- `_check_persona_readiness()` - Validates persona generation readiness
#### 3. OnboardingConfigService
**Purpose**: Manages onboarding configuration and provider setup information
**Key Methods**:
- `get_onboarding_config()` - Returns complete onboarding configuration
- `get_provider_setup_info()` - Provider-specific setup information
- `get_all_providers_info()` - All available providers
- `validate_provider_key()` - API key validation
- `get_enhanced_validation_status()` - Comprehensive validation status
#### 4. BusinessInfoService
**Purpose**: Handles business information management for users without websites
**Key Methods**:
- `save_business_info()` - Create new business information
- `get_business_info()` - Retrieve by ID
- `get_business_info_by_user()` - Retrieve by user ID
- `update_business_info()` - Update existing information
#### 5. APIKeyManagementService
**Purpose**: Manages API key operations with caching and security
**Key Methods**:
- `get_api_keys()` - Retrieves masked API keys with caching
- `save_api_key()` - Saves new API keys securely
- `validate_api_keys()` - Validates all configured keys
#### 6. StepManagementService
**Purpose**: Controls step progression and validation
**Key Methods**:
- `get_onboarding_status()` - Current onboarding status
- `get_onboarding_progress_full()` - Complete progress data
- `get_step_data()` - Specific step information
- `complete_step()` - Mark step as completed with environment setup
- `skip_step()` - Skip optional steps
- `validate_step_access()` - Validate step accessibility
#### 7. OnboardingControlService
**Purpose**: Manages onboarding session control
**Key Methods**:
- `start_onboarding()` - Initialize new onboarding session
- `reset_onboarding()` - Reset onboarding progress
- `get_resume_info()` - Resume information for incomplete sessions
#### 8. PersonaManagementService
**Purpose**: Handles persona generation and management
**Key Methods**:
- `check_persona_generation_readiness()` - Validate persona readiness
- `generate_persona_preview()` - Generate preview without saving
- `generate_writing_persona()` - Generate and save persona
- `get_user_writing_personas()` - Retrieve user personas
## 🔧 Integration Points
### Progressive Setup Integration
The onboarding system integrates with the progressive setup service:
```python
# In step_management_service.py
from services.progressive_setup_service import ProgressiveSetupService
# Initialize/upgrade user environment based on new step
if step_number == 1:
setup_service.initialize_user_environment(user_id)
else:
setup_service.upgrade_user_environment(user_id, step_number)
```
### User Isolation
Each user gets their own:
- **Workspace**: `lib/workspace/users/user_<id>/`
- **Database Tables**: `user_<id>_*` tables
- **Configuration**: User-specific settings
- **Progress**: Individual onboarding progress
### Authentication Integration
All services require authentication:
```python
from middleware.auth_middleware import get_current_user
async def endpoint_function(current_user: Dict[str, Any] = Depends(get_current_user)):
user_id = str(current_user.get('id'))
# Service logic here
```
## 📊 Data Flow
### 1. Onboarding Initialization
```
User Login → Authentication → Check Onboarding Status → Redirect to Appropriate Step
```
### 2. Step Completion
```
User Completes Step → Validate Step → Save Progress → Setup User Environment → Return Success
```
### 3. Environment Setup
```
Step Completed → Progressive Setup Service → User Workspace Creation → Feature Activation
```
### 4. Final Completion
```
All Steps Complete → Validation → Persona Generation → Environment Finalization → Onboarding Complete
```
## 🛠️ Development Guidelines
### Adding New Services
1. **Create Service Class**:
```python
class NewService:
def __init__(self):
# Initialize dependencies
async def main_method(self, params):
# Main functionality
pass
```
2. **Update __init__.py**:
```python
from .new_service import NewService
__all__ = [
# ... existing services
'NewService'
]
```
3. **Update Main Onboarding File**:
```python
async def new_endpoint():
try:
from onboarding_utils.new_service import NewService
service = NewService()
return await service.main_method()
except Exception as e:
logger.error(f"Error: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
```
### Error Handling Pattern
All services follow a consistent error handling pattern:
```python
try:
# Service logic
return result
except HTTPException:
raise # Re-raise HTTP exceptions
except Exception as e:
logger.error(f"Error in service: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
```
### Logging Guidelines
Use structured logging with context:
```python
logger.info(f"[service_name] Action for user {user_id}")
logger.success(f"✅ Operation completed for user {user_id}")
logger.warning(f"⚠️ Non-critical issue: {issue}")
logger.error(f"❌ Error in operation: {str(e)}")
```
## 🧪 Testing
### Unit Testing
Each service should have comprehensive unit tests:
```python
import pytest
from onboarding_utils.step_management_service import StepManagementService
class TestStepManagementService:
def setup_method(self):
self.service = StepManagementService()
async def test_get_onboarding_status(self):
# Test implementation
pass
```
### Integration Testing
Test service interactions:
```python
async def test_complete_onboarding_flow():
# Test complete onboarding workflow
pass
```
## 🔒 Security Considerations
### API Key Security
- Keys are masked in responses
- Encryption before storage
- Secure transmission only
### User Data Isolation
- User-specific workspaces
- Isolated database tables
- No cross-user data access
### Input Validation
- Validate all user inputs
- Sanitize data before processing
- Use Pydantic models for validation
## 📈 Performance Optimization
### Caching Strategy
- API key responses cached for 30 seconds
- User progress cached in memory
- Database queries optimized
### Database Optimization
- User-specific table indexing
- Efficient query patterns
- Connection pooling
### Resource Management
- Proper database session handling
- Memory-efficient data processing
- Background task optimization
## 🚀 Deployment Considerations
### Environment Variables
```bash
# Required for onboarding
CLERK_PUBLISHABLE_KEY=your_key
CLERK_SECRET_KEY=your_secret
GEMINI_API_KEY=your_gemini_key
EXA_API_KEY=your_exa_key
COPILOTKIT_API_KEY=your_copilotkit_key
```
### Database Setup
- User-specific tables created on demand
- Progressive table creation based on onboarding progress
- Automatic cleanup on user deletion
### Monitoring
- Track onboarding completion rates
- Monitor step abandonment points
- Performance metrics for each service
## 🔄 Maintenance
### Regular Tasks
- Review and update API key validation
- Monitor service performance
- Update documentation
- Clean up abandoned onboarding sessions
### Version Updates
- Maintain backward compatibility
- Gradual feature rollouts
- User migration strategies
## 📚 Additional Resources
### Related Documentation
- [User Environment Setup](../services/user_workspace_manager.py)
- [Progressive Setup Service](../services/progressive_setup_service.py)
- [Authentication Middleware](../middleware/auth_middleware.py)
### External Dependencies
- FastAPI for API framework
- SQLAlchemy for database operations
- Pydantic for data validation
- Loguru for logging
---
*This developer guide provides comprehensive information for maintaining and extending the ALwrity Onboarding System. For questions or contributions, please refer to the main project documentation.*

View File

@@ -0,0 +1,269 @@
# ALwrity Onboarding System
## Overview
The ALwrity Onboarding System is a comprehensive, user-friendly process designed to get new users up and running with AI-powered content creation capabilities. This system guides users through a structured 6-step process to configure their AI services, analyze their content style, and set up personalized content creation workflows.
## 🎯 What is Onboarding?
Onboarding is your first-time setup experience with ALwrity. It's designed to:
- **Configure your AI services** (Gemini, Exa, CopilotKit)
- **Analyze your existing content** to understand your writing style
- **Set up research preferences** for intelligent content creation
- **Personalize your experience** based on your brand and audience
- **Connect integrations** for seamless content publishing
- **Generate your writing persona** for consistent, on-brand content
## 📋 The 6-Step Onboarding Process
### Step 1: AI LLM Providers Setup
**Purpose**: Connect your AI services to enable intelligent content creation
**What you'll do**:
- Configure **Gemini API** for advanced content generation
- Set up **Exa AI** for intelligent web research
- Connect **CopilotKit** for AI-powered assistance
**Why it's important**: These services work together to provide comprehensive AI functionality for content creation, research, and assistance.
**Requirements**: All three services are mandatory to proceed.
### Step 2: Website Analysis
**Purpose**: Analyze your existing content to understand your writing style and brand voice
**What you'll do**:
- Provide your website URL
- Let ALwrity analyze your existing content
- Review style analysis results
**What ALwrity does**:
- Crawls your website content
- Analyzes writing patterns, tone, and voice
- Identifies your target audience
- Generates style guidelines for consistent content
**Benefits**: Ensures all AI-generated content matches your existing brand voice and style.
### Step 3: AI Research Configuration
**Purpose**: Set up intelligent research capabilities for fact-based content creation
**What you'll do**:
- Choose research depth (Basic, Standard, Comprehensive, Expert)
- Select content types you create
- Configure auto-research preferences
- Enable factual content verification
**Benefits**: Ensures your content is well-researched, accurate, and up-to-date.
### Step 4: Personalization Setup
**Purpose**: Customize ALwrity to match your specific needs and preferences
**What you'll do**:
- Set posting preferences (frequency, timing)
- Configure content types and formats
- Define your target audience
- Set brand voice parameters
**Benefits**: Creates a personalized experience that matches your content strategy.
### Step 5: Integrations (Optional)
**Purpose**: Connect external platforms for seamless content publishing
**Available integrations**:
- **Wix** - Direct publishing to your Wix website
- **LinkedIn** - Automated LinkedIn content posting
- **WordPress** - WordPress site integration
- **Other platforms** - Additional integrations as available
**Benefits**: Streamlines your content workflow from creation to publication.
### Step 6: Complete Setup
**Purpose**: Finalize your onboarding and generate your writing persona
**What happens**:
- Validates all required configurations
- Generates your personalized writing persona
- Sets up your user workspace
- Activates all configured features
**Result**: You're ready to start creating AI-powered content that matches your brand!
## 🔧 Technical Architecture
### Service-Based Design
The onboarding system is built with a modular, service-based architecture:
```
onboarding_utils/
├── onboarding_completion_service.py # Handles final onboarding completion
├── onboarding_summary_service.py # Generates comprehensive summaries
├── onboarding_config_service.py # Manages configuration and providers
├── business_info_service.py # Handles business information
├── api_key_management_service.py # Manages API key operations
├── step_management_service.py # Controls step progression
├── onboarding_control_service.py # Manages onboarding sessions
└── persona_management_service.py # Handles persona generation
```
### Key Features
- **User Isolation**: Each user gets their own workspace and configuration
- **Progressive Setup**: Features are enabled incrementally based on progress
- **Persistent Storage**: All settings are saved and persist across sessions
- **Validation**: Comprehensive validation at each step
- **Error Handling**: Graceful error handling with helpful messages
- **Security**: API keys are encrypted and stored securely
## 🚀 Getting Started
### For New Users
1. **Sign up** with your preferred authentication method
2. **Start onboarding** - You'll be automatically redirected
3. **Follow the 6-step process** - Each step builds on the previous
4. **Complete setup** - Generate your writing persona
5. **Start creating** - Begin using ALwrity's AI-powered features
### For Returning Users
- **Resume onboarding** - Continue where you left off
- **Skip optional steps** - Focus on what you need
- **Update configurations** - Modify settings anytime
- **Add integrations** - Connect new platforms as needed
## 📊 Progress Tracking
The system tracks your progress through:
- **Step completion status** - See which steps are done
- **Progress percentage** - Visual progress indicator
- **Validation status** - Know what needs attention
- **Resume information** - Pick up where you left off
## 🔒 Security & Privacy
- **API Key Encryption**: All API keys are encrypted before storage
- **User Isolation**: Your data is completely separate from other users
- **Secure Storage**: Data is stored securely on your device
- **No Data Sharing**: Your content and preferences are never shared
## 🛠️ Troubleshooting
### Common Issues
**"Cannot proceed to next step"**
- Complete all required fields in the current step
- Ensure API keys are valid and working
- Check for any validation errors
**"API key validation failed"**
- Verify your API key is correct
- Check if the service is available
- Ensure you have sufficient credits/quota
**"Website analysis failed"**
- Ensure your website is publicly accessible
- Check if the URL is correct
- Try again after a few minutes
### Getting Help
- **In-app help** - Use the "Get Help" button in each step
- **Documentation** - Check the detailed setup guides
- **Support** - Contact support for technical issues
## 🎨 Customization Options
### Writing Style
- **Tone**: Professional, Casual, Friendly, Authoritative
- **Voice**: First-person, Third-person, Brand voice
- **Complexity**: Simple, Intermediate, Advanced, Expert
### Content Preferences
- **Length**: Short, Medium, Long, Variable
- **Format**: Blog posts, Social media, Emails, Articles
- **Frequency**: Daily, Weekly, Monthly, Custom
### Research Settings
- **Depth**: Basic, Standard, Comprehensive, Expert
- **Sources**: Web, Academic, News, Social media
- **Verification**: Auto-fact-check, Manual review, AI-assisted
## 📈 Benefits of Completing Onboarding
### Immediate Benefits
- **AI-Powered Content Creation** - Generate high-quality content instantly
- **Style Consistency** - All content matches your brand voice
- **Research Integration** - Fact-based, well-researched content
- **Time Savings** - Reduce content creation time by 80%
### Long-term Benefits
- **Brand Consistency** - Maintain consistent voice across all content
- **Scalability** - Create more content without sacrificing quality
- **Efficiency** - Streamlined workflow from idea to publication
- **Growth** - Focus on strategy while AI handles execution
## 🔄 Updating Your Configuration
You can update your onboarding settings anytime:
- **API Keys** - Update or add new service keys
- **Website Analysis** - Re-analyze your content for style updates
- **Research Preferences** - Adjust research depth and sources
- **Personalization** - Update your brand voice and preferences
- **Integrations** - Add or remove platform connections
## 📞 Support & Resources
### Documentation
- **Setup Guides** - Step-by-step configuration instructions
- **API Documentation** - Technical reference for developers
- **Best Practices** - Tips for optimal onboarding experience
### Community
- **User Forum** - Connect with other ALwrity users
- **Feature Requests** - Suggest improvements
- **Success Stories** - Learn from other users' experiences
### Support Channels
- **In-app Support** - Get help directly within ALwrity
- **Email Support** - support@alwrity.com
- **Live Chat** - Available during business hours
- **Video Tutorials** - Visual guides for complex setups
## 🎯 Success Metrics
Track your onboarding success with these metrics:
- **Completion Rate** - Percentage of users who complete onboarding
- **Time to Value** - How quickly users see benefits
- **Feature Adoption** - Which features users engage with
- **Satisfaction Score** - User feedback on the experience
## 🔮 Future Enhancements
We're constantly improving the onboarding experience:
- **Smart Recommendations** - AI-suggested configurations
- **Template Library** - Pre-built setups for different industries
- **Advanced Analytics** - Detailed insights into your content performance
- **Mobile Experience** - Optimized mobile onboarding flow
- **Voice Setup** - Voice-based configuration for accessibility
---
## Quick Start Checklist
- [ ] **Step 1**: Configure Gemini, Exa, and CopilotKit API keys
- [ ] **Step 2**: Provide website URL for style analysis
- [ ] **Step 3**: Set research preferences and content types
- [ ] **Step 4**: Configure personalization settings
- [ ] **Step 5**: Connect desired integrations (optional)
- [ ] **Step 6**: Complete setup and generate writing persona
**🎉 You're ready to create amazing AI-powered content!**
---
*This onboarding system is designed to get you up and running quickly while ensuring your content maintains your unique brand voice and style. Take your time with each step - the more accurate your configuration, the better your AI-generated content will be.*

View File

@@ -0,0 +1,23 @@
"""
Onboarding utilities package.
"""
from .onboarding_completion_service import OnboardingCompletionService
from .onboarding_summary_service import OnboardingSummaryService
from .onboarding_config_service import OnboardingConfigService
from .business_info_service import BusinessInfoService
from .api_key_management_service import APIKeyManagementService
from .step_management_service import StepManagementService
from .onboarding_control_service import OnboardingControlService
from .persona_management_service import PersonaManagementService
__all__ = [
'OnboardingCompletionService',
'OnboardingSummaryService',
'OnboardingConfigService',
'BusinessInfoService',
'APIKeyManagementService',
'StepManagementService',
'OnboardingControlService',
'PersonaManagementService'
]

View File

@@ -0,0 +1,109 @@
"""
API Key Management Service
Handles API key operations for onboarding.
"""
import time
from typing import Dict, Any
from fastapi import HTTPException
from loguru import logger
from services.api_key_manager import APIKeyManager
from services.validation import check_all_api_keys
class APIKeyManagementService:
"""Service for handling API key management operations."""
def __init__(self):
self.api_key_manager = APIKeyManager()
# Simple cache for API keys
self._api_keys_cache = None
self._cache_timestamp = 0
self.CACHE_DURATION = 30 # Cache for 30 seconds
async def get_api_keys(self) -> Dict[str, Any]:
"""Get all configured API keys (masked)."""
current_time = time.time()
# Return cached result if still valid
if self._api_keys_cache and (current_time - self._cache_timestamp) < self.CACHE_DURATION:
logger.debug("Returning cached API keys")
return self._api_keys_cache
try:
self.api_key_manager.load_api_keys() # Load keys from environment
api_keys = self.api_key_manager.api_keys # Get the loaded keys
# Mask the API keys for security
masked_keys = {}
for provider, key in api_keys.items():
if key:
masked_keys[provider] = "*" * (len(key) - 4) + key[-4:] if len(key) > 4 else "*" * len(key)
else:
masked_keys[provider] = None
result = {
"api_keys": masked_keys,
"total_providers": len(api_keys),
"configured_providers": [k for k, v in api_keys.items() if v]
}
# Cache the result
self._api_keys_cache = result
self._cache_timestamp = current_time
return result
except Exception as e:
logger.error(f"Error getting API keys: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def get_api_keys_for_onboarding(self) -> Dict[str, Any]:
"""Get all configured API keys for onboarding (unmasked)."""
try:
self.api_key_manager.load_api_keys() # Load keys from environment
api_keys = self.api_key_manager.api_keys # Get the loaded keys
# Return actual API keys for onboarding pre-filling
result = {
"api_keys": api_keys,
"total_providers": len(api_keys),
"configured_providers": [k for k, v in api_keys.items() if v]
}
return result
except Exception as e:
logger.error(f"Error getting API keys for onboarding: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def save_api_key(self, provider: str, api_key: str, description: str = None) -> Dict[str, Any]:
"""Save an API key for a provider."""
try:
success = self.api_key_manager.save_api_key(provider, api_key)
if success:
return {
"message": f"API key for {provider} saved successfully",
"provider": provider,
"status": "saved"
}
else:
raise HTTPException(status_code=400, detail=f"Failed to save API key for {provider}")
except HTTPException:
raise
except Exception as e:
logger.error(f"Error saving API key: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def validate_api_keys(self) -> Dict[str, Any]:
"""Validate all configured API keys."""
try:
validation_results = check_all_api_keys(self.api_key_manager)
return {
"validation_results": validation_results.get('results', {}),
"all_valid": validation_results.get('all_valid', False),
"total_providers": len(validation_results.get('results', {}))
}
except Exception as e:
logger.error(f"Error validating API keys: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")

View File

@@ -0,0 +1,86 @@
"""
Business Information Service
Handles business information management for users without websites.
"""
from typing import Dict, Any, Optional
from fastapi import HTTPException
from loguru import logger
class BusinessInfoService:
"""Service for handling business information operations."""
def __init__(self):
pass
async def save_business_info(self, business_info: 'BusinessInfoRequest') -> Dict[str, Any]:
"""Save business information for users without websites."""
try:
from models.business_info_request import BusinessInfoRequest
from services.business_info_service import business_info_service
logger.info(f"🔄 Saving business info for user_id: {business_info.user_id}")
result = business_info_service.save_business_info(business_info)
logger.success(f"✅ Business info saved successfully for user_id: {business_info.user_id}")
return result
except Exception as e:
logger.error(f"❌ Error saving business info: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to save business info: {str(e)}")
async def get_business_info(self, business_info_id: int) -> Dict[str, Any]:
"""Get business information by ID."""
try:
from services.business_info_service import business_info_service
logger.info(f"🔄 Getting business info for ID: {business_info_id}")
result = business_info_service.get_business_info(business_info_id)
if result:
logger.success(f"✅ Business info retrieved for ID: {business_info_id}")
return result
else:
logger.warning(f"⚠️ No business info found for ID: {business_info_id}")
raise HTTPException(status_code=404, detail="Business info not found")
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Error getting business info: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to get business info: {str(e)}")
async def get_business_info_by_user(self, user_id: int) -> Dict[str, Any]:
"""Get business information by user ID."""
try:
from services.business_info_service import business_info_service
logger.info(f"🔄 Getting business info for user ID: {user_id}")
result = business_info_service.get_business_info_by_user(user_id)
if result:
logger.success(f"✅ Business info retrieved for user ID: {user_id}")
return result
else:
logger.warning(f"⚠️ No business info found for user ID: {user_id}")
raise HTTPException(status_code=404, detail="Business info not found")
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Error getting business info: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to get business info: {str(e)}")
async def update_business_info(self, business_info_id: int, business_info: 'BusinessInfoRequest') -> Dict[str, Any]:
"""Update business information."""
try:
from models.business_info_request import BusinessInfoRequest
from services.business_info_service import business_info_service
logger.info(f"🔄 Updating business info for ID: {business_info_id}")
result = business_info_service.update_business_info(business_info_id, business_info)
if result:
logger.success(f"✅ Business info updated for ID: {business_info_id}")
return result
else:
logger.warning(f"⚠️ No business info found to update for ID: {business_info_id}")
raise HTTPException(status_code=404, detail="Business info not found")
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Error updating business info: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to update business info: {str(e)}")

View File

@@ -0,0 +1,94 @@
"""
Onboarding Completion Service
Handles the complex logic for completing the onboarding process.
"""
from typing import Dict, Any, List
from fastapi import HTTPException
from loguru import logger
from services.api_key_manager import get_onboarding_progress_for_user, get_api_key_manager, StepStatus
from services.persona_analysis_service import PersonaAnalysisService
class OnboardingCompletionService:
"""Service for handling onboarding completion logic."""
def __init__(self):
self.required_steps = [1, 2, 3, 6] # Steps 1, 2, 3, and 6 are required
async def complete_onboarding(self, current_user: Dict[str, Any]) -> Dict[str, Any]:
"""Complete the onboarding process with full validation."""
try:
user_id = str(current_user.get('id'))
progress = get_onboarding_progress_for_user(user_id)
# Validate required steps are completed
missing_steps = self._validate_required_steps(progress)
if missing_steps:
missing_steps_str = ", ".join(missing_steps)
raise HTTPException(
status_code=400,
detail=f"Cannot complete onboarding. The following steps must be completed first: {missing_steps_str}"
)
# Validate API keys are configured
self._validate_api_keys()
# Generate writing persona from onboarding data
persona_generated = await self._generate_persona_from_onboarding(user_id)
# Complete the onboarding process
progress.complete_onboarding()
return {
"message": "Onboarding completed successfully",
"completed_at": progress.completed_at,
"completion_percentage": 100.0,
"persona_generated": persona_generated
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error completing onboarding: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
def _validate_required_steps(self, progress) -> List[str]:
"""Validate that all required steps are completed."""
missing_steps = []
for step_num in self.required_steps:
step = progress.get_step_data(step_num)
if step and step.status not in [StepStatus.COMPLETED, StepStatus.SKIPPED]:
missing_steps.append(step.title)
return missing_steps
def _validate_api_keys(self):
"""Validate that API keys are configured."""
api_manager = get_api_key_manager()
api_keys = api_manager.get_all_keys()
if not api_keys:
raise HTTPException(
status_code=400,
detail="Cannot complete onboarding. At least one AI provider API key must be configured."
)
async def _generate_persona_from_onboarding(self, user_id: str) -> bool:
"""Generate writing persona from onboarding data."""
try:
persona_service = PersonaAnalysisService()
# Use user_id = 1 for now (assuming single user system)
persona_user_id = 1
persona_result = persona_service.generate_persona_from_onboarding(persona_user_id)
if "error" not in persona_result:
logger.info(f"✅ Writing persona generated during onboarding completion: {persona_result.get('persona_id')}")
return True
else:
logger.warning(f"⚠️ Persona generation failed during onboarding: {persona_result['error']}")
return False
except Exception as e:
logger.warning(f"⚠️ Non-critical error generating persona during onboarding: {str(e)}")
return False

View File

@@ -0,0 +1,127 @@
"""
Onboarding Configuration Service
Handles onboarding configuration and provider setup information.
"""
from typing import Dict, Any
from fastapi import HTTPException
from loguru import logger
from services.api_key_manager import get_api_key_manager
from services.validation import check_all_api_keys
class OnboardingConfigService:
"""Service for handling onboarding configuration and provider setup."""
def __init__(self):
self.api_key_manager = get_api_key_manager()
def get_onboarding_config(self) -> Dict[str, Any]:
"""Get onboarding configuration and requirements."""
return {
"total_steps": 6,
"steps": [
{
"number": 1,
"title": "AI LLM Providers",
"description": "Configure AI language model providers",
"required": True,
"providers": ["openai", "gemini", "anthropic"]
},
{
"number": 2,
"title": "Website Analysis",
"description": "Set up website analysis and crawling",
"required": True
},
{
"number": 3,
"title": "AI Research",
"description": "Configure AI research capabilities",
"required": True
},
{
"number": 4,
"title": "Personalization",
"description": "Set up personalization features",
"required": False
},
{
"number": 5,
"title": "Integrations",
"description": "Configure ALwrity integrations",
"required": False
},
{
"number": 6,
"title": "Complete Setup",
"description": "Finalize and complete onboarding",
"required": True
}
],
"requirements": {
"min_api_keys": 1,
"required_providers": ["openai"],
"optional_providers": ["gemini", "anthropic"]
}
}
async def get_provider_setup_info(self, provider: str) -> Dict[str, Any]:
"""Get setup information for a specific provider."""
try:
providers_info = self.get_all_providers_info()
if provider in providers_info:
return providers_info[provider]
else:
raise HTTPException(status_code=404, detail=f"Provider {provider} not found")
except Exception as e:
logger.error(f"Error getting provider setup info: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
def get_all_providers_info(self) -> Dict[str, Any]:
"""Get setup information for all providers."""
return {
"openai": {
"name": "OpenAI",
"description": "GPT-4 and GPT-3.5 models for content generation",
"setup_url": "https://platform.openai.com/api-keys",
"required_fields": ["api_key"],
"optional_fields": ["organization_id"]
},
"gemini": {
"name": "Google Gemini",
"description": "Google's advanced AI models for content creation",
"setup_url": "https://makersuite.google.com/app/apikey",
"required_fields": ["api_key"],
"optional_fields": []
},
"anthropic": {
"name": "Anthropic",
"description": "Claude models for sophisticated content generation",
"setup_url": "https://console.anthropic.com/",
"required_fields": ["api_key"],
"optional_fields": []
}
}
async def validate_provider_key(self, provider: str, api_key: str) -> Dict[str, Any]:
"""Validate a specific provider's API key."""
try:
# This would need to be implemented based on the actual validation logic
# For now, return a basic validation result
return {
"provider": provider,
"valid": True,
"message": f"API key for {provider} is valid"
}
except Exception as e:
logger.error(f"Error validating provider key: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def get_enhanced_validation_status(self) -> Dict[str, Any]:
"""Get enhanced validation status for all configured services."""
try:
return await check_all_api_keys(self.api_key_manager)
except Exception as e:
logger.error(f"Error getting enhanced validation status: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")

View File

@@ -0,0 +1,73 @@
"""
Onboarding Control Service
Handles onboarding session control and management.
"""
from typing import Dict, Any
from fastapi import HTTPException
from loguru import logger
from services.api_key_manager import get_onboarding_progress, get_onboarding_progress_for_user
class OnboardingControlService:
"""Service for handling onboarding control operations."""
def __init__(self):
pass
async def start_onboarding(self, current_user: Dict[str, Any]) -> Dict[str, Any]:
"""Start a new onboarding session."""
try:
user_id = str(current_user.get('id'))
progress = get_onboarding_progress_for_user(user_id)
progress.reset_progress()
return {
"message": "Onboarding started successfully",
"current_step": progress.current_step,
"started_at": progress.started_at
}
except Exception as e:
logger.error(f"Error starting onboarding: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def reset_onboarding(self) -> Dict[str, Any]:
"""Reset the onboarding progress."""
try:
progress = get_onboarding_progress()
progress.reset_progress()
return {
"message": "Onboarding progress reset successfully",
"current_step": progress.current_step,
"started_at": progress.started_at
}
except Exception as e:
logger.error(f"Error resetting onboarding: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def get_resume_info(self) -> Dict[str, Any]:
"""Get information for resuming onboarding."""
try:
progress = get_onboarding_progress()
if progress.is_completed:
return {
"can_resume": False,
"message": "Onboarding is already completed",
"completion_percentage": 100.0
}
resume_step = progress.get_resume_step()
return {
"can_resume": True,
"resume_step": resume_step,
"current_step": progress.current_step,
"completion_percentage": progress.get_completion_percentage(),
"started_at": progress.started_at,
"last_updated": progress.last_updated
}
except Exception as e:
logger.error(f"Error getting resume info: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")

View File

@@ -0,0 +1,166 @@
"""
Onboarding Summary Service
Handles the complex logic for generating comprehensive onboarding summaries.
"""
from typing import Dict, Any, Optional
from fastapi import HTTPException
from loguru import logger
from services.api_key_manager import get_api_key_manager
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
class OnboardingSummaryService:
"""Service for handling onboarding summary generation with user isolation."""
def __init__(self, user_id: str):
"""
Initialize service with user-specific context.
Args:
user_id: Clerk user ID from authenticated request
"""
# Convert Clerk user ID to integer for database compatibility
try:
self.user_id_int = int(user_id.replace('user_', '').replace('-', '')[:8], 16) % 2147483647
except:
self.user_id_int = hash(user_id) % 2147483647
self.user_id = user_id # Store original Clerk ID for logging
self.session_id = self.user_id_int # Use user ID as session ID for backwards compatibility
async def get_onboarding_summary(self) -> Dict[str, Any]:
"""Get comprehensive onboarding summary for FinalStep."""
try:
# Get API keys
api_keys = self._get_api_keys()
# Get website analysis data
website_analysis = self._get_website_analysis()
# Get research preferences
research_preferences = self._get_research_preferences()
# Get personalization settings
personalization_settings = self._get_personalization_settings(research_preferences)
# Check persona generation readiness
persona_readiness = self._check_persona_readiness(website_analysis)
# Determine capabilities
capabilities = self._determine_capabilities(api_keys, website_analysis, research_preferences, personalization_settings, persona_readiness)
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": capabilities
}
except Exception as e:
logger.error(f"Error getting onboarding summary: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
def _get_api_keys(self) -> Dict[str, Any]:
"""Get configured API keys."""
api_manager = get_api_key_manager()
return api_manager.get_all_keys()
def _get_website_analysis(self) -> Optional[Dict[str, Any]]:
"""Get website analysis data."""
try:
db = next(get_db())
website_service = WebsiteAnalysisService(db)
return website_service.get_analysis_by_session(self.session_id)
except Exception as e:
logger.warning(f"Could not get website analysis: {str(e)}")
return None
def _get_research_preferences(self) -> Optional[Dict[str, Any]]:
"""Get research preferences data."""
try:
db = next(get_db())
research_service = ResearchPreferencesService(db)
return research_service.get_research_preferences(self.session_id)
except Exception as e:
logger.warning(f"Could not get research preferences: {str(e)}")
return None
def _get_personalization_settings(self, research_preferences: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
"""Get personalization settings from research preferences."""
if not research_preferences:
return None
return {
'writing_style': research_preferences.get('writing_style', {}).get('tone', 'Professional'),
'tone': research_preferences.get('writing_style', {}).get('voice', 'Formal'),
'brand_voice': research_preferences.get('writing_style', {}).get('complexity', 'Trustworthy and Expert')
}
def _check_persona_readiness(self, website_analysis: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
"""Check if persona can be generated."""
try:
persona_service = PersonaAnalysisService()
# Check if persona can be generated
onboarding_data = persona_service._collect_onboarding_data(self.user_id)
if onboarding_data:
data_sufficiency = persona_service._calculate_data_sufficiency(onboarding_data)
return {
"ready": data_sufficiency >= 50.0,
"data_sufficiency": data_sufficiency,
"can_generate": website_analysis is not None
}
return {"ready": False, "data_sufficiency": 0.0, "can_generate": False}
except Exception as e:
logger.warning(f"Could not check persona readiness: {str(e)}")
return {"ready": False, "error": str(e)}
def _determine_capabilities(self, api_keys: Dict[str, Any], website_analysis: Optional[Dict[str, Any]],
research_preferences: Optional[Dict[str, Any]],
personalization_settings: Optional[Dict[str, Any]],
persona_readiness: Optional[Dict[str, Any]]) -> Dict[str, bool]:
"""Determine user capabilities based on onboarding data."""
return {
"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
}
async def get_website_analysis_data(self) -> Optional[Dict[str, Any]]:
"""Get website analysis data for FinalStep."""
try:
analysis = self._get_website_analysis()
if analysis:
return {
"website_url": analysis.get('website_url'),
"style_analysis": analysis.get('style_analysis'),
"style_patterns": analysis.get('style_patterns'),
"style_guidelines": analysis.get('style_guidelines'),
"status": analysis.get('status'),
"completed_at": analysis.get('created_at')
}
else:
return None
except Exception as e:
logger.error(f"Error getting website analysis data: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def get_research_preferences_data(self) -> Optional[Dict[str, Any]]:
"""Get research preferences data for FinalStep."""
try:
return self._get_research_preferences()
except Exception as e:
logger.error(f"Error getting research preferences data: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")

View File

@@ -0,0 +1,51 @@
"""
Persona Management Service
Handles persona generation and management for onboarding.
"""
from typing import Dict, Any
from fastapi import HTTPException
from loguru import logger
class PersonaManagementService:
"""Service for handling persona management operations."""
def __init__(self):
pass
async def check_persona_generation_readiness(self, user_id: int = 1) -> Dict[str, Any]:
"""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(self, user_id: int = 1) -> Dict[str, Any]:
"""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(self, user_id: int = 1) -> Dict[str, Any]:
"""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(self, user_id: int = 1) -> Dict[str, Any]:
"""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")

View File

@@ -0,0 +1,518 @@
"""
Step 3 Research Service for Onboarding
This service handles the research phase of onboarding (Step 3), including
competitor discovery using Exa API and research data management.
Key Features:
- Competitor discovery using Exa API
- Research progress tracking
- Data storage and retrieval
- Integration with onboarding workflow
Author: ALwrity Team
Version: 1.0
Last Updated: January 2025
"""
from typing import Dict, List, Optional, Any
from datetime import datetime
from loguru import logger
from services.research.exa_service import ExaService
from services.database import get_db_session
from models.onboarding import OnboardingSession
from sqlalchemy.orm import Session
class Step3ResearchService:
"""
Service for managing Step 3 research phase of onboarding.
This service handles competitor discovery, research data storage,
and integration with the onboarding workflow.
"""
def __init__(self):
"""Initialize the Step 3 Research Service."""
self.exa_service = ExaService()
self.service_name = "step3_research"
logger.info(f"Initialized {self.service_name}")
async def discover_competitors_for_onboarding(
self,
user_url: str,
session_id: str,
industry_context: Optional[str] = None,
num_results: int = 25,
website_analysis_data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Discover competitors for onboarding Step 3.
Args:
user_url: The user's website URL
session_id: Onboarding session ID
industry_context: Industry context for better discovery
num_results: Number of competitors to discover
Returns:
Dictionary containing competitor discovery results
"""
try:
logger.info(f"Starting research analysis for session {session_id}, URL: {user_url}")
# Step 1: Discover social media accounts
logger.info("Step 1: Discovering social media accounts...")
social_media_results = await self.exa_service.discover_social_media_accounts(user_url)
if not social_media_results["success"]:
logger.warning(f"Social media discovery failed: {social_media_results.get('error')}")
# Continue with competitor discovery even if social media fails
social_media_results = {"success": False, "social_media_accounts": {}, "citations": []}
# Step 2: Discover competitors using Exa API
logger.info("Step 2: Discovering competitors...")
competitor_results = await self.exa_service.discover_competitors(
user_url=user_url,
num_results=num_results,
exclude_domains=None, # Let ExaService handle domain exclusion
industry_context=industry_context,
website_analysis_data=website_analysis_data
)
if not competitor_results["success"]:
logger.error(f"Competitor discovery failed: {competitor_results.get('error')}")
return competitor_results
# Process and enhance competitor data
enhanced_competitors = await self._enhance_competitor_data(
competitor_results["competitors"],
user_url,
industry_context
)
# Store research data in database
await self._store_research_data(
session_id=session_id,
user_url=user_url,
competitors=enhanced_competitors,
industry_context=industry_context,
analysis_metadata={
**competitor_results,
"social_media_data": social_media_results
}
)
# Generate research summary
research_summary = self._generate_research_summary(
enhanced_competitors,
industry_context
)
logger.info(f"Successfully discovered {len(enhanced_competitors)} competitors for session {session_id}")
return {
"success": True,
"session_id": session_id,
"user_url": user_url,
"competitors": enhanced_competitors,
"social_media_accounts": social_media_results.get("social_media_accounts", {}),
"social_media_citations": social_media_results.get("citations", []),
"research_summary": research_summary,
"total_competitors": len(enhanced_competitors),
"industry_context": industry_context,
"analysis_timestamp": datetime.utcnow().isoformat(),
"api_cost": competitor_results.get("api_cost", 0) + social_media_results.get("api_cost", 0)
}
except Exception as e:
logger.error(f"Error in competitor discovery for onboarding: {str(e)}")
return {
"success": False,
"error": str(e),
"session_id": session_id,
"user_url": user_url
}
async def _enhance_competitor_data(
self,
competitors: List[Dict[str, Any]],
user_url: str,
industry_context: Optional[str]
) -> List[Dict[str, Any]]:
"""
Enhance competitor data with additional analysis.
Args:
competitors: Raw competitor data from Exa API
user_url: User's website URL for comparison
industry_context: Industry context
Returns:
List of enhanced competitor data
"""
enhanced_competitors = []
for competitor in competitors:
try:
# Add competitive analysis
competitive_analysis = self._analyze_competitor_competitiveness(
competitor,
user_url,
industry_context
)
# Add content strategy insights
content_insights = self._analyze_content_strategy(competitor)
# Add market positioning
market_positioning = self._analyze_market_positioning(competitor)
enhanced_competitor = {
**competitor,
"competitive_analysis": competitive_analysis,
"content_insights": content_insights,
"market_positioning": market_positioning,
"enhanced_timestamp": datetime.utcnow().isoformat()
}
enhanced_competitors.append(enhanced_competitor)
except Exception as e:
logger.warning(f"Error enhancing competitor data: {str(e)}")
enhanced_competitors.append(competitor)
return enhanced_competitors
def _analyze_competitor_competitiveness(
self,
competitor: Dict[str, Any],
user_url: str,
industry_context: Optional[str]
) -> Dict[str, Any]:
"""
Analyze competitor competitiveness.
Args:
competitor: Competitor data
user_url: User's website URL
industry_context: Industry context
Returns:
Dictionary of competitive analysis
"""
analysis = {
"threat_level": "medium",
"competitive_strengths": [],
"competitive_weaknesses": [],
"market_share_estimate": "unknown",
"differentiation_opportunities": []
}
# Analyze threat level based on relevance score
relevance_score = competitor.get("relevance_score", 0)
if relevance_score > 0.8:
analysis["threat_level"] = "high"
elif relevance_score < 0.4:
analysis["threat_level"] = "low"
# Analyze competitive strengths from content
summary = competitor.get("summary", "").lower()
highlights = competitor.get("highlights", [])
# Extract strengths from content analysis
if "innovative" in summary or "cutting-edge" in summary:
analysis["competitive_strengths"].append("Innovation leadership")
if "comprehensive" in summary or "complete" in summary:
analysis["competitive_strengths"].append("Comprehensive solution")
if any("enterprise" in highlight.lower() for highlight in highlights):
analysis["competitive_strengths"].append("Enterprise focus")
# Generate differentiation opportunities
if not any("saas" in summary for summary in [summary]):
analysis["differentiation_opportunities"].append("SaaS platform differentiation")
return analysis
def _analyze_content_strategy(self, competitor: Dict[str, Any]) -> Dict[str, Any]:
"""
Analyze competitor's content strategy.
Args:
competitor: Competitor data
Returns:
Dictionary of content strategy analysis
"""
strategy = {
"content_focus": "general",
"target_audience": "unknown",
"content_types": [],
"publishing_frequency": "unknown",
"content_quality": "medium"
}
summary = competitor.get("summary", "").lower()
title = competitor.get("title", "").lower()
# Analyze content focus
if "technical" in summary or "developer" in summary:
strategy["content_focus"] = "technical"
elif "business" in summary or "enterprise" in summary:
strategy["content_focus"] = "business"
elif "marketing" in summary or "seo" in summary:
strategy["content_focus"] = "marketing"
# Analyze target audience
if "startup" in summary or "small business" in summary:
strategy["target_audience"] = "startups_small_business"
elif "enterprise" in summary or "large" in summary:
strategy["target_audience"] = "enterprise"
elif "developer" in summary or "technical" in summary:
strategy["target_audience"] = "developers"
# Analyze content quality
if len(summary) > 300:
strategy["content_quality"] = "high"
elif len(summary) < 100:
strategy["content_quality"] = "low"
return strategy
def _analyze_market_positioning(self, competitor: Dict[str, Any]) -> Dict[str, Any]:
"""
Analyze competitor's market positioning.
Args:
competitor: Competitor data
Returns:
Dictionary of market positioning analysis
"""
positioning = {
"market_tier": "unknown",
"pricing_position": "unknown",
"brand_positioning": "unknown",
"competitive_advantage": "unknown"
}
summary = competitor.get("summary", "").lower()
title = competitor.get("title", "").lower()
# Analyze market tier
if "enterprise" in summary or "enterprise" in title:
positioning["market_tier"] = "enterprise"
elif "startup" in summary or "small" in summary:
positioning["market_tier"] = "startup_small_business"
elif "premium" in summary or "professional" in summary:
positioning["market_tier"] = "premium"
# Analyze brand positioning
if "innovative" in summary or "cutting-edge" in summary:
positioning["brand_positioning"] = "innovator"
elif "reliable" in summary or "trusted" in summary:
positioning["brand_positioning"] = "trusted_leader"
elif "affordable" in summary or "cost-effective" in summary:
positioning["brand_positioning"] = "value_leader"
return positioning
def _generate_research_summary(
self,
competitors: List[Dict[str, Any]],
industry_context: Optional[str]
) -> Dict[str, Any]:
"""
Generate a summary of the research findings.
Args:
competitors: List of enhanced competitor data
industry_context: Industry context
Returns:
Dictionary containing research summary
"""
if not competitors:
return {
"total_competitors": 0,
"market_insights": "No competitors found",
"key_findings": [],
"recommendations": []
}
# Analyze market landscape
threat_levels = [comp.get("competitive_analysis", {}).get("threat_level", "medium") for comp in competitors]
high_threat_count = threat_levels.count("high")
# Extract common themes
content_focuses = [comp.get("content_insights", {}).get("content_focus", "general") for comp in competitors]
content_focus_distribution = {focus: content_focuses.count(focus) for focus in set(content_focuses)}
# Generate key findings
key_findings = []
if high_threat_count > len(competitors) * 0.3:
key_findings.append("Highly competitive market with multiple strong players")
if "technical" in content_focus_distribution:
key_findings.append("Technical content is a key differentiator in this market")
# Generate recommendations
recommendations = []
if high_threat_count > 0:
recommendations.append("Focus on unique value proposition to differentiate from strong competitors")
if "technical" in content_focus_distribution and content_focus_distribution["technical"] > 2:
recommendations.append("Consider developing technical content strategy")
return {
"total_competitors": len(competitors),
"high_threat_competitors": high_threat_count,
"content_focus_distribution": content_focus_distribution,
"market_insights": f"Found {len(competitors)} competitors in {industry_context or 'the market'}",
"key_findings": key_findings,
"recommendations": recommendations,
"competitive_landscape": "moderate" if high_threat_count < len(competitors) * 0.5 else "high"
}
async def _store_research_data(
self,
session_id: str,
user_url: str,
competitors: List[Dict[str, Any]],
industry_context: Optional[str],
analysis_metadata: Dict[str, Any]
) -> bool:
"""
Store research data in the database.
Args:
session_id: Onboarding session ID
user_url: User's website URL
competitors: Competitor data
industry_context: Industry context
analysis_metadata: Analysis metadata
Returns:
Boolean indicating success
"""
try:
with get_db_session() as db:
# Get or create onboarding session
session = db.query(OnboardingSession).filter(
OnboardingSession.id == session_id
).first()
if not session:
logger.error(f"Onboarding session {session_id} not found")
return False
# Update session with research data
research_data = {
"step3_research_data": {
"user_url": user_url,
"competitors": competitors,
"industry_context": industry_context,
"analysis_metadata": analysis_metadata,
"completed_at": datetime.utcnow().isoformat()
}
}
# Merge with existing data
if session.step_data:
session.step_data.update(research_data)
else:
session.step_data = research_data
db.commit()
logger.info(f"Research data stored for session {session_id}")
return True
except Exception as e:
logger.error(f"Error storing research data: {str(e)}")
return False
async def get_research_data(self, session_id: str) -> Dict[str, Any]:
"""
Retrieve research data for a session.
Args:
session_id: Onboarding session ID
Returns:
Dictionary containing research data
"""
try:
with get_db_session() as db:
session = db.query(OnboardingSession).filter(
OnboardingSession.id == session_id
).first()
if not session:
return {
"success": False,
"error": "Session not found"
}
research_data = session.step_data.get("step3_research_data") if session.step_data else None
if not research_data:
return {
"success": False,
"error": "No research data found for this session"
}
return {
"success": True,
"research_data": research_data,
"session_id": session_id
}
except Exception as e:
logger.error(f"Error retrieving research data: {str(e)}")
return {
"success": False,
"error": str(e)
}
def _extract_domain(self, url: str) -> str:
"""
Extract domain from URL.
Args:
url: Website URL
Returns:
Domain name
"""
try:
from urllib.parse import urlparse
parsed = urlparse(url)
return parsed.netloc
except Exception:
return url
async def health_check(self) -> Dict[str, Any]:
"""
Check the health of the Step 3 Research Service.
Returns:
Dictionary containing service health status
"""
try:
exa_health = await self.exa_service.health_check()
return {
"status": "healthy" if exa_health["status"] == "healthy" else "degraded",
"service": self.service_name,
"exa_service_status": exa_health["status"],
"timestamp": datetime.utcnow().isoformat()
}
except Exception as e:
return {
"status": "error",
"service": self.service_name,
"error": str(e),
"timestamp": datetime.utcnow().isoformat()
}

View File

@@ -0,0 +1,309 @@
"""
Step 3 Research Routes for Onboarding
FastAPI routes for Step 3 research phase of onboarding,
including competitor discovery and research data management.
Author: ALwrity Team
Version: 1.0
Last Updated: January 2025
"""
from fastapi import APIRouter, HTTPException, BackgroundTasks, Depends
from pydantic import BaseModel, HttpUrl, Field
from typing import Dict, List, Optional, Any
from datetime import datetime
import traceback
from loguru import logger
from middleware.auth_middleware import get_current_user
from .step3_research_service import Step3ResearchService
router = APIRouter(prefix="/api/onboarding/step3", tags=["Onboarding Step 3 - Research"])
# Request/Response Models
class CompetitorDiscoveryRequest(BaseModel):
"""Request model for competitor discovery."""
session_id: Optional[str] = Field(None, description="Deprecated - user identification comes from auth token")
user_url: str = Field(..., description="User's website URL")
industry_context: Optional[str] = Field(None, description="Industry context for better discovery")
num_results: int = Field(25, ge=1, le=100, description="Number of competitors to discover")
website_analysis_data: Optional[Dict[str, Any]] = Field(None, description="Website analysis data from Step 2 for better targeting")
class CompetitorDiscoveryResponse(BaseModel):
"""Response model for competitor discovery."""
success: bool
message: str
session_id: str
user_url: str
competitors: Optional[List[Dict[str, Any]]] = None
social_media_accounts: Optional[Dict[str, str]] = None
social_media_citations: Optional[List[Dict[str, Any]]] = None
research_summary: Optional[Dict[str, Any]] = None
total_competitors: Optional[int] = None
industry_context: Optional[str] = None
analysis_timestamp: Optional[str] = None
api_cost: Optional[float] = None
error: Optional[str] = None
class ResearchDataRequest(BaseModel):
"""Request model for retrieving research data."""
session_id: str = Field(..., description="Onboarding session ID")
class ResearchDataResponse(BaseModel):
"""Response model for research data retrieval."""
success: bool
message: str
session_id: Optional[str] = None
research_data: Optional[Dict[str, Any]] = None
error: Optional[str] = None
class ResearchHealthResponse(BaseModel):
"""Response model for research service health check."""
success: bool
message: str
service_status: Optional[Dict[str, Any]] = None
timestamp: Optional[str] = None
# Initialize service
step3_research_service = Step3ResearchService()
@router.post("/discover-competitors", response_model=CompetitorDiscoveryResponse)
async def discover_competitors(
request: CompetitorDiscoveryRequest,
background_tasks: BackgroundTasks,
current_user: dict = Depends(get_current_user)
) -> CompetitorDiscoveryResponse:
"""
Discover competitors for the user's website using Exa API with user isolation.
This endpoint performs neural search to find semantically similar websites
and analyzes their content for competitive intelligence.
"""
try:
# Get Clerk user ID for user isolation
clerk_user_id = str(current_user.get('id'))
logger.info(f"Starting competitor discovery for authenticated user {clerk_user_id}, URL: {request.user_url}")
logger.info(f"Request data - user_url: '{request.user_url}', industry_context: '{request.industry_context}', num_results: {request.num_results}")
# Validate URL format
if not request.user_url.startswith(('http://', 'https://')):
request.user_url = f"https://{request.user_url}"
# Perform competitor discovery with Clerk user ID
result = await step3_research_service.discover_competitors_for_onboarding(
user_url=request.user_url,
session_id=clerk_user_id, # Use Clerk user ID for isolation
industry_context=request.industry_context,
num_results=request.num_results,
website_analysis_data=request.website_analysis_data
)
if result["success"]:
logger.info(f"✅ Successfully discovered {result['total_competitors']} competitors for user {clerk_user_id}")
return CompetitorDiscoveryResponse(
success=True,
message=f"Successfully discovered {result['total_competitors']} competitors and social media accounts",
session_id=result["session_id"],
user_url=result["user_url"],
competitors=result["competitors"],
social_media_accounts=result.get("social_media_accounts"),
social_media_citations=result.get("social_media_citations"),
research_summary=result["research_summary"],
total_competitors=result["total_competitors"],
industry_context=result["industry_context"],
analysis_timestamp=result["analysis_timestamp"],
api_cost=result["api_cost"]
)
else:
logger.error(f"❌ Competitor discovery failed for user {clerk_user_id}: {result.get('error')}")
return CompetitorDiscoveryResponse(
success=False,
message="Competitor discovery failed",
session_id=clerk_user_id,
user_url=result.get("user_url", request.user_url),
error=result.get("error", "Unknown error occurred")
)
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Error in competitor discovery endpoint: {str(e)}")
logger.error(traceback.format_exc())
# Return error response with Clerk user ID
clerk_user_id = str(current_user.get('id', 'unknown'))
return CompetitorDiscoveryResponse(
success=False,
message="Internal server error during competitor discovery",
session_id=clerk_user_id,
user_url=request.user_url,
error=str(e)
)
@router.post("/research-data", response_model=ResearchDataResponse)
async def get_research_data(request: ResearchDataRequest) -> ResearchDataResponse:
"""
Retrieve research data for a specific onboarding session.
This endpoint returns the stored research data including competitor analysis
and research summary for the given session.
"""
try:
logger.info(f"Retrieving research data for session {request.session_id}")
# Validate session ID
if not request.session_id or len(request.session_id) < 10:
raise HTTPException(
status_code=400,
detail="Invalid session ID"
)
# Retrieve research data
result = await step3_research_service.get_research_data(request.session_id)
if result["success"]:
logger.info(f"Successfully retrieved research data for session {request.session_id}")
return ResearchDataResponse(
success=True,
message="Research data retrieved successfully",
session_id=result["session_id"],
research_data=result["research_data"]
)
else:
logger.warning(f"No research data found for session {request.session_id}")
return ResearchDataResponse(
success=False,
message="No research data found for this session",
session_id=request.session_id,
error=result.get("error", "Research data not found")
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error retrieving research data: {str(e)}")
logger.error(traceback.format_exc())
return ResearchDataResponse(
success=False,
message="Internal server error while retrieving research data",
session_id=request.session_id,
error=str(e)
)
@router.get("/health", response_model=ResearchHealthResponse)
async def health_check() -> ResearchHealthResponse:
"""
Check the health of the Step 3 research service.
This endpoint provides health status information for the research service
including Exa API connectivity and service status.
"""
try:
logger.info("Performing Step 3 research service health check")
health_status = await step3_research_service.health_check()
if health_status["status"] == "healthy":
return ResearchHealthResponse(
success=True,
message="Step 3 research service is healthy",
service_status=health_status,
timestamp=health_status["timestamp"]
)
else:
return ResearchHealthResponse(
success=False,
message=f"Step 3 research service is {health_status['status']}",
service_status=health_status,
timestamp=health_status["timestamp"]
)
except Exception as e:
logger.error(f"Error in health check: {str(e)}")
logger.error(traceback.format_exc())
return ResearchHealthResponse(
success=False,
message="Health check failed",
error=str(e),
timestamp=datetime.utcnow().isoformat()
)
@router.post("/validate-session")
async def validate_session(session_id: str) -> Dict[str, Any]:
"""
Validate that a session exists and is ready for Step 3.
This endpoint checks if the session exists and has completed previous steps.
"""
try:
logger.info(f"Validating session {session_id} for Step 3")
# Basic validation
if not session_id or len(session_id) < 10:
raise HTTPException(
status_code=400,
detail="Invalid session ID format"
)
# Check if session has completed Step 2 (website analysis)
# This would integrate with the existing session validation logic
return {
"success": True,
"message": "Session is valid for Step 3",
"session_id": session_id,
"ready_for_step3": True
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error validating session: {str(e)}")
return {
"success": False,
"message": "Session validation failed",
"error": str(e)
}
@router.get("/cost-estimate")
async def get_cost_estimate(
num_results: int = 25,
include_content: bool = True
) -> Dict[str, Any]:
"""
Get cost estimate for competitor discovery.
This endpoint provides cost estimates for Exa API usage
to help users understand the cost of competitor discovery.
"""
try:
logger.info(f"Getting cost estimate for {num_results} results, content: {include_content}")
cost_estimate = step3_research_service.exa_service.get_cost_estimate(
num_results=num_results,
include_content=include_content
)
return {
"success": True,
"cost_estimate": cost_estimate,
"message": "Cost estimate calculated successfully"
}
except Exception as e:
logger.error(f"Error calculating cost estimate: {str(e)}")
return {
"success": False,
"message": "Failed to calculate cost estimate",
"error": str(e)
}

View File

@@ -0,0 +1,217 @@
"""
Step Management Service
Handles onboarding step operations and progress tracking.
"""
from typing import Dict, Any, List, Optional
from fastapi import HTTPException
from loguru import logger
from services.api_key_manager import get_onboarding_progress_for_user, StepStatus
from services.progressive_setup_service import ProgressiveSetupService
from services.database import get_db_session
class StepManagementService:
"""Service for handling onboarding step management."""
def __init__(self):
pass
async def get_onboarding_status(self, current_user: Dict[str, Any]) -> Dict[str, Any]:
"""Get the current onboarding status (per user)."""
try:
user_id = str(current_user.get('id'))
progress = get_onboarding_progress_for_user(user_id)
# Safety check: if all steps are completed, ensure is_completed is True
all_steps_completed = all(s.status in [StepStatus.COMPLETED, StepStatus.SKIPPED] for s in progress.steps)
if all_steps_completed and not progress.is_completed:
logger.info(f"[get_onboarding_status] All steps completed but is_completed was False, fixing...")
progress.is_completed = True
progress.completed_at = progress.started_at # Use started_at as fallback
progress.current_step = len(progress.steps)
progress.save_progress()
return {
"is_completed": progress.is_completed,
"current_step": progress.current_step,
"completion_percentage": progress.get_completion_percentage(),
"next_step": progress.get_next_incomplete_step(),
"started_at": progress.started_at,
"completed_at": progress.completed_at,
"can_proceed_to_final": progress.can_complete_onboarding()
}
except Exception as e:
logger.error(f"Error getting onboarding status: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def get_onboarding_progress_full(self, current_user: Dict[str, Any]) -> Dict[str, Any]:
"""Get the full onboarding progress data."""
try:
user_id = str(current_user.get('id'))
progress = get_onboarding_progress_for_user(user_id)
# Convert StepData objects to dictionaries
step_data = []
for step in progress.steps:
step_data.append({
"step_number": step.step_number,
"title": step.title,
"description": step.description,
"status": step.status.value,
"completed_at": step.completed_at,
"data": step.data,
"validation_errors": step.validation_errors or []
})
return {
"steps": step_data,
"current_step": progress.current_step,
"started_at": progress.started_at,
"last_updated": progress.last_updated,
"is_completed": progress.is_completed,
"completed_at": progress.completed_at,
"completion_percentage": progress.get_completion_percentage()
}
except Exception as e:
logger.error(f"Error getting onboarding progress: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def get_step_data(self, step_number: int, current_user: Dict[str, Any]) -> Dict[str, Any]:
"""Get data for a specific step."""
try:
user_id = str(current_user.get('id'))
progress = get_onboarding_progress_for_user(user_id)
step = progress.get_step_data(step_number)
if not step:
raise HTTPException(status_code=404, detail=f"Step {step_number} not found")
return {
"step_number": step.step_number,
"title": step.title,
"description": step.description,
"status": step.status.value,
"completed_at": step.completed_at,
"data": step.data,
"validation_errors": step.validation_errors or []
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting step data: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def complete_step(self, step_number: int, request_data: Dict[str, Any], current_user: Dict[str, Any]) -> Dict[str, Any]:
"""Mark a step as completed."""
try:
logger.info(f"[complete_step] Completing step {step_number}")
user_id = str(current_user.get('id'))
progress = get_onboarding_progress_for_user(user_id)
step = progress.get_step_data(step_number)
if not step:
logger.error(f"[complete_step] Step {step_number} not found")
raise HTTPException(status_code=404, detail=f"Step {step_number} not found")
# Validate step data before marking as completed
from services.validation import validate_step_data
logger.info(f"[complete_step] Validating step {step_number} with data: {request_data}")
validation_errors = validate_step_data(step_number, request_data)
if validation_errors:
logger.warning(f"[complete_step] Step {step_number} validation failed: {validation_errors}")
raise HTTPException(status_code=400, detail=f"Step validation failed: {'; '.join(validation_errors)}")
# Mark step as completed
progress.mark_step_completed(step_number, request_data)
logger.info(f"[complete_step] Step {step_number} completed successfully")
# If this is step 1 (API keys), also save to global .env file
if step_number == 1 and request_data and 'api_keys' in request_data:
try:
from services.api_key_manager import APIKeyManager
api_manager = APIKeyManager()
# Save each API key to the global .env file
api_keys = request_data['api_keys']
for provider, api_key in api_keys.items():
if api_key: # Only save non-empty keys
api_manager.save_api_key(provider, api_key)
logger.info(f"[complete_step] Saved {provider} API key to global .env file")
except Exception as env_error:
logger.warning(f"Could not save API keys to global .env file: {env_error}")
# Don't fail the step completion for .env file issues
# Initialize/upgrade user environment based on new step
try:
db_session = get_db_session()
if db_session:
setup_service = ProgressiveSetupService(db_session)
# Initialize environment if first time, or upgrade if progressing
if step_number == 1:
setup_service.initialize_user_environment(user_id)
else:
setup_service.upgrade_user_environment(user_id, step_number)
db_session.close()
except Exception as env_error:
logger.warning(f"Could not set up user environment: {env_error}")
# Don't fail the step completion for environment setup issues
return {
"message": f"Step {step_number} completed successfully",
"step_number": step_number,
"data": request_data
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error completing step: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def skip_step(self, step_number: int, current_user: Dict[str, Any]) -> Dict[str, Any]:
"""Skip a step (for optional steps)."""
try:
user_id = str(current_user.get('id'))
progress = get_onboarding_progress_for_user(user_id)
step = progress.get_step_data(step_number)
if not step:
raise HTTPException(status_code=404, detail=f"Step {step_number} not found")
# Mark step as skipped
progress.mark_step_skipped(step_number)
return {
"message": f"Step {step_number} skipped successfully",
"step_number": step_number
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error skipping step: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def validate_step_access(self, step_number: int, current_user: Dict[str, Any]) -> Dict[str, Any]:
"""Validate if user can access a specific step."""
try:
user_id = str(current_user.get('id'))
progress = get_onboarding_progress_for_user(user_id)
if not progress.can_proceed_to_step(step_number):
return {
"can_proceed": False,
"validation_errors": [f"Cannot proceed to step {step_number}. Complete previous steps first."],
"step_status": "locked"
}
return {
"can_proceed": True,
"validation_errors": [],
"step_status": "available"
}
except Exception as e:
logger.error(f"Error validating step access: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")

View File

@@ -0,0 +1,140 @@
"""
User Environment API endpoints
Handles user-specific environment setup and management.
"""
from fastapi import APIRouter, HTTPException, Depends
from typing import Dict, Any, Optional
from loguru import logger
from services.progressive_setup_service import ProgressiveSetupService
from services.database import get_db_session
from middleware.auth_middleware import get_current_user
router = APIRouter(prefix="/api/user-environment", tags=["user-environment"])
@router.post("/initialize")
async def initialize_user_environment(current_user: Dict[str, Any] = Depends(get_current_user)):
"""Initialize user environment based on onboarding progress."""
try:
user_id = str(current_user.get('id'))
db_session = get_db_session()
if not db_session:
raise HTTPException(status_code=500, detail="Database connection failed")
setup_service = ProgressiveSetupService(db_session)
result = setup_service.initialize_user_environment(user_id)
return {
"message": "User environment initialized successfully",
"data": result
}
except Exception as e:
logger.error(f"Error initializing user environment: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error initializing user environment: {str(e)}")
finally:
if db_session:
db_session.close()
@router.get("/status")
async def get_user_environment_status(current_user: Dict[str, Any] = Depends(get_current_user)):
"""Get current user environment status."""
try:
user_id = str(current_user.get('id'))
db_session = get_db_session()
if not db_session:
raise HTTPException(status_code=500, detail="Database connection failed")
setup_service = ProgressiveSetupService(db_session)
status = setup_service.get_user_environment_status(user_id)
return status
except Exception as e:
logger.error(f"Error getting user environment status: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error getting user environment status: {str(e)}")
finally:
if db_session:
db_session.close()
@router.post("/upgrade")
async def upgrade_user_environment(
new_step: int,
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""Upgrade user environment when progressing in onboarding."""
try:
user_id = str(current_user.get('id'))
db_session = get_db_session()
if not db_session:
raise HTTPException(status_code=500, detail="Database connection failed")
setup_service = ProgressiveSetupService(db_session)
result = setup_service.upgrade_user_environment(user_id, new_step)
return {
"message": "User environment upgraded successfully",
"data": result
}
except Exception as e:
logger.error(f"Error upgrading user environment: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error upgrading user environment: {str(e)}")
finally:
if db_session:
db_session.close()
@router.delete("/cleanup")
async def cleanup_user_environment(current_user: Dict[str, Any] = Depends(get_current_user)):
"""Clean up user environment (for account deletion)."""
try:
user_id = str(current_user.get('id'))
db_session = get_db_session()
if not db_session:
raise HTTPException(status_code=500, detail="Database connection failed")
setup_service = ProgressiveSetupService(db_session)
success = setup_service.cleanup_user_environment(user_id)
if success:
return {"message": "User environment cleaned up successfully"}
else:
raise HTTPException(status_code=500, detail="Failed to cleanup user environment")
except Exception as e:
logger.error(f"Error cleaning up user environment: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error cleaning up user environment: {str(e)}")
finally:
if db_session:
db_session.close()
@router.get("/workspace")
async def get_user_workspace_info(current_user: Dict[str, Any] = Depends(get_current_user)):
"""Get user workspace information."""
try:
user_id = str(current_user.get('id'))
db_session = get_db_session()
if not db_session:
raise HTTPException(status_code=500, detail="Database connection failed")
setup_service = ProgressiveSetupService(db_session)
workspace_manager = setup_service.workspace_manager
workspace = workspace_manager.get_user_workspace(user_id)
if not workspace:
raise HTTPException(status_code=404, detail="User workspace not found")
return workspace
except Exception as e:
logger.error(f"Error getting user workspace: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error getting user workspace: {str(e)}")
finally:
if db_session:
db_session.close()

465
backend/api/wix_routes.py Normal file
View File

@@ -0,0 +1,465 @@
"""
Wix Integration API Routes
Handles Wix authentication, connection status, and blog publishing.
"""
from fastapi import APIRouter, HTTPException, Depends, Request
from typing import Dict, Any, Optional
from loguru import logger
from pydantic import BaseModel
from services.wix_service import WixService
from middleware.auth_middleware import get_current_user
router = APIRouter(prefix="/api/wix", tags=["Wix Integration"])
# Initialize Wix service
wix_service = WixService()
class WixAuthRequest(BaseModel):
"""Request model for Wix authentication"""
code: str
state: Optional[str] = None
class WixPublishRequest(BaseModel):
"""Request model for publishing to Wix"""
title: str
content: str
cover_image_url: Optional[str] = None
category_ids: Optional[list] = None
tag_ids: Optional[list] = None
publish: bool = True
# Optional access token for test-real publish flow
access_token: Optional[str] = None
class WixCreateCategoryRequest(BaseModel):
access_token: str
label: str
description: Optional[str] = None
language: Optional[str] = None
class WixCreateTagRequest(BaseModel):
access_token: str
label: str
language: Optional[str] = None
class WixConnectionStatus(BaseModel):
"""Response model for Wix connection status"""
connected: bool
has_permissions: bool
site_info: Optional[Dict[str, Any]] = None
permissions: Optional[Dict[str, Any]] = None
error: Optional[str] = None
@router.get("/auth/url")
async def get_authorization_url(state: Optional[str] = None) -> Dict[str, str]:
"""
Get Wix OAuth authorization URL
Args:
state: Optional state parameter for security
Returns:
Authorization URL
"""
try:
url = wix_service.get_authorization_url(state)
return {"authorization_url": url}
except Exception as e:
logger.error(f"Failed to generate authorization URL: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/auth/callback")
async def handle_oauth_callback(request: WixAuthRequest, current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
"""
Handle OAuth callback and exchange code for tokens
Args:
request: OAuth callback request with code
current_user: Current authenticated user
Returns:
Token information and connection status
"""
try:
# Exchange code for tokens
tokens = wix_service.exchange_code_for_tokens(request.code)
# Get site information
site_info = wix_service.get_site_info(tokens['access_token'])
# Check permissions
permissions = wix_service.check_blog_permissions(tokens['access_token'])
# TODO: Store tokens securely in database associated with current_user
# For now, we'll return them (in production, store in encrypted database)
return {
"success": True,
"tokens": {
"access_token": tokens['access_token'],
"refresh_token": tokens.get('refresh_token'),
"expires_in": tokens.get('expires_in'),
"token_type": tokens.get('token_type', 'Bearer')
},
"site_info": site_info,
"permissions": permissions,
"message": "Successfully connected to Wix"
}
except Exception as e:
logger.error(f"Failed to handle OAuth callback: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/connection/status")
async def get_connection_status(current_user: dict = Depends(get_current_user)) -> WixConnectionStatus:
"""
Check Wix connection status and permissions
Args:
current_user: Current authenticated user
Returns:
Connection status and permissions
"""
try:
# TODO: Retrieve stored tokens from database for current_user
# For now, we'll return a mock response
# In production, you'd check if tokens exist and are valid
return WixConnectionStatus(
connected=False,
has_permissions=False,
error="No Wix connection found. Please connect your Wix account first."
)
except Exception as e:
logger.error(f"Failed to check connection status: {e}")
return WixConnectionStatus(
connected=False,
has_permissions=False,
error=str(e)
)
@router.post("/publish")
async def publish_to_wix(request: WixPublishRequest, current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
"""
Publish blog post to Wix
Args:
request: Blog post data
current_user: Current authenticated user
Returns:
Published blog post information
"""
try:
# TODO: Retrieve stored access token from database for current_user
# For now, we'll return an error asking user to connect first
return {
"success": False,
"error": "Wix account not connected. Please connect your Wix account first.",
"message": "Use the /api/wix/auth/url endpoint to get the authorization URL"
}
# Example of what the actual implementation would look like:
# access_token = get_stored_access_token(current_user['id'])
#
# if not access_token:
# raise HTTPException(status_code=401, detail="Wix account not connected")
#
# # Check if token is still valid, refresh if needed
# try:
# site_info = wix_service.get_site_info(access_token)
# except:
# # Token expired, try to refresh
# refresh_token = get_stored_refresh_token(current_user['id'])
# if refresh_token:
# new_tokens = wix_service.refresh_access_token(refresh_token)
# access_token = new_tokens['access_token']
# # Store new tokens
# else:
# raise HTTPException(status_code=401, detail="Wix session expired. Please reconnect.")
#
# # Get current member ID (required for third-party apps)
# member_info = wix_service.get_current_member(access_token)
# member_id = member_info.get('member', {}).get('id')
#
# if not member_id:
# raise HTTPException(status_code=400, detail="Could not retrieve member ID")
#
# # Create blog post
# result = wix_service.create_blog_post(
# access_token=access_token,
# title=request.title,
# content=request.content,
# cover_image_url=request.cover_image_url,
# category_ids=request.category_ids,
# tag_ids=request.tag_ids,
# publish=request.publish,
# member_id=member_id # Required for third-party apps
# )
#
# return {
# "success": True,
# "post_id": result.get('draftPost', {}).get('id'),
# "url": result.get('draftPost', {}).get('url'),
# "message": "Blog post published successfully to Wix"
# }
except Exception as e:
logger.error(f"Failed to publish to Wix: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/categories")
async def get_blog_categories(current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
"""
Get available blog categories from Wix
Args:
current_user: Current authenticated user
Returns:
List of blog categories
"""
try:
# TODO: Retrieve stored access token from database for current_user
return {
"success": False,
"error": "Wix account not connected. Please connect your Wix account first."
}
# Example implementation:
# access_token = get_stored_access_token(current_user['id'])
# if not access_token:
# raise HTTPException(status_code=401, detail="Wix account not connected")
#
# categories = wix_service.get_blog_categories(access_token)
# return {"categories": categories}
except Exception as e:
logger.error(f"Failed to get blog categories: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/tags")
async def get_blog_tags(current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
"""
Get available blog tags from Wix
Args:
current_user: Current authenticated user
Returns:
List of blog tags
"""
try:
# TODO: Retrieve stored access token from database for current_user
return {
"success": False,
"error": "Wix account not connected. Please connect your Wix account first."
}
# Example implementation:
# access_token = get_stored_access_token(current_user['id'])
# if not access_token:
# raise HTTPException(status_code=401, detail="Wix account not connected")
#
# tags = wix_service.get_blog_tags(access_token)
# return {"tags": tags}
except Exception as e:
logger.error(f"Failed to get blog tags: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/disconnect")
async def disconnect_wix(current_user: dict = Depends(get_current_user)) -> Dict[str, Any]:
"""
Disconnect Wix account
Args:
current_user: Current authenticated user
Returns:
Disconnection status
"""
try:
# TODO: Remove stored tokens from database for current_user
return {
"success": True,
"message": "Wix account disconnected successfully"
}
except Exception as e:
logger.error(f"Failed to disconnect Wix: {e}")
raise HTTPException(status_code=500, detail=str(e))
# =============================================================================
# TEST ENDPOINTS - No authentication required for testing
# =============================================================================
@router.get("/test/connection/status")
async def get_test_connection_status() -> WixConnectionStatus:
"""
TEST ENDPOINT: Check Wix connection status without authentication
Returns:
Connection status and permissions
"""
try:
logger.info("TEST: Checking Wix connection status (no auth required)")
return WixConnectionStatus(
connected=False,
has_permissions=False,
error="No stored tokens found. Please connect your Wix account first."
)
except Exception as e:
logger.error(f"TEST: Failed to check connection status: {e}")
return WixConnectionStatus(
connected=False,
has_permissions=False,
error=str(e)
)
@router.get("/test/auth/url")
async def get_test_authorization_url(state: Optional[str] = None) -> Dict[str, str]:
"""
TEST ENDPOINT: Get Wix OAuth authorization URL without authentication
Args:
state: Optional state parameter for security
Returns:
Authorization URL for user to visit
"""
try:
logger.info("TEST: Generating Wix authorization URL (no auth required)")
# Check if Wix service is properly configured
if not wix_service.client_id:
logger.warning("TEST: Wix Client ID not configured, returning mock URL")
return {
"url": "https://www.wix.com/oauth/access?client_id=YOUR_CLIENT_ID&redirect_uri=http://localhost:3000/wix/callback&response_type=code&scope=BLOG.CREATE-DRAFT,BLOG.PUBLISH,MEDIA.MANAGE&code_challenge=test&code_challenge_method=S256",
"state": state or "test_state",
"message": "WIX_CLIENT_ID not configured. Please set it in your .env file to get a real authorization URL."
}
auth_url = wix_service.get_authorization_url(state)
return {"url": auth_url, "state": state or "test_state"}
except Exception as e:
logger.error(f"TEST: Failed to generate authorization URL: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/test/publish")
async def test_publish_to_wix(request: WixPublishRequest) -> Dict[str, Any]:
"""
TEST ENDPOINT: Simulate publishing a blog post to Wix without authentication.
Returns a fake success response so the frontend can validate the flow.
"""
try:
logger.info("TEST: Simulating publish to Wix (no auth required)")
return {
"success": True,
"post_id": "test_post_id",
"url": "https://example.com/blog/test-post",
"message": "Simulated blog post published successfully (test mode)"
}
except Exception as e:
logger.error(f"TEST: Failed to simulate publish: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/test/publish/real")
async def test_publish_real(payload: Dict[str, Any]) -> Dict[str, Any]:
"""
TEST ENDPOINT: Perform a real publish to Wix using a provided access token.
Notes:
- Expects request.access_token from the frontend's Wix SDK tokens
- Derives member_id server-side (required by Wix for third-party apps)
"""
try:
access_token = payload.get("access_token")
if not access_token:
raise HTTPException(status_code=400, detail="Missing access_token")
# Derive current member id from token (try local decode first, then API fallback)
member_id = wix_service.extract_member_id_from_access_token(access_token)
if not member_id:
member_info = wix_service.get_current_member(access_token)
member_id = (
(member_info.get("member") or {}).get("id")
or member_info.get("id")
)
if not member_id:
raise HTTPException(status_code=400, detail="Unable to resolve member_id from token")
result = wix_service.create_blog_post(
access_token=access_token,
title=payload.get("title") or "Untitled",
content=payload.get("content") or "",
cover_image_url=payload.get("cover_image_url"),
category_ids=payload.get("category_ids") or None,
tag_ids=payload.get("tag_ids") or None,
publish=bool(payload.get("publish", True)),
member_id=member_id,
)
return {
"success": True,
"post_id": (result.get("draftPost") or result.get("post") or {}).get("id"),
"url": (result.get("draftPost") or result.get("post") or {}).get("url"),
"message": "Blog post published to Wix",
"raw": result,
}
except HTTPException:
raise
except Exception as e:
logger.error(f"TEST: Real publish failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/test/category")
async def test_create_category(request: WixCreateCategoryRequest) -> Dict[str, Any]:
try:
result = wix_service.create_category(
access_token=request.access_token,
label=request.label,
description=request.description,
language=request.language,
)
return {"success": True, "category": result.get("category", {}), "raw": result}
except Exception as e:
logger.error(f"TEST: Create category failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/test/tag")
async def test_create_tag(request: WixCreateTagRequest) -> Dict[str, Any]:
try:
result = wix_service.create_tag(
access_token=request.access_token,
label=request.label,
language=request.language,
)
return {"success": True, "tag": result.get("tag", {}), "raw": result}
except Exception as e:
logger.error(f"TEST: Create tag failed: {e}")
raise HTTPException(status_code=500, detail=str(e))